Skip to content

Commit 86726aa

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

18 files changed

+430
-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

+143
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,75 @@
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+
* ../../src/index.js
8+
You may need an additional loader to handle the result of these loaders.
9+
| ___CSS_LOADER_EXPORT___.push([module.id, \\".AxgGhXfioDVZOzvFzEbSn {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]);
10+
| // Exports
11+
> export const class = \\"AxgGhXfioDVZOzvFzEbSn\\";
12+
|
13+
| export default ___CSS_LOADER_EXPORT___;",
14+
]
15+
`;
16+
17+
exports[`"esModule" option should emit error when class has unsupported name: warnings 1`] = `Array []`;
18+
19+
exports[`"esModule" option should emit error when exportNamed true && esModule false: errors 1`] = `
20+
Array [
21+
"ModuleError: Module Error (from \`replaced original path\`):
22+
\`Options.exportNamed\` can not use without \`options.esModule\`",
23+
]
24+
`;
25+
26+
exports[`"esModule" option should work js template with "exportNamed" option: errors 1`] = `Array []`;
27+
28+
exports[`"esModule" option should work js template with "exportNamed" option: module 1`] = `
29+
"// Imports
30+
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
31+
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
32+
// Module
33+
___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\\", \\"\\"]);
34+
// Exports
35+
export const headerBaz = \\"header-baz\\";
36+
export const body = \\"body\\";
37+
export const footer = \\"footer\\";
38+
39+
export default ___CSS_LOADER_EXPORT___;
40+
"
41+
`;
42+
43+
exports[`"esModule" option should work js template with "exportNamed" option: result 1`] = `
44+
Object {
45+
"css": Array [
46+
Array [
47+
"./es-module/named/template/index.css",
48+
".header-baz {
49+
color: red;
50+
}
51+
52+
.body {
53+
color: coral;
54+
}
55+
56+
.footer {
57+
color: blue;
58+
}
59+
",
60+
"",
61+
],
62+
],
63+
"html": "
64+
<div class=\\"header-baz\\">
65+
<div class=\\"body\\">
66+
<div class=\\"footer\\">
67+
",
68+
}
69+
`;
70+
71+
exports[`"esModule" option should work js template with "exportNamed" option: warnings 1`] = `Array []`;
72+
373
exports[`"esModule" option should work when not specified: errors 1`] = `Array []`;
474
575
exports[`"esModule" option should work when not specified: module 1`] = `
@@ -46,6 +116,79 @@ Array [
46116
47117
exports[`"esModule" option should work when not specified: warnings 1`] = `Array []`;
48118
119+
exports[`"esModule" option should work with "exportNamed" option with nested import: errors 1`] = `Array []`;
120+
121+
exports[`"esModule" option should work with "exportNamed" option with nested import: module 1`] = `
122+
"// Imports
123+
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
124+
import ___CSS_LOADER_ICSS_IMPORT_0___, * as ___CSS_LOADER_ICSS_IMPORT_0____NAMED___ from \\"-!../../../../../src/index.js??[ident]!../../../modules/composes/values.css\\";
125+
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
126+
___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, \\"\\", true);
127+
// Module
128+
___CSS_LOADER_EXPORT___.push([module.id, \\".qwCT06AE1ZDvQtiz0EQJ8 {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\";\\\\n}\\\\n\\", \\"\\"]);
129+
// Exports
130+
export const vDef = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\"\\";
131+
export const ghi = \\"qwCT06AE1ZDvQtiz0EQJ8\\";
132+
133+
export default ___CSS_LOADER_EXPORT___;
134+
"
135+
`;
136+
137+
exports[`"esModule" option should work with "exportNamed" option with nested import: result 1`] = `
138+
Array [
139+
Array [
140+
"../../src/index.js?[ident]!./modules/composes/values.css",
141+
"
142+
",
143+
"",
144+
],
145+
Array [
146+
"./es-module/named/nested/index.css",
147+
".qwCT06AE1ZDvQtiz0EQJ8 {
148+
color: red;
149+
}
150+
",
151+
"",
152+
],
153+
]
154+
`;
155+
156+
exports[`"esModule" option should work with "exportNamed" option with nested import: warnings 1`] = `Array []`;
157+
158+
exports[`"esModule" option should work with "exportNamed" option: errors 1`] = `Array []`;
159+
160+
exports[`"esModule" option should work with "exportNamed" option: module 1`] = `
161+
"// Imports
162+
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
163+
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
164+
// Module
165+
___CSS_LOADER_EXPORT___.push([module.id, \\"._1Cb_30DnkF22nebwtzVBFY {\\\\n color: red;\\\\n}\\\\n\\\\n.bar {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]);
166+
// Exports
167+
export const barBaz = \\"_1Cb_30DnkF22nebwtzVBFY\\";
168+
169+
export default ___CSS_LOADER_EXPORT___;
170+
"
171+
`;
172+
173+
exports[`"esModule" option should work with "exportNamed" option: result 1`] = `
174+
Array [
175+
Array [
176+
"./es-module/named/base/index.css",
177+
"._1Cb_30DnkF22nebwtzVBFY {
178+
color: red;
179+
}
180+
181+
.bar {
182+
color: red;
183+
}
184+
",
185+
"",
186+
],
187+
]
188+
`;
189+
190+
exports[`"esModule" option should work with "exportNamed" option: warnings 1`] = `Array []`;
191+
49192
exports[`"esModule" option should work with a value equal to "false": errors 1`] = `Array []`;
50193
51194
exports[`"esModule" option should work with a value equal to "false": module 1`] = `

0 commit comments

Comments
 (0)