Skip to content

Commit

Permalink
feat: esModule export named
Browse files Browse the repository at this point in the history
  • Loading branch information
cap-Bernardito committed Jul 15, 2020
1 parent 56c0427 commit d737aa6
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 30 deletions.
51 changes: 51 additions & 0 deletions README.md
Expand Up @@ -119,6 +119,7 @@ module.exports = {
| **[`localsConvention`](#localsconvention)** | `{String}` | `'asIs'` | Style of exported classnames |
| **[`onlyLocals`](#onlylocals)** | `{Boolean}` | `false` | Export only locals |
| **[`esModule`](#esmodule)** | `{Boolean}` | `false` | Use ES modules syntax |
| **[`namedExport`](#namedExport)** | `{Boolean}` | `false` | Use ES modules named export |

### `url`

Expand Down Expand Up @@ -1035,6 +1036,56 @@ module.exports = {
};
```

### `namedExport`

Type: `Boolean`
Default: `false`

Enable/disable ES modules named export for css classes.
Names of exported classes are converted to camelCase.

**styles.css**

```css
.foo-baz {
color: red;
}
.bar {
color: blue;
}
```

**index.js**

```js
import { fooBaz, bar } from './styles.css';

console.log(fooBaz, bar);
```

You can enable a ES module named export using:

**webpack.config.js**

```js
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
loader: 'css-loader',
options: {
esModule: true,
modules: {
namedExport: true,
},
},
},
],
},
};
```

## Examples

### Assets
Expand Down
33 changes: 29 additions & 4 deletions src/index.js
Expand Up @@ -171,26 +171,51 @@ export default function loader(content, map, meta) {
);
});

const { localsConvention } = options;
const namedExport =
typeof options.modules === 'object' &&
typeof options.modules.namedExport !== 'undefined'
? options.modules.namedExport
: false;

const { localsConvention } = namedExport
? { localsConvention: 'camelCaseOnly' }
: options;

const esModule =
typeof options.esModule !== 'undefined' ? options.esModule : false;

const importCode = getImportCode(this, exportType, imports, esModule);
if (Boolean(namedExport) && Boolean(namedExport) !== Boolean(esModule)) {
this.emitError(
new Error(
'`Options.module.namedExport` cannot be used without `options.esModule`'
)
);
}

const importCode = getImportCode(
this,
exportType,
imports,
esModule,
namedExport
);
const moduleCode = getModuleCode(
result,
exportType,
sourceMap,
apiImports,
urlReplacements,
icssReplacements,
esModule
esModule,
namedExport
);
const exportCode = getExportCode(
exports,
exportType,
localsConvention,
icssReplacements,
esModule
esModule,
namedExport
);

return callback(null, `${importCode}${moduleCode}${exportCode}`);
Expand Down
4 changes: 4 additions & 0 deletions src/options.json
Expand Up @@ -90,6 +90,10 @@
"instanceof": "Function"
}
]
},
"namedExport": {
"description": "Use the named export ES modules.",
"type": "boolean"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/plugins/postcss-icss-parser.js
Expand Up @@ -83,6 +83,7 @@ export default postcss.plugin(
value: {
// 'CSS_LOADER_ICSS_IMPORT'
order: 0,
icss: true,
importName,
url: options.urlHandler(resolvedUrl),
index: currentIndex,
Expand Down
48 changes: 39 additions & 9 deletions src/utils.js
Expand Up @@ -258,7 +258,13 @@ function getPreRequester({ loaders, loaderIndex }) {
};
}

function getImportCode(loaderContext, exportType, imports, esModule) {
function getImportCode(
loaderContext,
exportType,
imports,
esModule,
namedExport
) {
let code = '';

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

for (const item of imports) {
const { importName, url } = item;
const { importName, url, icss } = item;

code += esModule
? `import ${importName} from ${url};\n`
? icss && namedExport
? `import ${importName}, * as ${importName}_NAMED___ from ${url};\n`
: `import ${importName} from ${url};\n`
: `var ${importName} = require(${url});\n`;
}

Expand All @@ -290,7 +298,8 @@ function getModuleCode(
apiImports,
urlReplacements,
icssReplacements,
esModule
esModule,
namedExport
) {
if (exportType !== 'full') {
return 'var ___CSS_LOADER_EXPORT___ = {};\n';
Expand Down Expand Up @@ -339,9 +348,12 @@ function getModuleCode(
for (const replacement of icssReplacements) {
const { replacementName, importName, localName } = replacement;

code = code.replace(
new RegExp(replacementName, 'g'),
() => `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
code = code.replace(new RegExp(replacementName, 'g'), () =>
namedExport
? `" + ${importName}_NAMED___[${JSON.stringify(
camelCase(localName)
)}] + "`
: `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
);
}

Expand All @@ -359,17 +371,23 @@ function getExportCode(
exportType,
localsConvention,
icssReplacements,
esModule
esModule,
namedExport
) {
let code = '';
let localsCode = '';
let namedCode = '';

const addExportToLocalsCode = (name, value) => {
if (localsCode) {
localsCode += `,\n`;
}

localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;

if (namedExport) {
namedCode += `export const ${name} = ${JSON.stringify(value)};\n`;
}
};

for (const { name, value } of exports) {
Expand Down Expand Up @@ -416,10 +434,22 @@ function getExportCode(
new RegExp(replacementName, 'g'),
() => `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
);

if (namedExport) {
namedCode = namedCode.replace(
new RegExp(replacementName, 'g'),
() =>
`" + ${importName}_NAMED___[${JSON.stringify(
camelCase(localName)
)}] + "`
);
}
}

if (localsCode) {
code += `___CSS_LOADER_EXPORT___.locals = {\n${localsCode}\n};\n`;
code += namedCode
? `${namedCode}\n`
: `___CSS_LOADER_EXPORT___.locals = {\n${localsCode}\n};\n`;
}

code += `${
Expand Down
136 changes: 136 additions & 0 deletions test/__snapshots__/esModule-option.test.js.snap
@@ -1,5 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`"esModule" option should emit error when class has unsupported name: errors 1`] = `
Array [
"ModuleParseError: Module parse failed: Unexpected keyword 'class' (7:13)
File was processed with these loaders:",
]
`;

exports[`"esModule" option should emit error when class has unsupported name: warnings 1`] = `Array []`;

exports[`"esModule" option should emit error when exportNamed true && esModule false: errors 1`] = `
Array [
"ModuleError: Module Error (from \`replaced original path\`):
\`Options.module.namedExport\` cannot be used without \`options.esModule\`",
]
`;

exports[`"esModule" option should work js template with "exportNamed" option: errors 1`] = `Array []`;

exports[`"esModule" option should work js template with "exportNamed" option: module 1`] = `
"// Imports
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
// 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 const headerBaz = \\"header-baz\\";
export const body = \\"body\\";
export const footer = \\"footer\\";
export default ___CSS_LOADER_EXPORT___;
"
`;

exports[`"esModule" option should work js template with "exportNamed" option: result 1`] = `
Object {
"css": Array [
Array [
"./es-module/named/template/index.css",
".header-baz {
color: red;
}
.body {
color: coral;
}
.footer {
color: blue;
}
",
"",
],
],
"html": "
<div class=\\"header-baz\\">
<div class=\\"body\\">
<div class=\\"footer\\">
",
}
`;
exports[`"esModule" option should work js template with "exportNamed" option: warnings 1`] = `Array []`;
exports[`"esModule" option should work when not specified: errors 1`] = `Array []`;
exports[`"esModule" option should work when not specified: module 1`] = `
Expand Down Expand Up @@ -46,6 +109,79 @@ Array [
exports[`"esModule" option should work when not specified: warnings 1`] = `Array []`;
exports[`"esModule" option should work with "exportNamed" option with nested import: errors 1`] = `Array []`;
exports[`"esModule" option should work with "exportNamed" option with nested import: module 1`] = `
"// Imports
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
import ___CSS_LOADER_ICSS_IMPORT_0___, * as ___CSS_LOADER_ICSS_IMPORT_0____NAMED___ from \\"-!../../../../../src/index.js??[ident]!../../../modules/composes/values.css\\";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, \\"\\", true);
// Module
___CSS_LOADER_EXPORT___.push([module.id, \\".qwCT06AE1ZDvQtiz0EQJ8 {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\";\\\\n}\\\\n\\", \\"\\"]);
// Exports
export const vDef = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\"\\";
export const ghi = \\"qwCT06AE1ZDvQtiz0EQJ8\\";
export default ___CSS_LOADER_EXPORT___;
"
`;
exports[`"esModule" option should work with "exportNamed" option with nested import: result 1`] = `
Array [
Array [
"../../src/index.js?[ident]!./modules/composes/values.css",
"
",
"",
],
Array [
"./es-module/named/nested/index.css",
".qwCT06AE1ZDvQtiz0EQJ8 {
color: red;
}
",
"",
],
]
`;
exports[`"esModule" option should work with "exportNamed" option with nested import: warnings 1`] = `Array []`;
exports[`"esModule" option should work with "exportNamed" option: errors 1`] = `Array []`;
exports[`"esModule" option should work with "exportNamed" option: module 1`] = `
"// Imports
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
// Module
___CSS_LOADER_EXPORT___.push([module.id, \\"._1Cb_30DnkF22nebwtzVBFY {\\\\n color: red;\\\\n}\\\\n\\\\n.bar {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]);
// Exports
export const barBaz = \\"_1Cb_30DnkF22nebwtzVBFY\\";
export default ___CSS_LOADER_EXPORT___;
"
`;
exports[`"esModule" option should work with "exportNamed" option: result 1`] = `
Array [
Array [
"./es-module/named/base/index.css",
"._1Cb_30DnkF22nebwtzVBFY {
color: red;
}
.bar {
color: red;
}
",
"",
],
]
`;
exports[`"esModule" option should work with "exportNamed" option: warnings 1`] = `Array []`;
exports[`"esModule" option should work with a value equal to "false": errors 1`] = `Array []`;
exports[`"esModule" option should work with a value equal to "false": module 1`] = `
Expand Down

0 comments on commit d737aa6

Please sign in to comment.