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

Issue:
css-loader namedExport seems to break import-ability of exported classes
<!--
  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 10.14
- Node Version: 12.16.1
- NPM Version: yarn@2.2.2
- webpack Version: 5.0.0-beta.29
- css-loader Version: 4.3.0

### Expected Behavior

<!-- Remove this section if not reporting a bug or modification request. -->
Importing from a css-module with the config 

```js
      {
        test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              esModule: true,
              modules: {
                namedExport: true,
              },
            },
          },
        ],
      }
```

should result in locals exported being available when imported with `import * as s from './index.module.css`.

### Actual Behavior

`console.log(s)` with the wildcard import above results in an empty object.

<!-- Remove this section if not reporting a bug or modification request. -->

https://github.com/wegry/namedExport-and-css-loader/blob/master/webpack.config.js

additional code
https://github.com/wegry/namedExport-and-css-loader/blob/master/index.js

### How Do We Reproduce?

Clone/download https://github.com/wegry/namedExport-and-css-loader/ and open the index.html.



Pull request:
feat: added `modules.namedExport`
<!--
  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

fixes #483

### Breaking Changes

No

### Additional Info

No

Patch:
diff --git a/README.md b/README.md
index a9551a2..e896812 100644
--- a/README.md
+++ b/README.md
@@ -61,13 +61,14 @@ module.exports = {
 
 ## Options
 
-|              Name               |         Type         |  Default   | Description                                              |
-| :-----------------------------: | :------------------: | :--------: | :------------------------------------------------------- |
-| [**`injectType`**](#injecttype) |      `{String}`      | `styleTag` | Allows to setup how styles will be injected into the DOM |
-| [**`attributes`**](#attributes) |      `{Object}`      |    `{}`    | Adds custom attributes to tag                            |
-|     [**`insert`**](#insert)     | `{String\|Function}` |   `head`   | Inserts tag at the given position into the DOM           |
-|       [**`base`**](#base)       |      `{Number}`      |   `true`   | Sets module ID base (DLLPlugin)                          |
-|   [**`esModule`**](#esmodule)   |     `{Boolean}`      |  `false`   | Use ES modules syntax                                    |
+|              Name               |         Type         |   Default   | Description                                              |
+| :-----------------------------: | :------------------: | :---------: | :------------------------------------------------------- |
+| [**`injectType`**](#injecttype) |      `{String}`      | `styleTag`  | Allows to setup how styles will be injected into the DOM |
+| [**`attributes`**](#attributes) |      `{Object}`      |    `{}`     | Adds custom attributes to tag                            |
+|     [**`insert`**](#insert)     | `{String\|Function}` |   `head`    | Inserts tag at the given position into the DOM           |
+|       [**`base`**](#base)       |      `{Number}`      |   `true`    | Sets module ID base (DLLPlugin)                          |
+|   [**`esModule`**](#esmodule)   |     `{Boolean}`      |   `false`   | Use ES modules syntax                                    |
+|    [**`modules`**](#modules)    |      `{Object}`      | `undefined` | Configuration CSS Modules                                |
 
 ### `injectType`
 
@@ -580,6 +581,81 @@ module.exports = {
 };
 ```
 
+### `modules`
+
+Type: `Object`
+Default: `undefined`
+
+Configuration CSS Modules.
+
+#### `namedExport`
+
+Type: `Boolean`
+Default: `false`
+
+Enables/disables ES modules named export for locals.
+
+> ⚠ Names of locals are converted to `camelCase`.
+
+> ⚠ It is not allowed to use JavaScript reserved words in css class names.
+
+> ⚠ Options `esModule` and `modules.namedExport` in `css-loader` and `style-loader` should be enabled.
+
+**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$/,
+        use: [
+          {
+            loader: 'style-loader',
+            options: {
+              esModule: true,
+              modules: {
+                namedExport: true,
+              },
+            },
+          },
+          {
+            loader: 'css-loader',
+            options: {
+              esModule: true,
+              modules: {
+                namedExport: true,
+              },
+            },
+          },
+        ],
+      },
+    ],
+  },
+};
+```
+
 ## Examples
 
 ### Source maps
diff --git a/src/index.js b/src/index.js
index 27499db..3bbc0cb 100644
--- a/src/index.js
+++ b/src/index.js
@@ -26,6 +26,8 @@ loaderApi.pitch = function loader(request) {
   const injectType = options.injectType || 'styleTag';
   const esModule =
     typeof options.esModule !== 'undefined' ? options.esModule : false;
+  const namedExport =
+    esModule && options.modules && options.modules.namedExport;
   const runtimeOptions = {
     injectType: options.injectType,
     attributes: options.attributes,
@@ -104,20 +106,22 @@ ${esModule ? 'export default {}' : ''}`;
 if (module.hot) {
   if (!content.locals || module.hot.invalidate) {
     var isEqualLocals = ${isEqualLocals.toString()};
-    var oldLocals = content.locals;
+    var oldLocals = ${namedExport ? 'locals' : 'content.locals'};
 
     module.hot.accept(
       ${loaderUtils.stringifyRequest(this, `!!${request}`)},
       function () {
         ${
           esModule
-            ? `if (!isEqualLocals(oldLocals, content.locals)) {
+            ? `if (!isEqualLocals(oldLocals, ${
+                namedExport ? 'locals' : 'content.locals'
+              }, ${namedExport})) {
                 module.hot.invalidate();
 
                 return;
               }
 
-              oldLocals = content.locals;
+              oldLocals = ${namedExport ? 'locals' : 'content.locals'};
 
               if (update && refs > 0) {
                 update(content);
@@ -159,10 +163,9 @@ if (module.hot) {
               this,
               `!${path.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`
             )};
-            import content from ${loaderUtils.stringifyRequest(
-              this,
-              `!!${request}`
-            )};`
+            import content${
+              namedExport ? ', * as locals' : ''
+            } from ${loaderUtils.stringifyRequest(this, `!!${request}`)};`
           : `var api = require(${loaderUtils.stringifyRequest(
               this,
               `!${path.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`
@@ -188,7 +191,7 @@ options.singleton = ${isSingleton};
 
 var exported = {};
 
-exported.locals = content.locals || {};
+${namedExport ? '' : 'exported.locals = content.locals || {};'}
 exported.use = function() {
   if (!(refs++)) {
     update = api(content, options);
@@ -205,7 +208,20 @@ exported.unuse = function() {
 
 ${hmrCode}
 
-${esModule ? 'export default' : 'module.exports ='} exported;`;
+${
+  esModule
+    ? `${
+        namedExport
+          ? `export * from ${loaderUtils.stringifyRequest(
+              this,
+              `!!${request}`
+            )};`
+          : ''
+      };
+       export default exported;`
+    : 'module.exports = exported;'
+}
+`;
     }
 
     case 'styleTag':
@@ -218,20 +234,22 @@ ${esModule ? 'export default' : 'module.exports ='} exported;`;
 if (module.hot) {
   if (!content.locals || module.hot.invalidate) {
     var isEqualLocals = ${isEqualLocals.toString()};
-    var oldLocals = content.locals;
+    var oldLocals = ${namedExport ? 'locals' : 'content.locals'};
 
     module.hot.accept(
       ${loaderUtils.stringifyRequest(this, `!!${request}`)},
       function () {
         ${
           esModule
-            ? `if (!isEqualLocals(oldLocals, content.locals)) {
+            ? `if (!isEqualLocals(oldLocals, ${
+                namedExport ? 'locals' : 'content.locals'
+              }, ${namedExport})) {
                 module.hot.invalidate();
 
                 return;
               }
 
-              oldLocals = content.locals;
+              oldLocals = ${namedExport ? 'locals' : 'content.locals'};
 
               update(content);`
             : `content = require(${loaderUtils.stringifyRequest(
@@ -271,10 +289,9 @@ if (module.hot) {
               this,
               `!${path.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`
             )};
-            import content from ${loaderUtils.stringifyRequest(
-              this,
-              `!!${request}`
-            )};`
+            import content${
+              namedExport ? ', * as locals' : ''
+            } from ${loaderUtils.stringifyRequest(this, `!!${request}`)};`
           : `var api = require(${loaderUtils.stringifyRequest(
               this,
               `!${path.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`
@@ -300,7 +317,13 @@ var update = api(content, options);
 
 ${hmrCode}
 
-${esModule ? 'export default' : 'module.exports ='} content.locals || {};`;
+${
+  esModule
+    ? namedExport
+      ? `export * from ${loaderUtils.stringifyRequest(this, `!!${request}`)};`
+      : 'export default content.locals || {};'
+    : 'module.exports = content.locals || {};'
+}`;
     }
   }
 };
diff --git a/src/options.json b/src/options.json
index 76f8549..5478177 100644
--- a/src/options.json
+++ b/src/options.json
@@ -33,6 +33,16 @@
     "esModule": {
       "description": "Use the ES modules syntax (https://github.com/webpack-contrib/css-loader#esmodule).",
       "type": "boolean"
+    },
+    "modules": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "namedExport": {
+          "description": "Enables/disables ES modules named export for locals (https://webpack.js.org/plugins/mini-css-extract-plugin/#namedexport).",
+          "type": "boolean"
+        }
+      }
     }
   },
   "additionalProperties": false
diff --git a/src/runtime/isEqualLocals.js b/src/runtime/isEqualLocals.js
index 1002ff1..12b2626 100644
--- a/src/runtime/isEqualLocals.js
+++ b/src/runtime/isEqualLocals.js
@@ -1,4 +1,4 @@
-function isEqualLocals(a, b) {
+function isEqualLocals(a, b, isNamedExport) {
   if ((!a && b) || (a && !b)) {
     return false;
   }
@@ -6,12 +6,22 @@ function isEqualLocals(a, b) {
   let p;
 
   for (p in a) {
+    if (isNamedExport && p === 'default') {
+      // eslint-disable-next-line no-continue
+      continue;
+    }
+
     if (a[p] !== b[p]) {
       return false;
     }
   }
 
   for (p in b) {
+    if (isNamedExport && p === 'default') {
+      // eslint-disable-next-line no-continue
+      continue;
+    }
+
     if (!a[p]) {
       return false;
     }


Test patch:
diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap
new file mode 100644
index 0000000..af40d8d
--- /dev/null
+++ b/test/__snapshots__/modules-option.test.js.snap
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`"modules" option should work with the "lazySingletonStyleTag" inject type: DOM 1`] = `
+"<!DOCTYPE html><html><head>
+  <title>style-loader test</title>
+  <style id=\\"existing-style\\">.existing { color: yellow }</style>
+</head>
+<body>
+  <h1>Body</h1>
+  <div class=\\"target\\"></div>
+  <iframe class=\\"iframeTarget\\"></iframe>
+
+
+<div class=\\"KDvN8WTqb8V1KZHXZP-v\\">Water</div><div class=\\"_2ytpV86orKhgjYu_KpCdi _2SqzNMl3NVNNilgJp7bbug\\">Ground</div></body></html>"
+`;
+
+exports[`"modules" option should work with the "lazySingletonStyleTag" inject type: errors 1`] = `Array []`;
+
+exports[`"modules" option should work with the "lazySingletonStyleTag" inject type: warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with the "lazyStyleTag" inject type: DOM 1`] = `
+"<!DOCTYPE html><html><head>
+  <title>style-loader test</title>
+  <style id=\\"existing-style\\">.existing { color: yellow }</style>
+</head>
+<body>
+  <h1>Body</h1>
+  <div class=\\"target\\"></div>
+  <iframe class=\\"iframeTarget\\"></iframe>
+
+
+<div class=\\"KDvN8WTqb8V1KZHXZP-v\\">Water</div><div class=\\"_2ytpV86orKhgjYu_KpCdi _2SqzNMl3NVNNilgJp7bbug\\">Ground</div></body></html>"
+`;
+
+exports[`"modules" option should work with the "lazyStyleTag" inject type: errors 1`] = `Array []`;
+
+exports[`"modules" option should work with the "lazyStyleTag" inject type: warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with the "singletonStyleTag" inject type: DOM 1`] = `
+"<!DOCTYPE html><html><head>
+  <title>style-loader test</title>
+  <style id=\\"existing-style\\">.existing { color: yellow }</style>
+<style>._2SqzNMl3NVNNilgJp7bbug {
+  background: red;
+}
+.KDvN8WTqb8V1KZHXZP-v {
+    background: red;
+}
+
+._2ytpV86orKhgjYu_KpCdi {
+    color: blue;
+}
+</style></head>
+<body>
+  <h1>Body</h1>
+  <div class=\\"target\\"></div>
+  <iframe class=\\"iframeTarget\\"></iframe>
+
+
+<div class=\\"KDvN8WTqb8V1KZHXZP-v\\">Water</div><div class=\\"_2ytpV86orKhgjYu_KpCdi _2SqzNMl3NVNNilgJp7bbug\\">Ground</div></body></html>"
+`;
+
+exports[`"modules" option should work with the "singletonStyleTag" inject type: errors 1`] = `Array []`;
+
+exports[`"modules" option should work with the "singletonStyleTag" inject type: warnings 1`] = `Array []`;
+
+exports[`"modules" option should work with the "styleTag" inject type: DOM 1`] = `
+"<!DOCTYPE html><html><head>
+  <title>style-loader test</title>
+  <style id=\\"existing-style\\">.existing { color: yellow }</style>
+<style>._2SqzNMl3NVNNilgJp7bbug {
+  background: red;
+}
+</style><style>.KDvN8WTqb8V1KZHXZP-v {
+    background: red;
+}
+
+._2ytpV86orKhgjYu_KpCdi {
+    color: blue;
+}
+</style></head>
+<body>
+  <h1>Body</h1>
+  <div class=\\"target\\"></div>
+  <iframe class=\\"iframeTarget\\"></iframe>
+
+
+<div class=\\"KDvN8WTqb8V1KZHXZP-v\\">Water</div><div class=\\"_2ytpV86orKhgjYu_KpCdi _2SqzNMl3NVNNilgJp7bbug\\">Ground</div></body></html>"
+`;
+
+exports[`"modules" option should work with the "styleTag" inject type: errors 1`] = `Array []`;
+
+exports[`"modules" option should work with the "styleTag" inject type: warnings 1`] = `Array []`;
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index 8e62f1b..5cd90db 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -30,50 +30,62 @@ exports[`validate options should throw an error on the "insert" option with "tru
     * options.insert should be an instance of function."
 `;
 
+exports[`validate options should throw an error on the "modules" option with "true" value 1`] = `
+"Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
+ - options.modules should be an object:
+   object { namedExport? }"
+`;
+
+exports[`validate options should throw an error on the "modules" option with "true" value 2`] = `
+"Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
+ - options.modules should be an object:
+   object { namedExport? }"
+`;
+
 exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = `
 "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema.
  - options has an unknown property 'unknown'. These properties are valid:
-   object { injectType?, attributes?, insert?, base?, esModule? }"
+   object { injectType?, attributes?, insert?, base?, esModule?, modules? }"
 `;
diff --git a/test/fixtures/named-export.css b/test/fixtures/named-export.css
new file mode 100644
index 0000000..f6a6e75
--- /dev/null
+++ b/test/fixtures/named-export.css
@@ -0,0 +1,8 @@
+:local(.myClassName) {
+    background: red;
+}
+
+:local(.myComposingClass) {
+    composes: className from './css-modules-local-scoped.css';
+    color: blue;
+}
diff --git a/test/fixtures/named-export.js b/test/fixtures/named-export.js
new file mode 100644
index 0000000..9fbacf7
--- /dev/null
+++ b/test/fixtures/named-export.js
@@ -0,0 +1,17 @@
+import { myClassName, myComposingClass } from './named-export.css';
+
+const node1 = document.createElement("DIV");
+const textNode1 = document.createTextNode("Water");
+
+node1.appendChild(textNode1);
+node1.className = myClassName;
+
+document.body.appendChild(node1);
+
+const node2 = document.createElement("DIV");
+const textNode2 = document.createTextNode("Ground");
+
+node2.appendChild(textNode2);
+node2.className = myComposingClass;
+
+document.body.appendChild(node2);
diff --git a/test/manual/src/index.js b/test/manual/src/index.js
index 91916e9..4f54d7b 100644
--- a/test/manual/src/index.js
+++ b/test/manual/src/index.js
@@ -17,6 +17,18 @@ import two from './modules/two.module.css';
 import toolbar from './modules/toolbar.module.css';
 import page from './modules/page.module.css';
 import toogle from './toogle.lazy.css';
+import {
+  namedExportRed,
+  namedExportGreen,
+  namedExportBlue,
+  namedExportBackground,
+} from './style.named-export.module.css';
+import api2, {
+  namedExportLazyRed,
+  namedExportLazyGreen,
+  namedExportLazyBlue,
+  namedExportLazyBackground,
+} from './style.named-export.lazy.module.css';
 
 console.log('___LOCALS___');
 console.log(component);
@@ -140,3 +152,91 @@ button.addEventListener('click', () => {
 const toggleSection = document.getElementById('toggle-section');
 
 toggleSection.appendChild(button);
+
+console.log('___NAMED_EXPORT___');
+console.log(
+  namedExportRed,
+  namedExportGreen,
+  namedExportBlue,
+  namedExportBackground
+);
+
+const articleElement3 = document.createElement('article');
+const h3Element3 = document.createElement('h3');
+const h3TextNode3 = document.createTextNode('Named export');
+
+const divElement9 = document.createElement('div');
+const divElement1Content1 = document.createTextNode('Red');
+
+divElement9.className = namedExportRed;
+divElement9.appendChild(divElement1Content1);
+
+const divElement10 = document.createElement('div');
+const divElement2Content1 = document.createTextNode('Green');
+
+divElement10.className = namedExportGreen;
+divElement10.appendChild(divElement2Content1);
+
+const divElement11 = document.createElement('div');
+const divElement3Content1 = document.createTextNode('Blue');
+
+divElement11.className = namedExportBlue;
+divElement11.appendChild(divElement3Content1);
+
+const divElement12 = document.createElement('div');
+
+divElement12.className = namedExportBackground;
+
+h3Element3.appendChild(h3TextNode3);
+articleElement3.appendChild(h3Element3);
+articleElement3.appendChild(divElement9);
+articleElement3.appendChild(divElement10);
+articleElement3.appendChild(divElement11);
+articleElement3.appendChild(divElement12);
+
+document.querySelectorAll('section')[0].appendChild(articleElement3);
+
+console.log('___LAZY_NAMED_EXPORT___');
+console.log(
+  namedExportLazyRed,
+  namedExportLazyGreen,
+  namedExportLazyBlue,
+  namedExportLazyBackground
+);
+
+api2.use();
+
+const articleElement4 = document.createElement('article');
+const h3Element4 = document.createElement('h3');
+const h3TextNode4 = document.createTextNode('Named export');
+
+const divElement13 = document.createElement('div');
+const divElement5Content1 = document.createTextNode('Red');
+
+divElement13.className = namedExportLazyRed;
+divElement13.appendChild(divElement5Content1);
+
+const divElement14 = document.createElement('div');
+const divElement6Content2 = document.createTextNode('Green');
+
+divElement14.className = namedExportLazyGreen;
+divElement14.appendChild(divElement6Content2);
+
+const divElement15 = document.createElement('div');
+const divElement7Content2 = document.createTextNode('Blue');
+
+divElement15.className = namedExportLazyBlue;
+divElement15.appendChild(divElement7Content2);
+
+const divElement16 = document.createElement('div');
+
+divElement16.className = namedExportLazyBackground;
+
+h3Element4.appendChild(h3TextNode4);
+articleElement4.appendChild(h3Element4);
+articleElement4.appendChild(divElement13);
+articleElement4.appendChild(divElement14);
+articleElement4.appendChild(divElement15);
+articleElement4.appendChild(divElement16);
+
+document.querySelectorAll('section')[1].appendChild(articleElement4);
diff --git a/test/manual/src/style.named-export.lazy.module.css b/test/manual/src/style.named-export.lazy.module.css
new file mode 100644
index 0000000..6c60041
--- /dev/null
+++ b/test/manual/src/style.named-export.lazy.module.css
@@ -0,0 +1,16 @@
+.named-export-lazy-red {
+  color: red;
+}
+
+.named-export-lazy-green {
+  color: green;
+}
+
+.named-export-lazy-blue {
+  color: blue;
+}
+
+.named-export-lazy-background {
+  height: 1200px;
+  background: url('./logo.png') center no-repeat;
+}
diff --git a/test/manual/src/style.named-export.module.css b/test/manual/src/style.named-export.module.css
new file mode 100644
index 0000000..d518c21
--- /dev/null
+++ b/test/manual/src/style.named-export.module.css
@@ -0,0 +1,16 @@
+.named-export-red {
+  color: red;
+}
+
+.named-export-green {
+  color: green;
+}
+
+.named-export-blue {
+  color: blue;
+}
+
+.named-export-background {
+  height: 1200px;
+  background: url('./logo.png') center no-repeat;
+}
diff --git a/test/manual/webpack.config.js b/test/manual/webpack.config.js
index af5547a..2e8805b 100644
--- a/test/manual/webpack.config.js
+++ b/test/manual/webpack.config.js
@@ -50,7 +50,13 @@ module.exports = {
       },
       {
         test: /\.module\.css$/i,
-        exclude: [/\.lazy\.css$/i, /\.link\.css$/i, /\.lazy\.module\.css$/i],
+        exclude: [
+          /\.lazy\.css$/i,
+          /\.link\.css$/i,
+          /\.lazy\.module\.css$/i,
+          /\.named-export\.module\.css$/i,
+          /\.named-export\.lazy\.module\.css$/i,
+        ],
         use: [
           {
             loader: require.resolve('../../dist/cjs.js'),
@@ -84,6 +90,7 @@ module.exports = {
       },
       {
         test: /\.lazy\.module\.css$/i,
+        exclude: [/\.named-export\.lazy\.module\.css$/i],
         use: [
           {
             loader: require.resolve('../../dist/cjs.js'),
@@ -99,7 +106,6 @@ module.exports = {
           },
         ],
       },
-
       {
         test: /\.link\.css$/i,
         use: [
@@ -162,7 +168,55 @@ module.exports = {
           },
         ],
       },
-
+      {
+        test: /\.named-export\.module\.css$/i,
+        use: [
+          {
+            loader: require.resolve('../../dist/cjs.js'),
+            options: {
+              esModule: true,
+              modules: {
+                namedExport: true,
+              },
+            },
+          },
+          {
+            loader: 'css-loader',
+            options: {
+              sourceMap: ENABLE_SOURCE_MAP,
+              esModule: true,
+              modules: {
+                namedExport: true,
+              },
+            },
+          },
+        ],
+      },
+      {
+        test: /\.named-export\.lazy\.module\.css$/i,
+        use: [
+          {
+            loader: require.resolve('../../dist/cjs.js'),
+            options: {
+              injectType: 'lazyStyleTag',
+              esModule: true,
+              modules: {
+                namedExport: true,
+              },
+            },
+          },
+          {
+            loader: 'css-loader',
+            options: {
+              sourceMap: ENABLE_SOURCE_MAP,
+              esModule: true,
+              modules: {
+                namedExport: true,
+              },
+            },
+          },
+        ],
+      },
       {
         test: /\.png$/i,
         loader: 'file-loader',
diff --git a/test/modules-option.test.js b/test/modules-option.test.js
new file mode 100644
index 0000000..c51c579
--- /dev/null
+++ b/test/modules-option.test.js
@@ -0,0 +1,64 @@
+import path from 'path';
+
+import {
+  compile,
+  getCompiler,
+  getErrors,
+  getWarnings,
+  runInJsDom,
+} from './helpers/index';
+
+describe('"modules" option', () => {
+  const injectTypes = [
+    'styleTag',
+    'singletonStyleTag',
+    'lazyStyleTag',
+    'lazySingletonStyleTag',
+  ];
+
+  injectTypes.forEach((injectType) => {
+    it(`should work with the "${injectType}" inject type`, async () => {
+      expect.assertions(3);
+
+      const compiler = getCompiler(
+        './named-export.js',
+        {},
+        {
+          module: {
+            rules: [
+              {
+                test: /\.css$/i,
+                use: [
+                  {
+                    loader: path.resolve(__dirname, '../src/cjs.js'),
+                    options: {
+                      injectType,
+                      esModule: true,
+                      modules: { namedExport: true },
+                    },
+                  },
+                  {
+                    loader: 'css-loader',
+                    options: {
+                      modules: {
+                        namedExport: true,
+                      },
+                    },
+                  },
+                ],
+              },
+            ],
+          },
+        }
+      );
+      const stats = await compile(compiler);
+
+      runInJsDom('main.bundle.js', compiler, stats, (dom) => {
+        expect(dom.serialize()).toMatchSnapshot('DOM');
+      });
+
+      expect(getWarnings(stats)).toMatchSnapshot('warnings');
+      expect(getErrors(stats)).toMatchSnapshot('errors');
+    });
+  });
+});
diff --git a/test/runtime/isEqualLocals.test.js b/test/runtime/isEqualLocals.test.js
index 3871cd6..433e874 100644
--- a/test/runtime/isEqualLocals.test.js
+++ b/test/runtime/isEqualLocals.test.js
@@ -44,5 +44,22 @@ describe('isEqualLocals', () => {
     // eslint-disable-next-line no-undefined
     expect(isEqualLocals({ foo: undefined }, { foo: 'bar' })).toBe(false);
     expect(isEqualLocals({ foo: { foo: 'bar' } }, { foo: 'bar' })).toBe(false);
+
+    expect(isEqualLocals({ foo: 'bar' }, { foo: 'bar' }, true)).toBe(true);
+    expect(isEqualLocals({ foo: 'bar' }, { foo: 'baz' }, true)).toBe(false);
+    expect(
+      isEqualLocals(
+        { default: 'foo', foo: 'bar' },
+        { default: 'bar', foo: 'bar' },
+        true
+      )
+    ).toBe(true);
+    expect(
+      isEqualLocals(
+        { default: 'foo', foo: 'bar' },
+        { default: 'bar', foo: 'baz' },
+        true
+      )
+    ).toBe(false);
   });
 });
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index b066a5a..b9db060 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -24,6 +24,10 @@ describe('validate options', () => {
       success: [true, false],
       failure: ['true'],
     },
+    modules: {
+      success: [{ namedExport: true }],
+      failure: [true, 'true'],
+    },
     unknown: {
       success: [],
       failure: [1, true, false, 'test', /test/, [], {}, { foo: 'bar' }],