diff --git a/src/index.js b/src/index.js index f9fd77c4..977daeb2 100644 --- a/src/index.js +++ b/src/index.js @@ -87,13 +87,15 @@ export default function loader(content, map, meta) { : false, }) .then((result) => { - result - .warnings() - .forEach((warning) => this.emitWarning(new Warning(warning))); + for (const warning of result.warnings()) { + this.emitWarning(new Warning(warning)); + } const imports = []; + const apiImports = []; + const urlReplacements = []; + const icssReplacements = []; const exports = []; - const replacers = []; for (const message of result.messages) { // eslint-disable-next-line default-case @@ -101,12 +103,18 @@ export default function loader(content, map, meta) { case 'import': imports.push(message.value); break; + case 'api-import': + apiImports.push(message.value); + break; + case 'url-replacement': + urlReplacements.push(message.value); + break; + case 'icss-replacement': + icssReplacements.push(message.value); + break; case 'export': exports.push(message.value); break; - case 'replacer': - replacers.push(message.value); - break; } } @@ -118,7 +126,6 @@ export default function loader(content, map, meta) { this, imports, exportType, - sourceMap, importLoaders, esModule ); @@ -126,15 +133,19 @@ export default function loader(content, map, meta) { this, result, exportType, + esModule, sourceMap, - replacers + importLoaders, + apiImports, + urlReplacements, + icssReplacements ); const exportCode = getExportCode( this, exports, exportType, - replacers, localsConvention, + icssReplacements, esModule ); diff --git a/src/plugins/postcss-icss-parser.js b/src/plugins/postcss-icss-parser.js index 56409b2f..e86d0caf 100644 --- a/src/plugins/postcss-icss-parser.js +++ b/src/plugins/postcss-icss-parser.js @@ -2,9 +2,7 @@ import postcss from 'postcss'; import { extractICSS, replaceValueSymbols, replaceSymbols } from 'icss-utils'; import { urlToRequest } from 'loader-utils'; -const pluginName = 'postcss-icss-parser'; - -function normalizeIcssImports(icssImports) { +function makeRequestableIcssImports(icssImports) { return Object.keys(icssImports).reduce((accumulator, url) => { const tokensMap = icssImports[url]; const tokens = Object.keys(tokensMap); @@ -30,60 +28,50 @@ function normalizeIcssImports(icssImports) { }, {}); } -export default postcss.plugin( - pluginName, - () => - function process(css, result) { - const importReplacements = Object.create(null); - const { icssImports, icssExports } = extractICSS(css); - const normalizedIcssImports = normalizeIcssImports(icssImports); - - Object.keys(normalizedIcssImports).forEach((url, importIndex) => { - const importName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}___`; - - result.messages.push({ - pluginName, - type: 'import', - value: { type: 'icss-import', importName, url }, - }); - - const tokenMap = normalizedIcssImports[url]; - const tokens = Object.keys(tokenMap); - - tokens.forEach((token, replacementIndex) => { - const replacementName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}_REPLACEMENT_${replacementIndex}___`; - const localName = tokenMap[token]; - - importReplacements[token] = replacementName; - - result.messages.push({ - pluginName, - type: 'replacer', - value: { - type: 'icss-import', - importName, - replacementName, - localName, - }, - }); - }); - }); - - if (Object.keys(importReplacements).length > 0) { - replaceSymbols(css, importReplacements); +export default postcss.plugin('postcss-icss-parser', () => (css, result) => { + const importReplacements = Object.create(null); + const extractedICSS = extractICSS(css); + const icssImports = makeRequestableIcssImports(extractedICSS.icssImports); + + for (const [importIndex, url] of Object.keys(icssImports).entries()) { + const importName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}___`; + + result.messages.push( + { + type: 'import', + value: { type: 'icss', importName, url }, + }, + { + type: 'api-import', + value: { type: 'internal', importName, dedupe: true }, } + ); + + const tokenMap = icssImports[url]; + const tokens = Object.keys(tokenMap); + + for (const [replacementIndex, token] of tokens.entries()) { + const replacementName = `___CSS_LOADER_ICSS_IMPORT_${importIndex}_REPLACEMENT_${replacementIndex}___`; + const localName = tokenMap[token]; - Object.keys(icssExports).forEach((name) => { - const value = replaceValueSymbols( - icssExports[name], - importReplacements - ); - - result.messages.push({ - pluginName, - type: 'export', - value: { name, value }, - }); + importReplacements[token] = replacementName; + + result.messages.push({ + type: 'icss-replacement', + value: { replacementName, importName, localName }, }); } -); + } + + if (Object.keys(importReplacements).length > 0) { + replaceSymbols(css, importReplacements); + } + + const { icssExports } = extractedICSS; + + for (const name of Object.keys(icssExports)) { + const value = replaceValueSymbols(icssExports[name], importReplacements); + + result.messages.push({ type: 'export', value: { name, value } }); + } +}); diff --git a/src/plugins/postcss-import-parser.js b/src/plugins/postcss-import-parser.js index 46367d77..2399b04e 100644 --- a/src/plugins/postcss-import-parser.js +++ b/src/plugins/postcss-import-parser.js @@ -7,6 +7,8 @@ import { normalizeUrl } from '../utils'; const pluginName = 'postcss-import-parser'; export default postcss.plugin(pluginName, (options) => (css, result) => { + const importsMap = new Map(); + css.walkAtRules(/^import$/i, (atRule) => { // Convert only top-level @import if (atRule.parent.type !== 'root') { @@ -96,10 +98,32 @@ export default postcss.plugin(pluginName, (options) => (css, result) => { atRule.remove(); + if (isRequestable) { + const importKey = url; + let importName = importsMap.get(importKey); + + if (!importName) { + importName = `___CSS_LOADER_AT_RULE_IMPORT_${importsMap.size}___`; + importsMap.set(importKey, importName); + + result.messages.push({ + type: 'import', + value: { type: '@import', importName, url }, + }); + } + + result.messages.push({ + type: 'api-import', + value: { type: 'internal', importName, media }, + }); + + return; + } + result.messages.push({ pluginName, - type: 'import', - value: { type: '@import', isRequestable, url, media }, + type: 'api-import', + value: { type: 'external', url, media }, }); }); }); diff --git a/src/plugins/postcss-url-parser.js b/src/plugins/postcss-url-parser.js index 29424c42..e8493a84 100644 --- a/src/plugins/postcss-url-parser.js +++ b/src/plugins/postcss-url-parser.js @@ -32,7 +32,7 @@ function walkUrls(parsed, callback) { } if (isImageSetFunc.test(node.value)) { - node.nodes.forEach((nNode) => { + for (const nNode of node.nodes) { const { type, value } = nNode; if (type === 'function' && isUrlFunc.test(value)) { @@ -50,7 +50,7 @@ function walkUrls(parsed, callback) { if (type === 'string') { callback(nNode, value, true, true); } - }); + } // Do not traverse inside `image-set` // eslint-disable-next-line consistent-return @@ -61,7 +61,9 @@ function walkUrls(parsed, callback) { export default postcss.plugin(pluginName, (options) => (css, result) => { const importsMap = new Map(); - const replacersMap = new Map(); + const replacementsMap = new Map(); + + let hasHelper = false; css.walkDecls((decl) => { if (!needParseDecl.test(decl.value)) { @@ -100,6 +102,20 @@ export default postcss.plugin(pluginName, (options) => (css, result) => { importName = `___CSS_LOADER_URL_IMPORT_${importsMap.size}___`; importsMap.set(importKey, importName); + if (!hasHelper) { + result.messages.push({ + pluginName, + type: 'import', + value: { + type: 'url', + importName: '___CSS_LOADER_GET_URL_IMPORT___', + url: require.resolve('../runtime/getUrl.js'), + }, + }); + + hasHelper = true; + } + result.messages.push({ pluginName, type: 'import', @@ -111,25 +127,24 @@ export default postcss.plugin(pluginName, (options) => (css, result) => { }); } - const replacerKey = JSON.stringify({ importKey, hash, needQuotes }); - - let replacerName = replacersMap.get(replacerKey); + const replacementKey = JSON.stringify({ importKey, hash, needQuotes }); + let replacementName = replacementsMap.get(replacementKey); - if (!replacerName) { - replacerName = `___CSS_LOADER_URL_REPLACEMENT_${replacersMap.size}___`; - replacersMap.set(replacerKey, replacerName); + if (!replacementName) { + replacementName = `___CSS_LOADER_URL_REPLACEMENT_${replacementsMap.size}___`; + replacementsMap.set(replacementKey, replacementName); result.messages.push({ pluginName, - type: 'replacer', - value: { type: 'url', replacerName, importName, hash, needQuotes }, + type: 'url-replacement', + value: { replacementName, importName, hash, needQuotes }, }); } // eslint-disable-next-line no-param-reassign node.type = 'word'; // eslint-disable-next-line no-param-reassign - node.value = replacerName; + node.value = replacementName; }); // eslint-disable-next-line no-param-reassign diff --git a/src/utils.js b/src/utils.js index 7b9a21dd..abed024c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -199,16 +199,10 @@ function getImportCode( loaderContext, imports, exportType, - sourceMap, importLoaders, esModule ) { - const importItems = []; - const codeItems = []; - const atRuleImportNames = new Map(); - - let hasUrlHelper = false; - let importPrefix; + let code = ''; if (exportType === 'full') { const apiUrl = stringifyRequest( @@ -216,123 +210,38 @@ function getImportCode( require.resolve('./runtime/api') ); - importItems.push( - esModule - ? `import ___CSS_LOADER_API_IMPORT___ from ${apiUrl};` - : `var ___CSS_LOADER_API_IMPORT___ = require(${apiUrl});` - ); - codeItems.push( - esModule - ? `var exports = ___CSS_LOADER_API_IMPORT___(${sourceMap});` - : `exports = ___CSS_LOADER_API_IMPORT___(${sourceMap});` - ); + code += esModule + ? `import ___CSS_LOADER_API_IMPORT___ from ${apiUrl};\n` + : `var ___CSS_LOADER_API_IMPORT___ = require(${apiUrl});\n`; } - imports.forEach((item) => { - // eslint-disable-next-line default-case - switch (item.type) { - case '@import': - { - const { isRequestable, url, media } = item; - const preparedMedia = media ? `, ${JSON.stringify(media)}` : ''; - - if (!isRequestable) { - codeItems.push( - `exports.push([module.id, ${JSON.stringify( - `@import url(${url});` - )}${preparedMedia}]);` - ); - - return; - } - - let importName = atRuleImportNames.get(url); - - if (!importName) { - if (!importPrefix) { - importPrefix = getImportPrefix(loaderContext, importLoaders); - } - - const importUrl = stringifyRequest( - loaderContext, - importPrefix + url - ); - - importName = `___CSS_LOADER_AT_RULE_IMPORT_${atRuleImportNames.size}___`; - importItems.push( - esModule - ? `import ${importName} from ${importUrl};` - : `var ${importName} = require(${importUrl});` - ); - - atRuleImportNames.set(url, importName); - } - - codeItems.push(`exports.i(${importName}${preparedMedia});`); - } - break; - case 'url': - { - if (!hasUrlHelper) { - const helperUrl = stringifyRequest( - loaderContext, - require.resolve('./runtime/getUrl.js') - ); - - importItems.push( - esModule - ? `import ___CSS_LOADER_GET_URL_IMPORT___ from ${helperUrl};` - : `var ___CSS_LOADER_GET_URL_IMPORT___ = require(${helperUrl});` - ); - hasUrlHelper = true; - } - - const { importName, url } = item; - const importUrl = stringifyRequest(loaderContext, url); - - importItems.push( - esModule - ? `import ${importName} from ${importUrl};` - : `var ${importName} = require(${importUrl});` - ); - } - break; - case 'icss-import': - { - const { importName, url, media } = item; - const preparedMedia = media ? `, ${JSON.stringify(media)}` : ', ""'; - - if (!importPrefix) { - importPrefix = getImportPrefix(loaderContext, importLoaders); - } - - const importUrl = stringifyRequest(loaderContext, importPrefix + url); - - importItems.push( - esModule - ? `import ${importName} from ${importUrl};` - : `var ${importName} = require(${importUrl});` - ); - - if (exportType === 'full') { - codeItems.push(`exports.i(${importName}${preparedMedia}, true);`); - } - } - break; - } - }); + for (const item of imports) { + const { importName, url } = item; + const importUrl = stringifyRequest( + loaderContext, + item.type !== 'url' + ? getImportPrefix(loaderContext, importLoaders) + url + : url + ); - const items = importItems.concat(codeItems); + code += esModule + ? `import ${importName} from ${importUrl};\n` + : `var ${importName} = require(${importUrl});\n`; + } - return items.length > 0 ? `// Imports\n${items.join('\n')}\n` : ''; + return code ? `// Imports\n${code}` : ''; } function getModuleCode( loaderContext, result, exportType, + esModule, sourceMap, - replacers + importLoaders, + apiImports, + urlReplacements, + icssReplacements ) { if (exportType !== 'full') { return ''; @@ -341,40 +250,53 @@ function getModuleCode( const { css, map } = result; const sourceMapValue = sourceMap && map ? `,${map}` : ''; - let cssCode = JSON.stringify(css); - let replacersCode = ''; + let code = JSON.stringify(css); + let beforeCode = ''; - replacers.forEach((replacer) => { - const { type } = replacer; + beforeCode += esModule + ? `var exports = ___CSS_LOADER_API_IMPORT___(${sourceMap});\n` + : `exports = ___CSS_LOADER_API_IMPORT___(${sourceMap});\n`; - if (type === 'url') { - const { replacerName, importName, hash, needQuotes } = replacer; + for (const item of apiImports) { + const { type, media, dedupe } = item; - const getUrlOptions = [] - .concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []) - .concat(needQuotes ? 'needQuotes: true' : []); - const preparedOptions = - getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''; + beforeCode += + type === 'internal' + ? `exports.i(${item.importName}${ + media ? `, ${JSON.stringify(media)}` : dedupe ? ', ""' : '' + }${dedupe ? ', true' : ''});\n` + : `exports.push([module.id, ${JSON.stringify( + `@import url(${item.url});` + )}${media ? `, ${JSON.stringify(media)}` : ''}]);\n`; + } - replacersCode += `var ${replacerName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});\n`; + for (const item of urlReplacements) { + const { replacementName, importName, hash, needQuotes } = item; - cssCode = cssCode.replace( - new RegExp(replacerName, 'g'), - () => `" + ${replacerName} + "` - ); - } + const getUrlOptions = [] + .concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []) + .concat(needQuotes ? 'needQuotes: true' : []); + const preparedOptions = + getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''; - if (type === 'icss-import') { - const { importName, localName, replacementName } = replacer; + beforeCode += `var ${replacementName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});\n`; - cssCode = cssCode.replace( - new RegExp(replacementName, 'g'), - () => `" + ${importName}.locals[${JSON.stringify(localName)}] + "` - ); - } - }); + code = code.replace( + new RegExp(replacementName, 'g'), + () => `" + ${replacementName} + "` + ); + } + + for (const replacement of icssReplacements) { + const { replacementName, importName, localName } = replacement; - return `${replacersCode}// Module\nexports.push([module.id, ${cssCode}, ""${sourceMapValue}]);\n`; + code = code.replace( + new RegExp(replacementName, 'g'), + () => `" + ${importName}.locals[${JSON.stringify(localName)}] + "` + ); + } + + return `${beforeCode}// Module\nexports.push([module.id, ${code}, ""${sourceMapValue}]);\n`; } function dashesCamelCase(str) { @@ -387,92 +309,83 @@ function getExportCode( loaderContext, exports, exportType, - replacers, localsConvention, + icssReplacements, esModule ) { - const exportItems = []; - let exportLocalsCode; + let code = ''; + let localsCode = ''; + + const addExportToLocalsCode = (name, value, index) => { + const isLastItem = index === exports.length - 1; - if (exports.length > 0) { - const exportLocals = []; - const addExportedLocal = (name, value) => { - exportLocals.push(`\t${JSON.stringify(name)}: ${JSON.stringify(value)}`); - }; + localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}${ + isLastItem ? '' : ',\n' + }`; + }; - exports.forEach((item) => { - const { name, value } = item; + for (const [index, item] of exports.entries()) { + const { name, value } = item; - switch (localsConvention) { - case 'camelCase': { - addExportedLocal(name, value); + switch (localsConvention) { + case 'camelCase': { + addExportToLocalsCode(name, value, index); - const modifiedName = camelCase(name); + const modifiedName = camelCase(name); - if (modifiedName !== name) { - addExportedLocal(modifiedName, value); - } - break; + if (modifiedName !== name) { + addExportToLocalsCode(modifiedName, value, index); } - case 'camelCaseOnly': { - addExportedLocal(camelCase(name), value); - break; - } - case 'dashes': { - addExportedLocal(name, value); + break; + } + case 'camelCaseOnly': { + addExportToLocalsCode(camelCase(name), value, index); + break; + } + case 'dashes': { + addExportToLocalsCode(name, value); - const modifiedName = dashesCamelCase(name); + const modifiedName = dashesCamelCase(name, index); - if (modifiedName !== name) { - addExportedLocal(modifiedName, value); - } - break; - } - case 'dashesOnly': { - addExportedLocal(dashesCamelCase(name), value); - break; + if (modifiedName !== name) { + addExportToLocalsCode(modifiedName, value, index); } - case 'asIs': - default: - addExportedLocal(name, value); - break; + break; } - }); - - exportLocalsCode = exportLocals.join(',\n'); + case 'dashesOnly': { + addExportToLocalsCode(dashesCamelCase(name), value, index); + break; + } + case 'asIs': + default: + addExportToLocalsCode(name, value, index); + break; + } + } - replacers.forEach((replacer) => { - if (replacer.type === 'icss-import') { - const { replacementName, importName, localName } = replacer; + for (const replacement of icssReplacements) { + const { replacementName, importName, localName } = replacement; - exportLocalsCode = exportLocalsCode.replace( - new RegExp(replacementName, 'g'), - () => - exportType === 'locals' - ? `" + ${importName}[${JSON.stringify(localName)}] + "` - : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` - ); - } - }); + localsCode = localsCode.replace(new RegExp(replacementName, 'g'), () => + exportType === 'locals' + ? `" + ${importName}[${JSON.stringify(localName)}] + "` + : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` + ); } if (exportType === 'locals') { - exportItems.push( - `${esModule ? 'export default' : 'module.exports ='} ${ - exportLocalsCode ? `{\n${exportLocalsCode}\n}` : '{}' - };` - ); + code += `${esModule ? 'export default' : 'module.exports ='} ${ + localsCode ? `{\n${localsCode}\n}` : '{}' + };\n`; } else { - if (exportLocalsCode) { - exportItems.push(`exports.locals = {\n${exportLocalsCode}\n};`); + if (localsCode) { + code += `exports.locals = {\n${localsCode}\n};\n`; } - exportItems.push( - `${esModule ? 'export default' : 'module.exports ='} exports;` - ); + code += `${esModule ? 'export default' : 'module.exports ='} exports;\n`; } - return `// Exports\n${exportItems.join('\n')}\n`; + return `// Exports\n${code}`; } export { diff --git a/test/__snapshots__/localsConvention-option.test.js.snap b/test/__snapshots__/localsConvention-option.test.js.snap index 637ddd50..4dd156e1 100644 --- a/test/__snapshots__/localsConvention-option.test.js.snap +++ b/test/__snapshots__/localsConvention-option.test.js.snap @@ -208,7 +208,8 @@ exports.locals = { \\"btnInfo_isDisabled\\": \\"erBXHZCN_thRYfCnk-aH8\\", \\"btn--info_is-disabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\", \\"btnInfo_isDisabled_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\", - \\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\" + \\"simple\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\", + }; module.exports = exports; " diff --git a/test/helpers/getModuleSource.js b/test/helpers/getModuleSource.js index fd60583b..69085202 100644 --- a/test/helpers/getModuleSource.js +++ b/test/helpers/getModuleSource.js @@ -1,6 +1,6 @@ export default (id, stats) => { const { modules } = stats.toJson({ source: true }); - const module = modules.find((m) => m.id === id); + const module = modules.find((m) => m.name === id); let { source } = module; // Todo remove after drop webpack@4 support