From 5e6034cee840d47dbfd9fdede87a152e4cfc466a Mon Sep 17 00:00:00 2001 From: Evilebot Tnawi Date: Fri, 7 Dec 2018 01:01:15 +0300 Subject: [PATCH] feat: allow to filter import at-rules (#857) --- README.md | 78 +++-- src/index.js | 6 +- src/options.json | 9 +- test/__snapshots__/errors.test.js.snap | 2 + test/__snapshots__/import-option.test.js.snap | 271 ++++++++++++++++++ test/errors.test.js | 1 + test/import-option.test.js | 27 ++ 7 files changed, 375 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1a5c79ef..8d6885a8 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ module.exports = { | Name | Type | Default | Description | | :-----------------------------------------: | :-------------------: | :-------------: | :------------------------------------------ | | **[`url`](#url)** | `{Boolean\|Function}` | `true` | Enable/Disable `url()` handling | -| **[`import`](#import)** | `{Boolean}` | `true` | Enable/Disable @import handling | +| **[`import`](#import)** | `{Boolean\/Function}` | `true` | Enable/Disable @import handling | | **[`modules`](#modules)** | `{Boolean\|String}` | `false` | Enable/Disable CSS Modules and setup mode | | **[`localIdentName`](#localidentname)** | `{String}` | `[hash:base64]` | Configure the generated ident | | **[`sourceMap`](#sourcemap)** | `{Boolean}` | `false` | Enable/Disable Sourcemaps | @@ -130,19 +130,24 @@ module.exports = { Type: `Boolean|Function` Default: `true` -Control `url()` resolving. Absolute `urls` are not resolving by default. +Control `url()` resolving. Absolute urls are not resolving. Examples resolutions: ``` url(image.png) => require('./image.png') +url('image.png') => require('./image.png') url(./image.png) => require('./image.png') +url('./image.png') => require('./image.png') +url('http://dontwritehorriblecode.com/2112.png') => require('http://dontwritehorriblecode.com/2112.png') +image-set(url('image2x.png') 1x, url('image1x.png') 2x) => require('./image1x.png') and require('./image2x.png') ``` To import assets from a `node_modules` path (include `resolve.modules`) and for `alias`, prefix it with a `~`: ``` url(~module/image.png) => require('module/image.png') +url('~module/image.png') => require('module/image.png') url(~aliasDirectory/image.png) => require('otherDirectory/image.png') ``` @@ -170,7 +175,9 @@ module.exports = { #### `Function` -Allow to filter `url()`. All filtered `url()` will not be resolved. +Allow to filter `url()`. All filtered `url()` will not be resolved (left in the code as they were written). + +**webpack.config.js** ```js module.exports = { @@ -181,6 +188,8 @@ module.exports = { loader: 'css-loader', options: { url: (url, resourcePath) => { + // resourcePath - path to css file + // `url()` with `img.png` stay untouched return url.includes('img.png'); }, @@ -196,7 +205,31 @@ module.exports = { Type: `Boolean` Default: `true` -Enable/disable `@import` resolving. Absolute urls are not resolving. +Control `@import` resolving. Absolute urls in `@import` will be moved in runtime code. + +Examples resolutions: + +``` +@import 'style.css' => require('./style.css') +@import url(style.css) => require('./style.css') +@import url('style.css') => require('./style.css') +@import './style.css' => require('./style.css') +@import url(./style.css) => require('./style.css') +@import url('./style.css') => require('./style.css') +@import url('http://dontwritehorriblecode.com/style.css') => @import url('http://dontwritehorriblecode.com/style.css') in runtime +``` + +To import styles from a `node_modules` path (include `resolve.modules`) and for `alias`, prefix it with a `~`: + +``` +@import url(~module/style.css) => require('module/style.css') +@import url('~module/style.css') => require('module/style.css') +@import url(~aliasDirectory/style.css) => require('otherDirectory/style.css') +``` + +#### `Boolean` + +Enable/disable `@import` resolving. **webpack.config.js** @@ -216,22 +249,33 @@ module.exports = { }; ``` -Examples resolutions: +#### `Function` -``` -@import 'style.css' => require('./style.css') -@import url(style.css) => require('./style.css') -@import url('style.css') => require('./style.css') -@import './style.css' => require('./style.css') -@import url(./style.css) => require('./style.css') -@import url('./style.css') => require('./style.css') -``` +Allow to filter `@import`. All filtered `@import` will not be resolved (left in the code as they were written). -To import styles from a `node_modules` path (include `resolve.modules`) and for `alias`, prefix it with a `~`: +**webpack.config.js** -``` -@import url(~module/style.css) => require('module/style.css') -@import url(~aliasDirectory/style.css) => require('otherDirectory/style.css') +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/, + loader: 'css-loader', + options: { + import: (parsedImport, resourcePath) => { + // parsedImport.url - url of `@import` + // parsedImport.media - media query of `@import` + // resourcePath - path to css file + + // `@import` with `style.css` stay untouched + return parsedImport.url.includes('style.css'); + }, + }, + }, + ], + }, +}; ``` ### [`modules`](https://github.com/css-modules/css-modules) diff --git a/src/index.js b/src/index.js index 2413e360..60b74c83 100644 --- a/src/index.js +++ b/src/index.js @@ -99,7 +99,11 @@ export default function loader(content, map, meta) { } if (options.import !== false) { - plugins.push(importParser()); + plugins.push( + importParser({ + filter: getFilter(options.import, this.resourcePath), + }) + ); } if (options.url !== false) { diff --git a/src/options.json b/src/options.json index 8aa4f818..9100eefa 100644 --- a/src/options.json +++ b/src/options.json @@ -12,7 +12,14 @@ ] }, "import": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "instanceof": "Function" + } + ] }, "modules": { "anyOf": [ diff --git a/test/__snapshots__/errors.test.js.snap b/test/__snapshots__/errors.test.js.snap index 3b06b7c0..210dfcfb 100644 --- a/test/__snapshots__/errors.test.js.snap +++ b/test/__snapshots__/errors.test.js.snap @@ -13,6 +13,8 @@ exports[`validation 2`] = ` "CSS Loader Invalid Options options.import should be boolean +options.import should pass \\"instanceof\\" keyword validation +options.import should match some schema in anyOf " `; diff --git a/test/__snapshots__/import-option.test.js.snap b/test/__snapshots__/import-option.test.js.snap index d7bd79ee..704c33ff 100644 --- a/test/__snapshots__/import-option.test.js.snap +++ b/test/__snapshots__/import-option.test.js.snap @@ -1,5 +1,276 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`import option Function: errors 1`] = `Array []`; + +exports[`import option Function: module (evaluated) 1`] = ` +Array [ + Array [ + 3, + "a { + b: b; +} +", + "((min-width: 100px)) and (screen and print)", + ], + Array [ + 2, + ".test { + c: c; +} +", + "screen and print", + ], + Array [ + 1, + "@import url(http://example.com/style.css);", + "", + ], + Array [ + 1, + "@import url(http://example.com/style.css#hash);", + "", + ], + Array [ + 1, + "@import url(http://example.com/style.css?#hash);", + "", + ], + Array [ + 1, + "@import url(http://example.com/style.css?foo=bar#hash);", + "", + ], + Array [ + 1, + "@import url(http://example.com/other-style.css);", + "screen and print", + ], + Array [ + 1, + "@import url(//example.com/style.css);", + "", + ], + Array [ + 4, + ".query { + e: e; +} +", + "", + ], + Array [ + 5, + ".other-query { + f: f; +} +", + "", + ], + Array [ + 6, + ".other-query { + f: f; +} +", + "screen and print", + ], + Array [ + 1, + "@import url(https://fonts.googleapis.com/css?family=Roboto);", + "", + ], + Array [ + 1, + "@import url(https://fonts.googleapis.com/css?family=Noto+Sans+TC);", + "", + ], + Array [ + 1, + "@import url(https://fonts.googleapis.com/css?family=Noto+Sans+TC|Roboto);", + "", + ], + Array [ + 7, + ".relative { + color: red; +} +", + "", + ], + Array [ + 8, + ".top-relative { + color: black; +} +", + "", + ], + Array [ + 9, + ".tilde { + color: yellow; +} +", + "", + ], + Array [ + 10, + ".alias { + color: red; +} +", + "", + ], + Array [ + 11, + ".background-imported { + background: url(/webpack/public/path/img.png); +} +", + "", + ], + Array [ + 1, + "@import url(test.css); +@import url('test.css'); +@import url(\\"test.css\\"); +@IMPORT url(test.css); +@import URL(test.css); +@import url(test.css ); +@import url( test.css); +@import url( test.css ); +@import url( + test.css +); +@import url(); +@import url(''); +@import url(\\"\\"); +@import \\"test.css\\"; +@import 'test.css'; +@import ''; +@import \\"\\"; +@import \\" \\"; +@import \\" +\\"; +@import url(); +@import url(''); +@import url(\\"\\"); +@import url(test.css) screen and print; +@import url(test.css) SCREEN AND PRINT; +@import url(test.css)screen and print; +@import url(test.css) screen and print; +@import url(~package/test.css); +@import ; +@import foo-bar; +@import-normalize; +@import url('http://') :root {} + +.class { + a: b c d; +} + +.foo { + @import 'path.css'; +} + +.background { + background: url(/webpack/public/path/img.png); +} +", + "", + ], +] +`; + +exports[`import option Function: module 1`] = ` +"exports = module.exports = require(\\"../../../src/runtime/api.js\\")(false); +// Imports +exports.i(require(\\"-!../../../src/index.js??ref--4-0!./test-media.css\\"), \\"screen and print\\"); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!./test-other.css\\"), \\"(min-width: 100px)\\"); +exports.push([module.id, \\"@import url(http://example.com/style.css);\\", \\"\\"]); +exports.push([module.id, \\"@import url(http://example.com/style.css#hash);\\", \\"\\"]); +exports.push([module.id, \\"@import url(http://example.com/style.css?#hash);\\", \\"\\"]); +exports.push([module.id, \\"@import url(http://example.com/style.css?foo=bar#hash);\\", \\"\\"]); +exports.push([module.id, \\"@import url(http://example.com/other-style.css);\\", \\"screen and print\\"]); +exports.push([module.id, \\"@import url(//example.com/style.css);\\", \\"\\"]); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!./query.css?foo=1&bar=1\\"), \\"\\"); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!./other-query.css?foo=1&bar=1#hash\\"), \\"\\"); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!./other-query.css?foo=1&bar=1#hash\\"), \\"screen and print\\"); +exports.push([module.id, \\"@import url(https://fonts.googleapis.com/css?family=Roboto);\\", \\"\\"]); +exports.push([module.id, \\"@import url(https://fonts.googleapis.com/css?family=Noto+Sans+TC);\\", \\"\\"]); +exports.push([module.id, \\"@import url(https://fonts.googleapis.com/css?family=Noto+Sans+TC|Roboto);\\", \\"\\"]); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!./relative.css\\"), \\"\\"); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!../import/top-relative.css\\"), \\"\\"); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!package/tilde.css\\"), \\"\\"); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!aliasesImport/alias.css\\"), \\"\\"); +exports.i(require(\\"-!../../../src/index.js??ref--4-0!./url.css\\"), \\"\\"); +var urlEscape = require(\\"../../../src/runtime/url-escape.js\\"); +var ___CSS_LOADER_URL___0___ = urlEscape(require(\\"./img.png\\")); + +// Module +exports.push([module.id, \\"@import url(test.css);\\\\n@import url('test.css');\\\\n@import url(\\\\\\"test.css\\\\\\");\\\\n@IMPORT url(test.css);\\\\n@import URL(test.css);\\\\n@import url(test.css );\\\\n@import url( test.css);\\\\n@import url( test.css );\\\\n@import url(\\\\n test.css\\\\n);\\\\n@import url();\\\\n@import url('');\\\\n@import url(\\\\\\"\\\\\\");\\\\n@import \\\\\\"test.css\\\\\\";\\\\n@import 'test.css';\\\\n@import '';\\\\n@import \\\\\\"\\\\\\";\\\\n@import \\\\\\" \\\\\\";\\\\n@import \\\\\\"\\\\n\\\\\\";\\\\n@import url();\\\\n@import url('');\\\\n@import url(\\\\\\"\\\\\\");\\\\n@import url(test.css) screen and print;\\\\n@import url(test.css) SCREEN AND PRINT;\\\\n@import url(test.css)screen and print;\\\\n@import url(test.css) screen and print;\\\\n@import url(~package/test.css);\\\\n@import ;\\\\n@import foo-bar;\\\\n@import-normalize;\\\\n@import url('http://') :root {}\\\\n\\\\n.class {\\\\n a: b c d;\\\\n}\\\\n\\\\n.foo {\\\\n @import 'path.css';\\\\n}\\\\n\\\\n.background {\\\\n background: url(\\" + ___CSS_LOADER_URL___0___ + \\");\\\\n}\\\\n\\", \\"\\"]); + +" +`; + +exports[`import option Function: warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(12:1) Unable to find uri in '@import url()'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(13:1) Unable to find uri in '@import url('')'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(14:1) Unable to find uri in '@import url(\\"\\")'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(17:1) Unable to find uri in '@import '''", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(18:1) Unable to find uri in '@import \\"\\"'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(19:1) Unable to find uri in '@import \\" \\"'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(20:1) Unable to find uri in '@import \\" +\\"'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(22:1) Unable to find uri in '@import url()'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(23:1) Unable to find uri in '@import url('')'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(24:1) Unable to find uri in '@import url(\\"\\")'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(40:1) Unable to find uri in '@import '", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(41:1) Unable to find uri in '@import foo-bar'", + "ModuleWarning: Module Warning (from \`replaced original path\`): +Warning + +(43:1) It looks like you didn't end your @import statement correctly. Child nodes are attached to it.", +] +`; + exports[`import option false: errors 1`] = `Array []`; exports[`import option false: module (evaluated) 1`] = ` diff --git a/test/errors.test.js b/test/errors.test.js index 4a6d4a21..14b87d36 100644 --- a/test/errors.test.js +++ b/test/errors.test.js @@ -27,6 +27,7 @@ it('validation', () => { expect(() => validate({ import: true })).not.toThrow(); expect(() => validate({ import: false })).not.toThrow(); + expect(() => validate({ import: () => {} })).not.toThrow(); expect(() => validate({ import: 'true' })).toThrowErrorMatchingSnapshot(); expect(() => validate({ modules: true })).not.toThrow(); diff --git a/test/import-option.test.js b/test/import-option.test.js index 215f9cda..d4b946ba 100644 --- a/test/import-option.test.js +++ b/test/import-option.test.js @@ -54,4 +54,31 @@ describe('import option', () => { ); }); }); + + it('Function', async () => { + const config = { + loader: { + options: { + import: (parsedImport, resourcePath) => { + expect(typeof resourcePath === 'string').toBe(true); + + return parsedImport.url.includes('test.css'); + }, + }, + }, + }; + const testId = './import/import.css'; + const stats = await webpack(testId, config); + const { modules } = stats.toJson(); + const module = modules.find((m) => m.id === testId); + + expect(module.source).toMatchSnapshot('module'); + expect(evaluated(module.source, modules)).toMatchSnapshot( + 'module (evaluated)' + ); + expect(normalizeErrors(stats.compilation.warnings)).toMatchSnapshot( + 'warnings' + ); + expect(normalizeErrors(stats.compilation.errors)).toMatchSnapshot('errors'); + }); });