From c5bbb5419fad128012441238a5c0d1d0ae314b5b Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Wed, 28 Nov 2018 16:26:59 +0300 Subject: [PATCH] refactor: plugins --- lib/loader.js | 12 +- lib/plugins/index.js | 9 ++ lib/plugins/postcss-import-parser.js | 72 ++++++++++++ lib/plugins/postcss-parser.js | 103 +++++++++++++++++ lib/plugins/postcss-url-parser.js | 56 +++++++++ lib/postcss-css-loader-parser.js | 167 --------------------------- lib/processCss.js | 19 ++- lib/utils.js | 2 +- 8 files changed, 260 insertions(+), 180 deletions(-) create mode 100644 lib/plugins/index.js create mode 100644 lib/plugins/postcss-import-parser.js create mode 100644 lib/plugins/postcss-parser.js create mode 100644 lib/plugins/postcss-url-parser.js delete mode 100644 lib/postcss-css-loader-parser.js diff --git a/lib/loader.js b/lib/loader.js index e42ca414..372f5437 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -50,12 +50,6 @@ module.exports = function loader(content, map) { const alreadyImported = {}; const importJs = result.importItems - .map((imp) => { - // fixes #781 when importing `url(filename.css )` - // eslint-disable-next-line no-param-reassign - imp.url = imp.url.trim(); - return imp; - }) .filter((imp) => { if (!imp.mediaQuery) { if (alreadyImported[imp.url]) { @@ -98,7 +92,11 @@ module.exports = function loader(content, map) { // helper for ensuring valid CSS strings from requires let urlEscapeHelper = ''; - if (options.url !== false && result.urlItems.length > 0) { + if ( + options.url !== false && + result.urlItems && + result.urlItems.length > 0 + ) { urlEscapeHelper = `var escape = require(${loaderUtils.stringifyRequest( this, require.resolve('./runtime/escape.js') diff --git a/lib/plugins/index.js b/lib/plugins/index.js new file mode 100644 index 00000000..8272227b --- /dev/null +++ b/lib/plugins/index.js @@ -0,0 +1,9 @@ +const importParser = require('./postcss-import-parser'); +const parser = require('./postcss-parser'); +const urlParser = require('./postcss-url-parser'); + +module.exports = { + importParser, + parser, + urlParser, +}; diff --git a/lib/plugins/postcss-import-parser.js b/lib/plugins/postcss-import-parser.js new file mode 100644 index 00000000..cde65b4f --- /dev/null +++ b/lib/plugins/postcss-import-parser.js @@ -0,0 +1,72 @@ +const postcss = require('postcss'); +const loaderUtils = require('loader-utils'); +const Tokenizer = require('css-selector-tokenizer'); + +const pluginName = 'postcss-import-parser'; + +module.exports = postcss.plugin( + pluginName, + (options) => + function process(css, result) { + const importItems = []; + + css.walkAtRules(/^import$/i, (atrule) => { + // Convert only top-level @import + if (atrule.parent.type !== 'root') { + return; + } + + if (atrule.nodes) { + result.warn( + "It looks like you didn't end your @import statement correctly. " + + 'Child nodes are attached to it.', + { node: atrule } + ); + return; + } + + const values = Tokenizer.parseValues(atrule.params); + let [url] = values.nodes[0].nodes; + + if (url && url.type === 'url') { + ({ url } = url); + } else if (url && url.type === 'string') { + url = url.value; + } else { + result.warn(`Unable to find uri in '${atrule.toString()}'`, { + node: atrule, + }); + + return; + } + + if (!url.replace(/\s/g, '').length) { + result.warn(`Unable to find uri in '${atrule.toString()}'`, { + node: atrule, + }); + + return; + } + + values.nodes[0].nodes.shift(); + + const mediaQuery = Tokenizer.stringifyValues(values); + + url = url.trim(); + + if (loaderUtils.isUrlRequest(url)) { + url = loaderUtils.urlToRequest(url); + } + + importItems.push({ + url, + mediaQuery, + }); + + atrule.remove(); + }); + + // eslint-disable-next-line no-param-reassign + options.importItems = importItems; + } +); diff --git a/lib/plugins/postcss-parser.js b/lib/plugins/postcss-parser.js new file mode 100644 index 00000000..9fb6b5f9 --- /dev/null +++ b/lib/plugins/postcss-parser.js @@ -0,0 +1,103 @@ +const postcss = require('postcss'); +const valueParser = require('postcss-value-parser'); +const icssUtils = require('icss-utils'); +const Tokenizer = require('css-selector-tokenizer'); +const loaderUtils = require('loader-utils'); + +module.exports = postcss.plugin( + 'postcss-parser', + (options) => + function process(css) { + const imports = {}; + let exports = {}; + const importItems = options.importItems || []; + const urlItems = options.urlItems || []; + + function replaceImportsInString(str) { + if (options.import) { + const tokens = valueParser(str); + + tokens.walk((node) => { + if (node.type !== 'word') { + return; + } + + const token = node.value; + const importIndex = imports[`$${token}`]; + + if (typeof importIndex === 'number') { + // eslint-disable-next-line no-param-reassign + node.value = `___CSS_LOADER_IMPORT___${importIndex}___`; + } + }); + + return tokens.toString(); + } + return str; + } + + const icss = icssUtils.extractICSS(css); + + exports = icss.icssExports; + + Object.keys(icss.icssImports).forEach((key) => { + const url = loaderUtils.parseString(key); + + Object.keys(icss.icssImports[key]).forEach((prop) => { + imports[`$${prop}`] = importItems.length; + importItems.push({ + url, + export: icss.icssImports[key][prop], + }); + }); + }); + + Object.keys(exports).forEach((exportName) => { + exports[exportName] = replaceImportsInString(exports[exportName]); + }); + + function processNode(item) { + switch (item.type) { + case 'value': + item.nodes.forEach(processNode); + break; + case 'nested-item': + item.nodes.forEach(processNode); + break; + case 'item': { + const importIndex = imports[`$${item.name}`]; + if (typeof importIndex === 'number') { + // eslint-disable-next-line no-param-reassign + item.name = `___CSS_LOADER_IMPORT___${importIndex}___`; + } + break; + } + // no default + } + } + + css.walkDecls((decl) => { + const values = Tokenizer.parseValues(decl.value); + + values.nodes.forEach((value) => { + value.nodes.forEach(processNode); + }); + + // eslint-disable-next-line no-param-reassign + decl.value = Tokenizer.stringifyValues(values); + }); + + css.walkAtRules((atrule) => { + if (typeof atrule.params === 'string') { + // eslint-disable-next-line no-param-reassign + atrule.params = replaceImportsInString(atrule.params); + } + }); + + /* eslint-disable no-param-reassign */ + options.importItems = importItems; + options.urlItems = urlItems; + options.exports = exports; + /* eslint-enable no-param-reassign */ + } +); diff --git a/lib/plugins/postcss-url-parser.js b/lib/plugins/postcss-url-parser.js new file mode 100644 index 00000000..d15e9c01 --- /dev/null +++ b/lib/plugins/postcss-url-parser.js @@ -0,0 +1,56 @@ +const postcss = require('postcss'); +const Tokenizer = require('css-selector-tokenizer'); +const loaderUtils = require('loader-utils'); + +const pluginName = 'postcss-url-parser'; + +module.exports = postcss.plugin( + pluginName, + (options) => + function process(css) { + const urlItems = []; + + function processNode(item) { + switch (item.type) { + case 'value': + item.nodes.forEach(processNode); + break; + case 'nested-item': + item.nodes.forEach(processNode); + break; + case 'url': + if ( + item.url.replace(/\s/g, '').length && + !/^#/.test(item.url) && + loaderUtils.isUrlRequest(item.url) + ) { + // Strip quotes, they will be re-added if the module needs them + /* eslint-disable no-param-reassign */ + item.stringType = ''; + delete item.innerSpacingBefore; + delete item.innerSpacingAfter; + const { url } = item; + item.url = `___CSS_LOADER_URL___${urlItems.length}___`; + /* eslint-enable no-param-reassign */ + urlItems.push({ + url, + }); + } + break; + // no default + } + } + + css.walkDecls((decl) => { + const values = Tokenizer.parseValues(decl.value); + values.nodes.forEach((value) => { + value.nodes.forEach(processNode); + }); + // eslint-disable-next-line no-param-reassign + decl.value = Tokenizer.stringifyValues(values); + }); + + // eslint-disable-next-line no-param-reassign + options.urlItems = urlItems; + } +); diff --git a/lib/postcss-css-loader-parser.js b/lib/postcss-css-loader-parser.js deleted file mode 100644 index 9203ec2b..00000000 --- a/lib/postcss-css-loader-parser.js +++ /dev/null @@ -1,167 +0,0 @@ -const postcss = require('postcss'); -const valueParser = require('postcss-value-parser'); -const icssUtils = require('icss-utils'); -const Tokenizer = require('css-selector-tokenizer'); -const loaderUtils = require('loader-utils'); - -module.exports = postcss.plugin( - 'postcss-css-loader-parser', - (options) => (css, result) => { - const imports = {}; - let exports = {}; - const importItems = []; - const urlItems = []; - - function replaceImportsInString(str) { - if (options.import) { - const tokens = valueParser(str); - tokens.walk((node) => { - if (node.type !== 'word') { - return; - } - const token = node.value; - const importIndex = imports[`$${token}`]; - if (typeof importIndex === 'number') { - // eslint-disable-next-line no-param-reassign - node.value = `___CSS_LOADER_IMPORT___${importIndex}___`; - } - }); - return tokens.toString(); - } - return str; - } - - if (options.import) { - css.walkAtRules(/^import$/i, (atrule) => { - // Convert only top-level @import - if (atrule.parent.type !== 'root') { - return; - } - - if (atrule.nodes) { - result.warn( - "It looks like you didn't end your @import statement correctly. " + - 'Child nodes are attached to it.', - { node: atrule } - ); - return; - } - - const values = Tokenizer.parseValues(atrule.params); - let [url] = values.nodes[0].nodes; - - if (url && url.type === 'url') { - ({ url } = url); - } else if (url && url.type === 'string') { - url = url.value; - } else { - result.warn(`Unable to find uri in '${atrule.toString()}'`, { - node: atrule, - }); - - return; - } - - if (!url.replace(/\s/g, '').length) { - result.warn(`Unable to find uri in '${atrule.toString()}'`, { - node: atrule, - }); - - return; - } - - values.nodes[0].nodes.shift(); - - const mediaQuery = Tokenizer.stringifyValues(values); - - if (loaderUtils.isUrlRequest(url)) { - url = loaderUtils.urlToRequest(url); - } - - importItems.push({ - url, - mediaQuery, - }); - atrule.remove(); - }); - } - - const icss = icssUtils.extractICSS(css); - exports = icss.icssExports; - Object.keys(icss.icssImports).forEach((key) => { - const url = loaderUtils.parseString(key); - Object.keys(icss.icssImports[key]).forEach((prop) => { - imports[`$${prop}`] = importItems.length; - importItems.push({ - url, - export: icss.icssImports[key][prop], - }); - }); - }); - - Object.keys(exports).forEach((exportName) => { - exports[exportName] = replaceImportsInString(exports[exportName]); - }); - - function processNode(item) { - switch (item.type) { - case 'value': - item.nodes.forEach(processNode); - break; - case 'nested-item': - item.nodes.forEach(processNode); - break; - case 'item': { - const importIndex = imports[`$${item.name}`]; - if (typeof importIndex === 'number') { - // eslint-disable-next-line no-param-reassign - item.name = `___CSS_LOADER_IMPORT___${importIndex}___`; - } - break; - } - case 'url': - if ( - options.url && - item.url.replace(/\s/g, '').length && - !/^#/.test(item.url) && - loaderUtils.isUrlRequest(item.url) - ) { - // Strip quotes, they will be re-added if the module needs them - /* eslint-disable no-param-reassign */ - item.stringType = ''; - delete item.innerSpacingBefore; - delete item.innerSpacingAfter; - const { url } = item; - item.url = `___CSS_LOADER_URL___${urlItems.length}___`; - /* eslint-enable no-param-reassign */ - urlItems.push({ - url, - }); - } - break; - // no default - } - } - - css.walkDecls((decl) => { - const values = Tokenizer.parseValues(decl.value); - values.nodes.forEach((value) => { - value.nodes.forEach(processNode); - }); - // eslint-disable-next-line no-param-reassign - decl.value = Tokenizer.stringifyValues(values); - }); - css.walkAtRules((atrule) => { - if (typeof atrule.params === 'string') { - // eslint-disable-next-line no-param-reassign - atrule.params = replaceImportsInString(atrule.params); - } - }); - - /* eslint-disable no-param-reassign */ - options.importItems = importItems; - options.urlItems = urlItems; - options.exports = exports; - /* eslint-enable no-param-reassign */ - } -); diff --git a/lib/processCss.js b/lib/processCss.js index fa348569..d5c5d502 100644 --- a/lib/processCss.js +++ b/lib/processCss.js @@ -10,7 +10,7 @@ const extractImports = require('postcss-modules-extract-imports'); const modulesScope = require('postcss-modules-scope'); const modulesValues = require('postcss-modules-values'); -const cssLoaderParser = require('./postcss-css-loader-parser'); +const { importParser, parser, urlParser } = require('./plugins'); const Warning = require('./Warning'); const CssSyntaxError = require('./CssSyntaxError'); @@ -26,7 +26,7 @@ module.exports = function processCss(content, map, options, callback) { import: loaderOptions.import !== false, }; - const pipeline = postcss([ + const plugins = [ modulesValues, localByDefault({ mode: loaderOptions.modules ? 'local' : 'global', @@ -63,10 +63,19 @@ module.exports = function processCss(content, map, options, callback) { ); }, }), - cssLoaderParser(parserOptions), - ]); + ]; - pipeline + if (loaderOptions.import !== false) { + plugins.push(importParser(parserOptions)); + } + + if (loaderOptions.url !== false) { + plugins.push(urlParser(parserOptions)); + } + + plugins.push(parser(parserOptions)); + + postcss(plugins) .process(content, { // we need a prefix to avoid path rewriting of PostCSS from: `/css-loader!${loaderUtils diff --git a/lib/utils.js b/lib/utils.js index f3b32cd6..0fad5e75 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -14,7 +14,7 @@ function dashesCamelCase(str) { } function compileExports(result, importItemMatcher, camelCaseKeys) { - if (!Object.keys(result.exports).length) { + if (!result.exports || !Object.keys(result.exports).length) { return ''; }