Skip to content

Commit 2054896

Browse files
feat: esModule export named
1 parent 56c0427 commit 2054896

18 files changed

+423
-22
lines changed

README.md

+50
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ module.exports = {
119119
| **[`localsConvention`](#localsconvention)** | `{String}` | `'asIs'` | Style of exported classnames |
120120
| **[`onlyLocals`](#onlylocals)** | `{Boolean}` | `false` | Export only locals |
121121
| **[`esModule`](#esmodule)** | `{Boolean}` | `false` | Use ES modules syntax |
122+
| **[`exportNamed`](#exportNamed)** | `{Boolean}` | `false` | Use ES modules named export |
122123

123124
### `url`
124125

@@ -1035,6 +1036,55 @@ module.exports = {
10351036
};
10361037
```
10371038

1039+
### `exportNamed`
1040+
1041+
Type: `Boolean`
1042+
Default: `false`
1043+
1044+
Enable/disable ES modules named export for css classes.
1045+
Names of exported classes are converted to camelCase.
1046+
1047+
**styles.css**
1048+
1049+
```css
1050+
.foo-baz {
1051+
color: red;
1052+
}
1053+
.bar {
1054+
color: blue;
1055+
}
1056+
```
1057+
1058+
**index.js**
1059+
1060+
```js
1061+
import { fooBaz, bar } from './styles.css';
1062+
1063+
console.log(fooBaz, bar);
1064+
```
1065+
1066+
You can enable a ES module named export using:
1067+
1068+
**webpack.config.js**
1069+
1070+
```js
1071+
module.exports = {
1072+
module: {
1073+
rules: [
1074+
{
1075+
test: /\.css$/i,
1076+
loader: 'css-loader',
1077+
options: {
1078+
esModule: true,
1079+
modules: true,
1080+
exportNamed: true,
1081+
},
1082+
},
1083+
],
1084+
},
1085+
};
1086+
```
1087+
10381088
## Examples
10391089

10401090
### Assets

src/index.js

+28-4
Original file line numberDiff line numberDiff line change
@@ -171,26 +171,50 @@ export default function loader(content, map, meta) {
171171
);
172172
});
173173

174-
const { localsConvention } = options;
174+
const exportNamed =
175+
typeof options.exportNamed !== 'undefined'
176+
? options.exportNamed
177+
: false;
178+
179+
const { localsConvention } = exportNamed
180+
? { localsConvention: 'camelCaseOnly' }
181+
: options;
182+
175183
const esModule =
176184
typeof options.esModule !== 'undefined' ? options.esModule : false;
177185

178-
const importCode = getImportCode(this, exportType, imports, esModule);
186+
if (Boolean(exportNamed) && Boolean(exportNamed) !== Boolean(esModule)) {
187+
this.emitError(
188+
new Error(
189+
'`Options.exportNamed` can not use without `options.esModule`'
190+
)
191+
);
192+
}
193+
194+
const importCode = getImportCode(
195+
this,
196+
exportType,
197+
imports,
198+
esModule,
199+
exportNamed
200+
);
179201
const moduleCode = getModuleCode(
180202
result,
181203
exportType,
182204
sourceMap,
183205
apiImports,
184206
urlReplacements,
185207
icssReplacements,
186-
esModule
208+
esModule,
209+
exportNamed
187210
);
188211
const exportCode = getExportCode(
189212
exports,
190213
exportType,
191214
localsConvention,
192215
icssReplacements,
193-
esModule
216+
esModule,
217+
exportNamed
194218
);
195219

196220
return callback(null, `${importCode}${moduleCode}${exportCode}`);

src/options.json

+4
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
"esModule": {
122122
"description": "Use the ES modules syntax (https://github.com/webpack-contrib/css-loader#esmodule).",
123123
"type": "boolean"
124+
},
125+
"exportNamed": {
126+
"description": "Use the named export ES modules.",
127+
"type": "boolean"
124128
}
125129
},
126130
"type": "object"

src/plugins/postcss-icss-parser.js

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export default postcss.plugin(
8383
value: {
8484
// 'CSS_LOADER_ICSS_IMPORT'
8585
order: 0,
86+
icss: true,
8687
importName,
8788
url: options.urlHandler(resolvedUrl),
8889
index: currentIndex,

src/utils.js

+39-9
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,13 @@ function getPreRequester({ loaders, loaderIndex }) {
258258
};
259259
}
260260

261-
function getImportCode(loaderContext, exportType, imports, esModule) {
261+
function getImportCode(
262+
loaderContext,
263+
exportType,
264+
imports,
265+
esModule,
266+
exportNamed
267+
) {
262268
let code = '';
263269

264270
if (exportType === 'full') {
@@ -273,10 +279,12 @@ function getImportCode(loaderContext, exportType, imports, esModule) {
273279
}
274280

275281
for (const item of imports) {
276-
const { importName, url } = item;
282+
const { importName, url, icss } = item;
277283

278284
code += esModule
279-
? `import ${importName} from ${url};\n`
285+
? icss && exportNamed
286+
? `import ${importName}, * as ${importName}_NAMED___ from ${url};\n`
287+
: `import ${importName} from ${url};\n`
280288
: `var ${importName} = require(${url});\n`;
281289
}
282290

@@ -290,7 +298,8 @@ function getModuleCode(
290298
apiImports,
291299
urlReplacements,
292300
icssReplacements,
293-
esModule
301+
esModule,
302+
exportNamed
294303
) {
295304
if (exportType !== 'full') {
296305
return 'var ___CSS_LOADER_EXPORT___ = {};\n';
@@ -339,9 +348,12 @@ function getModuleCode(
339348
for (const replacement of icssReplacements) {
340349
const { replacementName, importName, localName } = replacement;
341350

342-
code = code.replace(
343-
new RegExp(replacementName, 'g'),
344-
() => `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
351+
code = code.replace(new RegExp(replacementName, 'g'), () =>
352+
exportNamed
353+
? `" + ${importName}_NAMED___[${JSON.stringify(
354+
camelCase(localName)
355+
)}] + "`
356+
: `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
345357
);
346358
}
347359

@@ -359,17 +371,23 @@ function getExportCode(
359371
exportType,
360372
localsConvention,
361373
icssReplacements,
362-
esModule
374+
esModule,
375+
exportNamed
363376
) {
364377
let code = '';
365378
let localsCode = '';
379+
let namedCode = '';
366380

367381
const addExportToLocalsCode = (name, value) => {
368382
if (localsCode) {
369383
localsCode += `,\n`;
370384
}
371385

372386
localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;
387+
388+
if (exportNamed) {
389+
namedCode += `export const ${name} = ${JSON.stringify(value)};\n`;
390+
}
373391
};
374392

375393
for (const { name, value } of exports) {
@@ -416,10 +434,22 @@ function getExportCode(
416434
new RegExp(replacementName, 'g'),
417435
() => `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
418436
);
437+
438+
if (exportNamed) {
439+
namedCode = namedCode.replace(
440+
new RegExp(replacementName, 'g'),
441+
() =>
442+
`" + ${importName}_NAMED___[${JSON.stringify(
443+
camelCase(localName)
444+
)}] + "`
445+
);
446+
}
419447
}
420448

421449
if (localsCode) {
422-
code += `___CSS_LOADER_EXPORT___.locals = {\n${localsCode}\n};\n`;
450+
code += namedCode
451+
? `${namedCode}\n`
452+
: `___CSS_LOADER_EXPORT___.locals = {\n${localsCode}\n};\n`;
423453
}
424454

425455
code += `${

test/__snapshots__/esModule-option.test.js.snap

+136
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,68 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`"esModule" option should emit error when class has unsupported name: errors 1`] = `
4+
Array [
5+
"ModuleParseError: Module parse failed: Unexpected keyword 'class' (7:13)
6+
File was processed with these loaders:",
7+
]
8+
`;
9+
10+
exports[`"esModule" option should emit error when class has unsupported name: warnings 1`] = `Array []`;
11+
12+
exports[`"esModule" option should emit error when exportNamed true && esModule false: errors 1`] = `
13+
Array [
14+
"ModuleError: Module Error (from \`replaced original path\`):
15+
\`Options.exportNamed\` can not use without \`options.esModule\`",
16+
]
17+
`;
18+
19+
exports[`"esModule" option should work js template with "exportNamed" option: errors 1`] = `Array []`;
20+
21+
exports[`"esModule" option should work js template with "exportNamed" option: module 1`] = `
22+
"// Imports
23+
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
24+
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
25+
// Module
26+
___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\\", \\"\\"]);
27+
// Exports
28+
export const headerBaz = \\"header-baz\\";
29+
export const body = \\"body\\";
30+
export const footer = \\"footer\\";
31+
32+
export default ___CSS_LOADER_EXPORT___;
33+
"
34+
`;
35+
36+
exports[`"esModule" option should work js template with "exportNamed" option: result 1`] = `
37+
Object {
38+
"css": Array [
39+
Array [
40+
"./es-module/named/template/index.css",
41+
".header-baz {
42+
color: red;
43+
}
44+
45+
.body {
46+
color: coral;
47+
}
48+
49+
.footer {
50+
color: blue;
51+
}
52+
",
53+
"",
54+
],
55+
],
56+
"html": "
57+
<div class=\\"header-baz\\">
58+
<div class=\\"body\\">
59+
<div class=\\"footer\\">
60+
",
61+
}
62+
`;
63+
64+
exports[`"esModule" option should work js template with "exportNamed" option: warnings 1`] = `Array []`;
65+
366
exports[`"esModule" option should work when not specified: errors 1`] = `Array []`;
467
568
exports[`"esModule" option should work when not specified: module 1`] = `
@@ -46,6 +109,79 @@ Array [
46109
47110
exports[`"esModule" option should work when not specified: warnings 1`] = `Array []`;
48111
112+
exports[`"esModule" option should work with "exportNamed" option with nested import: errors 1`] = `Array []`;
113+
114+
exports[`"esModule" option should work with "exportNamed" option with nested import: module 1`] = `
115+
"// Imports
116+
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
117+
import ___CSS_LOADER_ICSS_IMPORT_0___, * as ___CSS_LOADER_ICSS_IMPORT_0____NAMED___ from \\"-!../../../../../src/index.js??[ident]!../../../modules/composes/values.css\\";
118+
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
119+
___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, \\"\\", true);
120+
// Module
121+
___CSS_LOADER_EXPORT___.push([module.id, \\".qwCT06AE1ZDvQtiz0EQJ8 {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\";\\\\n}\\\\n\\", \\"\\"]);
122+
// Exports
123+
export const vDef = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\"\\";
124+
export const ghi = \\"qwCT06AE1ZDvQtiz0EQJ8\\";
125+
126+
export default ___CSS_LOADER_EXPORT___;
127+
"
128+
`;
129+
130+
exports[`"esModule" option should work with "exportNamed" option with nested import: result 1`] = `
131+
Array [
132+
Array [
133+
"../../src/index.js?[ident]!./modules/composes/values.css",
134+
"
135+
",
136+
"",
137+
],
138+
Array [
139+
"./es-module/named/nested/index.css",
140+
".qwCT06AE1ZDvQtiz0EQJ8 {
141+
color: red;
142+
}
143+
",
144+
"",
145+
],
146+
]
147+
`;
148+
149+
exports[`"esModule" option should work with "exportNamed" option with nested import: warnings 1`] = `Array []`;
150+
151+
exports[`"esModule" option should work with "exportNamed" option: errors 1`] = `Array []`;
152+
153+
exports[`"esModule" option should work with "exportNamed" option: module 1`] = `
154+
"// Imports
155+
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
156+
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
157+
// Module
158+
___CSS_LOADER_EXPORT___.push([module.id, \\"._1Cb_30DnkF22nebwtzVBFY {\\\\n color: red;\\\\n}\\\\n\\\\n.bar {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]);
159+
// Exports
160+
export const barBaz = \\"_1Cb_30DnkF22nebwtzVBFY\\";
161+
162+
export default ___CSS_LOADER_EXPORT___;
163+
"
164+
`;
165+
166+
exports[`"esModule" option should work with "exportNamed" option: result 1`] = `
167+
Array [
168+
Array [
169+
"./es-module/named/base/index.css",
170+
"._1Cb_30DnkF22nebwtzVBFY {
171+
color: red;
172+
}
173+
174+
.bar {
175+
color: red;
176+
}
177+
",
178+
"",
179+
],
180+
]
181+
`;
182+
183+
exports[`"esModule" option should work with "exportNamed" option: warnings 1`] = `Array []`;
184+
49185
exports[`"esModule" option should work with a value equal to "false": errors 1`] = `Array []`;
50186
51187
exports[`"esModule" option should work with a value equal to "false": module 1`] = `

0 commit comments

Comments
 (0)