From 3c4b35718273baaf9e0480db715b596fbe5d7453 Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Mon, 19 Jul 2021 13:13:29 -0700 Subject: [PATCH] feat: allow the `exportLocalsConvention` option can be a function useful for named export (#1351) --- README.md | 84 ++++- src/index.js | 12 +- src/options.json | 19 +- src/utils.js | 51 ++- .../__snapshots__/modules-option.test.js.snap | 329 ++++++++++++++++++ .../validate-options.test.js.snap | 17 +- .../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 | 150 ++++++++ test/validate-options.test.js | 4 + 11 files changed, 666 insertions(+), 30 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/README.md b/README.md index bf95021e..d44cfaf9 100644 --- a/README.md +++ b/README.md @@ -1031,6 +1031,9 @@ module.exports = { }; ``` +To set a custom name for namedExport, can use [`exportLocalsConvention`](#exportLocalsConvention) option as a function. +Example below in the [`examples`](#examples) section. + ##### `exportGlobals` Type: `Boolean` @@ -1060,11 +1063,13 @@ module.exports = { ##### `exportLocalsConvention` -Type: `String` +Type: `String|Function` Default: based on the `modules.namedExport` option value, if `true` - `camelCaseOnly`, otherwise `asIs` Style of exported class names. +###### `String` + By default, the exported JSON keys mirror the class names (i.e `asIs` value). > ⚠ Only `camelCaseOnly` value allowed if you set the `namedExport` value to `true`. @@ -1110,6 +1115,58 @@ module.exports = { }; ``` +###### `Function` + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + exportLocalsConvention: function (name) { + return name.replace(/-/g, "_"); + }, + }, + }, + }, + ], + }, +}; +``` + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + exportLocalsConvention: function (name) { + return [ + name.replace(/-/g, "_"), + // dashesCamelCase + name.replace(/-+(\w)/g, (match, firstLetter) => + firstLetter.toUpperCase() + ), + ]; + }, + }, + }, + }, + ], + }, +}; +``` + ##### `exportOnlyLocals` Type: `Boolean` @@ -1434,6 +1491,31 @@ module.exports = { }; ``` +### Named export with custom export names + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + namedExport: true, + exportLocalsConvention: function (name) { + return name.replace(/-/g, "_"); + }, + }, + }, + }, + ], + }, +}; +``` + ### Separating `Interoperable CSS`-only and `CSS Module` features The following setup is an example of allowing `Interoperable CSS` features only (such as `:import` and `:export`) without using further `CSS Module` functionality by setting `mode` option for all files that do not match `*.module.scss` naming convention. This is for reference as having `ICSS` features applied to all files was default `css-loader` behavior before v4. diff --git a/src/index.js b/src/index.js index 7b20b92c..4945d634 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 ddbffb7a..9715c923 100644 --- a/src/options.json +++ b/src/options.json @@ -145,12 +145,19 @@ "exportLocalsConvention": { "description": "Style of exported classnames.", "link": "https://github.com/webpack-contrib/css-loader#localsconvention", - "enum": [ - "asIs", - "camelCase", - "camelCaseOnly", - "dashes", - "dashesOnly" + "anyOf": [ + { + "enum": [ + "asIs", + "camelCase", + "camelCaseOnly", + "dashes", + "dashesOnly" + ] + }, + { + "instanceof": "Function" + } ] }, "exportOnlyLocals": { diff --git a/src/utils.js b/src/utils.js index 30b796af..f0845dde 100644 --- a/src/utils.js +++ b/src/utils.js @@ -485,6 +485,12 @@ function getFilter(filter, resourcePath) { } function getValidLocalName(localName, exportLocalsConvention) { + if (typeof exportLocalsConvention === "function") { + const result = exportLocalsConvention(localName); + + return Array.isArray(result) ? result[0] : result; + } + if (exportLocalsConvention === "dashesOnly") { return dashesCamelCase(localName); } @@ -588,6 +594,7 @@ function getModulesOptions(rawOptions, loaderContext) { } if ( + typeof modulesOptions.exportLocalsConvention === "string" && modulesOptions.exportLocalsConvention !== "camelCaseOnly" && modulesOptions.exportLocalsConvention !== "dashesOnly" ) { @@ -957,28 +964,40 @@ function getExportCode(exports, replacements, needToUseIcssPlugin, options) { let localsCode = ""; - const addExportToLocalsCode = (name, value) => { - if (options.modules.namedExport) { - localsCode += `export var ${name} = ${JSON.stringify(value)};\n`; - } else { - if (localsCode) { - localsCode += `,\n`; - } + const addExportToLocalsCode = (names, value) => { + const normalizedNames = Array.isArray(names) + ? new Set(names) + : new Set([names]); - localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`; + for (const name of normalizedNames) { + if (options.modules.namedExport) { + localsCode += `export var ${name} = ${JSON.stringify(value)};\n`; + } else { + if (localsCode) { + localsCode += `,\n`; + } + + localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`; + } } }; for (const { name, value } of exports) { + if (typeof options.modules.exportLocalsConvention === "function") { + addExportToLocalsCode( + options.modules.exportLocalsConvention(name), + value + ); + + // eslint-disable-next-line no-continue + continue; + } + switch (options.modules.exportLocalsConvention) { case "camelCase": { - addExportToLocalsCode(name, value); - const modifiedName = camelCase(name); - if (modifiedName !== name) { - addExportToLocalsCode(modifiedName, value); - } + addExportToLocalsCode([name, modifiedName], value); break; } case "camelCaseOnly": { @@ -986,13 +1005,9 @@ function getExportCode(exports, replacements, needToUseIcssPlugin, options) { break; } case "dashes": { - addExportToLocalsCode(name, value); - const modifiedName = dashesCamelCase(name); - if (modifiedName !== name) { - addExportToLocalsCode(modifiedName, value); - } + addExportToLocalsCode([name, modifiedName], value); break; } case "dashesOnly": { diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index 76a91c77..bc48d34a 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 "exportLocalsConvention" 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 "exportLocalsConvention" 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\`): @@ -2562,6 +2571,116 @@ Array [ exports[`"modules" option should work and respect the "context" option: warnings 1`] = `Array []`; +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type and returns array names: errors 1`] = `Array []`; + +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type and returns array names: 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, \\".eFSx39d7lf2DbavLOZEH {\\\\n color: blue;\\\\n}\\\\n\\\\n._XcV1pTGsk1DDypSCcav {\\\\n color: blue;\\\\n}\\\\n\\\\n._JxN_SGMxSzstCVbNTUy {\\\\n color: red;\\\\n}\\\\n\\\\na {\\\\n color: yellow;\\\\n}\\\\n\\\\n._krAefTYwrSG1l87lmV3 {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]); +// Exports +___CSS_LOADER_EXPORT___.locals = { + \\"foo_TEST_1\\": \\"bar\\", + \\"foo_TEST_3\\": \\"bar\\", + \\"my_btn_info_is_disabled_TEST_1\\": \\"value\\", + \\"my_btn_info_is_disabled_TEST_3\\": \\"value\\", + \\"btn_info_is_disabled_TEST_1\\": \\"eFSx39d7lf2DbavLOZEH\\", + \\"btn_info_is_disabled_TEST_3\\": \\"eFSx39d7lf2DbavLOZEH\\", + \\"btn__info_is_disabled_1_TEST_1\\": \\"_XcV1pTGsk1DDypSCcav\\", + \\"btn__info_is_disabled_1_TEST_3\\": \\"_XcV1pTGsk1DDypSCcav\\", + \\"simple_TEST_1\\": \\"_JxN_SGMxSzstCVbNTUy\\", + \\"simple_TEST_3\\": \\"_JxN_SGMxSzstCVbNTUy\\", + \\"foo_bar_TEST_1\\": \\"_krAefTYwrSG1l87lmV3\\", + \\"foo_bar_TEST_3\\": \\"_krAefTYwrSG1l87lmV3\\" +}; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type and returns array names: result 1`] = ` +Array [ + Array [ + "./modules/localsConvention/localsConvention.css", + ".eFSx39d7lf2DbavLOZEH { + color: blue; +} + +._XcV1pTGsk1DDypSCcav { + color: blue; +} + +._JxN_SGMxSzstCVbNTUy { + color: red; +} + +a { + color: yellow; +} + +._krAefTYwrSG1l87lmV3 { + color: red; +} +", + "", + ], +] +`; + +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type and returns array names: warnings 1`] = `Array []`; + +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type: errors 1`] = `Array []`; + +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type: 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, \\".eFSx39d7lf2DbavLOZEH {\\\\n color: blue;\\\\n}\\\\n\\\\n._XcV1pTGsk1DDypSCcav {\\\\n color: blue;\\\\n}\\\\n\\\\n._JxN_SGMxSzstCVbNTUy {\\\\n color: red;\\\\n}\\\\n\\\\na {\\\\n color: yellow;\\\\n}\\\\n\\\\n._krAefTYwrSG1l87lmV3 {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]); +// Exports +___CSS_LOADER_EXPORT___.locals = { + \\"foo_TEST\\": \\"bar\\", + \\"my_btn_info_is_disabled_TEST\\": \\"value\\", + \\"btn_info_is_disabled_TEST\\": \\"eFSx39d7lf2DbavLOZEH\\", + \\"btn__info_is_disabled_1_TEST\\": \\"_XcV1pTGsk1DDypSCcav\\", + \\"simple_TEST\\": \\"_JxN_SGMxSzstCVbNTUy\\", + \\"foo_bar_TEST\\": \\"_krAefTYwrSG1l87lmV3\\" +}; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type: result 1`] = ` +Array [ + Array [ + "./modules/localsConvention/localsConvention.css", + ".eFSx39d7lf2DbavLOZEH { + color: blue; +} + +._XcV1pTGsk1DDypSCcav { + color: blue; +} + +._JxN_SGMxSzstCVbNTUy { + color: red; +} + +a { + color: yellow; +} + +._krAefTYwrSG1l87lmV3 { + color: red; +} +", + "", + ], +] +`; + +exports[`"modules" option should work and respect the "exportLocalsConvention" option with the "function" type: warnings 1`] = `Array []`; + exports[`"modules" option should work and respect the "exportOnlyLocals" option: errors 1`] = `Array []`; exports[`"modules" option should work and respect the "exportOnlyLocals" option: module 1`] = ` @@ -4606,6 +4725,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 "exportLocalsConvention" option is function: errors 1`] = `Array []`; + +exports[`"modules" option should work js template with "namedExport" option when "exportLocalsConvention" 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 "exportLocalsConvention" 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 "exportLocalsConvention" 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`] = ` @@ -4772,6 +4937,39 @@ exports[`"modules" option should work when the "auto" option is "true" with othe exports[`"modules" option should work when the "auto" option is "true" with other options: warnings 1`] = `Array []`; +exports[`"modules" option should work when the "exportLocalsConvention" option is function: errors 1`] = `Array []`; + +exports[`"modules" option should work when the "exportLocalsConvention" 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 "exportLocalsConvention" 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 "exportLocalsConvention" option is function: warnings 1`] = `Array []`; + exports[`"modules" option should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value: errors 1`] = `Array []`; exports[`"modules" option should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value: module 1`] = ` @@ -13412,6 +13610,137 @@ Array [ exports[`"modules" option should work with case \`values-10\` (\`modules\` value is \`true)\`: warnings 1`] = `Array []`; +exports[`"modules" option should work with composes when the "exportLocalsConvention" is function and return array names: errors 1`] = `Array []`; + +exports[`"modules" option should work with composes when the "exportLocalsConvention" is function and return array names: 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, \\".hrlxzfp4noajRWPVp_UC {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def_TEST_1\\"] + \\";\\\\n}\\\\n\\\\n.Wr3wRBz8YFj3jWOISgia {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"s_white_TEST_1\\"] + \\";\\\\n}\\\\n\\\\n.N3CMKfMxK4wVelMiMLmQ {\\\\n display: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"m_small_TEST_1\\"] + \\";\\\\n}\\\\n\\\\n.pgrezI_6B1TIw29jJdO0 {\\\\n width: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def_TEST_1\\"] + \\";\\\\n}\\\\n\\\\n._wneqlVHVVNWG_hbM3Ti {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_other_other_TEST_1\\"] + \\";\\\\n}\\\\n\\", \\"\\"]); +// Exports +export var v_def_TEST_1 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def_TEST_1\\"] + \\"\\"; +export var v_def_TEST_3 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def_TEST_1\\"] + \\"\\"; +export var v_other_other_TEST_1 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_other_other_TEST_1\\"] + \\"\\"; +export var v_other_other_TEST_3 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_other_other_TEST_1\\"] + \\"\\"; +export var s_white_TEST_1 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"s_white_TEST_1\\"] + \\"\\"; +export var s_white_TEST_3 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"s_white_TEST_1\\"] + \\"\\"; +export var m_small_TEST_1 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"m_small_TEST_1\\"] + \\"\\"; +export var m_small_TEST_3 = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"m_small_TEST_1\\"] + \\"\\"; +export var ghi_TEST_1 = \\"hrlxzfp4noajRWPVp_UC\\"; +export var ghi_TEST_3 = \\"hrlxzfp4noajRWPVp_UC\\"; +export var my_class_TEST_1 = \\"Wr3wRBz8YFj3jWOISgia\\"; +export var my_class_TEST_3 = \\"Wr3wRBz8YFj3jWOISgia\\"; +export var other_TEST_1 = \\"N3CMKfMxK4wVelMiMLmQ\\"; +export var other_TEST_3 = \\"N3CMKfMxK4wVelMiMLmQ\\"; +export var other_other_TEST_1 = \\"pgrezI_6B1TIw29jJdO0\\"; +export var other_other_TEST_3 = \\"pgrezI_6B1TIw29jJdO0\\"; +export var green_TEST_1 = \\"_wneqlVHVVNWG_hbM3Ti\\"; +export var green_TEST_3 = \\"_wneqlVHVVNWG_hbM3Ti\\"; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work with composes when the "exportLocalsConvention" is function and return array names: result 1`] = ` +Array [ + Array [ + "../../src/index.js??ruleSet[1].rules[0].use[0]!./modules/namedExport/composes/values.css", + " +", + "", + ], + Array [ + "./modules/namedExport/composes/composes.css", + ".hrlxzfp4noajRWPVp_UC { + color: red; +} + +.Wr3wRBz8YFj3jWOISgia { + color: white; +} + +.N3CMKfMxK4wVelMiMLmQ { + display: (min-width: 320px); +} + +.pgrezI_6B1TIw29jJdO0 { + width: red; +} + +._wneqlVHVVNWG_hbM3Ti { + color: green; +} +", + "", + ], +] +`; + +exports[`"modules" option should work with composes when the "exportLocalsConvention" is function and return array names: warnings 1`] = `Array []`; + +exports[`"modules" option should work with composes when the "exportLocalsConvention" is function: errors 1`] = `Array []`; + +exports[`"modules" option should work with composes when the "exportLocalsConvention" 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 "exportLocalsConvention" 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 "exportLocalsConvention" is function: warnings 1`] = `Array []`; + exports[`"modules" option should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value: errors 1`] = `Array []`; exports[`"modules" option should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value: module 1`] = ` diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 1e3a942a..98841ae6 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -92,10 +92,19 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"exportLocalsConvention":"unknown"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - - options.modules.exportLocalsConvention should be one of these: - \\"asIs\\" | \\"camelCase\\" | \\"camelCaseOnly\\" | \\"dashes\\" | \\"dashesOnly\\" - -> Style of exported classnames. - -> Read more at https://github.com/webpack-contrib/css-loader#localsconvention" + - 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.exportLocalsConvention should be one of these: + \\"asIs\\" | \\"camelCase\\" | \\"camelCaseOnly\\" | \\"dashes\\" | \\"dashesOnly\\" | function + -> Style of exported classnames. + -> Read more at https://github.com/webpack-contrib/css-loader#localsconvention + Details: + * options.modules.exportLocalsConvention should be one of these: + \\"asIs\\" | \\"camelCase\\" | \\"camelCaseOnly\\" | \\"dashes\\" | \\"dashesOnly\\" + * options.modules.exportLocalsConvention should be an instance of function." `; exports[`validate options should throw an error on the "modules" option with "{"exportOnlyLocals":"invalid"}" 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 00000000..b3ccc301 --- /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 00000000..8dc4c0e0 --- /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 00000000..de051701 --- /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 d365fc99..d658af16 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -1211,6 +1211,32 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work and respect the "exportLocalsConvention" option with the "function" type and returns array names', async () => { + const compiler = getCompiler( + "./modules/localsConvention/localsConvention.js", + { + modules: { + mode: "local", + exportLocalsConvention: (localName) => [ + `${localName.replace(/-/g, "_")}_TEST_1`, + `${localName.replace(/-/g, "_")}_TEST_1`, + `${localName.replace(/-/g, "_")}_TEST_3`, + ], + }, + } + ); + const stats = await compile(compiler); + + expect( + getModuleSource("./modules/localsConvention/localsConvention.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 and respect the "localConvention" option with the "camelCase" value', async () => { const compiler = getCompiler( "./modules/localsConvention/localsConvention.js", @@ -1299,6 +1325,29 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work and respect the "exportLocalsConvention" option with the "function" type', async () => { + const compiler = getCompiler( + "./modules/localsConvention/localsConvention.js", + { + modules: { + mode: "local", + exportLocalsConvention: (localName) => + `${localName.replace(/-/g, "_")}_TEST`, + }, + } + ); + const stats = await compile(compiler); + + expect( + getModuleSource("./modules/localsConvention/localsConvention.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 and respect the "exportOnlyLocals" option', async () => { const compiler = getCompiler("./modules/composes/composes.js", { modules: { @@ -1393,6 +1442,26 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work when the "exportLocalsConvention" option is function', async () => { + const compiler = getCompiler("./modules/namedExport/base/index.js", { + modules: { + namedExport: true, + exportLocalsConvention: (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 +1501,28 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work js template with "namedExport" option when "exportLocalsConvention" option is function', async () => { + const compiler = getCompiler("./modules/namedExport/template-2/index.js", { + esModule: true, + modules: { + localIdentName: "[local]", + namedExport: true, + exportLocalsConvention: (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 +1563,65 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work with composes when the "exportLocalsConvention" is function and return array names', async () => { + const compiler = getCompiler("./modules/namedExport/composes/composes.js", { + modules: { + namedExport: true, + exportLocalsConvention: (localName) => [ + `${localName.replace(/-/g, "_")}_TEST_1`, + `${localName.replace(/-/g, "_")}_TEST_1`, + `${localName.replace(/-/g, "_")}_TEST_3`, + ], + }, + }); + 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 work with composes when the "exportLocalsConvention" is function', async () => { + const compiler = getCompiler("./modules/namedExport/composes/composes.js", { + modules: { + localIdentName: "_[local]", + namedExport: true, + exportLocalsConvention: (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 "exportLocalsConvention" function throw error', async () => { + const compiler = getCompiler("./modules/namedExport/composes/composes.js", { + modules: { + namedExport: true, + exportLocalsConvention: () => { + 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 bec7561f..068a8a22 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -45,6 +45,10 @@ describe("validate options", () => { { exportLocalsConvention: "camelCaseOnly" }, { exportLocalsConvention: "dashes" }, { exportLocalsConvention: "dashesOnly" }, + { + exportLocalsConvention: (localName) => + `${localName.replace(/-/g, "_")}`, + }, { namedExport: true }, { namedExport: false }, { exportOnlyLocals: true },