diff --git a/src/index.js b/src/index.js index 7a30b7dd..d9390259 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,6 @@ import { isUrlRequest, getRemainingRequest, getCurrentRequest, - stringifyRequest, } from 'loader-utils'; import schema from './options.json'; @@ -19,12 +18,13 @@ import { importParser, icssParser, urlParser } from './plugins'; import { normalizeSourceMap, getModulesPlugins, - placholderRegExps, getImportPrefix, - getImportItemReplacer, + getIcssItemReplacer, getFilter, - getExports, - getImports, + getRuntimeCode, + getImportCode, + getModuleCode, + getExportCode, } from './utils'; import Warning from './Warning'; import CssSyntaxError from './CssSyntaxError'; @@ -103,71 +103,37 @@ export default function loader(content, map, meta) { .warnings() .forEach((warning) => this.emitWarning(new Warning(warning))); - const messages = result.messages || []; - const { exportOnlyLocals, importLoaders, exportLocalsStyle } = options; + if (!result.messages) { + // eslint-disable-next-line no-param-reassign + result.messages = []; + } + const { + exportOnlyLocals: onlyLocals, + exportLocalsStyle: localsStyle, + } = options; // Run other loader (`postcss-loader`, `sass-loader` and etc) for importing CSS - const importPrefix = getImportPrefix(this, importLoaders); - + const importPrefix = getImportPrefix(this, options.importLoaders); // Prepare replacer to change from `___CSS_LOADER_IMPORT___INDEX___` to `require('./file.css').locals` - const importItemReplacer = getImportItemReplacer( - messages, + const replacer = getIcssItemReplacer( + result, this, importPrefix, - exportOnlyLocals - ); - - const exportItems = getExports( - messages, - exportLocalsStyle, - importItemReplacer - ); - - const exportsCode = - exportItems.length > 0 - ? exportOnlyLocals - ? `module.exports = {\n${exportItems.join(',\n')}\n};` - : `// Exports\nexports.locals = {\n${exportItems.join(',\n')}\n};` - : ''; - - if (exportOnlyLocals) { - return callback(null, exportsCode); - } - - let cssAsString = JSON.stringify(result.css).replace( - placholderRegExps.importItemG, - importItemReplacer + onlyLocals ); - const importItems = getImports( - messages, + // eslint-disable-next-line no-param-reassign + result.cssLoaderBuildInfo = { + onlyLocals, + localsStyle, importPrefix, - this, - (message) => { - if (message.type !== 'url') { - return; - } + replacer, + }; - const { placeholder } = message.item; - - cssAsString = cssAsString.replace( - new RegExp(placeholder, 'g'), - () => `" + ${placeholder} + "` - ); - } - ); - - const runtimeCode = `exports = module.exports = require(${stringifyRequest( - this, - require.resolve('./runtime/api') - )})(${!!sourceMap});\n`; - const importCode = - importItems.length > 0 - ? `// Imports\n${importItems.join('\n')}\n\n` - : ''; - const moduleCode = `// Module\nexports.push([module.id, ${cssAsString}, ""${ - result.map ? `,${result.map}` : '' - }]);\n\n`; + const runtimeCode = getRuntimeCode(result, this, sourceMap); + const importCode = getImportCode(result, this); + const moduleCode = getModuleCode(result); + const exportsCode = getExportCode(result); return callback( null, diff --git a/src/utils.js b/src/utils.js index e9dcba97..dae63830 100644 --- a/src/utils.js +++ b/src/utils.js @@ -113,12 +113,77 @@ function getFilter(filter, resourcePath, defaultFilter = null) { }; } -function getImportItemReplacer( - messages, - loaderContext, - importUrlPrefix, - exportOnlyLocals -) { +function getModulesPlugins(options, loaderContext) { + let modulesOptions = { + mode: 'local', + localIdentName: '[hash:base64]', + getLocalIdent, + context: null, + hashPrefix: '', + localIdentRegExp: null, + }; + + if ( + typeof options.modules === 'boolean' || + typeof options.modules === 'string' + ) { + modulesOptions.mode = + typeof options.modules === 'string' ? options.modules : 'local'; + } else { + modulesOptions = Object.assign({}, modulesOptions, options.modules); + } + + return [ + modulesValues, + localByDefault({ mode: modulesOptions.mode }), + extractImports(), + modulesScope({ + generateScopedName: function generateScopedName(exportName) { + return modulesOptions.getLocalIdent( + loaderContext, + modulesOptions.localIdentName, + exportName, + { + context: modulesOptions.context, + hashPrefix: modulesOptions.hashPrefix, + regExp: modulesOptions.localIdentRegExp, + } + ); + }, + }), + ]; +} + +function normalizeSourceMap(map) { + let newMap = map; + + // Some loader emit source map as string + // Strip any JSON XSSI avoidance prefix from the string (as documented in the source maps specification), and then parse the string as JSON. + if (typeof newMap === 'string') { + newMap = JSON.parse(newMap.replace(/^\)]}'[^\n]*\n/, '')); + } + + // Source maps should use forward slash because it is URLs (https://github.com/mozilla/source-map/issues/91) + // We should normalize path because previous loaders like `sass-loader` using backslash when generate source map + + if (newMap.file) { + newMap.file = normalizePath(newMap.file); + } + + if (newMap.sourceRoot) { + newMap.sourceRoot = normalizePath(newMap.sourceRoot); + } + + if (newMap.sources) { + newMap.sources = newMap.sources.map((source) => normalizePath(source)); + } + + return newMap; +} + +function getIcssItemReplacer(result, loaderContext, importPrefix, onlyLocals) { + const { messages } = result; + return function replacer(placeholder) { const match = placholderRegExps.importItem.exec(placeholder); const idx = Number(match[1]); @@ -136,9 +201,9 @@ function getImportItemReplacer( } const { item } = message; - const importUrl = importUrlPrefix + urlToRequest(item.url); + const importUrl = importPrefix + urlToRequest(item.url); - if (exportOnlyLocals) { + if (onlyLocals) { return `" + require(${stringifyRequest( loaderContext, importUrl @@ -152,9 +217,139 @@ function getImportItemReplacer( }; } -function getExports(messages, exportLocalsStyle, importItemReplacer) { - return messages - .filter((message) => message.type === 'export') +function getRuntimeCode(result, loaderContext, sourceMap) { + const { cssLoaderBuildInfo } = result; + const { onlyLocals } = cssLoaderBuildInfo; + + if (onlyLocals) { + return ''; + } + + return `exports = module.exports = require(${stringifyRequest( + loaderContext, + require.resolve('./runtime/api') + )})(${!!sourceMap});\n`; +} + +function getImportCode(result, loaderContext) { + const { cssLoaderBuildInfo, messages } = result; + const { importPrefix, onlyLocals } = cssLoaderBuildInfo; + + if (onlyLocals) { + return ''; + } + + const importItems = []; + + // Helper for getting url + let hasUrlHelper = false; + + messages + .filter( + (message) => + message.pluginName === 'postcss-url-parser' || + message.pluginName === 'postcss-import-parser' || + message.pluginName === 'postcss-icss-parser' + ) + .forEach((message) => { + if (message.type === 'import') { + const { url } = message.item; + const media = message.item.media || ''; + + if (!isUrlRequest(url)) { + importItems.push( + `exports.push([module.id, ${JSON.stringify( + `@import url(${url});` + )}, ${JSON.stringify(media)}]);` + ); + } else { + const importUrl = importPrefix + urlToRequest(url); + + importItems.push( + `exports.i(require(${stringifyRequest( + loaderContext, + importUrl + )}), ${JSON.stringify(media)});` + ); + } + } + + if (message.type === 'url') { + if (!hasUrlHelper) { + importItems.push( + `var getUrl = require(${stringifyRequest( + loaderContext, + require.resolve('./runtime/get-url.js') + )});` + ); + + hasUrlHelper = true; + } + + const { url, placeholder, needQuotes } = message.item; + // Remove `#hash` and `?#hash` from `require` + const [normalizedUrl, singleQuery, hashValue] = url.split(/(\?)?#/); + const hash = + singleQuery || hashValue + ? `"${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}"` + : ''; + + importItems.push( + `var ${placeholder} = getUrl(require(${stringifyRequest( + loaderContext, + urlToRequest(normalizedUrl) + )})${hash ? ` + ${hash}` : ''}${needQuotes ? ', true' : ''});` + ); + } + }); + + return importItems.length > 0 + ? `// Imports\n${importItems.join('\n')}\n\n` + : ''; +} + +function getModuleCode(result) { + const { cssLoaderBuildInfo, css, messages, map } = result; + const { replacer, onlyLocals } = cssLoaderBuildInfo; + + if (onlyLocals) { + return ''; + } + + let cssAsString = JSON.stringify(css).replace( + placholderRegExps.importItemG, + replacer + ); + + messages + .filter( + (message) => + message.pluginName === 'postcss-url-parser' && message.type === 'url' + ) + .forEach((message) => { + const { placeholder } = message.item; + + cssAsString = cssAsString.replace( + new RegExp(placeholder, 'g'), + () => `" + ${placeholder} + "` + ); + }); + + return `// Module\nexports.push([module.id, ${cssAsString}, ""${ + map ? `,${map}` : '' + }]);\n\n`; +} + +function getExportCode(result) { + const { messages, cssLoaderBuildInfo } = result; + const { replacer, localsStyle, onlyLocals } = cssLoaderBuildInfo; + + const exportItems = messages + .filter( + (message) => + message.pluginName === 'postcss-icss-parser' && + message.type === 'export' + ) .reduce((accumulator, message) => { const { key, value } = message.item; @@ -162,7 +357,7 @@ function getExports(messages, exportLocalsStyle, importItemReplacer) { valueAsString = valueAsString.replace( placholderRegExps.importItemG, - importItemReplacer + replacer ); function addEntry(k) { @@ -171,7 +366,7 @@ function getExports(messages, exportLocalsStyle, importItemReplacer) { let targetKey; - switch (exportLocalsStyle) { + switch (localsStyle) { case 'camelCase': addEntry(key); targetKey = camelCase(key); @@ -202,137 +397,12 @@ function getExports(messages, exportLocalsStyle, importItemReplacer) { return accumulator; }, []); -} - -function getImports(messages, importUrlPrefix, loaderContext, callback) { - const imports = []; - - // Helper for getting url - let hasUrlHelper = false; - - messages.forEach((message) => { - if (message.type === 'import') { - const { url } = message.item; - const media = message.item.media || ''; - - if (!isUrlRequest(url)) { - imports.push( - `exports.push([module.id, ${JSON.stringify( - `@import url(${url});` - )}, ${JSON.stringify(media)}]);` - ); - } else { - const importUrl = importUrlPrefix + urlToRequest(url); - - imports.push( - `exports.i(require(${stringifyRequest( - loaderContext, - importUrl - )}), ${JSON.stringify(media)});` - ); - } - } - - if (message.type === 'url') { - if (!hasUrlHelper) { - imports.push( - `var getUrl = require(${stringifyRequest( - loaderContext, - require.resolve('./runtime/get-url.js') - )});` - ); - - hasUrlHelper = true; - } - const { url, placeholder, needQuotes } = message.item; - // Remove `#hash` and `?#hash` from `require` - const [normalizedUrl, singleQuery, hashValue] = url.split(/(\?)?#/); - const hash = - singleQuery || hashValue - ? `"${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}"` - : ''; - - imports.push( - `var ${placeholder} = getUrl(require(${stringifyRequest( - loaderContext, - urlToRequest(normalizedUrl) - )})${hash ? ` + ${hash}` : ''}${needQuotes ? ', true' : ''});` - ); - } - - callback(message); - }); - - return imports; -} - -function getModulesPlugins(options, loaderContext) { - let modulesOptions = { - mode: 'local', - localIdentName: '[hash:base64]', - getLocalIdent, - context: null, - hashPrefix: '', - localIdentRegExp: null, - }; - - if ( - typeof options.modules === 'boolean' || - typeof options.modules === 'string' - ) { - modulesOptions.mode = - typeof options.modules === 'string' ? options.modules : 'local'; - } else { - modulesOptions = Object.assign({}, modulesOptions, options.modules); - } - - return [ - modulesValues, - localByDefault({ mode: modulesOptions.mode }), - extractImports(), - modulesScope({ - generateScopedName: function generateScopedName(exportName) { - return modulesOptions.getLocalIdent( - loaderContext, - modulesOptions.localIdentName, - exportName, - { - context: modulesOptions.context, - hashPrefix: modulesOptions.hashPrefix, - regExp: modulesOptions.localIdentRegExp, - } - ); - }, - }), - ]; -} - -function normalizeSourceMap(map) { - let newMap = map; - - // Some loader emit source map as string - // Strip any JSON XSSI avoidance prefix from the string (as documented in the source maps specification), and then parse the string as JSON. - if (typeof newMap === 'string') { - newMap = JSON.parse(newMap.replace(/^\)]}'[^\n]*\n/, '')); - } - - // Source maps should use forward slash because it is URLs (https://github.com/mozilla/source-map/issues/91) - // We should normalize path because previous loaders like `sass-loader` using backslash when generate source map - - if (newMap.file) { - newMap.file = normalizePath(newMap.file); - } - - if (newMap.sourceRoot) { - newMap.sourceRoot = normalizePath(newMap.sourceRoot); - } - - if (newMap.sources) { - newMap.sources = newMap.sources.map((source) => normalizePath(source)); - } - - return newMap; + return exportItems.length > 0 + ? onlyLocals + ? `module.exports = {\n${exportItems.join(',\n')}\n};` + : `// Exports\nexports.locals = {\n${exportItems.join(',\n')}\n};` + : ''; } export { @@ -340,9 +410,11 @@ export { getLocalIdent, placholderRegExps, getFilter, - getImportItemReplacer, - getExports, - getImports, + getIcssItemReplacer, getModulesPlugins, normalizeSourceMap, + getRuntimeCode, + getImportCode, + getModuleCode, + getExportCode, };