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": " +