diff --git a/lib/loader.js b/lib/loader.js index 60a953eb..a2338012 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -7,13 +7,19 @@ const localByDefault = require('postcss-modules-local-by-default'); const extractImports = require('postcss-modules-extract-imports'); const modulesScope = require('postcss-modules-scope'); const modulesValues = require('postcss-modules-values'); -const loaderUtils = require('loader-utils'); +const { + getOptions, + isUrlRequest, + urlToRequest, + getRemainingRequest, + getCurrentRequest, + stringifyRequest, +} = require('loader-utils'); const { importParser, icssParser, urlParser } = require('./plugins'); const { getLocalIdent, getImportPrefix, - placeholderImportItemReplacer, compileExports, placholderRegExps, } = require('./utils'); @@ -22,7 +28,7 @@ const CssSyntaxError = require('./CssSyntaxError'); module.exports = function loader(content, map) { const callback = this.async(); - const options = loaderUtils.getOptions(this) || {}; + const options = getOptions(this) || {}; const sourceMap = options.sourceMap || false; /* eslint-disable no-param-reassign */ @@ -43,40 +49,38 @@ module.exports = function loader(content, map) { } /* eslint-enable no-param-reassign */ + const parserOptions = {}; + const resolveImport = options.import !== false; + const resolveUrl = options.url !== false; const loaderContext = this; - const localIdentName = options.localIdentName || '[hash:base64]'; - const customGetLocalIdent = options.getLocalIdent || getLocalIdent; - - const parserOptions = { - url: options.url !== false, - import: options.import !== false, - }; const plugins = [ modulesValues, localByDefault({ mode: options.modules ? 'local' : 'global', rewriteUrl(global, url) { - if (parserOptions.url) { + if (resolveUrl) { // eslint-disable-next-line no-param-reassign url = url.trim(); - if ( - !url.replace(/\s/g, '').length || - !loaderUtils.isUrlRequest(url) - ) { + if (!url.replace(/\s/g, '').length || !isUrlRequest(url)) { return url; } + if (global) { - return loaderUtils.urlToRequest(url); + return urlToRequest(url); } } + return url; }, }), extractImports(), modulesScope({ generateScopedName: function generateScopedName(exportName) { + const localIdentName = options.localIdentName || '[hash:base64]'; + const customGetLocalIdent = options.getLocalIdent || getLocalIdent; + return customGetLocalIdent(loaderContext, localIdentName, exportName, { regExp: options.localIdentRegExp, hashPrefix: options.hashPrefix || '', @@ -86,11 +90,11 @@ module.exports = function loader(content, map) { }), ]; - if (options.import !== false) { + if (resolveImport) { plugins.push(importParser(parserOptions)); } - if (options.url !== false) { + if (resolveUrl) { plugins.push(urlParser(parserOptions)); } @@ -99,12 +103,10 @@ module.exports = function loader(content, map) { postcss(plugins) .process(content, { // we need a prefix to avoid path rewriting of PostCSS - from: `/css-loader!${loaderUtils - .getRemainingRequest(this) + from: `/css-loader!${getRemainingRequest(this) .split('!') .pop()}`, - to: loaderUtils - .getCurrentRequest(this) + to: getCurrentRequest(this) .split('!') .pop(), map: options.sourceMap @@ -121,46 +123,59 @@ module.exports = function loader(content, map) { .warnings() .forEach((warning) => this.emitWarning(new Warning(warning))); - // for importing CSS - const importUrlPrefix = getImportPrefix(this, options); + const { camelCase, exportOnlyLocals, importLoaders } = options; + const { importItems, urlItems, exports } = parserOptions; + // Run other loader (`postcss-loader`, `sass-loader` and etc) for importing CSS + const importUrlPrefix = getImportPrefix(this, importLoaders); + // Prepare replacer to change from `___CSS_LOADER_IMPORT___INDEX___` to `require('./file.css').locals` + const importItemReplacer = (item) => { + const match = placholderRegExps.importItem.exec(item); + const idx = +match[1]; + const importItem = importItems[idx]; + const importUrl = importUrlPrefix + importItem.url; + + if (exportOnlyLocals) { + return `" + require(${stringifyRequest( + this, + importUrl + )})[${JSON.stringify(importItem.export)}] + "`; + } - let exportJs = compileExports( - parserOptions.exports, - placeholderImportItemReplacer( + return `" + require(${stringifyRequest( this, - parserOptions.importItems, - importUrlPrefix, - options.exportOnlyLocals - ), - options.camelCase - ); + importUrl + )}).locals[${JSON.stringify(importItem.export)}] + "`; + }; - if (options.exportOnlyLocals) { - if (exportJs) { - exportJs = `module.exports = ${exportJs};`; - } + let exportCode = compileExports(exports, camelCase, (valueAsString) => + valueAsString.replace(placholderRegExps.importItemG, importItemReplacer) + ); - return callback(null, exportJs); + if (exportOnlyLocals) { + return callback( + null, + exportCode ? `module.exports = ${exportCode};` : exportCode + ); } - let cssAsString = JSON.stringify(result.css); - const alreadyImported = {}; - const importJs = parserOptions.importItems + const importCode = importItems .filter((imp) => { if (!imp.media) { if (alreadyImported[imp.url]) { return false; } + alreadyImported[imp.url] = true; } + return true; }) .map((imp) => { const { url } = imp; const media = imp.media || ''; - if (!loaderUtils.isUrlRequest(url)) { + if (!isUrlRequest(url)) { return `exports.push([module.id, ${JSON.stringify( `@import url(${url});` )}, ${JSON.stringify(media)}]);`; @@ -168,31 +183,23 @@ module.exports = function loader(content, map) { const importUrl = importUrlPrefix + url; - return `exports.i(require(${loaderUtils.stringifyRequest( + return `exports.i(require(${stringifyRequest( this, importUrl )}), ${JSON.stringify(media)});`; }, this) .join('\n'); - cssAsString = cssAsString.replace( + let cssAsString = JSON.stringify(result.css).replace( placholderRegExps.importItemG, - placeholderImportItemReplacer( - this, - parserOptions.importItems, - importUrlPrefix - ) + importItemReplacer ); // helper for ensuring valid CSS strings from requires - let urlEscapeHelper = ''; - - if ( - options.url !== false && - parserOptions.urlItems && - parserOptions.urlItems.length > 0 - ) { - urlEscapeHelper = `var escape = require(${loaderUtils.stringifyRequest( + let urlEscapeHelperCode = ''; + + if (resolveUrl && urlItems && urlItems.length > 0) { + urlEscapeHelperCode = `var escape = require(${stringifyRequest( this, require.resolve('./runtime/escape.js') )});\n`; @@ -202,7 +209,7 @@ module.exports = function loader(content, map) { (item) => { const match = placholderRegExps.urlItem.exec(item); let idx = +match[1]; - const urlItem = parserOptions.urlItems[idx]; + const urlItem = urlItems[idx]; const { url } = urlItem; idx = url.indexOf('?#'); @@ -217,7 +224,7 @@ module.exports = function loader(content, map) { // idx === 0 is catched by isUrlRequest // in cases like url('webfont.eot?#iefix') urlRequest = url.substr(0, idx); - return `" + escape(require(${loaderUtils.stringifyRequest( + return `" + escape(require(${stringifyRequest( this, urlRequest )}) + "${url.substr(idx)}") + "`; @@ -225,7 +232,7 @@ module.exports = function loader(content, map) { urlRequest = url; - return `" + escape(require(${loaderUtils.stringifyRequest( + return `" + escape(require(${stringifyRequest( this, urlRequest )})) + "`; @@ -233,18 +240,18 @@ module.exports = function loader(content, map) { ); } - if (exportJs) { - exportJs = `exports.locals = ${exportJs};`; + if (exportCode) { + exportCode = `exports.locals = ${exportCode};`; } - let moduleJs; - if (sourceMap && result.map) { - /* eslint-disable no-param-reassign */ + let newMap = result.map; + + if (sourceMap && newMap) { // Add a SourceMap - map = result.map.toJSON(); + newMap = newMap.toJSON(); - if (map.sources) { - map.sources = map.sources.map( + if (newMap.sources) { + newMap.sources = newMap.sources.map( (source) => source .split('!') @@ -252,31 +259,31 @@ module.exports = function loader(content, map) { .replace(/\\/g, '/'), this ); - map.sourceRoot = ''; + newMap.sourceRoot = ''; } - map.file = map.file + newMap.file = newMap.file .split('!') .pop() .replace(/\\/g, '/'); - map = JSON.stringify(map); - /* eslint-enable no-param-reassign */ - - moduleJs = `exports.push([module.id, ${cssAsString}, "", ${map}]);`; - } else { - moduleJs = `exports.push([module.id, ${cssAsString}, ""]);`; + newMap = JSON.stringify(newMap); } + const runtimeCode = `exports = module.exports = require(${stringifyRequest( + this, + require.resolve('./runtime/api') + )})(${!!sourceMap});`; + const moduleCode = `exports.push([module.id, ${cssAsString}, ""${ + newMap ? `,${newMap}` : '' + }]);`; + // Embed runtime return callback( null, - `${urlEscapeHelper}exports = module.exports = require(${loaderUtils.stringifyRequest( - this, - require.resolve('./runtime/api.js') - )})(${sourceMap});\n` + - `// imports\n${importJs}\n\n` + - `// module\n${moduleJs}\n\n` + - `// exports\n${exportJs}` + `${urlEscapeHelperCode}${runtimeCode}\n` + + `// imports\n${importCode}\n\n` + + `// module\n${moduleCode}\n\n` + + `// exports\n${exportCode}` ); }) .catch((error) => { diff --git a/lib/utils.js b/lib/utils.js index 7c617d71..01808ef9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -14,13 +14,30 @@ const placholderRegExps = { urlItem: /___CSS_LOADER_URL___([0-9]+)___/, }; +function getImportPrefix(loaderContext, importLoaders) { + if (importLoaders === false) { + return ''; + } + + const numberImportedLoaders = parseInt(importLoaders, 10) || 0; + const loadersRequest = loaderContext.loaders + .slice( + loaderContext.loaderIndex, + loaderContext.loaderIndex + 1 + numberImportedLoaders + ) + .map((x) => x.request) + .join('!'); + + return `-!${loadersRequest}!`; +} + function dashesCamelCase(str) { return str.replace(/-+(\w)/g, (match, firstLetter) => firstLetter.toUpperCase() ); } -function compileExports(exports, importItemMatcher, camelCaseKeys) { +function compileExports(exports, camelCaseKeys, valueHandler) { if (!exports || Object.keys(exports).length === 0) { return ''; } @@ -29,10 +46,7 @@ function compileExports(exports, importItemMatcher, camelCaseKeys) { .reduce((res, key) => { let valueAsString = JSON.stringify(exports[key]); - valueAsString = valueAsString.replace( - placholderRegExps.importItemG, - importItemMatcher - ); + valueAsString = valueHandler(valueAsString); function addEntry(k) { res.push(`\t${JSON.stringify(k)}: ${valueAsString}`); @@ -75,22 +89,6 @@ function compileExports(exports, importItemMatcher, camelCaseKeys) { return `{\n${exportJs}\n}`; } -function getImportPrefix(loaderContext, query) { - if (query.importLoaders === false) { - return ''; - } - - const importLoaders = parseInt(query.importLoaders, 10) || 0; - const loadersRequest = loaderContext.loaders - .slice( - loaderContext.loaderIndex, - loaderContext.loaderIndex + 1 + importLoaders - ) - .map((x) => x.request) - .join('!'); - return `-!${loadersRequest}!`; -} - function getLocalIdent(loaderContext, localIdentName, localName, options) { if (!options.context) { if (loaderContext.rootContext) { @@ -107,53 +105,29 @@ function getLocalIdent(loaderContext, localIdentName, localName, options) { options.context = loaderContext.context; } } + const request = path.relative(options.context, loaderContext.resourcePath); + // eslint-disable-next-line no-param-reassign options.content = `${options.hashPrefix + request.replace(/\\/g, '/')}+${localName}`; // eslint-disable-next-line no-param-reassign localIdentName = localIdentName.replace(/\[local\]/gi, localName); + const hash = loaderUtils.interpolateName( loaderContext, localIdentName, options ); + return hash .replace(new RegExp('[^a-zA-Z0-9\\-_\u00A0-\uFFFF]', 'g'), '-') .replace(/^((-?[0-9])|--)/, '_$1'); } -function placeholderImportItemReplacer( - loaderContext, - importItems, - importUrlPrefix, - onlyLocals = false -) { - return (item) => { - const match = placholderRegExps.importItem.exec(item); - const idx = +match[1]; - const importItem = importItems[idx]; - const importUrl = importUrlPrefix + importItem.url; - - if (onlyLocals) { - return `" + require(${loaderUtils.stringifyRequest( - loaderContext, - importUrl - )})[${JSON.stringify(importItem.export)}] + "`; - } - - return `" + require(${loaderUtils.stringifyRequest( - loaderContext, - importUrl - )}).locals[${JSON.stringify(importItem.export)}] + "`; - }; -} - module.exports = { - dashesCamelCase, compileExports, getImportPrefix, getLocalIdent, - placeholderImportItemReplacer, placholderRegExps, };