From d9f6bbaf0c5e38ec8eca4682eb1e4206a3d2ecd3 Mon Sep 17 00:00:00 2001 From: Michael Ciniawsky Date: Mon, 22 May 2017 02:53:35 +0200 Subject: [PATCH 1/2] refactor(loader): v1.0.0 --- src/index.js | 3 +- src/lib/Error.js | 20 ++++++ src/lib/plugins/import.js | 98 +++++++++++++++++++++++++++++ src/lib/plugins/url.js | 114 ++++++++++++++++++++++++++++++++++ src/lib/runtime.js | 80 ++++++++++++++++++++++++ test/fixtures/urls/file.css | 3 + test/fixtures/urls/fixture.js | 1 + 7 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 src/lib/Error.js create mode 100644 src/lib/plugins/import.js create mode 100644 src/lib/plugins/url.js create mode 100644 src/lib/runtime.js create mode 100644 test/fixtures/urls/file.css diff --git a/src/index.js b/src/index.js index ea75f63e..b68bdc05 100644 --- a/src/index.js +++ b/src/index.js @@ -35,6 +35,7 @@ export default function loader (css, map, meta) { ); validateOptions(schema, options, 'CSS Loader'); + // Loader Mode (Async) const cb = this.async(); const file = this.resourcePath; @@ -47,7 +48,6 @@ export default function loader (css, map, meta) { map = false; } - // CSS Plugins const plugins = []; // URL Plugin @@ -55,6 +55,7 @@ export default function loader (css, map, meta) { plugins.push(urls()); } + // Import Plugin if (options.import) { plugins.push(imports()); diff --git a/src/lib/Error.js b/src/lib/Error.js new file mode 100644 index 00000000..7c53aaca --- /dev/null +++ b/src/lib/Error.js @@ -0,0 +1,20 @@ +class SyntaxError extends Error { + constructor(err) { + super(err); + + this.name = 'Syntax Error'; + this.message = ''; + + if (err.line) { + this.message += `\n\n[${err.line}:${err.column}] ${err.reason}`; + } + + if (err.input.source) { + this.message += `\n\n${err.showSourceCode()}\n`; + } + + Error.captureStackTrace(this, this.constructor); + } +} + +export default SyntaxError; diff --git a/src/lib/plugins/import.js b/src/lib/plugins/import.js new file mode 100644 index 00000000..4cb57499 --- /dev/null +++ b/src/lib/plugins/import.js @@ -0,0 +1,98 @@ +/* eslint-disable */ +import postcss from 'postcss'; +import valueParser from 'postcss-value-parser'; +// ICSS {String} +// import { createICSSRules } from "icss-utils"; + +const plugin = 'postcss-icss-import'; + +const getArg = nodes => + (nodes.length !== 0 && nodes[0].type === 'string' + ? nodes[0].value + : valueParser.stringify(nodes)); + +const getUrl = (node) => { + if (node.type === 'function' && node.value === 'url') { + return getArg(node.nodes); + } + if (node.type === 'string') { + return node.value; + } + return ''; +}; + +const parseImport = (params) => { + const { nodes } = valueParser(params); + + if (nodes.length === 0) { + return null; + } + + const url = getUrl(nodes[0]); + + if (url.trim().length === 0) { + return null; + } + + return { + url, + media: valueParser.stringify(nodes.slice(1)).trim(), + }; +}; + +const isExternalUrl = url => /^\w+:\/\//.test(url) || url.startsWith('//'); + +const walkImports = (css, callback) => { + css.each((node) => { + if (node.type === 'atrule' && node.name.toLowerCase() === 'import') { + callback(node); + } + }); +}; + +export default postcss.plugin(plugin, () => (css, result) => { + let idx = 0; + const imports = {}; + + walkImports(css, (atrule) => { + if (atrule.nodes) { + return result.warn( + 'It looks like you didn\'t end your @import statement correctly.\nChild nodes are attached to it.', + { node: atrule }, + ); + } + + const parsed = parseImport(atrule.params); + + if (parsed === null) { + return result.warn(`Unable to find uri in '${atrule.toString()}'`, { + node: atrule, + }); + } + + if (!isExternalUrl(parsed.url)) { + atrule.remove(); + + imports[`CSS__IMPORT__${idx}`] = `${parsed.url}`; + + idx++; + + // ICSS {String} + // imports[`'${parsed.url}'`] = { + // import: "default" + + // (parsed.media.length === 0 ? "" : ` ${parsed.media}`) + // }; + } + }); + + result.messages.push({ imports }); + + // result.messages.push({ + // type: 'dependency', + // plugin: 'postcss-icss-import', + // imports + // }) + + // ICSS {String} + // css.prepend(createICSSRules(imports, {})); +}); diff --git a/src/lib/plugins/url.js b/src/lib/plugins/url.js new file mode 100644 index 00000000..63acaffa --- /dev/null +++ b/src/lib/plugins/url.js @@ -0,0 +1,114 @@ +/* eslint-disable */ +import postcss from 'postcss'; +import valueParser from 'postcss-value-parser'; +// ICSS {String} +// import { createICSSRules } from "icss-utils"; + +const walkUrls = (parsed, callback) => { + parsed.walk((node) => { + if (node.type === 'function' && node.value === 'url') { + const content = node.nodes.length !== 0 && node.nodes[0].type === 'string' + ? node.nodes[0].value + : valueParser.stringify(node.nodes); + + if (content.trim().length !== 0) { + callback(node, content); + } + + // do not traverse inside url + return false; + } + }); +}; + +const mapUrls = (parsed, map) => { + walkUrls(parsed, (node, content) => { + node.nodes = [{ type: 'word', value: map(content) }]; + }); +}; + +const filterUrls = (parsed, filter) => { + const result = []; + + walkUrls(parsed, (node, content) => { + if (filter(content)) { + result.push(content); + } + }); + + return result; +}; + +const walkDeclsWithUrl = (css, filter) => { + const result = []; + + css.walkDecls((decl) => { + if (decl.value.includes('url(')) { + const parsed = valueParser(decl.value); + const values = filterUrls(parsed, filter); + + if (values.length) { + result.push({ + decl, + parsed, + values, + }); + } + } + }); + + return result; +}; + +const filterValues = value => !/^\w+:\/\//.test(value) && + !value.startsWith('//') && + !value.startsWith('#') && + !value.startsWith('data:'); + +const flatten = arr => arr.reduce((acc, d) => [...acc, ...d], []); +const uniq = arr => arr.reduce( + (acc, d) => (acc.indexOf(d) === -1 ? [...acc, d] : acc), + [], +); + +module.exports = postcss.plugin('postcss-icss-url', () => (css, result) => { + const traversed = walkDeclsWithUrl(css, filterValues); + const paths = uniq(flatten(traversed.map(item => item.values))); + + // ICSS imports {String} + const urls = {}; + const aliases = {}; + + paths.forEach((path, index) => { + // ICSS Placeholder + const alias = '${' + `CSS__URL__${index}` + '}'; + + // console.log(alias) + + // ICSS {String} + // imports[`'${path}'`] = { + // [alias]: "default" + // }; + + urls[`CSS__URL__${index}`] = `${path}`; + + aliases[path] = alias; + }); + + traversed.forEach((item) => { + mapUrls(item.parsed, value => aliases[value]); + + item.decl.value = item.parsed.toString(); + }); + + result.messages.push({ urls }); + + // result.messages.push({ + // type: 'dependency', + // plugin: 'postcss-icss-url', + // urls + // }) + + // ICSS {String} + // css.prepend(createICSSRules(imports, {})); +}); diff --git a/src/lib/runtime.js b/src/lib/runtime.js new file mode 100644 index 00000000..0f860579 --- /dev/null +++ b/src/lib/runtime.js @@ -0,0 +1,80 @@ +/* eslint-disable */ + +// CSS (Loader) Runtime +// TODO Update to ESM (if needed) +// @see css-loader/new-loader +module.exports = function (sourceMap) { + var list = []; + + // return the list of modules as css string + list.toString = function toString() { + return this.map(function (item) { + var css = cssWithMappingToString(item, sourceMap); + + if (item[2]) return "@media " + item[2] + "{" + css + "}"; + + return css; + }).join(""); + }; + + // import a list of modules into the list + list.i = function (modules, mediaQuery) { + if(typeof modules === "string") modules = [[null, modules, ""]]; + + var isImported = {}; + + for (var i = 0; i < this.length; i++) { + var id = this[i][0]; + + if (typeof id === "number") isImported[id] = true; + } + + for (i = 0; i < modules.length; i++) { + var item = modules[i]; + // skip already imported module + // this implementation is not 100% perfect for weird media query combos + // when a module is imported multiple times with different media queries. + // I hope this will never occur (Hey this way we have smaller bundles) + if (typeof item[0] !== "number" || !isImported[item[0]]) { + if (mediaQuery && !item[2]) item[2] = mediaQuery; + else if (mediaQuery) { + item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; + } + + list.push(item); + } + } + }; + + return list; +}; + +function cssWithMappingToString (item, sourceMap) { + var css = item[1] || ''; + var map = item[3]; + + if (!map) { + return css; + } + + if (sourceMap && typeof btoa === 'function') { + var sourceMapping = toComment(map); + + var sourceURLs = map.sources.map(function (source) { + return '/*# sourceURL=' + map.sourceRoot + source + ' */' + }); + + return [css].concat(sourceURLs).concat([sourceMapping]).join('\n'); + } + + return [css].join('\n'); +} + +// Adapted from convert-source-map (MIT) +function toComment (sourceMap) { + // eslint-disable-next-line no-undef + var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); + var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64; + + return '/*# ' + data + ' */'; +} diff --git a/test/fixtures/urls/file.css b/test/fixtures/urls/file.css new file mode 100644 index 00000000..ff4c65d9 --- /dev/null +++ b/test/fixtures/urls/file.css @@ -0,0 +1,3 @@ +.url { + background: url('./file.png') +} diff --git a/test/fixtures/urls/fixture.js b/test/fixtures/urls/fixture.js index 916b5df3..2462b27e 100644 --- a/test/fixtures/urls/fixture.js +++ b/test/fixtures/urls/fixture.js @@ -1 +1,2 @@ import css from './fixture.css'; + From 16e5130ccbadc7a61154d0e9e2f013ca992a2578 Mon Sep 17 00:00:00 2001 From: Michael Ciniawsky Date: Fri, 5 Jan 2018 03:30:43 +0100 Subject: [PATCH 2/2] feat(url): add `url()` filter support (`options.url`) --- README.md | 27 +++++ src/index.js | 80 +++++++------- src/lib/Error.js | 20 ---- src/lib/plugins/import.js | 98 ----------------- src/lib/plugins/url.js | 114 -------------------- src/lib/runtime.js | 80 -------------- src/options.json | 7 +- src/plugins/url.js | 73 +++++++++---- test/fixtures/imports/fixture.js | 2 + test/fixtures/urls/filter/fixture.css | 4 + test/fixtures/urls/filter/fixture.js | 3 + test/fixtures/urls/fixture.js | 1 + test/loader.test.js | 2 +- test/options/__snapshots__/url.test.js.snap | 26 +++++ test/options/minimize.test.js | 4 +- test/options/sourceMap.test.js | 12 +-- test/options/url.test.js | 34 ++++++ test/runtime.test.js | 54 ++++++---- 18 files changed, 233 insertions(+), 408 deletions(-) delete mode 100644 src/lib/Error.js delete mode 100644 src/lib/plugins/import.js delete mode 100644 src/lib/plugins/url.js delete mode 100644 src/lib/runtime.js create mode 100644 test/fixtures/urls/filter/fixture.css create mode 100644 test/fixtures/urls/filter/fixture.js diff --git a/README.md b/README.md index d703ce1b..dbea3e9b 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ export default ` } ` ``` + #### `{Boolean}` To disable `url()` resolving by `css-loader` set the option to `false` @@ -88,6 +89,32 @@ To disable `url()` resolving by `css-loader` set the option to `false` } ``` +#### `{RegExp}` + +**webpack.config.js** +```js +{ + loader: 'css-loader', + options: { + url: /filter/ + } +} +``` + +#### `{Function}` + +**webpack.config.js** +```js +{ + loader: 'css-loader', + options: { + url (url) { + return /filter/.test(url) + } + } +} +``` + ### `import` ```css diff --git a/src/index.js b/src/index.js index b68bdc05..0e2710f3 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ import { getOptions } from 'loader-utils'; import validateOptions from 'schema-utils'; import postcss from 'postcss'; -// TODO(michael-ciniawsky) +// TODO(michael-ciniawsky) // replace with postcss-icss-{url, import} import urls from './plugins/url'; import imports from './plugins/import'; @@ -26,13 +26,9 @@ const DEFAULTS = { sourceMap: false, }; -export default function loader (css, map, meta) { +export default function loader(css, map, meta) { // Loader Options - const options = Object.assign( - {}, - DEFAULTS, - getOptions(this) - ); + const options = Object.assign({}, DEFAULTS, getOptions(this)); validateOptions(schema, options, 'CSS Loader'); @@ -52,9 +48,8 @@ export default function loader (css, map, meta) { // URL Plugin if (options.url) { - plugins.push(urls()); + plugins.push(urls(options)); } - // Import Plugin if (options.import) { @@ -65,7 +60,7 @@ export default function loader (css, map, meta) { if (options.minimize) { plugins.push(minifier()); } - + if (meta) { const { ast } = meta; // Reuse CSS AST (PostCSS AST e.g 'postcss-loader') @@ -74,34 +69,33 @@ export default function loader (css, map, meta) { css = ast.root; } } - - map = options.sourceMap + + map = options.sourceMap ? { - prev: map || false, - inline: false, - annotation: false, - sourcesContent: true, - } - : false - + prev: map || false, + inline: false, + annotation: false, + sourcesContent: true, + } + : false; + return postcss(plugins) .process(css, { from: `/css-loader!${file}`, map, to: file, - }).then(({ css, map, messages }) => { + }) + .then(({ css, map, messages }) => { if (meta && meta.messages) { - messages = messages.concat(meta.messages) + messages = messages.concat(meta.messages); } - + // CSS Imports const imports = messages - .filter((msg) => msg.type === 'import' ? msg : false) + .filter((msg) => (msg.type === 'import' ? msg : false)) .reduce((imports, msg) => { try { - msg = typeof msg.import === 'function' - ? msg.import() - : msg.import; + msg = typeof msg.import === 'function' ? msg.import() : msg.import; imports += msg; } catch (err) { @@ -110,17 +104,15 @@ export default function loader (css, map, meta) { this.emitError(err); } - return imports - }, '') - + return imports; + }, ''); + // CSS Exports const exports = messages - .filter((msg) => msg.type === 'export' ? msg : false) - .reduce((exports, msg) => { - try { - msg = typeof msg.export === 'function' - ? msg.export() - : msg.export; + .filter((msg) => (msg.type === 'export' ? msg : false)) + .reduce((exports, msg) => { + try { + msg = typeof msg.export === 'function' ? msg.export() : msg.export; exports += msg; } catch (err) { @@ -130,23 +122,27 @@ export default function loader (css, map, meta) { } return exports; - }, '') - - // TODO(michael-ciniawsky) + }, ''); + + // TODO(michael-ciniawsky) // triage if and add CSS runtime back const result = [ imports ? `// CSS Imports\n${imports}\n` : false, exports ? `// CSS Exports\n${exports}\n` : false, - `// CSS\nexport default \`${css}\`` + `// CSS\nexport default \`${css}\``, ] .filter(Boolean) .join('\n'); - + cb(null, result, map ? map.toJSON() : null); return null; }) .catch((err) => { - err.name === 'CssSyntaxError' ? cb(new SyntaxError(err)) : cb(err); + err = err.name === 'CssSyntaxError' ? new SyntaxError(err) : err; + + cb(err); + + return null; }); -}; +} diff --git a/src/lib/Error.js b/src/lib/Error.js deleted file mode 100644 index 7c53aaca..00000000 --- a/src/lib/Error.js +++ /dev/null @@ -1,20 +0,0 @@ -class SyntaxError extends Error { - constructor(err) { - super(err); - - this.name = 'Syntax Error'; - this.message = ''; - - if (err.line) { - this.message += `\n\n[${err.line}:${err.column}] ${err.reason}`; - } - - if (err.input.source) { - this.message += `\n\n${err.showSourceCode()}\n`; - } - - Error.captureStackTrace(this, this.constructor); - } -} - -export default SyntaxError; diff --git a/src/lib/plugins/import.js b/src/lib/plugins/import.js deleted file mode 100644 index 4cb57499..00000000 --- a/src/lib/plugins/import.js +++ /dev/null @@ -1,98 +0,0 @@ -/* eslint-disable */ -import postcss from 'postcss'; -import valueParser from 'postcss-value-parser'; -// ICSS {String} -// import { createICSSRules } from "icss-utils"; - -const plugin = 'postcss-icss-import'; - -const getArg = nodes => - (nodes.length !== 0 && nodes[0].type === 'string' - ? nodes[0].value - : valueParser.stringify(nodes)); - -const getUrl = (node) => { - if (node.type === 'function' && node.value === 'url') { - return getArg(node.nodes); - } - if (node.type === 'string') { - return node.value; - } - return ''; -}; - -const parseImport = (params) => { - const { nodes } = valueParser(params); - - if (nodes.length === 0) { - return null; - } - - const url = getUrl(nodes[0]); - - if (url.trim().length === 0) { - return null; - } - - return { - url, - media: valueParser.stringify(nodes.slice(1)).trim(), - }; -}; - -const isExternalUrl = url => /^\w+:\/\//.test(url) || url.startsWith('//'); - -const walkImports = (css, callback) => { - css.each((node) => { - if (node.type === 'atrule' && node.name.toLowerCase() === 'import') { - callback(node); - } - }); -}; - -export default postcss.plugin(plugin, () => (css, result) => { - let idx = 0; - const imports = {}; - - walkImports(css, (atrule) => { - if (atrule.nodes) { - return result.warn( - 'It looks like you didn\'t end your @import statement correctly.\nChild nodes are attached to it.', - { node: atrule }, - ); - } - - const parsed = parseImport(atrule.params); - - if (parsed === null) { - return result.warn(`Unable to find uri in '${atrule.toString()}'`, { - node: atrule, - }); - } - - if (!isExternalUrl(parsed.url)) { - atrule.remove(); - - imports[`CSS__IMPORT__${idx}`] = `${parsed.url}`; - - idx++; - - // ICSS {String} - // imports[`'${parsed.url}'`] = { - // import: "default" + - // (parsed.media.length === 0 ? "" : ` ${parsed.media}`) - // }; - } - }); - - result.messages.push({ imports }); - - // result.messages.push({ - // type: 'dependency', - // plugin: 'postcss-icss-import', - // imports - // }) - - // ICSS {String} - // css.prepend(createICSSRules(imports, {})); -}); diff --git a/src/lib/plugins/url.js b/src/lib/plugins/url.js deleted file mode 100644 index 63acaffa..00000000 --- a/src/lib/plugins/url.js +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable */ -import postcss from 'postcss'; -import valueParser from 'postcss-value-parser'; -// ICSS {String} -// import { createICSSRules } from "icss-utils"; - -const walkUrls = (parsed, callback) => { - parsed.walk((node) => { - if (node.type === 'function' && node.value === 'url') { - const content = node.nodes.length !== 0 && node.nodes[0].type === 'string' - ? node.nodes[0].value - : valueParser.stringify(node.nodes); - - if (content.trim().length !== 0) { - callback(node, content); - } - - // do not traverse inside url - return false; - } - }); -}; - -const mapUrls = (parsed, map) => { - walkUrls(parsed, (node, content) => { - node.nodes = [{ type: 'word', value: map(content) }]; - }); -}; - -const filterUrls = (parsed, filter) => { - const result = []; - - walkUrls(parsed, (node, content) => { - if (filter(content)) { - result.push(content); - } - }); - - return result; -}; - -const walkDeclsWithUrl = (css, filter) => { - const result = []; - - css.walkDecls((decl) => { - if (decl.value.includes('url(')) { - const parsed = valueParser(decl.value); - const values = filterUrls(parsed, filter); - - if (values.length) { - result.push({ - decl, - parsed, - values, - }); - } - } - }); - - return result; -}; - -const filterValues = value => !/^\w+:\/\//.test(value) && - !value.startsWith('//') && - !value.startsWith('#') && - !value.startsWith('data:'); - -const flatten = arr => arr.reduce((acc, d) => [...acc, ...d], []); -const uniq = arr => arr.reduce( - (acc, d) => (acc.indexOf(d) === -1 ? [...acc, d] : acc), - [], -); - -module.exports = postcss.plugin('postcss-icss-url', () => (css, result) => { - const traversed = walkDeclsWithUrl(css, filterValues); - const paths = uniq(flatten(traversed.map(item => item.values))); - - // ICSS imports {String} - const urls = {}; - const aliases = {}; - - paths.forEach((path, index) => { - // ICSS Placeholder - const alias = '${' + `CSS__URL__${index}` + '}'; - - // console.log(alias) - - // ICSS {String} - // imports[`'${path}'`] = { - // [alias]: "default" - // }; - - urls[`CSS__URL__${index}`] = `${path}`; - - aliases[path] = alias; - }); - - traversed.forEach((item) => { - mapUrls(item.parsed, value => aliases[value]); - - item.decl.value = item.parsed.toString(); - }); - - result.messages.push({ urls }); - - // result.messages.push({ - // type: 'dependency', - // plugin: 'postcss-icss-url', - // urls - // }) - - // ICSS {String} - // css.prepend(createICSSRules(imports, {})); -}); diff --git a/src/lib/runtime.js b/src/lib/runtime.js deleted file mode 100644 index 0f860579..00000000 --- a/src/lib/runtime.js +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable */ - -// CSS (Loader) Runtime -// TODO Update to ESM (if needed) -// @see css-loader/new-loader -module.exports = function (sourceMap) { - var list = []; - - // return the list of modules as css string - list.toString = function toString() { - return this.map(function (item) { - var css = cssWithMappingToString(item, sourceMap); - - if (item[2]) return "@media " + item[2] + "{" + css + "}"; - - return css; - }).join(""); - }; - - // import a list of modules into the list - list.i = function (modules, mediaQuery) { - if(typeof modules === "string") modules = [[null, modules, ""]]; - - var isImported = {}; - - for (var i = 0; i < this.length; i++) { - var id = this[i][0]; - - if (typeof id === "number") isImported[id] = true; - } - - for (i = 0; i < modules.length; i++) { - var item = modules[i]; - // skip already imported module - // this implementation is not 100% perfect for weird media query combos - // when a module is imported multiple times with different media queries. - // I hope this will never occur (Hey this way we have smaller bundles) - if (typeof item[0] !== "number" || !isImported[item[0]]) { - if (mediaQuery && !item[2]) item[2] = mediaQuery; - else if (mediaQuery) { - item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; - } - - list.push(item); - } - } - }; - - return list; -}; - -function cssWithMappingToString (item, sourceMap) { - var css = item[1] || ''; - var map = item[3]; - - if (!map) { - return css; - } - - if (sourceMap && typeof btoa === 'function') { - var sourceMapping = toComment(map); - - var sourceURLs = map.sources.map(function (source) { - return '/*# sourceURL=' + map.sourceRoot + source + ' */' - }); - - return [css].concat(sourceURLs).concat([sourceMapping]).join('\n'); - } - - return [css].join('\n'); -} - -// Adapted from convert-source-map (MIT) -function toComment (sourceMap) { - // eslint-disable-next-line no-undef - var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); - var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64; - - return '/*# ' + data + ' */'; -} diff --git a/src/options.json b/src/options.json index 2eae86b2..3ecc7e64 100644 --- a/src/options.json +++ b/src/options.json @@ -2,7 +2,12 @@ "type": "object", "properties": { "url": { - "type": "boolean" + "anyOf": [ + { "type": "string" }, + { "type": "boolean" }, + { "instanceof": "RegExp" }, + { "instanceof": "Function" } + ] }, "import": { "type": "boolean" diff --git a/src/plugins/url.js b/src/plugins/url.js index 962bfc9b..c881672b 100644 --- a/src/plugins/url.js +++ b/src/plugins/url.js @@ -1,10 +1,8 @@ /* eslint-disable */ import postcss from 'postcss'; import valueParser from 'postcss-value-parser'; -// ICSS {String} -// import { createICSSRules } from "icss-utils"; -const walkUrls = (parsed, callback) => { +const walkUrls = (parsed, cb) => { parsed.walk((node) => { if (node.type === 'function' && node.value === 'url') { const content = node.nodes.length !== 0 && node.nodes[0].type === 'string' @@ -12,7 +10,7 @@ const walkUrls = (parsed, callback) => { : valueParser.stringify(node.nodes); if (content.trim().length !== 0) { - callback(node, content); + cb(node, content); } // do not traverse inside url @@ -22,30 +20,32 @@ const walkUrls = (parsed, callback) => { }; const mapUrls = (parsed, map) => { - walkUrls(parsed, (node, content) => { - node.nodes = [{ type: 'word', value: map(content) }]; + walkUrls(parsed, (node, url) => { + node.nodes = [{ type: 'word', value: map(url) }]; }); }; -const filterUrls = (parsed, filter) => { +const filterUrls = (parsed, filter, options) => { const result = []; - walkUrls(parsed, (node, content) => { - if (filter(content)) { - result.push(content); + walkUrls(parsed, (node, url) => { + if (filter(url, options)) { + return false } + + return result.push(url); }); return result; }; -const walkDeclsWithUrl = (css, filter) => { +const walkDeclsWithUrl = (css, filter, options) => { const result = []; css.walkDecls((decl) => { if (decl.value.includes('url(')) { const parsed = valueParser(decl.value); - const values = filterUrls(parsed, filter); + const values = filterUrls(parsed, filter, options); if (values.length) { result.push({ @@ -60,10 +60,39 @@ const walkDeclsWithUrl = (css, filter) => { return result; }; -const filterValues = value => !/^\w+:\/\//.test(value) && - !value.startsWith('//') && - !value.startsWith('#') && - !value.startsWith('data:'); +const URL = /^\w+:\/\//; + +const filter = (url, options) => { + if (URL.test(url)) { + return true; + } + + if (url.startsWith('//')) { + return true; + } + + if (url.startsWith('//')) { + return true; + } + + if (url.startsWith('#')) { + return true; + } + + if (url.startsWith('data:')) { + return true; + } + + if (options.url instanceof RegExp) { + return options.url.test(url); + } + + if (typeof options.url === 'function') { + return options.url(url); + } + + return false; +} const flatten = arr => arr.reduce((acc, d) => [...acc, ...d], []); @@ -72,24 +101,23 @@ const uniq = arr => arr.reduce( [], ); -module.exports = postcss.plugin('postcss-icss-url', () => (css, result) => { - const traversed = walkDeclsWithUrl(css, filterValues); +module.exports = postcss.plugin('postcss-icss-url', (options) => (css, result) => { + const traversed = walkDeclsWithUrl(css, filter, options); const paths = uniq(flatten(traversed.map(item => item.values))); - // ICSS imports {String} const aliases = {}; paths.forEach((url, idx) => { - // ICSS Placeholder + // CSS Content Placeholder const alias = '${' + `CSS__URL__${idx}` + '}'; - + aliases[url] = alias; result.messages.push({ type: 'import', plugin: 'postcss-icss-url', import: `import CSS__URL__${idx} from '${url}';\n` - }) + }); }); traversed.forEach((item) => { @@ -98,3 +126,4 @@ module.exports = postcss.plugin('postcss-icss-url', () => (css, result) => { item.decl.value = item.parsed.toString(); }); }); + diff --git a/test/fixtures/imports/fixture.js b/test/fixtures/imports/fixture.js index 916b5df3..8c459de3 100644 --- a/test/fixtures/imports/fixture.js +++ b/test/fixtures/imports/fixture.js @@ -1 +1,3 @@ import css from './fixture.css'; + +export default css; diff --git a/test/fixtures/urls/filter/fixture.css b/test/fixtures/urls/filter/fixture.css new file mode 100644 index 00000000..7654a9a6 --- /dev/null +++ b/test/fixtures/urls/filter/fixture.css @@ -0,0 +1,4 @@ +.url { + background: url('./file.png'); + background: url('./filter/file.png'); +} diff --git a/test/fixtures/urls/filter/fixture.js b/test/fixtures/urls/filter/fixture.js new file mode 100644 index 00000000..8c459de3 --- /dev/null +++ b/test/fixtures/urls/filter/fixture.js @@ -0,0 +1,3 @@ +import css from './fixture.css'; + +export default css; diff --git a/test/fixtures/urls/fixture.js b/test/fixtures/urls/fixture.js index 2462b27e..8c459de3 100644 --- a/test/fixtures/urls/fixture.js +++ b/test/fixtures/urls/fixture.js @@ -1,2 +1,3 @@ import css from './fixture.css'; +export default css; diff --git a/test/loader.test.js b/test/loader.test.js index b933c97f..5ef5b316 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -14,7 +14,7 @@ describe('Loader', () => { const stats = await webpack('fixture.js', config); const { source } = stats.toJson().modules[1]; - + expect(source).toMatchSnapshot(); }); }); diff --git a/test/options/__snapshots__/url.test.js.snap b/test/options/__snapshots__/url.test.js.snap index 72fad704..dfc1302c 100644 --- a/test/options/__snapshots__/url.test.js.snap +++ b/test/options/__snapshots__/url.test.js.snap @@ -11,3 +11,29 @@ export default \`.url { } \`" `; + +exports[`Options url {Function} 1`] = ` +"// CSS Imports +import CSS__URL__0 from './file.png'; + + +// CSS +export default \`.url { + background: url(\${CSS__URL__0}); + background: url('./filter/file.png'); +} +\`" +`; + +exports[`Options url {RegExp} 1`] = ` +"// CSS Imports +import CSS__URL__0 from './file.png'; + + +// CSS +export default \`.url { + background: url(\${CSS__URL__0}); + background: url('./filter/file.png'); +} +\`" +`; diff --git a/test/options/minimize.test.js b/test/options/minimize.test.js index 86cf88e3..88934808 100644 --- a/test/options/minimize.test.js +++ b/test/options/minimize.test.js @@ -10,7 +10,7 @@ describe('Options', () => { loader: { test: /\.css$/, options: { - minimize: true + minimize: true, }, }, }; @@ -21,4 +21,4 @@ describe('Options', () => { expect(source).toMatchSnapshot(); }); }); -}); \ No newline at end of file +}); diff --git a/test/options/sourceMap.test.js b/test/options/sourceMap.test.js index bb0059b6..29bd9ef6 100644 --- a/test/options/sourceMap.test.js +++ b/test/options/sourceMap.test.js @@ -1,5 +1,6 @@ /* eslint-disable prefer-destructuring, + no-param-reassign, no-underscore-dangle, */ import path from 'path'; @@ -21,14 +22,13 @@ describe('Options', () => { const { map } = stats.compilation.modules[1]._source.sourceAndMap(); // Strip host specific paths for CI - map.sources = map.sources - .map((src) => { - src = src.split('!'); + map.sources = map.sources.map((src) => { + src = src.split('!'); - src[1] = path.relative(__dirname, src[1]); + src[1] = path.relative(__dirname, src[1]); - return src.join('!'); - }); + return src.join('!'); + }); expect(map).toMatchSnapshot(); }); diff --git a/test/options/url.test.js b/test/options/url.test.js index 1baf51e9..2583497c 100644 --- a/test/options/url.test.js +++ b/test/options/url.test.js @@ -18,5 +18,39 @@ describe('Options', () => { expect(source).toMatchSnapshot(); }); + + test('{RegExp}', async () => { + const config = { + loader: { + test: /\.css$/, + options: { + url: /filter/, + }, + }, + }; + + const stats = await webpack('urls/filter/fixture.js', config); + const { source } = stats.toJson().modules[1]; + + expect(source).toMatchSnapshot(); + }); + + test('{Function}', async () => { + const config = { + loader: { + test: /\.css$/, + options: { + url(url) { + return /filter/.test(url); + }, + }, + }, + }; + + const stats = await webpack('urls/filter/fixture.js', config); + const { source } = stats.toJson().modules[1]; + + expect(source).toMatchSnapshot(); + }); }); }); diff --git a/test/runtime.test.js b/test/runtime.test.js index 197b937f..f9799925 100644 --- a/test/runtime.test.js +++ b/test/runtime.test.js @@ -58,25 +58,32 @@ describe('Runtime', () => { m.push(m1); - expect(m.toString()).toEqual('body { b: 2; }' + - 'body { c: 3; }' + - '@media print{body { d: 4; }}' + - '@media screen{body { a: 1; }}'); + expect(m.toString()).toEqual( + 'body { b: 2; }' + + 'body { c: 3; }' + + '@media print{body { d: 4; }}' + + '@media screen{body { a: 1; }}' + ); }); test('should toString with source mapping', () => { const m = runtime(true); - m.push([1, 'body { a: 1; }', '', { - file: 'test.scss', - sources: [ - './path/to/test.scss', - ], - mappings: 'AAAA;', - sourceRoot: 'webpack://', - }]); - - expect(m.toString()).toEqual('body { a: 1; }\n/*# sourceURL=webpack://./path/to/test.scss */\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJmaWxlIjoidGVzdC5zY3NzIiwic291cmNlcyI6WyIuL3BhdGgvdG8vdGVzdC5zY3NzIl0sIm1hcHBpbmdzIjoiQUFBQTsiLCJzb3VyY2VSb290Ijoid2VicGFjazovLyJ9 */'); + m.push([ + 1, + 'body { a: 1; }', + '', + { + file: 'test.scss', + sources: ['./path/to/test.scss'], + mappings: 'AAAA;', + sourceRoot: 'webpack://', + }, + ]); + + expect(m.toString()).toEqual( + 'body { a: 1; }\n/*# sourceURL=webpack://./path/to/test.scss */\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJmaWxlIjoidGVzdC5zY3NzIiwic291cmNlcyI6WyIuL3BhdGgvdG8vdGVzdC5zY3NzIl0sIm1hcHBpbmdzIjoiQUFBQTsiLCJzb3VyY2VSb290Ijoid2VicGFjazovLyJ9 */' + ); }); test('should toString without source mapping if btoa not avalibale', () => { @@ -84,14 +91,17 @@ describe('Runtime', () => { const m = runtime(true); - m.push([1, 'body { a: 1; }', '', { - file: 'test.scss', - sources: [ - './path/to/test.scss', - ], - mappings: 'AAAA;', - sourceRoot: 'webpack://', - }]); + m.push([ + 1, + 'body { a: 1; }', + '', + { + file: 'test.scss', + sources: ['./path/to/test.scss'], + mappings: 'AAAA;', + sourceRoot: 'webpack://', + }, + ]); expect(m.toString()).toEqual('body { a: 1; }'); });