diff --git a/src/index.js b/src/index.js index 0ad95e72..37a9e5ab 100644 --- a/src/index.js +++ b/src/index.js @@ -48,27 +48,10 @@ export default async function loader(content, map, meta) { return; } - if (shouldUseModulesPlugins(options)) { - const icssResolver = this.getResolve({ - mainFields: ['css', 'style', 'main', '...'], - mainFiles: ['index', '...'], - extensions: [], - conditionNames: ['style'], - }); + const needUseModulesPlugins = shouldUseModulesPlugins(options); - plugins.push( - ...getModulesPlugins(options, this), - icssParser({ - context: this.context, - rootContext: this.rootContext, - resolver: icssResolver, - urlHandler: (url) => - stringifyRequest( - this, - getPreRequester(this)(options.importLoaders) + url - ), - }) - ); + if (needUseModulesPlugins) { + plugins.push(...getModulesPlugins(options, this)); } if (shouldUseImportPlugin(options)) { @@ -113,6 +96,28 @@ export default async function loader(content, map, meta) { ); } + if (needUseModulesPlugins) { + const icssResolver = this.getResolve({ + mainFields: ['css', 'style', 'main', '...'], + mainFiles: ['index', '...'], + extensions: [], + conditionNames: ['style'], + }); + + plugins.push( + icssParser({ + context: this.context, + rootContext: this.rootContext, + resolver: icssResolver, + urlHandler: (url) => + stringifyRequest( + this, + getPreRequester(this)(options.importLoaders) + url + ), + }) + ); + } + // Reuse CSS AST (PostCSS AST e.g 'postcss-loader') to avoid reparsing if (meta) { const { ast } = meta; @@ -160,8 +165,7 @@ export default async function loader(content, map, meta) { const imports = []; const apiImports = []; - const urlReplacements = []; - const icssReplacements = []; + const replacements = []; const exports = []; for (const message of result.messages) { @@ -173,11 +177,8 @@ export default async function loader(content, map, meta) { case 'api-import': apiImports.push(message.value); break; - case 'url-replacement': - urlReplacements.push(message.value); - break; - case 'icss-replacement': - icssReplacements.push(message.value); + case 'replacement': + replacements.push(message.value); break; case 'export': exports.push(message.value); @@ -189,14 +190,8 @@ export default async function loader(content, map, meta) { apiImports.sort(sortImports); const importCode = getImportCode(this, imports, options); - const moduleCode = getModuleCode( - result, - apiImports, - urlReplacements, - icssReplacements, - options - ); - const exportCode = getExportCode(exports, icssReplacements, options); + const moduleCode = getModuleCode(result, apiImports, replacements, options); + const exportCode = getExportCode(exports, replacements, options); callback(null, `${importCode}${moduleCode}${exportCode}`); } diff --git a/src/plugins/postcss-icss-parser.js b/src/plugins/postcss-icss-parser.js index df24aa90..2b52433b 100644 --- a/src/plugins/postcss-icss-parser.js +++ b/src/plugins/postcss-icss-parser.js @@ -76,8 +76,8 @@ export default postcss.plugin( importReplacements[token] = replacementName; result.messages.push({ - type: 'icss-replacement', - value: { replacementName, importName, localName }, + type: 'replacement', + value: { type: 'icss', replacementName, importName, localName }, }); } } diff --git a/src/plugins/postcss-url-parser.js b/src/plugins/postcss-url-parser.js index 61d6b461..7bd5e19c 100644 --- a/src/plugins/postcss-url-parser.js +++ b/src/plugins/postcss-url-parser.js @@ -233,15 +233,8 @@ export default postcss.plugin(pluginName, (options) => async (css, result) => { result.messages.push({ pluginName, - type: 'url-replacement', - value: { - replacementName, - importName, - hash, - needQuotes, - index, - order: 4, - }, + type: 'replacement', + value: { type: 'url', replacementName, importName, hash, needQuotes }, }); } diff --git a/src/utils.js b/src/utils.js index 6b5eb5fc..e5b75cda 100644 --- a/src/utils.js +++ b/src/utils.js @@ -354,13 +354,7 @@ function getImportCode(loaderContext, imports, options) { return code ? `// Imports\n${code}` : ''; } -function getModuleCode( - result, - apiImports, - urlReplacements, - icssReplacements, - options -) { +function getModuleCode(result, apiImports, replacements, options) { if (options.modules.exportOnlyLocals === true) { return 'var ___CSS_LOADER_EXPORT___ = {};\n'; } @@ -383,33 +377,35 @@ function getModuleCode( )}${media ? `, ${JSON.stringify(media)}` : ''}]);\n`; } - for (const item of urlReplacements) { - const { replacementName, importName, hash, needQuotes } = item; + for (const replacement of replacements) { + const { replacementName, importName, type } = replacement; - const getUrlOptions = [] - .concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []) - .concat(needQuotes ? 'needQuotes: true' : []); - const preparedOptions = - getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''; + if (type === 'url') { + const { hash, needQuotes } = replacement; - beforeCode += `var ${replacementName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});\n`; - - code = code.replace( - new RegExp(replacementName, 'g'), - () => `" + ${replacementName} + "` - ); - } + const getUrlOptions = [] + .concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []) + .concat(needQuotes ? 'needQuotes: true' : []); + const preparedOptions = + getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''; - for (const replacement of icssReplacements) { - const { replacementName, importName, localName } = replacement; + beforeCode += `var ${replacementName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});\n`; - code = code.replace(new RegExp(replacementName, 'g'), () => - options.modules.namedExport - ? `" + ${importName}_NAMED___[${JSON.stringify( - camelCase(localName) - )}] + "` - : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` - ); + code = code.replace( + new RegExp(replacementName, 'g'), + () => `" + ${replacementName} + "` + ); + } else { + const { localName } = replacement; + + code = code.replace(new RegExp(replacementName, 'g'), () => + options.modules.namedExport + ? `" + ${importName}_NAMED___[${JSON.stringify( + camelCase(localName) + )}] + "` + : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` + ); + } } return `${beforeCode}// Module\n___CSS_LOADER_EXPORT___.push([module.id, ${code}, ""${sourceMapValue}]);\n`; @@ -421,7 +417,7 @@ function dashesCamelCase(str) { ); } -function getExportCode(exports, icssReplacements, options) { +function getExportCode(exports, replacements, options) { let code = ''; let localsCode = ''; @@ -476,16 +472,25 @@ function getExportCode(exports, icssReplacements, options) { } } - for (const replacement of icssReplacements) { - const { replacementName, importName, localName } = replacement; + for (const replacement of replacements) { + const { replacementName, type } = replacement; - localsCode = localsCode.replace(new RegExp(replacementName, 'g'), () => - options.modules.namedExport - ? `" + ${importName}_NAMED___[${JSON.stringify( - camelCase(localName) - )}] + "` - : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` - ); + if (type === 'url') { + localsCode = localsCode.replace( + new RegExp(replacementName, 'g'), + () => `" + ${replacementName} + "` + ); + } else { + const { importName, localName } = replacement; + + localsCode = localsCode.replace(new RegExp(replacementName, 'g'), () => + options.modules.namedExport + ? `" + ${importName}_NAMED___[${JSON.stringify( + camelCase(localName) + )}] + "` + : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` + ); + } } if (localsCode) { diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index b9cc001b..c6330783 100644 --- a/test/__snapshots__/modules-option.test.js.snap +++ b/test/__snapshots__/modules-option.test.js.snap @@ -3676,6 +3676,98 @@ Object { exports[`"modules" option should work js template with "namedExport" option: warnings 1`] = `Array []`; +exports[`"modules" option should work with "url" and "namedExport": errors 1`] = `Array []`; + +exports[`"modules" option should work with "url" and "namedExport": module 1`] = ` +"// Imports +import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\"; +import ___CSS_LOADER_ICSS_IMPORT_0___, * as ___CSS_LOADER_ICSS_IMPORT_0____NAMED___ from \\"-!../../../../src/index.js??[ident]!./shared.css\\"; +import ___CSS_LOADER_GET_URL_IMPORT___ from \\"../../../../src/runtime/getUrl.js\\"; +import ___CSS_LOADER_URL_IMPORT_0___ from \\"./img.png\\"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false); +___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, \\"\\", true); +var ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \\"a {\\\\n background: url(\\" + ___CSS_LOADER_URL_REPLACEMENT_0___ + \\");\\\\n}\\\\n\\\\nbody {\\\\n background: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vUrlOther\\"] + \\";\\\\n}\\\\n\\", \\"\\"]); +// Exports +export const vUrl = \\"url(\\" + ___CSS_LOADER_URL_REPLACEMENT_0___ + \\")\\"; +export const vUrlOther = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vUrlOther\\"] + \\"\\"; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work with "url" and "namedExport": result 1`] = ` +Array [ + Array [ + "../../src/index.js?[ident]!./modules/url/shared.css", + " +", + "", + ], + Array [ + "./modules/url/source.css", + "a { + background: url(/webpack/public/path/img.png); +} + +body { + background: url(/webpack/public/path/img.png); +} +", + "", + ], +] +`; + +exports[`"modules" option should work with "url" and "namedExport": warnings 1`] = `Array []`; + +exports[`"modules" option should work with "url": errors 1`] = `Array []`; + +exports[`"modules" option should work with "url": module 1`] = ` +"// Imports +import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\"; +import ___CSS_LOADER_ICSS_IMPORT_0___ from \\"-!../../../../src/index.js??[ident]!./shared.css\\"; +import ___CSS_LOADER_GET_URL_IMPORT___ from \\"../../../../src/runtime/getUrl.js\\"; +import ___CSS_LOADER_URL_IMPORT_0___ from \\"./img.png\\"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false); +___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, \\"\\", true); +var ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \\"a {\\\\n background: url(\\" + ___CSS_LOADER_URL_REPLACEMENT_0___ + \\");\\\\n}\\\\n\\\\nbody {\\\\n background: \\" + ___CSS_LOADER_ICSS_IMPORT_0___.locals[\\"v-url-other\\"] + \\";\\\\n}\\\\n\\", \\"\\"]); +// Exports +___CSS_LOADER_EXPORT___.locals = { + \\"v-url\\": \\"url(\\" + ___CSS_LOADER_URL_REPLACEMENT_0___ + \\")\\", + \\"v-url-other\\": \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0___.locals[\\"v-url-other\\"] + \\"\\" +}; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work with "url": result 1`] = ` +Array [ + Array [ + "../../src/index.js?[ident]!./modules/url/shared.css", + " +", + "", + ], + Array [ + "./modules/url/source.css", + "a { + background: url(/webpack/public/path/img.png); +} + +body { + background: url(/webpack/public/path/img.png); +} +", + "", + ], +] +`; + +exports[`"modules" option should work with "url": warnings 1`] = `Array []`; + exports[`"modules" option should work with a modules.auto Function that returns "false": errors 1`] = `Array []`; exports[`"modules" option should work with a modules.auto Function that returns "false": module 1`] = ` diff --git a/test/fixtures/modules/url/img.png b/test/fixtures/modules/url/img.png new file mode 100644 index 00000000..b74b839e Binary files /dev/null and b/test/fixtures/modules/url/img.png differ diff --git a/test/fixtures/modules/url/shared.css b/test/fixtures/modules/url/shared.css new file mode 100644 index 00000000..7662cfb5 --- /dev/null +++ b/test/fixtures/modules/url/shared.css @@ -0,0 +1 @@ +@value v-url-other: url('./img.png'); diff --git a/test/fixtures/modules/url/source.css b/test/fixtures/modules/url/source.css new file mode 100644 index 00000000..44c9cc8a --- /dev/null +++ b/test/fixtures/modules/url/source.css @@ -0,0 +1,10 @@ +@value v-url: url('./img.png'); +@value v-url-other from './shared.css'; + +a { + background: v-url; +} + +body { + background: v-url-other; +} diff --git a/test/fixtures/modules/url/source.js b/test/fixtures/modules/url/source.js new file mode 100644 index 00000000..1996779e --- /dev/null +++ b/test/fixtures/modules/url/source.js @@ -0,0 +1,5 @@ +import css from './source.css'; + +__export__ = css; + +export default css; diff --git a/test/modules-option.test.js b/test/modules-option.test.js index cf6f1d46..0de057a1 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -1101,4 +1101,38 @@ describe('"modules" option', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats, true)).toMatchSnapshot('errors'); }); + + it('should work with "url"', async () => { + const compiler = getCompiler('./modules/url/source.js', { + modules: true, + }); + const stats = await compile(compiler); + + expect(getModuleSource('./modules/url/source.css', stats)).toMatchSnapshot( + 'module' + ); + expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot( + 'result' + ); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should work with "url" and "namedExport"', async () => { + const compiler = getCompiler('./modules/url/source.js', { + modules: { + namedExport: true, + }, + }); + const stats = await compile(compiler); + + expect(getModuleSource('./modules/url/source.css', stats)).toMatchSnapshot( + 'module' + ); + expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot( + 'result' + ); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); });