diff --git a/lib/loader.js b/lib/loader.js index 262f751e..60a953eb 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -2,14 +2,23 @@ MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ +const postcss = require('postcss'); +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 processCss = require('./processCss'); +const { importParser, icssParser, urlParser } = require('./plugins'); const { + getLocalIdent, getImportPrefix, placeholderImportItemReplacer, compileExports, + placholderRegExps, } = require('./utils'); +const Warning = require('./Warning'); +const CssSyntaxError = require('./CssSyntaxError'); module.exports = function loader(content, map) { const callback = this.async(); @@ -34,27 +43,92 @@ module.exports = function loader(content, map) { } /* eslint-enable no-param-reassign */ - processCss( - content, - map, - { - loaderContext: this, - loaderOptions: options, - sourceMap, - }, - (err, result) => { - if (err) { - return callback(err); - } + 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) { + // eslint-disable-next-line no-param-reassign + url = url.trim(); + + if ( + !url.replace(/\s/g, '').length || + !loaderUtils.isUrlRequest(url) + ) { + return url; + } + if (global) { + return loaderUtils.urlToRequest(url); + } + } + return url; + }, + }), + extractImports(), + modulesScope({ + generateScopedName: function generateScopedName(exportName) { + return customGetLocalIdent(loaderContext, localIdentName, exportName, { + regExp: options.localIdentRegExp, + hashPrefix: options.hashPrefix || '', + context: options.context, + }); + }, + }), + ]; + + if (options.import !== false) { + plugins.push(importParser(parserOptions)); + } + + if (options.url !== false) { + plugins.push(urlParser(parserOptions)); + } + + plugins.push(icssParser(parserOptions)); + + postcss(plugins) + .process(content, { + // we need a prefix to avoid path rewriting of PostCSS + from: `/css-loader!${loaderUtils + .getRemainingRequest(this) + .split('!') + .pop()}`, + to: loaderUtils + .getCurrentRequest(this) + .split('!') + .pop(), + map: options.sourceMap + ? { + prev: map, + sourcesContent: true, + inline: false, + annotation: false, + } + : null, + }) + .then((result) => { + result + .warnings() + .forEach((warning) => this.emitWarning(new Warning(warning))); // for importing CSS const importUrlPrefix = getImportPrefix(this, options); let exportJs = compileExports( - result, + parserOptions.exports, placeholderImportItemReplacer( this, - result, + parserOptions.importItems, importUrlPrefix, options.exportOnlyLocals ), @@ -69,10 +143,10 @@ module.exports = function loader(content, map) { return callback(null, exportJs); } - let cssAsString = JSON.stringify(result.source); + let cssAsString = JSON.stringify(result.css); const alreadyImported = {}; - const importJs = result.importItems + const importJs = parserOptions.importItems .filter((imp) => { if (!imp.media) { if (alreadyImported[imp.url]) { @@ -102,8 +176,12 @@ module.exports = function loader(content, map) { .join('\n'); cssAsString = cssAsString.replace( - result.importItemRegExpG, - placeholderImportItemReplacer(this, result, importUrlPrefix) + placholderRegExps.importItemG, + placeholderImportItemReplacer( + this, + parserOptions.importItems, + importUrlPrefix + ) ); // helper for ensuring valid CSS strings from requires @@ -111,39 +189,48 @@ module.exports = function loader(content, map) { if ( options.url !== false && - result.urlItems && - result.urlItems.length > 0 + parserOptions.urlItems && + parserOptions.urlItems.length > 0 ) { urlEscapeHelper = `var escape = require(${loaderUtils.stringifyRequest( this, require.resolve('./runtime/escape.js') )});\n`; - cssAsString = cssAsString.replace(result.urlItemRegExpG, (item) => { - const match = result.urlItemRegExp.exec(item); - let idx = +match[1]; - const urlItem = result.urlItems[idx]; - const { url } = urlItem; - idx = url.indexOf('?#'); - if (idx < 0) { - idx = url.indexOf('#'); - } - let urlRequest; - if (idx > 0) { - // idx === 0 is catched by isUrlRequest - // in cases like url('webfont.eot?#iefix') - urlRequest = url.substr(0, idx); + cssAsString = cssAsString.replace( + placholderRegExps.urlItemG, + (item) => { + const match = placholderRegExps.urlItem.exec(item); + let idx = +match[1]; + const urlItem = parserOptions.urlItems[idx]; + const { url } = urlItem; + + idx = url.indexOf('?#'); + + if (idx < 0) { + idx = url.indexOf('#'); + } + + let urlRequest; + + if (idx > 0) { + // idx === 0 is catched by isUrlRequest + // in cases like url('webfont.eot?#iefix') + urlRequest = url.substr(0, idx); + return `" + escape(require(${loaderUtils.stringifyRequest( + this, + urlRequest + )}) + "${url.substr(idx)}") + "`; + } + + urlRequest = url; + return `" + escape(require(${loaderUtils.stringifyRequest( this, urlRequest - )}) + "${url.substr(idx)}") + "`; + )})) + "`; } - urlRequest = url; - return `" + escape(require(${loaderUtils.stringifyRequest( - this, - urlRequest - )})) + "`; - }); + ); } if (exportJs) { @@ -154,7 +241,8 @@ module.exports = function loader(content, map) { if (sourceMap && result.map) { /* eslint-disable no-param-reassign */ // Add a SourceMap - ({ map } = result); + map = result.map.toJSON(); + if (map.sources) { map.sources = map.sources.map( (source) => @@ -166,12 +254,14 @@ module.exports = function loader(content, map) { ); map.sourceRoot = ''; } + map.file = map.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}, ""]);`; @@ -188,6 +278,10 @@ module.exports = function loader(content, map) { `// module\n${moduleJs}\n\n` + `// exports\n${exportJs}` ); - } - ); + }) + .catch((error) => { + callback( + error.name === 'CssSyntaxError' ? new CssSyntaxError(error) : error + ); + }); }; diff --git a/lib/processCss.js b/lib/processCss.js deleted file mode 100644 index 1968d655..00000000 --- a/lib/processCss.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -const postcss = require('postcss'); -const loaderUtils = require('loader-utils'); - -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 { importParser, icssParser, urlParser } = require('./plugins'); - -const Warning = require('./Warning'); -const CssSyntaxError = require('./CssSyntaxError'); -const { getLocalIdent } = require('./utils'); - -module.exports = function processCss(content, map, options, callback) { - const { loaderContext, loaderOptions } = options; - const localIdentName = loaderOptions.localIdentName || '[hash:base64]'; - const customGetLocalIdent = loaderOptions.getLocalIdent || getLocalIdent; - - const parserOptions = { - url: loaderOptions.url !== false, - import: loaderOptions.import !== false, - }; - - const plugins = [ - modulesValues, - localByDefault({ - mode: loaderOptions.modules ? 'local' : 'global', - rewriteUrl(global, url) { - if (parserOptions.url) { - // eslint-disable-next-line no-param-reassign - url = url.trim(); - - if ( - !url.replace(/\s/g, '').length || - !loaderUtils.isUrlRequest(url) - ) { - return url; - } - if (global) { - return loaderUtils.urlToRequest(url); - } - } - return url; - }, - }), - extractImports(), - modulesScope({ - generateScopedName: function generateScopedName(exportName) { - return customGetLocalIdent( - options.loaderContext, - localIdentName, - exportName, - { - regExp: loaderOptions.localIdentRegExp, - hashPrefix: loaderOptions.hashPrefix || '', - context: loaderOptions.context, - } - ); - }, - }), - ]; - - if (loaderOptions.import !== false) { - plugins.push(importParser(parserOptions)); - } - - if (loaderOptions.url !== false) { - plugins.push(urlParser(parserOptions)); - } - - plugins.push(icssParser(parserOptions)); - - postcss(plugins) - .process(content, { - // we need a prefix to avoid path rewriting of PostCSS - from: `/css-loader!${loaderUtils - .getRemainingRequest(loaderContext) - .split('!') - .pop()}`, - to: loaderUtils - .getCurrentRequest(loaderContext) - .split('!') - .pop(), - map: options.sourceMap - ? { - prev: map, - sourcesContent: true, - inline: false, - annotation: false, - } - : null, - }) - .then((result) => { - result - .warnings() - .forEach((warning) => loaderContext.emitWarning(new Warning(warning))); - - callback(null, { - source: result.css, - map: result.map && result.map.toJSON(), - exports: parserOptions.exports, - importItems: parserOptions.importItems, - importItemRegExpG: /___CSS_LOADER_IMPORT___([0-9]+)___/g, - importItemRegExp: /___CSS_LOADER_IMPORT___([0-9]+)___/, - urlItems: parserOptions.urlItems, - urlItemRegExpG: /___CSS_LOADER_URL___([0-9]+)___/g, - urlItemRegExp: /___CSS_LOADER_URL___([0-9]+)___/, - }); - }) - .catch((error) => { - callback( - error.name === 'CssSyntaxError' ? new CssSyntaxError(error) : error - ); - }); -}; diff --git a/lib/utils.js b/lib/utils.js index b195e70f..7c617d71 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,33 +7,44 @@ const path = require('path'); const camelCase = require('lodash/camelCase'); const loaderUtils = require('loader-utils'); +const placholderRegExps = { + importItemG: /___CSS_LOADER_IMPORT___([0-9]+)___/g, + importItem: /___CSS_LOADER_IMPORT___([0-9]+)___/, + urlItemG: /___CSS_LOADER_URL___([0-9]+)___/g, + urlItem: /___CSS_LOADER_URL___([0-9]+)___/, +}; + function dashesCamelCase(str) { return str.replace(/-+(\w)/g, (match, firstLetter) => firstLetter.toUpperCase() ); } -function compileExports(result, importItemMatcher, camelCaseKeys) { - if (!result.exports || !Object.keys(result.exports).length) { +function compileExports(exports, importItemMatcher, camelCaseKeys) { + if (!exports || Object.keys(exports).length === 0) { return ''; } - const exportJs = Object.keys(result.exports) + const exportJs = Object.keys(exports) .reduce((res, key) => { - let valueAsString = JSON.stringify(result.exports[key]); + let valueAsString = JSON.stringify(exports[key]); + valueAsString = valueAsString.replace( - result.importItemRegExpG, + placholderRegExps.importItemG, importItemMatcher ); + function addEntry(k) { res.push(`\t${JSON.stringify(k)}: ${valueAsString}`); } let targetKey; + switch (camelCaseKeys) { case true: addEntry(key); targetKey = camelCase(key); + if (targetKey !== key) { addEntry(targetKey); } @@ -41,6 +52,7 @@ function compileExports(result, importItemMatcher, camelCaseKeys) { case 'dashes': addEntry(key); targetKey = dashesCamelCase(key); + if (targetKey !== key) { addEntry(targetKey); } @@ -55,6 +67,7 @@ function compileExports(result, importItemMatcher, camelCaseKeys) { addEntry(key); break; } + return res; }, []) .join(',\n'); @@ -112,14 +125,14 @@ function getLocalIdent(loaderContext, localIdentName, localName, options) { function placeholderImportItemReplacer( loaderContext, - result, + importItems, importUrlPrefix, onlyLocals = false ) { return (item) => { - const match = result.importItemRegExp.exec(item); + const match = placholderRegExps.importItem.exec(item); const idx = +match[1]; - const importItem = result.importItems[idx]; + const importItem = importItems[idx]; const importUrl = importUrlPrefix + importItem.url; if (onlyLocals) { @@ -142,4 +155,5 @@ module.exports = { getImportPrefix, getLocalIdent, placeholderImportItemReplacer, + placholderRegExps, };