Repository: webpack-contrib/css-loader
Pull request #1125 (2020-07-22 19:15:57)

refactor: named export
<!--
  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**
- [ ] new **feature**
- [x] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [x] **metadata update**

### Motivation / Use-Case

fixes #1113

### Breaking Changes

No

### Additional Info

No

codecov[bot]: # [Codecov](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125?src=pr&el=h1) Report
> Merging [#1125](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125?src=pr&el=desc) into [master](https://codecov.io/gh/webpack-contrib/css-loader/commit/01e8c7683a5f66eda7e050896a383099d6ea14e3&el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125/graphs/tree.svg?width=650&height=150&src=pr&token=DlN3OQ0JnO)](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1125   +/-   ##
=======================================
  Coverage   99.50%   99.51%           
=======================================
  Files          10       10           
  Lines         605      613    +8     
  Branches      186      192    +6     
=======================================
+ Hits          602      610    +8     
  Misses          3        3           
```


| [Impacted Files](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/index.js](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125/diff?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `100.00% <100.00%> (ø)` | |
| [src/utils.js](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125/diff?src=pr&el=tree#diff-c3JjL3V0aWxzLmpz) | `98.78% <100.00%> (+0.02%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125?src=pr&el=footer). Last update [01e8c76...af9531a](https://codecov.io/gh/webpack-contrib/css-loader/pull/1125?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 7dcaae6..7548eae 100644
--- a/README.md
+++ b/README.md
@@ -844,9 +844,10 @@ Type: `Boolean`
 Default: `false`
 
 Enables/disables ES modules named export for locals.
-Names of locals are converted to camelCase.
 
-> i It is not allowed to use JavaScript reserved words in css class names
+> ⚠ Names of locals are converted to camelcase, i.e. the `exportLocalsConvention` option has `camelCaseOnly` value by default.
+
+> ⚠ It is not allowed to use JavaScript reserved words in css class names.
 
 **styles.css**
 
@@ -920,12 +921,14 @@ module.exports = {
 ##### `exportlocalsConvention`
 
 Type: `String`
-Default: `'asIs'`
+Default: based on the `modules.namedExport` option value, if `true` - `camelCaseOnly`, otherwise `asIs`
 
 Style of exported class names.
 
 By default, the exported JSON keys mirror the class names (i.e `asIs` value).
 
+> ⚠ Only `camelCaseOnly` value allowed if you set the `namedExport` value to `true`.
+
 |         Name          |    Type    | Description                                                                                      |
 | :-------------------: | :--------: | :----------------------------------------------------------------------------------------------- |
 |     **`'asIs'`**      | `{String}` | Class names will be exported as is.                                                              |
diff --git a/src/index.js b/src/index.js
index d7e75f8..0ad95e7 100644
--- a/src/index.js
+++ b/src/index.js
@@ -36,7 +36,17 @@ export default async function loader(content, map, meta) {
   });
 
   const plugins = [];
-  const options = normalizeOptions(rawOptions, this);
+  const callback = this.async();
+
+  let options;
+
+  try {
+    options = normalizeOptions(rawOptions, this);
+  } catch (error) {
+    callback(error);
+
+    return;
+  }
 
   if (shouldUseModulesPlugins(options)) {
     const icssResolver = this.getResolve({
@@ -117,8 +127,6 @@ export default async function loader(content, map, meta) {
     }
   }
 
-  const callback = this.async();
-
   let result;
 
   try {
diff --git a/src/utils.js b/src/utils.js
index 6441cc7..6b5eb5f 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -163,6 +163,13 @@ function getModulesOptions(rawOptions, loaderContext) {
           return false;
         }
       }
+
+      if (
+        rawOptions.modules.namedExport === true &&
+        typeof rawOptions.modules.exportLocalsConvention === 'undefined'
+      ) {
+        modulesOptions.exportLocalsConvention = 'camelCaseOnly';
+      }
     }
 
     modulesOptions = { ...modulesOptions, ...(rawOptions.modules || {}) };
@@ -172,12 +179,18 @@ function getModulesOptions(rawOptions, loaderContext) {
     modulesOptions.mode = modulesOptions.mode(loaderContext.resourcePath);
   }
 
-  if (modulesOptions.namedExport === true && rawOptions.esModule === false) {
-    loaderContext.emitError(
-      new Error(
-        '`Options.module.namedExport` cannot be used without `options.esModule`'
-      )
-    );
+  if (modulesOptions.namedExport === true) {
+    if (rawOptions.esModule === false) {
+      throw new Error(
+        'The "modules.namedExport" option requires the "esModules" option to be enabled'
+      );
+    }
+
+    if (modulesOptions.exportLocalsConvention !== 'camelCaseOnly') {
+      throw new Error(
+        'The "modules.namedExport" option requires the "modules.exportLocalsConvention" option to be "camelCaseOnly"'
+      );
+    }
   }
 
   return modulesOptions;
@@ -411,19 +424,18 @@ function dashesCamelCase(str) {
 function getExportCode(exports, icssReplacements, options) {
   let code = '';
   let localsCode = '';
-  let namedCode = '';
 
   const addExportToLocalsCode = (name, value) => {
-    if (localsCode) {
-      localsCode += `,\n`;
-    }
-
-    localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;
-
     if (options.modules.namedExport) {
-      namedCode += `export const ${camelCase(name)} = ${JSON.stringify(
+      localsCode += `export const ${camelCase(name)} = ${JSON.stringify(
         value
       )};\n`;
+    } else {
+      if (localsCode) {
+        localsCode += `,\n`;
+      }
+
+      localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;
     }
   };
 
@@ -467,25 +479,18 @@ function getExportCode(exports, icssReplacements, options) {
   for (const replacement of icssReplacements) {
     const { replacementName, importName, localName } = replacement;
 
-    localsCode = localsCode.replace(
-      new RegExp(replacementName, 'g'),
-      () => `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
-    );
-
-    if (options.modules.namedExport) {
-      namedCode = namedCode.replace(
-        new RegExp(replacementName, 'g'),
-        () =>
-          `" + ${importName}_NAMED___[${JSON.stringify(
+    localsCode = localsCode.replace(new RegExp(replacementName, 'g'), () =>
+      options.modules.namedExport
+        ? `" + ${importName}_NAMED___[${JSON.stringify(
             camelCase(localName)
           )}] + "`
-      );
-    }
+        : `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
+    );
   }
 
   if (localsCode) {
-    code += namedCode
-      ? `${namedCode}\n`
+    code += options.modules.namedExport
+      ? `${localsCode}`
       : `___CSS_LOADER_EXPORT___.locals = {\n${localsCode}\n};\n`;
   }
 


Test patch:
diff --git a/test/__snapshots__/esModule-option.test.js.snap b/test/__snapshots__/esModule-option.test.js.snap
index d749a59..f6efb7f 100644
--- a/test/__snapshots__/esModule-option.test.js.snap
+++ b/test/__snapshots__/esModule-option.test.js.snap
@@ -1,68 +1,5 @@
 // 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`] = `
@@ -109,79 +46,6 @@ 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, \\"._1yYSY3W2VgnkKdMmuxCIL1 {\\\\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 = \\"_1yYSY3W2VgnkKdMmuxCIL1\\";
-
-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",
-    "._1yYSY3W2VgnkKdMmuxCIL1 {
-  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, \\"._3yAjbn27wJ9GVH2M-pj-hs {\\\\n  color: red;\\\\n}\\\\n\\\\n.bar {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
-// Exports
-export const barBaz = \\"_3yAjbn27wJ9GVH2M-pj-hs\\";
-
-export default ___CSS_LOADER_EXPORT___;
-"
-`;
-
-exports[`"esModule" option should work with "namedExport" option: result 1`] = `
-Array [
-  Array [
-    "./es-module/named/base/index.css",
-    "._3yAjbn27wJ9GVH2M-pj-hs {
-  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__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap
index ec3f187..b9cc001 100644
--- a/test/__snapshots__/modules-option.test.js.snap
+++ b/test/__snapshots__/modules-option.test.js.snap
@@ -1442,6 +1442,33 @@ Error: Can't resolve './unresolved.css' in '/test/fixtures/modules/unresolved'",
 
 exports[`"modules" option should throw an error on unresolved import: warnings 1`] = `Array []`;
 
+exports[`"modules" option should throw an error when class has unsupported name (JavaScript reserved words): errors 1`] = `
+Array [
+  "ModuleParseError: Module parse failed: Unexpected keyword 'class' (7:13)
+File was processed with these loaders:",
+]
+`;
+
+exports[`"modules" option should throw an error when class has unsupported name (JavaScript reserved words): warnings 1`] = `Array []`;
+
+exports[`"modules" option should throw an error when the "namedExport" is enabled and the "exportLocalsConvention" options has not "camelCaseOnly" value: errors 1`] = `
+Array [
+  "ModuleBuildError: Module build failed (from \`replaced original path\`):
+Error: The \\"modules.namedExport\\" option requires the \\"modules.exportLocalsConvention\\" option to be \\"camelCaseOnly\\"",
+]
+`;
+
+exports[`"modules" option should throw an error when the "namedExport" is enabled and the "exportLocalsConvention" options has not "camelCaseOnly" value: warnings 1`] = `Array []`;
+
+exports[`"modules" option should throw an error when the "namedExport" option is "true", but the "esModule" is "false": errors 1`] = `
+Array [
+  "ModuleBuildError: Module build failed (from \`replaced original path\`):
+Error: The \\"modules.namedExport\\" option requires the \\"esModules\\" option to be enabled",
+]
+`;
+
+exports[`"modules" option should throw an error when the "namedExport" option is "true", but the "esModule" is "false": warnings 1`] = `Array []`;
+
 exports[`"modules" option should work and correctly replace escaped symbols: errors 1`] = `Array []`;
 
 exports[`"modules" option should work and correctly replace escaped symbols: module 1`] = `
@@ -3603,6 +3630,52 @@ h1 #_14PgYX9AnCgTBatBghNmOB {
 
 exports[`"modules" option should work and support "pure" mode: warnings 1`] = `Array []`;
 
+exports[`"modules" option should work js template with "namedExport" option: errors 1`] = `Array []`;
+
+exports[`"modules" 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[`"modules" option should work js template with "namedExport" option: result 1`] = `
+Object {
+  "css": Array [
+    Array [
+      "./modules/namedExport/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[`"modules" option should work js template with "namedExport" option: warnings 1`] = `Array []`;
+
 exports[`"modules" option should work with a modules.auto Function that returns "false": errors 1`] = `Array []`;
 
 exports[`"modules" option should work with a modules.auto Function that returns "false": module 1`] = `
@@ -11866,6 +11939,93 @@ Array [
 
 exports[`"modules" option should work with the "auto" when it is "true": warnings 1`] = `Array []`;
 
+exports[`"modules" option should work with the "namedExport" option with nested import: errors 1`] = `Array []`;
+
+exports[`"modules" option should work with the "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]!../../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, \\"._10XHvKY7SkdmM8ZvRFtWzU {\\\\n  color: #BF4040;\\\\n  padding: 0 10px;\\\\n}\\\\n\\\\n._1kluA9lYOATstWNhEJp14n {\\\\n  color: black;\\\\n}\\\\n\\\\n@media (min-width: 960px) and (max-width: 1024px) {\\\\n  ._10XHvKY7SkdmM8ZvRFtWzU {\\\\n    color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\";\\\\n    padding: 0 20px;\\\\n  }\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+export const vDef = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"vDef\\"] + \\"\\";
+export const vPrimary = \\"#BF4040\\";
+export const sBlack = \\"black-selector\\";
+export const mLarge = \\"(min-width: 960px)\\";
+export const header = \\"_10XHvKY7SkdmM8ZvRFtWzU\\";
+export const blackSelector = \\"_1kluA9lYOATstWNhEJp14n\\";
+export default ___CSS_LOADER_EXPORT___;
+"
+`;
+
+exports[`"modules" option should work with the "namedExport" option with nested import: result 1`] = `
+Array [
+  Array [
+    "../../src/index.js?[ident]!./modules/composes/values.css",
+    "
+",
+    "",
+  ],
+  Array [
+    "./modules/namedExport/nested/index.css",
+    "._10XHvKY7SkdmM8ZvRFtWzU {
+  color: #BF4040;
+  padding: 0 10px;
+}
+
+._1kluA9lYOATstWNhEJp14n {
+  color: black;
+}
+
+@media (min-width: 960px) and (max-width: 1024px) {
+  ._10XHvKY7SkdmM8ZvRFtWzU {
+    color: red;
+    padding: 0 20px;
+  }
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with the "namedExport" option with nested import: warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with the "namedExport" option: errors 1`] = `Array []`;
+
+exports[`"modules" option should work with the "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, \\"._1DWrSJ4evr5bF-6Ojpv9nG {\\\\n  color: red;\\\\n}\\\\n\\\\n.bar {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+export const barBaz = \\"_1DWrSJ4evr5bF-6Ojpv9nG\\";
+export default ___CSS_LOADER_EXPORT___;
+"
+`;
+
+exports[`"modules" option should work with the "namedExport" option: result 1`] = `
+Array [
+  Array [
+    "./modules/namedExport/base/index.css",
+    "._1DWrSJ4evr5bF-6Ojpv9nG {
+  color: red;
+}
+
+.bar {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option should work with the "namedExport" option: warnings 1`] = `Array []`;
+
 exports[`"modules" option should work with the \`exportGlobals\` option (the \`mode\` option is \`global\`): errors 1`] = `Array []`;
 
 exports[`"modules" option should work with the \`exportGlobals\` option (the \`mode\` option is \`global\`): module 1`] = `
diff --git a/test/esModule-option.test.js b/test/esModule-option.test.js
index 592fcab..3988d4d 100644
--- a/test/esModule-option.test.js
+++ b/test/esModule-option.test.js
@@ -106,64 +106,6 @@ describe('"esModule" option', () => {
     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');
-  });
-
   const styleLoaderTests = [
     {
       localLoaderMode: 'commonjs',
@@ -296,29 +238,4 @@ describe('"esModule" option', () => {
       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/nested/index.css b/test/fixtures/es-module/named/nested/index.css
deleted file mode 100644
index ff9669f..0000000
--- a/test/fixtures/es-module/named/nested/index.css
+++ /dev/null
@@ -1,5 +0,0 @@
-@value v-def from '../../../modules/composes/values.css';
-
-.ghi {
-  color: v-def;
-}
diff --git a/test/fixtures/es-module/named/base/index.css b/test/fixtures/modules/namedExport/base/index.css
similarity index 100%
rename from test/fixtures/es-module/named/base/index.css
rename to test/fixtures/modules/namedExport/base/index.css
diff --git a/test/fixtures/es-module/named/base/index.js b/test/fixtures/modules/namedExport/base/index.js
similarity index 100%
rename from test/fixtures/es-module/named/base/index.js
rename to test/fixtures/modules/namedExport/base/index.js
diff --git a/test/fixtures/es-module/named/broken/index.css b/test/fixtures/modules/namedExport/broken/index.css
similarity index 100%
rename from test/fixtures/es-module/named/broken/index.css
rename to test/fixtures/modules/namedExport/broken/index.css
diff --git a/test/fixtures/es-module/named/broken/index.js b/test/fixtures/modules/namedExport/broken/index.js
similarity index 100%
rename from test/fixtures/es-module/named/broken/index.js
rename to test/fixtures/modules/namedExport/broken/index.js
diff --git a/test/fixtures/modules/namedExport/nested/index.css b/test/fixtures/modules/namedExport/nested/index.css
new file mode 100644
index 0000000..0d26086
--- /dev/null
+++ b/test/fixtures/modules/namedExport/nested/index.css
@@ -0,0 +1,20 @@
+@value v-def from '../../composes/values.css';
+@value v-primary: #BF4040;
+@value s-black: black-selector;
+@value m-large: (min-width: 960px);
+
+.header {
+  color: v-primary;
+  padding: 0 10px;
+}
+
+.s-black {
+  color: black;
+}
+
+@media m-large and (max-width: 1024px) {
+  .header {
+    color: v-def;
+    padding: 0 20px;
+  }
+}
diff --git a/test/fixtures/es-module/named/nested/index.js b/test/fixtures/modules/namedExport/nested/index.js
similarity index 100%
rename from test/fixtures/es-module/named/nested/index.js
rename to test/fixtures/modules/namedExport/nested/index.js
diff --git a/test/fixtures/es-module/named/template/index.css b/test/fixtures/modules/namedExport/template/index.css
similarity index 100%
rename from test/fixtures/es-module/named/template/index.css
rename to test/fixtures/modules/namedExport/template/index.css
diff --git a/test/fixtures/es-module/named/template/index.js b/test/fixtures/modules/namedExport/template/index.js
similarity index 100%
rename from test/fixtures/es-module/named/template/index.js
rename to test/fixtures/modules/namedExport/template/index.js
diff --git a/test/fixtures/es-module/named/template/template.js b/test/fixtures/modules/namedExport/template/template.js
similarity index 100%
rename from test/fixtures/es-module/named/template/template.js
rename to test/fixtures/modules/namedExport/template/template.js
diff --git a/test/modules-option.test.js b/test/modules-option.test.js
index 158b7d7..cf6f1d4 100644
--- a/test/modules-option.test.js
+++ b/test/modules-option.test.js
@@ -1004,4 +1004,101 @@ describe('"modules" option', () => {
     expect(getWarnings(stats)).toMatchSnapshot('warnings');
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
+
+  it('should work with the "namedExport" option', async () => {
+    const compiler = getCompiler('./modules/namedExport/base/index.js', {
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/namedExport/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 the "namedExport" option with nested import', async () => {
+    const compiler = getCompiler('./modules/namedExport/nested/index.js', {
+      esModule: true,
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/namedExport/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('./modules/namedExport/template/index.js', {
+      esModule: true,
+      modules: {
+        localIdentName: '[local]',
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/namedExport/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 throw an error when the "namedExport" option is "true", but the "esModule" is "false"', async () => {
+    const compiler = getCompiler('./modules/namedExport/base/index.js', {
+      esModule: false,
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats, true)).toMatchSnapshot('errors');
+  });
+
+  it('should throw an error when the "namedExport" is enabled and the "exportLocalsConvention" options has not "camelCaseOnly" value', async () => {
+    const compiler = getCompiler('./modules/namedExport/broken/index.js', {
+      esModule: true,
+      modules: {
+        namedExport: true,
+        exportLocalsConvention: 'dashes',
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats, true)).toMatchSnapshot('errors');
+  });
+
+  it('should throw an error when class has unsupported name (JavaScript reserved words)', async () => {
+    const compiler = getCompiler('./modules/namedExport/broken/index.js', {
+      esModule: true,
+      modules: {
+        namedExport: true,
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats, true)).toMatchSnapshot('errors');
+  });
 });