From 0d8ac3bcb831bc747657c914aba106b93840737e Mon Sep 17 00:00:00 2001 From: cap-Bernardito Date: Mon, 6 Apr 2020 14:39:02 +0300 Subject: [PATCH] feat: the `modules.mode` option may be a function (#1065) --- README.md | 41 ++++++- src/options.json | 9 +- src/utils.js | 62 +++++++---- .../__snapshots__/modules-option.test.js.snap | 101 ++++++++++++++++++ .../validate-options.test.js.snap | 48 +++++++-- test/fixtures/modules/issue-1063/global.css | 7 ++ .../fixtures/modules/issue-1063/issue-1063.js | 6 ++ test/fixtures/modules/issue-1063/local.css | 7 ++ test/modules-option.test.js | 50 +++++++++ test/validate-options.test.js | 1 + 10 files changed, 299 insertions(+), 33 deletions(-) create mode 100644 test/fixtures/modules/issue-1063/global.css create mode 100644 test/fixtures/modules/issue-1063/issue-1063.js create mode 100644 test/fixtures/modules/issue-1063/local.css diff --git a/README.md b/README.md index 8020201e..737f15c5 100644 --- a/README.md +++ b/README.md @@ -541,11 +541,17 @@ module.exports = { ##### `mode` -Type: `String` +Type: `String|Function` Default: `'local'` Setup `mode` option. You can omit the value when you want `local` mode. +###### `String` + +Possible values: + +`local`, `global`, `pure` + **webpack.config.js** ```js @@ -566,6 +572,39 @@ module.exports = { }; ``` +###### `Function` + +Allows set different values for the `mode` option based on a filename + +Possible return values - `local`, `global` and `pure` + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: 'css-loader', + options: { + modules: { + // Callback must return "local", "global" or "pure" + mode: (filename) => { + if (/global.css$/i.test(filename)) { + return 'global'; + } + + return 'local'; + }, + }, + }, + }, + ], + }, +}; +``` + ##### `localIdentName` Type: `String` diff --git a/src/options.json b/src/options.json index 77015844..742c28ce 100644 --- a/src/options.json +++ b/src/options.json @@ -37,7 +37,14 @@ "additionalProperties": false, "properties": { "mode": { - "enum": ["local", "global", "pure"] + "anyOf": [ + { + "enum": ["local", "global", "pure"] + }, + { + "instanceof": "Function" + } + ] }, "localIdentName": { "type": "string" diff --git a/src/utils.js b/src/utils.js index cd53cbeb..0bfed952 100644 --- a/src/utils.js +++ b/src/utils.js @@ -115,25 +115,24 @@ function getModulesPlugins(options, loaderContext) { modulesOptions = Object.assign({}, modulesOptions, options.modules); } - return [ - modulesValues, - localByDefault({ mode: modulesOptions.mode }), - extractImports(), - modulesScope({ - generateScopedName: function generateScopedName(exportName) { - let localIdent = modulesOptions.getLocalIdent( - loaderContext, - modulesOptions.localIdentName, - exportName, - { - context: modulesOptions.context, - hashPrefix: modulesOptions.hashPrefix, - regExp: modulesOptions.localIdentRegExp, - } - ); + if (typeof modulesOptions.mode === 'function') { + const modeFromFunction = modulesOptions.mode(loaderContext.resourcePath); + + if (modeFromFunction === 'local' || modeFromFunction === 'global') { + modulesOptions.mode = modeFromFunction; + } + } + + let plugins = []; - if (!localIdent) { - localIdent = getLocalIdent( + try { + plugins = [ + modulesValues, + localByDefault({ mode: modulesOptions.mode }), + extractImports(), + modulesScope({ + generateScopedName: function generateScopedName(exportName) { + let localIdent = modulesOptions.getLocalIdent( loaderContext, modulesOptions.localIdentName, exportName, @@ -143,12 +142,29 @@ function getModulesPlugins(options, loaderContext) { regExp: modulesOptions.localIdentRegExp, } ); - } - return localIdent; - }, - }), - ]; + if (!localIdent) { + localIdent = getLocalIdent( + loaderContext, + modulesOptions.localIdentName, + exportName, + { + context: modulesOptions.context, + hashPrefix: modulesOptions.hashPrefix, + regExp: modulesOptions.localIdentRegExp, + } + ); + } + + return localIdent; + }, + }), + ]; + } catch (error) { + loaderContext.emitError(error); + } + + return plugins; } function normalizeSourceMap(map) { diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index 85e61ce9..ae8349df 100644 --- a/test/__snapshots__/modules-option.test.js.snap +++ b/test/__snapshots__/modules-option.test.js.snap @@ -363,6 +363,107 @@ div:not(.\\\\1F600) { exports[`"modules" option issue #995: warnings 1`] = `Array []`; +exports[`"modules" option issue #1063 throw error: errors 1`] = ` +Array [ + "ModuleError: Module Error (from \`replaced original path\`): +options.mode must be either \\"global\\", \\"local\\" or \\"pure\\" (default \\"local\\")", + "ModuleError: Module Error (from \`replaced original path\`): +options.mode must be either \\"global\\", \\"local\\" or \\"pure\\" (default \\"local\\")", +] +`; + +exports[`"modules" option issue #1063 throw error: module 1`] = ` +"// Imports +var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\"); +exports = ___CSS_LOADER_API_IMPORT___(false); +// Module +exports.push([module.id, \\".classNameLocalFile {\\\\n color: green;\\\\n}\\\\n\\\\n:global(.otherClassLocalFile) {\\\\n color: blue;\\\\n}\\\\n\\", \\"\\"]); +// Exports +module.exports = exports; +" +`; + +exports[`"modules" option issue #1063 throw error: module 2`] = ` +"// Imports +var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\"); +exports = ___CSS_LOADER_API_IMPORT___(false); +// Module +exports.push([module.id, \\".classNameGlobalFile {\\\\n color: black;\\\\n}\\\\n\\\\n:local(.otherClassGlobalFile) {\\\\n color: coral;\\\\n}\\\\n\\", \\"\\"]); +// Exports +module.exports = exports; +" +`; + +exports[`"modules" option issue #1063 throw error: result 1`] = ` +".classNameLocalFile { + color: green; +} + +:global(.otherClassLocalFile) { + color: blue; +} +.classNameGlobalFile { + color: black; +} + +:local(.otherClassGlobalFile) { + color: coral; +} +" +`; + +exports[`"modules" option issue #1063 throw error: warnings 1`] = `Array []`; + +exports[`"modules" option issue #1063: errors 1`] = `Array []`; + +exports[`"modules" option issue #1063: module 1`] = ` +"// Imports +var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\"); +exports = ___CSS_LOADER_API_IMPORT___(false); +// Module +exports.push([module.id, \\"._2cZDGA9F7Kx1vfQmWcLP51 {\\\\n color: green;\\\\n}\\\\n\\\\n.otherClassLocalFile {\\\\n color: blue;\\\\n}\\\\n\\", \\"\\"]); +// Exports +exports.locals = { + \\"classNameLocalFile\\": \\"_2cZDGA9F7Kx1vfQmWcLP51\\" +}; +module.exports = exports; +" +`; + +exports[`"modules" option issue #1063: module 2`] = ` +"// Imports +var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\"); +exports = ___CSS_LOADER_API_IMPORT___(false); +// Module +exports.push([module.id, \\".classNameGlobalFile {\\\\n color: black;\\\\n}\\\\n\\\\n._2rcag09JpwrP4_hfyyRmm- {\\\\n color: coral;\\\\n}\\\\n\\", \\"\\"]); +// Exports +exports.locals = { + \\"otherClassGlobalFile\\": \\"_2rcag09JpwrP4_hfyyRmm-\\" +}; +module.exports = exports; +" +`; + +exports[`"modules" option issue #1063: result 1`] = ` +"._2cZDGA9F7Kx1vfQmWcLP51 { + color: green; +} + +.otherClassLocalFile { + color: blue; +} +.classNameGlobalFile { + color: black; +} + +._2rcag09JpwrP4_hfyyRmm- { + color: coral; +} +" +`; + +exports[`"modules" option issue #1063: warnings 1`] = `Array []`; + exports[`"modules" option should avoid unnecessary "require": errors 1`] = `Array []`; exports[`"modules" option should avoid unnecessary "require": module 1`] = ` diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index f65d36cc..e7f0351f 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -86,26 +86,58 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":"globals"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - - options.modules.mode should be one of these: - \\"local\\" | \\"global\\" | \\"pure\\"" + - options.modules should be one of these: + boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? } + -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules). + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" | function + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" + * options.modules.mode should be an instance of function." `; exports[`validate options should throw an error on the "modules" option with "{"mode":"locals"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - - options.modules.mode should be one of these: - \\"local\\" | \\"global\\" | \\"pure\\"" + - options.modules should be one of these: + boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? } + -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules). + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" | function + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" + * options.modules.mode should be an instance of function." `; exports[`validate options should throw an error on the "modules" option with "{"mode":"pures"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - - options.modules.mode should be one of these: - \\"local\\" | \\"global\\" | \\"pure\\"" + - options.modules should be one of these: + boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? } + -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules). + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" | function + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" + * options.modules.mode should be an instance of function." `; exports[`validate options should throw an error on the "modules" option with "{"mode":true}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - - options.modules.mode should be one of these: - \\"local\\" | \\"global\\" | \\"pure\\"" + - options.modules should be one of these: + boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { mode?, localIdentName?, localIdentRegExp?, context?, hashPrefix?, getLocalIdent? } + -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules). + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" | function + Details: + * options.modules.mode should be one of these: + \\"local\\" | \\"global\\" | \\"pure\\" + * options.modules.mode should be an instance of function." `; exports[`validate options should throw an error on the "modules" option with "globals" value 1`] = ` diff --git a/test/fixtures/modules/issue-1063/global.css b/test/fixtures/modules/issue-1063/global.css new file mode 100644 index 00000000..95eb9774 --- /dev/null +++ b/test/fixtures/modules/issue-1063/global.css @@ -0,0 +1,7 @@ +.classNameGlobalFile { + color: black; +} + +:local(.otherClassGlobalFile) { + color: coral; +} diff --git a/test/fixtures/modules/issue-1063/issue-1063.js b/test/fixtures/modules/issue-1063/issue-1063.js new file mode 100644 index 00000000..c602c8c7 --- /dev/null +++ b/test/fixtures/modules/issue-1063/issue-1063.js @@ -0,0 +1,6 @@ +import css1 from './local.css'; +import css2 from './global.css'; + +__export__ = css1 + css2; + +export default css1 + css2; diff --git a/test/fixtures/modules/issue-1063/local.css b/test/fixtures/modules/issue-1063/local.css new file mode 100644 index 00000000..f68ef05a --- /dev/null +++ b/test/fixtures/modules/issue-1063/local.css @@ -0,0 +1,7 @@ +.classNameLocalFile { + color: green; +} + +:global(.otherClassLocalFile) { + color: blue; +} diff --git a/test/modules-option.test.js b/test/modules-option.test.js index 6ba9d5fb..b62a7084 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -565,4 +565,54 @@ describe('"modules" option', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats)).toMatchSnapshot('errors'); }); + + it('issue #1063', async () => { + const compiler = getCompiler('./modules/issue-1063/issue-1063.js', { + modules: { + mode: (resourcePath) => { + if (/global.css$/i.test(resourcePath)) { + return 'global'; + } + + return 'local'; + }, + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource('./modules/issue-1063/local.css', stats) + ).toMatchSnapshot('module'); + expect( + getModuleSource('./modules/issue-1063/global.css', stats) + ).toMatchSnapshot('module'); + expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot( + 'result' + ); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('issue #1063 throw error', async () => { + const compiler = getCompiler('./modules/issue-1063/issue-1063.js', { + modules: { + mode: () => { + return 'not local, global or pure'; + }, + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource('./modules/issue-1063/local.css', stats) + ).toMatchSnapshot('module'); + expect( + getModuleSource('./modules/issue-1063/global.css', stats) + ).toMatchSnapshot('module'); + expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot( + 'result' + ); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); }); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 969765e4..db5ad6cd 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -20,6 +20,7 @@ describe('validate options', () => { { mode: 'global' }, { mode: 'local' }, { mode: 'pure' }, + { mode: () => 'local' }, { localIdentName: '[path][name]__[local]--[hash:base64:5]' }, { context: 'context' }, { hashPrefix: 'hash' },