diff --git a/README.md b/README.md index 8020201e..9b44d56b 100644 --- a/README.md +++ b/README.md @@ -783,20 +783,21 @@ This may change in the future when the module system (i. e. webpack) supports lo ### `localsConvention` -Type: `String` +Type: `String` | `Function` Default: `'asIs'` Style of exported classnames. By default, the exported JSON keys mirror the class names (i.e `asIs` value). -| Name | Type | Description | -| :-------------------: | :--------: | :----------------------------------------------------------------------------------------------- | -| **`'asIs'`** | `{String}` | Class names will be exported as is. | -| **`'camelCase'`** | `{String}` | Class names will be camelized, the original class name will not to be removed from the locals | -| **`'camelCaseOnly'`** | `{String}` | Class names will be camelized, the original class name will be removed from the locals | -| **`'dashes'`** | `{String}` | Only dashes in class names will be camelized | -| **`'dashesOnly'`** | `{String}` | Dashes in class names will be camelized, the original class name will be removed from the locals | +| Name | Type | Description | +| :-------------------: | :----------: | :----------------------------------------------------------------------------------------------- | +| **`'asIs'`** | `{String}` | Class names will be exported as is. | +| **`'camelCase'`** | `{String}` | Class names will be camelized, the original class name will not to be removed from the locals | +| **`'camelCaseOnly'`** | `{String}` | Class names will be camelized, the original class name will be removed from the locals | +| **`'dashes'`** | `{String}` | Only dashes in class names will be camelized | +| **`'dashesOnly'`** | `{String}` | Dashes in class names will be camelized, the original class name will be removed from the locals | +| | `{Function}` | Takes the original class name as first argument and returns the exported class name | **file.css** diff --git a/package.json b/package.json index b92fbf1c..b787cb09 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "css-loader", + "name": "css-loader-functional-locals-convention", "version": "3.4.0", - "description": "css loader module for webpack", + "description": "A fork of css loader with an additional option for locals convention", "license": "MIT", "repository": "webpack-contrib/css-loader", "author": "Tobias Koppers @sokra", diff --git a/src/options.json b/src/options.json index e3125073..f36a4b60 100644 --- a/src/options.json +++ b/src/options.json @@ -89,7 +89,14 @@ }, "localsConvention": { "description": "Style of exported classnames (https://github.com/webpack-contrib/css-loader#localsconvention).", - "enum": ["asIs", "camelCase", "camelCaseOnly", "dashes", "dashesOnly"] + "anyOf": [ + { + "enum": ["asIs", "camelCase", "camelCaseOnly", "dashes", "dashesOnly"] + }, + { + "instanceof": "Function" + } + ] }, "onlyLocals": { "description": "Export only locals (https://github.com/webpack-contrib/css-loader#onlylocals).", diff --git a/src/utils.js b/src/utils.js index c5b00d10..930443ba 100644 --- a/src/utils.js +++ b/src/utils.js @@ -425,39 +425,47 @@ function getExportCode( exports.forEach((item) => { const { name, value } = item; - switch (localsConvention) { - case 'camelCase': { - addExportedLocal(name, value); + if (typeof localsConvention === 'function') { + const modifiedName = localsConvention(name); - const modifiedName = camelCase(name); + if (modifiedName !== name) { + addExportedLocal(modifiedName, value); + } + } else { + switch (localsConvention) { + case 'camelCase': { + addExportedLocal(name, value); + + const modifiedName = camelCase(name); - if (modifiedName !== name) { - addExportedLocal(modifiedName, value); + if (modifiedName !== name) { + addExportedLocal(modifiedName, value); + } + break; } - break; - } - case 'camelCaseOnly': { - addExportedLocal(camelCase(name), value); - break; - } - case 'dashes': { - addExportedLocal(name, value); + case 'camelCaseOnly': { + addExportedLocal(camelCase(name), value); + break; + } + case 'dashes': { + addExportedLocal(name, value); - const modifiedName = dashesCamelCase(name); + const modifiedName = dashesCamelCase(name); - if (modifiedName !== name) { - addExportedLocal(modifiedName, value); + if (modifiedName !== name) { + addExportedLocal(modifiedName, value); + } + break; } - break; - } - case 'dashesOnly': { - addExportedLocal(dashesCamelCase(name), value); - break; + case 'dashesOnly': { + addExportedLocal(dashesCamelCase(name), value); + break; + } + case 'asIs': + default: + addExportedLocal(name, value); + break; } - case 'asIs': - default: - addExportedLocal(name, value); - break; } }); diff --git a/test/__snapshots__/localsConvention-option.test.js.snap b/test/__snapshots__/localsConvention-option.test.js.snap index 637ddd50..df22f789 100644 --- a/test/__snapshots__/localsConvention-option.test.js.snap +++ b/test/__snapshots__/localsConvention-option.test.js.snap @@ -287,3 +287,50 @@ a { `; exports[`"localsConvention" option should work with a value equal to "dashesOnly": warnings 1`] = `Array []`; + +exports[`"localsConvention" option should work with a value equal to a custom function: errors 1`] = `Array []`; + +exports[`"localsConvention" option should work with a value equal to a custom function: module 1`] = ` +"// Imports +var ___CSS_LOADER_API_IMPORT___ = require(\\"../../../../src/runtime/api.js\\"); +exports = ___CSS_LOADER_API_IMPORT___(false); +// Module +exports.push([module.id, \\".erBXHZCN_thRYfCnk-aH8 {\\\\n color: blue;\\\\n}\\\\n\\\\n._2YsQE-S0o0NRXfC6XNApz2 {\\\\n color: blue;\\\\n}\\\\n\\\\n._3gGBcJHZU3seQVP5aq7Ksq {\\\\n color: red;\\\\n}\\\\n\\\\na {\\\\n color: yellow;\\\\n}\\\\n\\", \\"\\"]); +// Exports +exports.locals = { + \\"FOO\\": \\"bar\\", + \\"MY-BTN-INFO_IS-DISABLED\\": \\"value\\", + \\"BTN-INFO_IS-DISABLED\\": \\"erBXHZCN_thRYfCnk-aH8\\", + \\"BTN--INFO_IS-DISABLED_1\\": \\"_2YsQE-S0o0NRXfC6XNApz2\\", + \\"SIMPLE\\": \\"_3gGBcJHZU3seQVP5aq7Ksq\\" +}; +module.exports = exports; +" +`; + +exports[`"localsConvention" option should work with a value equal to a custom function: result 1`] = ` +Array [ + Array [ + "./modules/localsConvention/localsConvention.css", + ".erBXHZCN_thRYfCnk-aH8 { + color: blue; +} + +._2YsQE-S0o0NRXfC6XNApz2 { + color: blue; +} + +._3gGBcJHZU3seQVP5aq7Ksq { + color: red; +} + +a { + color: yellow; +} +", + "", + ], +] +`; + +exports[`"localsConvention" option should work with a value equal to a custom function: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 3b259f02..9d97b534 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -29,8 +29,12 @@ exports[`validate options should throw an error on the "importLoaders" option wi exports[`validate options should throw an error on the "localsConvention" option with "unknown" value 1`] = ` "Invalid options object. CSS Loader has been initialised using an options object that does not match the API schema. - options.localsConvention should be one of these: - \\"asIs\\" | \\"camelCase\\" | \\"camelCaseOnly\\" | \\"dashes\\" | \\"dashesOnly\\" - -> Style of exported classnames (https://github.com/webpack-contrib/css-loader#localsconvention)." + \\"asIs\\" | \\"camelCase\\" | \\"camelCaseOnly\\" | \\"dashes\\" | \\"dashesOnly\\" | function + -> Style of exported classnames (https://github.com/webpack-contrib/css-loader#localsconvention). + Details: + * options.localsConvention should be one of these: + \\"asIs\\" | \\"camelCase\\" | \\"camelCaseOnly\\" | \\"dashes\\" | \\"dashesOnly\\" + * options.localsConvention should be an instance of function." `; exports[`validate options should throw an error on the "modules" option with "{"context":true}" value 1`] = ` diff --git a/test/localsConvention-option.test.js b/test/localsConvention-option.test.js index ad62de97..f15202aa 100644 --- a/test/localsConvention-option.test.js +++ b/test/localsConvention-option.test.js @@ -126,4 +126,24 @@ describe('"localsConvention" option', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats)).toMatchSnapshot('errors'); }); + + it('should work with a value equal to a custom function', async () => { + const compiler = getCompiler( + './modules/localsConvention/localsConvention.js', + { + modules: true, + localsConvention: (className) => className.toUpperCase(), + } + ); + 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'); + }); }); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 814ee799..28a673c3 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -48,7 +48,7 @@ describe('validate options', () => { failure: ['true'], }, localsConvention: { - success: ['camelCase', 'camelCaseOnly', 'dashes', 'dashesOnly'], + success: ['camelCase', 'camelCaseOnly', 'dashes', 'dashesOnly', () => {}], failure: ['unknown'], }, importLoaders: {