From 392848869c181022be12d93d5e43ec055289e109 Mon Sep 17 00:00:00 2001 From: cap-Bernardito Date: Mon, 19 Jul 2021 17:14:52 +0300 Subject: [PATCH] feat: namedexport option could be a function --- src/index.js | 12 +- src/options.json | 9 +- src/utils.js | 26 ++- .../__snapshots__/modules-option.test.js.snap | 149 ++++++++++++++++++ .../validate-options.test.js.snap | 15 +- .../modules/namedExport/template-2/index.css | 11 ++ .../modules/namedExport/template-2/index.js | 8 + .../namedExport/template-2/template.js | 11 ++ test/modules-option.test.js | 71 +++++++++ test/validate-options.test.js | 1 + 10 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/modules/namedExport/template-2/index.css create mode 100644 test/fixtures/modules/namedExport/template-2/index.js create mode 100644 test/fixtures/modules/namedExport/template-2/template.js diff --git a/src/index.js b/src/index.js index 7b20b92ca..4945d634d 100644 --- a/src/index.js +++ b/src/index.js @@ -216,7 +216,17 @@ export default async function loader(content, map, meta) { } const importCode = getImportCode(imports, options); - const moduleCode = getModuleCode(result, api, replacements, options, this); + + let moduleCode; + + try { + moduleCode = getModuleCode(result, api, replacements, options, this); + } catch (error) { + callback(error); + + return; + } + const exportCode = getExportCode( exports, replacements, diff --git a/src/options.json b/src/options.json index ddbffb7a4..2fb5ce09d 100644 --- a/src/options.json +++ b/src/options.json @@ -135,7 +135,14 @@ "namedExport": { "description": "Enables/disables ES modules named export for locals.", "link": "https://github.com/webpack-contrib/css-loader#namedexport", - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "instanceof": "Function" + } + ] }, "exportGlobals": { "description": "Allows to export names from global class or id, so you can use that as local name.", diff --git a/src/utils.js b/src/utils.js index 30b796afd..bddcfd96f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -484,7 +484,11 @@ function getFilter(filter, resourcePath) { }; } -function getValidLocalName(localName, exportLocalsConvention) { +function getValidLocalName(localName, exportLocalsConvention, namedExportFn) { + if (namedExportFn) { + return namedExportFn(localName); + } + if (exportLocalsConvention === "dashesOnly") { return dashesCamelCase(localName); } @@ -546,6 +550,10 @@ function getModulesOptions(rawOptions, loaderContext) { ...rawModulesOptions, }; + if (typeof modulesOptions.namedExport === "function") { + modulesOptions.namedExportFn = modulesOptions.namedExport; + } + if (typeof modulesOptions.auto === "boolean") { const isModules = modulesOptions.auto && IS_MODULES.test(resourcePath); @@ -914,7 +922,8 @@ function getModuleCode(result, api, replacements, options, loaderContext) { ? `" + ${importName}_NAMED___[${JSON.stringify( getValidLocalName( localName, - options.modules.exportLocalsConvention + options.modules.exportLocalsConvention, + options.modules.namedExportFn ) )}] + "` : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` @@ -970,6 +979,13 @@ function getExportCode(exports, replacements, needToUseIcssPlugin, options) { }; for (const { name, value } of exports) { + if (options.modules.namedExportFn) { + addExportToLocalsCode(options.modules.namedExportFn(name), value); + + // eslint-disable-next-line no-continue + continue; + } + switch (options.modules.exportLocalsConvention) { case "camelCase": { addExportToLocalsCode(name, value); @@ -1015,7 +1031,11 @@ function getExportCode(exports, replacements, needToUseIcssPlugin, options) { localsCode = localsCode.replace(new RegExp(replacementName, "g"), () => { if (options.modules.namedExport) { return `" + ${importName}_NAMED___[${JSON.stringify( - getValidLocalName(localName, options.modules.exportLocalsConvention) + getValidLocalName( + localName, + options.modules.exportLocalsConvention, + options.modules.namedExportFn + ) )}] + "`; } else if (options.modules.exportOnlyLocals) { return `" + ${importName}[${JSON.stringify(localName)}] + "`; diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index 76a91c778..450220520 100644 --- a/test/__snapshots__/modules-option.test.js.snap +++ b/test/__snapshots__/modules-option.test.js.snap @@ -1749,6 +1749,15 @@ Error: The \\"modules.namedExport\\" option requires the \\"esModules\\" option exports[`"modules" option should throw an error when the "namedExport" option is "true", but the "esModule" is "false": warnings 1`] = `Array []`; +exports[`"modules" option should throw error when the "namedExport" function throw error: errors 1`] = ` +Array [ + "ModuleBuildError: Module build failed (from \`replaced original path\`): +Error: namedExportFn error", +] +`; + +exports[`"modules" option should throw error when the "namedExport" function throw error: warnings 1`] = `Array []`; + exports[`"modules" option should throw error with composes when the "namedExport" is enabled and "exportLocalsConvention" options has invalid value: errors 1`] = ` Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): @@ -4606,6 +4615,52 @@ h1 #pWzFEVR2SnlD5kUmOw_N { exports[`"modules" option should work and support "pure" mode: warnings 1`] = `Array []`; +exports[`"modules" option should work js template with "namedExport" option when "namedExport" option is function: errors 1`] = `Array []`; + +exports[`"modules" option should work js template with "namedExport" option when "namedExport" option is function: module 1`] = ` +"// Imports +import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \\".header-baz {\\\\n color: red;\\\\n}\\\\n\\\\n.body {\\\\n color: coral;\\\\n}\\\\n\\\\n.footer {\\\\n color: blue;\\\\n}\\\\n\\", \\"\\"]); +// Exports +export var header_baz_TEST = \\"header-baz\\"; +export var body_TEST = \\"body\\"; +export var footer_TEST = \\"footer\\"; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work js template with "namedExport" option when "namedExport" option is function: result 1`] = ` +Object { + "css": Array [ + Array [ + "./modules/namedExport/template-2/index.css", + ".header-baz { + color: red; +} + +.body { + color: coral; +} + +.footer { + color: blue; +} +", + "", + ], + ], + "html": " +
+
+
+", +} +`; + +exports[`"modules" option should work js template with "namedExport" option when "namedExport" option is function: warnings 1`] = `Array []`; + exports[`"modules" option should work js template with "namedExport" option: errors 1`] = `Array []`; exports[`"modules" option should work js template with "namedExport" option: module 1`] = ` @@ -4805,6 +4860,39 @@ Array [ exports[`"modules" option should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value: warnings 1`] = `Array []`; +exports[`"modules" option should work when the "namedExport" option is function: errors 1`] = `Array []`; + +exports[`"modules" option should work when the "namedExport" option is function: module 1`] = ` +"// Imports +import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \\"._pV82SQbfroU2_cQrb3p {\\\\n color: red;\\\\n}\\\\n\\\\n.bar {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]); +// Exports +export var bar_baz_TEST = \\"_pV82SQbfroU2_cQrb3p\\"; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work when the "namedExport" option is function: result 1`] = ` +Array [ + Array [ + "./modules/namedExport/base/index.css", + "._pV82SQbfroU2_cQrb3p { + color: red; +} + +.bar { + color: red; +} +", + "", + ], +] +`; + +exports[`"modules" option should work when the "namedExport" option is function: warnings 1`] = `Array []`; + exports[`"modules" option should work with "exportOnlyLocals" and "esModule" with "false" value options: errors 1`] = `Array []`; exports[`"modules" option should work with "exportOnlyLocals" and "esModule" with "false" value options: module 1`] = ` @@ -13473,6 +13561,67 @@ Array [ exports[`"modules" option should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value: warnings 1`] = `Array []`; +exports[`"modules" option should work with composes when the "namedExport" is function: errors 1`] = `Array []`; + +exports[`"modules" option should work with composes when the "namedExport" is function: module 1`] = ` +"// Imports +import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\"; +import ___CSS_LOADER_ICSS_IMPORT_0___, * as ___CSS_LOADER_ICSS_IMPORT_0____NAMED___ from \\"-!../../../../../src/index.js??ruleSet[1].rules[0].use[0]!./values.css\\"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); +___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, \\"\\", true); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \\"._ghi {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def_TEST\\"] + \\";\\\\n}\\\\n\\\\n._my-class {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"s_white_TEST\\"] + \\";\\\\n}\\\\n\\\\n._other {\\\\n display: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"m_small_TEST\\"] + \\";\\\\n}\\\\n\\\\n._other-other {\\\\n width: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def_TEST\\"] + \\";\\\\n}\\\\n\\\\n._green {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_other_other_TEST\\"] + \\";\\\\n}\\\\n\\", \\"\\"]); +// Exports +export var v_def_TEST = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def_TEST\\"] + \\"\\"; +export var v_other_other_TEST = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_other_other_TEST\\"] + \\"\\"; +export var s_white_TEST = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"s_white_TEST\\"] + \\"\\"; +export var m_small_TEST = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"m_small_TEST\\"] + \\"\\"; +export var ghi_TEST = \\"_ghi\\"; +export var my_class_TEST = \\"_my-class\\"; +export var other_TEST = \\"_other\\"; +export var other_other_TEST = \\"_other-other\\"; +export var green_TEST = \\"_green\\"; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work with composes when the "namedExport" is function: result 1`] = ` +Array [ + Array [ + "../../src/index.js??ruleSet[1].rules[0].use[0]!./modules/namedExport/composes/values.css", + " +", + "", + ], + Array [ + "./modules/namedExport/composes/composes.css", + "._ghi { + color: red; +} + +._my-class { + color: white; +} + +._other { + display: (min-width: 320px); +} + +._other-other { + width: red; +} + +._green { + color: green; +} +", + "", + ], +] +`; + +exports[`"modules" option should work with composes when the "namedExport" is function: warnings 1`] = `Array []`; + exports[`"modules" option should work with the "[local]" placeholder for the "localIdentName" option: errors 1`] = `Array []`; exports[`"modules" option should work with the "[local]" placeholder for the "localIdentName" option: module 1`] = ` diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 1e3a942a0..92b2ad622 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -219,9 +219,18 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"namedExport":"invalid"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - - options.modules.namedExport should be a boolean. - -> Enables/disables ES modules named export for locals. - -> Read more at https://github.com/webpack-contrib/css-loader#namedexport" + - options.modules should be one of these: + boolean | \\"local\\" | \\"global\\" | \\"pure\\" | \\"icss\\" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + -> Allows to enable/disable CSS Modules or ICSS and setup configuration. + -> Read more at https://github.com/webpack-contrib/css-loader#modules + Details: + * options.modules.namedExport should be one of these: + boolean | function + -> Enables/disables ES modules named export for locals. + -> Read more at https://github.com/webpack-contrib/css-loader#namedexport + Details: + * options.modules.namedExport should be a boolean. + * options.modules.namedExport 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/namedExport/template-2/index.css b/test/fixtures/modules/namedExport/template-2/index.css new file mode 100644 index 000000000..b3ccc301c --- /dev/null +++ b/test/fixtures/modules/namedExport/template-2/index.css @@ -0,0 +1,11 @@ +:local(.header-baz) { + color: red; +} + +:local(.body) { + color: coral; +} + +:local(.footer) { + color: blue; +} diff --git a/test/fixtures/modules/namedExport/template-2/index.js b/test/fixtures/modules/namedExport/template-2/index.js new file mode 100644 index 000000000..8dc4c0e08 --- /dev/null +++ b/test/fixtures/modules/namedExport/template-2/index.js @@ -0,0 +1,8 @@ +import css from './index.css'; +import html from './template.js'; + +const result = {css, html}; + +__export__ = result; + +export default result; diff --git a/test/fixtures/modules/namedExport/template-2/template.js b/test/fixtures/modules/namedExport/template-2/template.js new file mode 100644 index 000000000..de0517012 --- /dev/null +++ b/test/fixtures/modules/namedExport/template-2/template.js @@ -0,0 +1,11 @@ +import { header_baz_TEST, body_TEST, footer_TEST } from './index.css'; + +let html = '\n'; + +html += `
\n`; +html += `
\n`; +html += `
\n`; + +__export__ = html; + +export default html; diff --git a/test/modules-option.test.js b/test/modules-option.test.js index d365fc99b..0fc68a883 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -1393,6 +1393,24 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work when the "namedExport" option is function', async () => { + const compiler = getCompiler("./modules/namedExport/base/index.js", { + modules: { + namedExport: (localName) => `${localName.replace(/-/g, "_")}_TEST`, + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource("./modules/namedExport/base/index.css", stats) + ).toMatchSnapshot("module"); + expect(getExecutedCode("main.bundle.js", compiler, stats)).toMatchSnapshot( + "result" + ); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + it('should work with the "namedExport" option with nested import', async () => { const compiler = getCompiler("./modules/namedExport/nested/index.js", { esModule: true, @@ -1432,6 +1450,26 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work js template with "namedExport" option when "namedExport" option is function', async () => { + const compiler = getCompiler("./modules/namedExport/template-2/index.js", { + esModule: true, + modules: { + localIdentName: "[local]", + namedExport: (localName) => `${localName.replace(/-/g, "_")}_TEST`, + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource("./modules/namedExport/template-2/index.css", stats) + ).toMatchSnapshot("module"); + expect(getExecutedCode("main.bundle.js", compiler, stats)).toMatchSnapshot( + "result" + ); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + it('should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value', async () => { const compiler = getCompiler("./modules/namedExport/dashesOnly/index.js", { modules: { @@ -1472,6 +1510,39 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work with composes when the "namedExport" is function', async () => { + const compiler = getCompiler("./modules/namedExport/composes/composes.js", { + modules: { + localIdentName: "_[local]", + namedExport: (localName) => `${localName.replace(/-/g, "_")}_TEST`, + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource("./modules/namedExport/composes/composes.css", stats) + ).toMatchSnapshot("module"); + expect(getExecutedCode("main.bundle.js", compiler, stats)).toMatchSnapshot( + "result" + ); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it('should throw error when the "namedExport" function throw error', async () => { + const compiler = getCompiler("./modules/namedExport/composes/composes.js", { + modules: { + namedExport: () => { + throw new Error("namedExportFn error"); + }, + }, + }); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats, true)).toMatchSnapshot("errors"); + }); + it('should throw error with composes when the "namedExport" is enabled and "exportLocalsConvention" options has invalid value', async () => { const compiler = getCompiler("./modules/namedExport/composes/composes.js", { modules: { diff --git a/test/validate-options.test.js b/test/validate-options.test.js index bec7561f1..ede86ecd7 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -47,6 +47,7 @@ describe("validate options", () => { { exportLocalsConvention: "dashesOnly" }, { namedExport: true }, { namedExport: false }, + { namedExport: (localName) => `${localName.replace(/-/g, "_")}_TEST` }, { exportOnlyLocals: true }, { exportOnlyLocals: false }, ],