diff --git a/src/index.js b/src/index.js index 47ea7e1d..6a1ca3cb 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,6 @@ import { getImportCode, getModuleCode, getExportCode, - prepareCode, } from './utils'; import Warning from './Warning'; import CssSyntaxError from './CssSyntaxError'; @@ -58,19 +57,11 @@ export default function loader(content, map, meta) { // Run other loader (`postcss-loader`, `sass-loader` and etc) for importing CSS const importPrefix = getImportPrefix(this, options.importLoaders); - plugins.push( - icssParser({ - loaderContext: this, - importPrefix, - localsConvention: options.localsConvention, - }) - ); + plugins.push(icssParser()); if (options.import !== false) { plugins.push( importParser({ - loaderContext: this, - importPrefix, filter: getFilter(options.import, this.resourcePath), }) ); @@ -79,7 +70,6 @@ export default function loader(content, map, meta) { if (options.url !== false) { plugins.push( urlParser({ - loaderContext: this, filter: getFilter(options.url, this.resourcePath, (value) => isUrlRequest(value) ), @@ -92,54 +82,55 @@ export default function loader(content, map, meta) { from: this.remainingRequest.split('!').pop(), to: this.currentRequest.split('!').pop(), map: options.sourceMap - ? { - prev: map, - inline: false, - annotation: false, - } - : null, + ? { prev: map, inline: false, annotation: false } + : false, }) .then((result) => { result .warnings() .forEach((warning) => this.emitWarning(new Warning(warning))); - if (!result.messages) { - // eslint-disable-next-line no-param-reassign - result.messages = []; + const imports = []; + const exports = []; + const replacers = []; + + for (const message of result.messages) { + // eslint-disable-next-line default-case + switch (message.type) { + case 'import': + imports.push(message.value); + break; + case 'export': + exports.push(message.value); + break; + case 'replacer': + replacers.push(message.value); + break; + } } - const { onlyLocals } = options; - - const importItems = result.messages - .filter((message) => (message.type === 'import' ? message : false)) - .reduce((accumulator, currentValue) => { - accumulator.push(currentValue.import); - - return accumulator; - }, []); - const exportItems = result.messages - .filter((message) => (message.type === 'export' ? message : false)) - .reduce((accumulator, currentValue) => { - accumulator.push(currentValue.export); - - return accumulator; - }, []); - - const importCode = getImportCode(importItems, onlyLocals); - const moduleCode = getModuleCode(result, sourceMap, onlyLocals); - const exportCode = getExportCode(exportItems, onlyLocals); - const apiCode = getApiCode(this, sourceMap, onlyLocals); + const isNormalMode = !options.onlyLocals; + + const apiCode = isNormalMode ? getApiCode(this, sourceMap) : ''; + const importCode = + isNormalMode && imports.length > 0 + ? getImportCode(this, imports, { importPrefix }) + : ''; + const moduleCode = isNormalMode + ? getModuleCode(this, result, replacers, { sourceMap, importPrefix }) + : ''; + const exportCode = + exports.length > 0 + ? getExportCode(this, exports, replacers, { + importPrefix, + localsConvention: options.localsConvention, + onlyLocals: options.onlyLocals, + }) + : ''; return callback( null, - prepareCode( - { apiCode, importCode, moduleCode, exportCode }, - result.messages, - this, - importPrefix, - onlyLocals - ) + [apiCode, importCode, moduleCode, exportCode].join('') ); }) .catch((error) => { diff --git a/src/plugins/postcss-icss-parser.js b/src/plugins/postcss-icss-parser.js index 003e3326..5fc3f5dc 100644 --- a/src/plugins/postcss-icss-parser.js +++ b/src/plugins/postcss-icss-parser.js @@ -2,8 +2,6 @@ import postcss from 'postcss'; import { extractICSS, replaceValueSymbols, replaceSymbols } from 'icss-utils'; import loaderUtils from 'loader-utils'; -import { getExportItemCode, getImportItemCode } from '../utils'; - const pluginName = 'postcss-icss-parser'; function hasImportMessage(messages, url) { @@ -11,14 +9,15 @@ function hasImportMessage(messages, url) { (message) => message.pluginName === pluginName && message.type === 'import' && - message.item.url === url && - message.item.media === '' + message.value && + message.value.url === url && + message.value.media === '' ); } export default postcss.plugin( pluginName, - (options = {}) => + () => function process(css, result) { const importReplacements = Object.create(null); const { icssImports, icssExports } = extractICSS(css); @@ -29,28 +28,27 @@ export default postcss.plugin( const url = loaderUtils.parseString(importUrl); for (const token of Object.keys(icssImports[importUrl])) { + const name = `___CSS_LOADER_IMPORT___${index}___`; + index += 1; - importReplacements[token] = `___CSS_LOADER_IMPORT___${index}___`; + importReplacements[token] = name; result.messages.push({ pluginName, - type: 'icss-import', - item: { url, export: icssImports[importUrl][token], index }, + type: 'replacer', + value: { + type: 'icss-import', + name, + url, + export: icssImports[importUrl][token], + }, }); if (!hasImportMessage(result.messages, url)) { - const media = ''; - const { loaderContext, importPrefix } = options; - result.messages.push({ pluginName, type: 'import', - import: getImportItemCode( - { url, media }, - loaderContext, - importPrefix - ), - item: { url, media }, + value: { type: 'icss-import', url, media: '', name }, }); } } @@ -67,9 +65,8 @@ export default postcss.plugin( result.messages.push({ pluginName, - export: getExportItemCode(name, value, options.localsConvention), type: 'export', - item: { name, value }, + value: { name, value }, }); } } diff --git a/src/plugins/postcss-import-parser.js b/src/plugins/postcss-import-parser.js index 36bb3796..f5d04457 100644 --- a/src/plugins/postcss-import-parser.js +++ b/src/plugins/postcss-import-parser.js @@ -1,7 +1,7 @@ import postcss from 'postcss'; import valueParser from 'postcss-value-parser'; -import { uniqWith, getImportItemCode } from '../utils'; +import { uniqWith } from '../utils'; const pluginName = 'postcss-import-parser'; @@ -88,7 +88,7 @@ function walkAtRules(css, result, filter) { export default postcss.plugin( pluginName, - (options = {}) => + (options) => function process(css, result) { const traversed = walkAtRules(css, result, options.filter); const paths = uniqWith( @@ -100,11 +100,7 @@ export default postcss.plugin( result.messages.push({ pluginName, type: 'import', - import: getImportItemCode( - item, - options.loaderContext, - options.importPrefix - ), + value: { type: '@import', url: item.url, media: item.media }, }); }); } diff --git a/src/plugins/postcss-url-parser.js b/src/plugins/postcss-url-parser.js index c499cdc9..d5b33848 100644 --- a/src/plugins/postcss-url-parser.js +++ b/src/plugins/postcss-url-parser.js @@ -1,7 +1,7 @@ import postcss from 'postcss'; import valueParser from 'postcss-value-parser'; -import { uniqWith, flatten, getUrlHelperCode, getUrlItemCode } from '../utils'; +import { uniqWith, flatten } from '../utils'; const pluginName = 'postcss-url-parser'; @@ -106,7 +106,7 @@ function walkDeclsWithUrl(css, result, filter) { export default postcss.plugin( pluginName, - (options = {}) => + (options) => function process(css, result) { const traversed = walkDeclsWithUrl(css, result, options.filter); const paths = uniqWith( @@ -121,36 +121,24 @@ export default postcss.plugin( const placeholders = []; - let hasUrlHelper = false; - paths.forEach((path, index) => { - const { loaderContext } = options; - const placeholder = `___CSS_LOADER_URL___${index}___`; + const name = `___CSS_LOADER_URL___${index}___`; const { url, needQuotes } = path; - placeholders.push({ placeholder, path }); + placeholders.push({ name, path }); - if (!hasUrlHelper) { - result.messages.push({ + result.messages.push( + { pluginName, type: 'import', - import: getUrlHelperCode(loaderContext), - }); - - // eslint-disable-next-line no-param-reassign - hasUrlHelper = true; - } - - result.messages.push({ - pluginName, - type: 'import', - import: getUrlItemCode( - { url, placeholder, needQuotes }, - loaderContext - ), - importType: 'url', - placeholder, - }); + value: { type: 'url', url, name, needQuotes }, + }, + { + pluginName, + type: 'replacer', + value: { type: 'url', name }, + } + ); }); traversed.forEach((item) => { @@ -165,12 +153,12 @@ export default postcss.plugin( return; } - const { placeholder } = value; + const { name } = value; // eslint-disable-next-line no-param-reassign node.type = 'word'; // eslint-disable-next-line no-param-reassign - node.value = placeholder; + node.value = name; }); // eslint-disable-next-line no-param-reassign diff --git a/src/utils.js b/src/utils.js index 26140edf..f5d437fa 100644 --- a/src/utils.js +++ b/src/utils.js @@ -28,12 +28,6 @@ function flatten(array) { return array.reduce((a, b) => a.concat(b), []); } -function dashesCamelCase(str) { - return str.replace(/-+(\w)/g, (match, firstLetter) => - firstLetter.toUpperCase() - ); -} - function getImportPrefix(loaderContext, importLoaders) { if (importLoaders === false) { return ''; @@ -207,207 +201,182 @@ function normalizeSourceMap(map) { return newMap; } -function getImportItemCode(item, loaderContext, importPrefix) { - const { url } = item; - const media = item.media || ''; +function getApiCode(loaderContext, sourceMap) { + const url = stringifyRequest(loaderContext, require.resolve('./runtime/api')); - if (!isUrlRequest(url)) { - return `exports.push([module.id, ${JSON.stringify( - `@import url(${url});` - )}, ${JSON.stringify(media)}]);`; - } + return `exports = module.exports = require(${url})(${sourceMap});\n`; +} - const importUrl = importPrefix + urlToRequest(url); +function getImportCode(loaderContext, imports, options) { + const items = []; - return `exports.i(require(${stringifyRequest( - loaderContext, - importUrl - )}), ${JSON.stringify(media)});`; -} + let hasUrlHelperCode = false; -function getUrlHelperCode(loaderContext) { - return `var getUrl = require(${stringifyRequest( - loaderContext, - require.resolve('./runtime/getUrl.js') - )});`; -} + imports.forEach((item) => { + if (item.type === '@import' || item.type === 'icss-import') { + const url = !isUrlRequest(item.url) + ? JSON.stringify(`@import url(${item.url});`) + : stringifyRequest( + loaderContext, + options.importPrefix + urlToRequest(item.url) + ); + const media = JSON.stringify(item.media); -function getUrlItemCode(item, loaderContext) { - const { url, placeholder, needQuotes } = item; + if (!isUrlRequest(item.url)) { + items.push(`exports.push([module.id, ${url}, ${media}]);`); - // Remove `#hash` and `?#hash` from `require` - const [normalizedUrl, singleQuery, hashValue] = url.split(/(\?)?#/); - const hash = - singleQuery || hashValue - ? `"${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}"` - : ''; + return; + } - const options = []; + items.push(`exports.i(require(${url}), ${media});`); + } - if (hash) { - options.push(`hash: ${hash}`); - } + if (item.type === 'url') { + if (!hasUrlHelperCode) { + const pathToGetUrl = require.resolve('./runtime/getUrl.js'); + const url = stringifyRequest(loaderContext, pathToGetUrl); - if (needQuotes) { - options.push(`needQuotes: true`); - } + items.push(`var getUrl = require(${url});`); - const preparedOptions = - options.length > 0 ? `, { ${options.join(', ')} }` : ''; + hasUrlHelperCode = true; + } - return `var ${placeholder} = getUrl(require(${stringifyRequest( - loaderContext, - urlToRequest(normalizedUrl) - )})${preparedOptions});`; -} + const { url, name, needQuotes } = item; + const [normalizedUrl, singleQuery, hashValue] = url.split(/(\?)?#/); + const hash = + singleQuery || hashValue + ? `"${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}"` + : ''; -function getApiCode(loaderContext, sourceMap, onlyLocals) { - if (onlyLocals) { - return ''; - } + const getUrlOptions = []; - return `exports = module.exports = require(${stringifyRequest( - loaderContext, - require.resolve('./runtime/api') - )})(${sourceMap});\n`; -} + if (hash) { + getUrlOptions.push(`hash: ${hash}`); + } -function getImportCode(importItems, onlyLocals) { - if (importItems.length === 0 || onlyLocals) { - return ''; - } + if (needQuotes) { + getUrlOptions.push(`needQuotes: true`); + } - return `// Imports\n${importItems.join('\n')}\n`; -} + const preparedUrl = stringifyRequest( + loaderContext, + urlToRequest(normalizedUrl) + ); + const preparedOptions = + getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''; -function getModuleCode(result, sourceMap, onlyLocals) { - if (onlyLocals) { - return ''; - } + items.push( + `var ${name} = getUrl(require(${preparedUrl})${preparedOptions});` + ); + } + }); - return `// Module\nexports.push([module.id, ${JSON.stringify( - result.css - )}, ""${sourceMap && result.map ? `,${result.map}` : ''}]);\n`; + return `// Imports\n${items.join('\n')}\n`; } -function getExportItemCode(key, value, localsConvention) { - let targetKey; - const items = []; +function getModuleCode(loaderContext, result, replacers, options) { + const { css, map } = result; + const sourceMapValue = options.sourceMap && map ? `,${map}` : ''; + let cssCode = JSON.stringify(css); - function addEntry(k) { - items.push(`\t${JSON.stringify(k)}: ${JSON.stringify(value)}`); - } + replacers.forEach((replacer) => { + const { type, name } = replacer; - switch (localsConvention) { - case 'camelCase': - addEntry(key); - targetKey = camelCase(key); + if (type === 'url') { + cssCode = cssCode.replace(new RegExp(name, 'g'), () => `" + ${name} + "`); + } - if (targetKey !== key) { - addEntry(targetKey); - } - break; - case 'camelCaseOnly': - addEntry(camelCase(key)); - break; - case 'dashes': - addEntry(key); - targetKey = dashesCamelCase(key); - - if (targetKey !== key) { - addEntry(targetKey); - } - break; - case 'dashesOnly': - addEntry(dashesCamelCase(key)); - break; - case 'asIs': - default: - addEntry(key); - break; - } + if (type === 'icss-import') { + const url = stringifyRequest( + loaderContext, + options.importPrefix + urlToRequest(replacer.url) + ); + const exportName = JSON.stringify(replacer.export); + + // eslint-disable-next-line no-param-reassign + cssCode = cssCode.replace( + new RegExp(replacer.name, 'g'), + `" + require(${url}).locals[${exportName}] + "` + ); + } + }); - return items.join(',\n'); + return `// Module\nexports.push([module.id, ${cssCode}, ""${sourceMapValue}]);\n`; } -function getExportCode(exportItems, onlyLocals) { - if (exportItems.length === 0) { - return ''; - } - - return `// Exports\n${ - onlyLocals ? 'module.exports' : 'exports.locals' - } = {\n${exportItems.join(',\n')}\n};`; +function dashesCamelCase(str) { + return str.replace(/-+(\w)/g, (match, firstLetter) => + firstLetter.toUpperCase() + ); } -function getIcssReplacer(item, loaderContext, importPrefix, onlyLocals) { - const importUrl = importPrefix + urlToRequest(item.url); +function getExportCode(loaderContext, exports, replacers, options) { + const items = []; - return () => - onlyLocals - ? `" + require(${stringifyRequest( - loaderContext, - importUrl - )})[${JSON.stringify(item.export)}] + "` - : `" + require(${stringifyRequest( - loaderContext, - importUrl - )}).locals[${JSON.stringify(item.export)}] + "`; -} + function addExportedItem(name, value) { + items.push(`\t${JSON.stringify(name)}: ${JSON.stringify(value)}`); + } -function prepareCode(file, messages, loaderContext, importPrefix, onlyLocals) { - const { apiCode, importCode } = file; - let { moduleCode, exportCode } = file; + exports.forEach((item) => { + const { name, value } = item; - messages - .filter( - (message) => - message.type === 'icss-import' || - (message.type === 'import' && message.importType === 'url') - ) - .forEach((message) => { - // Replace all urls on `require` - if (message.type === 'import') { - const { placeholder } = message; - - if (moduleCode) { - // eslint-disable-next-line no-param-reassign - moduleCode = moduleCode.replace( - new RegExp(placeholder, 'g'), - () => `" + ${placeholder} + "` - ); - } - } + switch (options.localsConvention) { + case 'camelCase': { + addExportedItem(name, value); - // Replace external ICSS import on `require` - if (message.type === 'icss-import') { - const { item } = message; - const replacer = getIcssReplacer( - item, - loaderContext, - importPrefix, - onlyLocals - ); + const modifiedName = camelCase(name); - if (moduleCode) { - // eslint-disable-next-line no-param-reassign - moduleCode = moduleCode.replace( - new RegExp(`___CSS_LOADER_IMPORT___(${item.index})___`, 'g'), - replacer - ); + if (modifiedName !== name) { + addExportedItem(modifiedName, value); } + break; + } + case 'camelCaseOnly': { + addExportedItem(camelCase(name), value); + break; + } + case 'dashes': { + addExportedItem(name, value); - if (exportCode) { - // eslint-disable-next-line no-param-reassign - exportCode = exportCode.replace( - new RegExp(`___CSS_LOADER_IMPORT___(${item.index})___`, 'g'), - replacer - ); + const modifiedName = dashesCamelCase(name); + + if (modifiedName !== name) { + addExportedItem(modifiedName, value); } + break; + } + case 'dashesOnly': { + addExportedItem(dashesCamelCase(name), value); + break; } - }); + case 'asIs': + default: + addExportedItem(name, value); + break; + } + }); + + const exportType = options.onlyLocals ? 'module.exports' : 'exports.locals'; + let exportCode = `// Exports\n${exportType} = {\n${items.join(',\n')}\n};`; + + replacers.forEach((replacer) => { + const { type } = replacer; + + if (type === 'icss-import') { + const importUrl = options.importPrefix + urlToRequest(replacer.url); + + exportCode = exportCode.replace(new RegExp(replacer.name, 'g'), () => { + const url = stringifyRequest(loaderContext, importUrl); + const importName = JSON.stringify(replacer.export); + + return options.onlyLocals + ? `" + require(${url})[${importName}] + "` + : `" + require(${url}).locals[${importName}] + "`; + }); + } + }); - return [apiCode, importCode, moduleCode, exportCode].filter(Boolean).join(''); + return exportCode; } export { @@ -419,13 +388,8 @@ export { getFilter, getModulesPlugins, normalizeSourceMap, - getImportItemCode, - getUrlHelperCode, - getUrlItemCode, getApiCode, getImportCode, getModuleCode, - getExportItemCode, getExportCode, - prepareCode, };