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 ea75f63e..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,15 +26,12 @@ 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'); + // Loader Mode (Async) const cb = this.async(); const file = this.resourcePath; @@ -47,14 +44,13 @@ export default function loader (css, map, meta) { map = false; } - // CSS Plugins const plugins = []; // URL Plugin if (options.url) { - plugins.push(urls()); + plugins.push(urls(options)); } - + // Import Plugin if (options.import) { plugins.push(imports()); @@ -64,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') @@ -73,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) { @@ -109,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) { @@ -129,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/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/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/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 916b5df3..8c459de3 100644 --- a/test/fixtures/urls/fixture.js +++ b/test/fixtures/urls/fixture.js @@ -1 +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; }'); });