Repository: webpack-contrib/css-loader (js)

Issue:
Option for esModule to export named exports instead of default export
<!--
  Issues are so 🔥

  If you remove or skip this template, you'll make the 🐼 sad and the mighty god
  of Github will appear and pile-drive the close button from a great height
  while making animal noises.

  👉🏽 Need support, advice, or help? Don't open an issue!
  Head to StackOverflow or https://gitter.im/webpack/webpack.
-->

- Operating System: macOS
- Node Version: 12.13.1
- NPM Version: 6.13.4
- webpack Version: 4.41.4
- css-loader Version: 3.4.0

### Expected Behavior / Situation

When the new `esModule` option is enabled, I hoped that each CSS class name would be exported as a named export:

```css
.foo {color: red}
.bar {color: blue}
```

```js
export const foo = 'abc'
export const bar = 'def'
```

### Actual Behavior / Situation

Instead, an object of class names to hashes is exported as a default export:

```js
export default {foo: 'abc', bar: 'def'}
```

This means that tree-shaking + inlining is very limited. For example, this is able to tree-shake:

```js
import css from './styles.css'
console.log(css.foo)
```

```js
function(e,t,r){"use strict";r.r(t);console.log("foo-3TUTh")}
```

But this is not able to tree-shake or inline:

```js
import css from './styles.css'
function fn() {
  return css.foo
}
console.log(fn)
```

```js
function(e,r,t){"use strict";t.r(r);var n={foo:"foo-3TUTh",bar:"bar-1JkxR"};console.log((function(){return n.foo}))}
```

### Modification Proposal

If `esModule` used named exports, tree-shaking + inlining would always work as expected.

This would significantly reduce bundle size for apps using CSS modules.

Pull request description:
feat: esModule  export named
<!--
  HOLY CRAP a Pull Request. We ❤️ those!

  If you remove or skip this template, you'll make the 🐼 sad and the mighty god
  of Github will appear and pile-drive the close button from a great height
  while making animal noises.

  Please place an x (no spaces!) in all [ ] that apply
-->

This PR contains a:

- [ ] **bugfix**
- [x] new **feature**
- [ ] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [x] **metadata update**

### Motivation / Use-Case

esModule export named
Fixes #1029 

### Breaking Changes

No

### Additional Info

No

Patch:
diff --git a/README.md b/README.md
index 0ccc595..6566cc0 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 4d4b7f6..494789f 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 78f7f38..7189313 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 f2de17b..bc80062 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 12e24f9..78ee259 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 += `${


Test patch:
diff --git a/test/__snapshots__/esModule-option.test.js.snap b/test/__snapshots__/esModule-option.test.js.snap
index 3f0b0b7..8226333 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": "
+<div class=\\"header-baz\\">
+<div class=\\"body\\">
+<div class=\\"footer\\">
+",
+}
+`;
+
+exports[`"esModule" option should work js template with "namedExport" 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`] = `
@@ -46,6 +109,79 @@ Array [
 
 exports[`"esModule" option should work when not specified: warnings 1`] = `Array []`;
 
+exports[`"esModule" option should work with "namedExport" option with nested import: errors 1`] = `Array []`;
+
+exports[`"esModule" option should work with "namedExport" 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, \\".lOpALu8t_iv2-GccTMbIq {\\\\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 = \\"lOpALu8t_iv2-GccTMbIq\\";
+
+export default ___CSS_LOADER_EXPORT___;
+"
+`;
+
+exports[`"esModule" option should work with "namedExport" option with nested import: result 1`] = `
+Array [
+  Array [
+    "../../src/index.js?[ident]!./modules/composes/values.css",
+    "
+",
+    "",
+  ],
+  Array [
+    "./es-module/named/nested/index.css",
+    ".lOpALu8t_iv2-GccTMbIq {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"esModule" option should work with "namedExport" option with nested import: warnings 1`] = `Array []`;
+
+exports[`"esModule" option should work with "namedExport" option: errors 1`] = `Array []`;
+
+exports[`"esModule" option should work 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, \\".jSf5EjnYI1bvqKHBrOPz6 {\\\\n  color: red;\\\\n}\\\\n\\\\n.bar {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+export const barBaz = \\"jSf5EjnYI1bvqKHBrOPz6\\";
+
+export default ___CSS_LOADER_EXPORT___;
+"
+`;
+
+exports[`"esModule" option should work with "namedExport" option: result 1`] = `
+Array [
+  Array [
+    "./es-module/named/base/index.css",
+    ".jSf5EjnYI1bvqKHBrOPz6 {
+  color: red;
+}
+
+.bar {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"esModule" option should work with "namedExport" 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`] = `
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index cb4bbd1..c0d1bbc 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -39,7 +39,7 @@ exports[`validate options should throw an error on the "importLoaders" option wi
 exports[`validate options should throw an error on the "modules" option with "{"auto":"invalid"}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.auto should be one of these:
@@ -63,7 +63,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"getLocalIdent":[]}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.getLocalIdent should be one of these:
@@ -86,7 +86,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"localIdentRegExp":true}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.localIdentRegExp should be one of these:
@@ -106,7 +106,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":"globals"}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -120,7 +120,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":"locals"}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -134,7 +134,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":"pures"}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -148,7 +148,7 @@ exports[`validate options should throw an error on the "modules" option with "{"
 exports[`validate options should throw an error on the "modules" option with "{"mode":true}" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules.mode should be one of these:
@@ -159,56 +159,62 @@ exports[`validate options should throw an error on the "modules" option with "{"
        * options.modules.mode should be an instance of function."
 `;
 
+exports[`validate options should throw an error on the "modules" option with "{"namedExport":"invalid"}" value 1`] = `
+"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
+ - options.modules.namedExport should be a boolean.
+   -> Use the named export ES modules."
+`;
+
 exports[`validate options should throw an error on the "modules" option with "globals" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }"
 `;
 
 exports[`validate options should throw an error on the "modules" option with "locals" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }"
 `;
 
 exports[`validate options should throw an error on the "modules" option with "pures" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }"
 `;
 
 exports[`validate options should throw an error on the "modules" option with "true" value 1`] = `
 "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
  - options.modules should be one of these:
-   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }
+   boolean | \\"local\\" | \\"global\\" | \\"pure\\" | object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }
    -> Enables/Disables CSS Modules and their configuration (https://github.com/webpack-contrib/css-loader#modules).
    Details:
     * options.modules should be a boolean.
     * options.modules should be one of these:
       \\"local\\" | \\"global\\" | \\"pure\\"
     * options.modules should be an object:
-      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent? }"
+      object { auto?, mode?, exportGlobals?, localIdentName?, localIdentRegExp?, localsConvention?, context?, hashPrefix?, getLocalIdent?, namedExport? }"
 `;
 
 exports[`validate options should throw an error on the "onlyLocals" option with "true" value 1`] = `
diff --git a/test/esModule-option.test.js b/test/esModule-option.test.js
index 3bfdce6..6e51ac5 100644
--- a/test/esModule-option.test.js
+++ b/test/esModule-option.test.js
@@ -100,4 +100,87 @@ describe('"esModule" option', () => {
     expect(getWarnings(stats)).toMatchSnapshot('warnings');
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
+
+  it('should work with "namedExport" option', async () => {
+    const compiler = getCompiler('./es-module/named/base/index.js', {
+      esModule: true,
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./es-module/named/base/index.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should work with "namedExport" option with nested import', async () => {
+    const compiler = getCompiler('./es-module/named/nested/index.js', {
+      esModule: true,
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./es-module/named/nested/index.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should work js template with "namedExport" option', async () => {
+    const compiler = getCompiler('./es-module/named/template/index.js', {
+      esModule: true,
+      modules: {
+        localIdentName: '[local]',
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./es-module/named/template/index.css', stats)
+    ).toMatchSnapshot('module');
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
+  it('should emit error when class has unsupported name', async () => {
+    const compiler = getCompiler('./es-module/named/broken/index.js', {
+      esModule: true,
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats, true)).toMatchSnapshot('errors');
+  });
+
+  it('should emit error when namedExport true && esModule false', async () => {
+    const compiler = getCompiler('./es-module/named/base/index.js', {
+      esModule: false,
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
 });
diff --git a/test/fixtures/es-module/named/base/index.css b/test/fixtures/es-module/named/base/index.css
new file mode 100644
index 0000000..628f3ed
--- /dev/null
+++ b/test/fixtures/es-module/named/base/index.css
@@ -0,0 +1,7 @@
+:local(.bar-baz) {
+  color: red;
+}
+
+:global(.bar) {
+  color: red;
+}
diff --git a/test/fixtures/es-module/named/base/index.js b/test/fixtures/es-module/named/base/index.js
new file mode 100644
index 0000000..e1a1930
--- /dev/null
+++ b/test/fixtures/es-module/named/base/index.js
@@ -0,0 +1,5 @@
+import css from './index.css';
+
+__export__ = css;
+
+export default css;
diff --git a/test/fixtures/es-module/named/broken/index.css b/test/fixtures/es-module/named/broken/index.css
new file mode 100644
index 0000000..a219fc2
--- /dev/null
+++ b/test/fixtures/es-module/named/broken/index.css
@@ -0,0 +1,3 @@
+:local(.class) {
+  color: red;
+}
diff --git a/test/fixtures/es-module/named/broken/index.js b/test/fixtures/es-module/named/broken/index.js
new file mode 100644
index 0000000..e1a1930
--- /dev/null
+++ b/test/fixtures/es-module/named/broken/index.js
@@ -0,0 +1,5 @@
+import css from './index.css';
+
+__export__ = css;
+
+export default css;
diff --git a/test/fixtures/es-module/named/nested/index.css b/test/fixtures/es-module/named/nested/index.css
new file mode 100644
index 0000000..ff9669f
--- /dev/null
+++ b/test/fixtures/es-module/named/nested/index.css
@@ -0,0 +1,5 @@
+@value v-def from '../../../modules/composes/values.css';
+
+.ghi {
+  color: v-def;
+}
diff --git a/test/fixtures/es-module/named/nested/index.js b/test/fixtures/es-module/named/nested/index.js
new file mode 100644
index 0000000..e1a1930
--- /dev/null
+++ b/test/fixtures/es-module/named/nested/index.js
@@ -0,0 +1,5 @@
+import css from './index.css';
+
+__export__ = css;
+
+export default css;
diff --git a/test/fixtures/es-module/named/template/index.css b/test/fixtures/es-module/named/template/index.css
new file mode 100644
index 0000000..b3ccc30
--- /dev/null
+++ b/test/fixtures/es-module/named/template/index.css
@@ -0,0 +1,11 @@
+:local(.header-baz) {
+  color: red;
+}
+
+:local(.body) {
+  color: coral;
+}
+
+:local(.footer) {
+  color: blue;
+}
diff --git a/test/fixtures/es-module/named/template/index.js b/test/fixtures/es-module/named/template/index.js
new file mode 100644
index 0000000..8dc4c0e
--- /dev/null
+++ b/test/fixtures/es-module/named/template/index.js
@@ -0,0 +1,8 @@
+import css from './index.css';
+import html from './template.js';
+
+const result = {css, html};
+
+__export__ =  result;
+
+export default  result;
diff --git a/test/fixtures/es-module/named/template/template.js b/test/fixtures/es-module/named/template/template.js
new file mode 100644
index 0000000..6bdb73a
--- /dev/null
+++ b/test/fixtures/es-module/named/template/template.js
@@ -0,0 +1,11 @@
+import { headerBaz, body, footer } from './index.css';
+
+let html = '\n';
+
+html += `<div class="${headerBaz}">\n`;
+html += `<div class="${body}">\n`;
+html += `<div class="${footer}">\n`;
+
+__export__ =  html;
+
+export default  html;
diff --git a/test/fixtures/simple.css b/test/fixtures/simple.css
index 19fce73..b58cc07 100644
--- a/test/fixtures/simple.css
+++ b/test/fixtures/simple.css
@@ -1,3 +1,3 @@
-.class {
+.some-class {
   color: red;
 }
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index df7217a..b53b24a 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -37,6 +37,8 @@ describe('validate options', () => {
         { localsConvention: 'camelCaseOnly' },
         { localsConvention: 'dashes' },
         { localsConvention: 'dashesOnly' },
+        { namedExport: true },
+        { namedExport: false },
       ],
       failure: [
         'true',
@@ -55,6 +57,7 @@ describe('validate options', () => {
         { exportGlobals: 'invalid' },
         { auto: 'invalid' },
         { localsConvention: 'unknown' },
+        { namedExport: 'invalid' },
       ],
     },
     sourceMap: {
@@ -94,7 +97,18 @@ describe('validate options', () => {
     it(`should ${
       type === 'success' ? 'successfully validate' : 'throw an error on'
     } the "${key}" option with "${stringifyValue(value)}" value`, async () => {
-      const compiler = getCompiler('simple.js', { [key]: value });
+      const options = { [key]: value };
+
+      if (
+        key === 'modules' &&
+        typeof value === 'object' &&
+        value.namedExport === true
+      ) {
+        options.esModule = true;
+      }
+
+      const compiler = getCompiler('simple.js', options);
+
       let stats;
 
       try {