diff --git a/README.md b/README.md index 0ccc5957..6566cc04 100644 --- a/README.md +++ b/README.md @@ -534,6 +534,7 @@ module.exports = { localsConvention: 'camelCase', context: path.resolve(__dirname, 'src'), hashPrefix: 'my-custom-hash', + namedExport: true, }, }, }, @@ -756,7 +757,7 @@ module.exports = { }; ``` -### `localsConvention` +##### `localsConvention` Type: `String` Default: `'asIs'` @@ -915,6 +916,58 @@ module.exports = { }; ``` +##### `namedExport` + +Type: `Boolean` +Default: `false` + +Enable/disable ES modules named export for css classes. +Names of exported classes are converted to camelCase. + +> i It is not allowed to use JavaScript reserved words in css class names + +**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, + }, + }, + }, + ], + }, +}; +``` + ### `sourceMap` Type: `Boolean` diff --git a/src/index.js b/src/index.js index 4d4b7f63..494789f3 100644 --- a/src/index.js +++ b/src/index.js @@ -41,11 +41,22 @@ export default function loader(content, map, meta) { const urlHandler = (url) => stringifyRequest(this, preRequester(options.importLoaders) + url); + const esModule = + typeof options.esModule !== 'undefined' ? options.esModule : false; + let modulesOptions; if (shouldUseModulesPlugins(options.modules, this.resourcePath)) { modulesOptions = getModulesOptions(options, this); + if (modulesOptions.namedExport === true && esModule === false) { + this.emitError( + new Error( + '`Options.module.namedExport` cannot be used without `options.esModule`' + ) + ); + } + plugins.push(...getModulesPlugins(modulesOptions, this)); const icssResolver = this.getResolve({ @@ -177,10 +188,13 @@ export default function loader(content, map, meta) { ); }); - const esModule = - typeof options.esModule !== 'undefined' ? options.esModule : false; - - const importCode = getImportCode(this, exportType, imports, esModule); + const importCode = getImportCode( + this, + exportType, + imports, + esModule, + modulesOptions + ); const moduleCode = getModuleCode( result, exportType, @@ -188,7 +202,8 @@ export default function loader(content, map, meta) { apiImports, urlReplacements, icssReplacements, - esModule + esModule, + modulesOptions ); const exportCode = getExportCode( exports, diff --git a/src/options.json b/src/options.json index 78f7f38c..7189313e 100644 --- a/src/options.json +++ b/src/options.json @@ -100,6 +100,10 @@ "instanceof": "Function" } ] + }, + "namedExport": { + "description": "Use the named export ES modules.", + "type": "boolean" } } } diff --git a/src/plugins/postcss-icss-parser.js b/src/plugins/postcss-icss-parser.js index f2de17ba..bc800625 100644 --- a/src/plugins/postcss-icss-parser.js +++ b/src/plugins/postcss-icss-parser.js @@ -83,6 +83,7 @@ export default postcss.plugin( value: { // 'CSS_LOADER_ICSS_IMPORT' order: 0, + icss: true, importName, url: options.urlHandler(resolvedUrl), index: currentIndex, diff --git a/src/utils.js b/src/utils.js index 12e24f95..78ee2591 100644 --- a/src/utils.js +++ b/src/utils.js @@ -146,6 +146,7 @@ function getModulesOptions(options, loaderContext) { getLocalIdent, hashPrefix: '', exportGlobals: false, + namedExport: false, }; if ( @@ -264,7 +265,13 @@ function getPreRequester({ loaders, loaderIndex }) { }; } -function getImportCode(loaderContext, exportType, imports, esModule) { +function getImportCode( + loaderContext, + exportType, + imports, + esModule, + modulesOptions +) { let code = ''; if (exportType === 'full') { @@ -279,10 +286,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 && modulesOptions.namedExport + ? `import ${importName}, * as ${importName}_NAMED___ from ${url};\n` + : `import ${importName} from ${url};\n` : `var ${importName} = require(${url});\n`; } @@ -296,7 +305,8 @@ function getModuleCode( apiImports, urlReplacements, icssReplacements, - esModule + esModule, + modulesOptions ) { if (exportType !== 'full') { return 'var ___CSS_LOADER_EXPORT___ = {};\n'; @@ -345,9 +355,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'), () => + modulesOptions.namedExport + ? `" + ${importName}_NAMED___[${JSON.stringify( + camelCase(localName) + )}] + "` + : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` ); } @@ -369,6 +382,7 @@ function getExportCode( ) { let code = ''; let localsCode = ''; + let namedCode = ''; const addExportToLocalsCode = (name, value) => { if (localsCode) { @@ -376,6 +390,12 @@ function getExportCode( } localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`; + + if (modulesOptions.namedExport) { + namedCode += `export const ${camelCase(name)} = ${JSON.stringify( + value + )};\n`; + } }; for (const { name, value } of exports) { @@ -422,10 +442,22 @@ function getExportCode( new RegExp(replacementName, 'g'), () => `" + ${importName}.locals[${JSON.stringify(localName)}] + "` ); + + if (modulesOptions.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 += `${ diff --git a/test/__snapshots__/esModule-option.test.js.snap b/test/__snapshots__/esModule-option.test.js.snap index 3f0b0b7b..82263331 100644 --- a/test/__snapshots__/esModule-option.test.js.snap +++ b/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 namedExport 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 "namedExport" option: errors 1`] = `Array []`; + +exports[`"esModule" option should work js template with "namedExport" 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 "namedExport" option: result 1`] = ` +Object { + "css": Array [ + Array [ + "./es-module/named/template/index.css", + ".header-baz { + color: red; +} + +.body { + color: coral; +} + +.footer { + color: blue; +} +", + "", + ], + ], + "html": " +