Skip to content

Commit 3c4b357

Browse files
feat: allow the exportLocalsConvention option can be a function useful for named export (#1351)
1 parent cc0c1b4 commit 3c4b357

11 files changed

+666
-30
lines changed

README.md

+83-1
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,9 @@ module.exports = {
10311031
};
10321032
```
10331033

1034+
To set a custom name for namedExport, can use [`exportLocalsConvention`](#exportLocalsConvention) option as a function.
1035+
Example below in the [`examples`](#examples) section.
1036+
10341037
##### `exportGlobals`
10351038

10361039
Type: `Boolean`
@@ -1060,11 +1063,13 @@ module.exports = {
10601063

10611064
##### `exportLocalsConvention`
10621065

1063-
Type: `String`
1066+
Type: `String|Function`
10641067
Default: based on the `modules.namedExport` option value, if `true` - `camelCaseOnly`, otherwise `asIs`
10651068

10661069
Style of exported class names.
10671070

1071+
###### `String`
1072+
10681073
By default, the exported JSON keys mirror the class names (i.e `asIs` value).
10691074

10701075
> ⚠ Only `camelCaseOnly` value allowed if you set the `namedExport` value to `true`.
@@ -1110,6 +1115,58 @@ module.exports = {
11101115
};
11111116
```
11121117

1118+
###### `Function`
1119+
1120+
**webpack.config.js**
1121+
1122+
```js
1123+
module.exports = {
1124+
module: {
1125+
rules: [
1126+
{
1127+
test: /\.css$/i,
1128+
loader: "css-loader",
1129+
options: {
1130+
modules: {
1131+
exportLocalsConvention: function (name) {
1132+
return name.replace(/-/g, "_");
1133+
},
1134+
},
1135+
},
1136+
},
1137+
],
1138+
},
1139+
};
1140+
```
1141+
1142+
**webpack.config.js**
1143+
1144+
```js
1145+
module.exports = {
1146+
module: {
1147+
rules: [
1148+
{
1149+
test: /\.css$/i,
1150+
loader: "css-loader",
1151+
options: {
1152+
modules: {
1153+
exportLocalsConvention: function (name) {
1154+
return [
1155+
name.replace(/-/g, "_"),
1156+
// dashesCamelCase
1157+
name.replace(/-+(\w)/g, (match, firstLetter) =>
1158+
firstLetter.toUpperCase()
1159+
),
1160+
];
1161+
},
1162+
},
1163+
},
1164+
},
1165+
],
1166+
},
1167+
};
1168+
```
1169+
11131170
##### `exportOnlyLocals`
11141171

11151172
Type: `Boolean`
@@ -1434,6 +1491,31 @@ module.exports = {
14341491
};
14351492
```
14361493

1494+
### Named export with custom export names
1495+
1496+
**webpack.config.js**
1497+
1498+
```js
1499+
module.exports = {
1500+
module: {
1501+
rules: [
1502+
{
1503+
test: /\.css$/i,
1504+
loader: "css-loader",
1505+
options: {
1506+
modules: {
1507+
namedExport: true,
1508+
exportLocalsConvention: function (name) {
1509+
return name.replace(/-/g, "_");
1510+
},
1511+
},
1512+
},
1513+
},
1514+
],
1515+
},
1516+
};
1517+
```
1518+
14371519
### Separating `Interoperable CSS`-only and `CSS Module` features
14381520

14391521
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.

src/index.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,17 @@ export default async function loader(content, map, meta) {
216216
}
217217

218218
const importCode = getImportCode(imports, options);
219-
const moduleCode = getModuleCode(result, api, replacements, options, this);
219+
220+
let moduleCode;
221+
222+
try {
223+
moduleCode = getModuleCode(result, api, replacements, options, this);
224+
} catch (error) {
225+
callback(error);
226+
227+
return;
228+
}
229+
220230
const exportCode = getExportCode(
221231
exports,
222232
replacements,

src/options.json

+13-6
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,19 @@
145145
"exportLocalsConvention": {
146146
"description": "Style of exported classnames.",
147147
"link": "https://github.com/webpack-contrib/css-loader#localsconvention",
148-
"enum": [
149-
"asIs",
150-
"camelCase",
151-
"camelCaseOnly",
152-
"dashes",
153-
"dashesOnly"
148+
"anyOf": [
149+
{
150+
"enum": [
151+
"asIs",
152+
"camelCase",
153+
"camelCaseOnly",
154+
"dashes",
155+
"dashesOnly"
156+
]
157+
},
158+
{
159+
"instanceof": "Function"
160+
}
154161
]
155162
},
156163
"exportOnlyLocals": {

src/utils.js

+33-18
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,12 @@ function getFilter(filter, resourcePath) {
485485
}
486486

487487
function getValidLocalName(localName, exportLocalsConvention) {
488+
if (typeof exportLocalsConvention === "function") {
489+
const result = exportLocalsConvention(localName);
490+
491+
return Array.isArray(result) ? result[0] : result;
492+
}
493+
488494
if (exportLocalsConvention === "dashesOnly") {
489495
return dashesCamelCase(localName);
490496
}
@@ -588,6 +594,7 @@ function getModulesOptions(rawOptions, loaderContext) {
588594
}
589595

590596
if (
597+
typeof modulesOptions.exportLocalsConvention === "string" &&
591598
modulesOptions.exportLocalsConvention !== "camelCaseOnly" &&
592599
modulesOptions.exportLocalsConvention !== "dashesOnly"
593600
) {
@@ -957,42 +964,50 @@ function getExportCode(exports, replacements, needToUseIcssPlugin, options) {
957964

958965
let localsCode = "";
959966

960-
const addExportToLocalsCode = (name, value) => {
961-
if (options.modules.namedExport) {
962-
localsCode += `export var ${name} = ${JSON.stringify(value)};\n`;
963-
} else {
964-
if (localsCode) {
965-
localsCode += `,\n`;
966-
}
967+
const addExportToLocalsCode = (names, value) => {
968+
const normalizedNames = Array.isArray(names)
969+
? new Set(names)
970+
: new Set([names]);
967971

968-
localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;
972+
for (const name of normalizedNames) {
973+
if (options.modules.namedExport) {
974+
localsCode += `export var ${name} = ${JSON.stringify(value)};\n`;
975+
} else {
976+
if (localsCode) {
977+
localsCode += `,\n`;
978+
}
979+
980+
localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;
981+
}
969982
}
970983
};
971984

972985
for (const { name, value } of exports) {
986+
if (typeof options.modules.exportLocalsConvention === "function") {
987+
addExportToLocalsCode(
988+
options.modules.exportLocalsConvention(name),
989+
value
990+
);
991+
992+
// eslint-disable-next-line no-continue
993+
continue;
994+
}
995+
973996
switch (options.modules.exportLocalsConvention) {
974997
case "camelCase": {
975-
addExportToLocalsCode(name, value);
976-
977998
const modifiedName = camelCase(name);
978999

979-
if (modifiedName !== name) {
980-
addExportToLocalsCode(modifiedName, value);
981-
}
1000+
addExportToLocalsCode([name, modifiedName], value);
9821001
break;
9831002
}
9841003
case "camelCaseOnly": {
9851004
addExportToLocalsCode(camelCase(name), value);
9861005
break;
9871006
}
9881007
case "dashes": {
989-
addExportToLocalsCode(name, value);
990-
9911008
const modifiedName = dashesCamelCase(name);
9921009

993-
if (modifiedName !== name) {
994-
addExportToLocalsCode(modifiedName, value);
995-
}
1010+
addExportToLocalsCode([name, modifiedName], value);
9961011
break;
9971012
}
9981013
case "dashesOnly": {

0 commit comments

Comments
 (0)