Repository: webpack-contrib/mini-css-extract-plugin (js)

Issue:
Can't use exportLocalsOnly in css-loader
### Bug report

When I configure mini-css-extract-plugin in conjunction with css-loader and `exportLocalsOnly` I encounter an error.

Reproducible repo: https://github.com/Istanful/export-locals-only-mini-css-error

### Actual Behavior

When I configure mini-css-extract-plugin in conjunction with css-loader and `exportLocalsOnly` I encounter an error.

### Expected Behavior

It should ignore that no default export exists and compile anyways.

### How Do We Reproduce?

I create a webpack.config.js file. This is the relevant configuration.

```javascript
        {
          test: /\.css$/i,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
            },
            {
              loader: "css-loader",
              options: {
                esModule: true,
                modules: {
                  namedExport: true,
                  exportLocalsConvention: "camelCaseOnly",
                  localIdentName: "[path][name]__[local]--[hash:base64:5]",
                },
              },
            },
          ],
        },
```

When I then run `webpack serve` I get the following error:

```bash
TypeError: Cannot use 'in' operator to search for 'locals' in undefined
```

It happens [here](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/26feefefd9f73549333f797b8a1a055c1ba3437c/src/loader.js#L101) as it tries to check if locals is in the default export.

I wish to not have a default export as I want to only use CSS module locals.

### Please paste the results of `npx webpack-cli info` here, and mention other relevant information

```

  System:
    OS: macOS 10.15.7
    CPU: (12) x64 Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
    Memory: 561.29 MB / 16.00 GB
  Binaries:
    Node: 12.18.3 - /usr/local/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 6.14.6 - /usr/local/bin/npm
  Browsers:
    Chrome: 94.0.4606.71
    Safari: 14.1.2
  Packages:
    extract-text-webpack-plugin: ^3.0.2 => 3.0.2
    webpack: ^5.54.0 => 5.54.0
    webpack-cli: ^4.8.0 => 4.8.0
    webpack-dev-server: ^4.3.0 => 4.3.0
```


Pull request description:
fix: crash with the `exportLocalsConvention` option
<!--
  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:

- [x] **bugfix**
- [ ] new **feature**
- [ ] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

fixes #841

### Breaking Changes

No

### Additional Info

No

Patch:
diff --git a/src/loader.js b/src/loader.js
index d509562..001b0fd 100644
--- a/src/loader.js
+++ b/src/loader.js
@@ -98,7 +98,8 @@ export function pitch(request) {
 
       namedExport =
         // eslint-disable-next-line no-underscore-dangle
-        originalExports.__esModule && !("locals" in originalExports.default);
+        originalExports.__esModule &&
+        (!originalExports.default || !("locals" in originalExports.default));
 
       if (namedExport) {
         Object.keys(originalExports).forEach((key) => {


Test patch:
diff --git a/test/cases/export-only-locals-and-es-named-export/expected/main.js b/test/cases/export-only-locals-and-es-named-export/expected/main.js
new file mode 100644
index 0000000..0dc7fbd
--- /dev/null
+++ b/test/cases/export-only-locals-and-es-named-export/expected/main.js
@@ -0,0 +1,89 @@
+/******/ (() => { // webpackBootstrap
+/******/ 	"use strict";
+/******/ 	var __webpack_modules__ = ([
+/* 0 */,
+/* 1 */
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */   "aClass": () => (/* binding */ aClass),
+/* harmony export */   "bClass": () => (/* binding */ bClass),
+/* harmony export */   "cClass": () => (/* binding */ cClass)
+/* harmony export */ });
+// extracted by mini-css-extract-plugin
+var aClass = "foo__style__a-class";
+var bClass = "foo__style__b__class";
+var cClass = "foo__style__cClass";
+
+/***/ })
+/******/ 	]);
+/************************************************************************/
+/******/ 	// The module cache
+/******/ 	var __webpack_module_cache__ = {};
+/******/ 	
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/ 		// Check if module is in cache
+/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
+/******/ 		if (cachedModule !== undefined) {
+/******/ 			return cachedModule.exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = __webpack_module_cache__[moduleId] = {
+/******/ 			// no module.id needed
+/******/ 			// no module.loaded needed
+/******/ 			exports: {}
+/******/ 		};
+/******/ 	
+/******/ 		// Execute the module function
+/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/ 	
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/ 	
+/************************************************************************/
+/******/ 	/* webpack/runtime/define property getters */
+/******/ 	(() => {
+/******/ 		// define getter functions for harmony exports
+/******/ 		__webpack_require__.d = (exports, definition) => {
+/******/ 			for(var key in definition) {
+/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ 				}
+/******/ 			}
+/******/ 		};
+/******/ 	})();
+/******/ 	
+/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
+/******/ 	(() => {
+/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ 	})();
+/******/ 	
+/******/ 	/* webpack/runtime/make namespace object */
+/******/ 	(() => {
+/******/ 		// define __esModule on exports
+/******/ 		__webpack_require__.r = (exports) => {
+/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ 			}
+/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
+/******/ 		};
+/******/ 	})();
+/******/ 	
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _style_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+
+
+// eslint-disable-next-line no-console
+console.log({ aClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.aClass, bClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.bClass, cClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.cClass });
+
+})();
+
+/******/ })()
+;
\ No newline at end of file
diff --git a/test/cases/export-only-locals-and-es-named-export/index.js b/test/cases/export-only-locals-and-es-named-export/index.js
new file mode 100644
index 0000000..6087369
--- /dev/null
+++ b/test/cases/export-only-locals-and-es-named-export/index.js
@@ -0,0 +1,4 @@
+import { aClass, bClass, cClass } from "./style.css";
+
+// eslint-disable-next-line no-console
+console.log({ aClass, bClass, cClass });
diff --git a/test/cases/export-only-locals-and-es-named-export/style.css b/test/cases/export-only-locals-and-es-named-export/style.css
new file mode 100644
index 0000000..a908540
--- /dev/null
+++ b/test/cases/export-only-locals-and-es-named-export/style.css
@@ -0,0 +1,11 @@
+.a-class {
+  background: red;
+}
+
+.b__class {
+  color: green;
+}
+
+.cClass {
+  color: blue;
+}
diff --git a/test/cases/export-only-locals-and-es-named-export/webpack.config.js b/test/cases/export-only-locals-and-es-named-export/webpack.config.js
new file mode 100644
index 0000000..2cbb967
--- /dev/null
+++ b/test/cases/export-only-locals-and-es-named-export/webpack.config.js
@@ -0,0 +1,32 @@
+import Self from "../../../src";
+
+module.exports = {
+  entry: "./index.js",
+  module: {
+    rules: [
+      {
+        test: /\.css$/,
+        use: [
+          {
+            loader: Self.loader,
+          },
+          {
+            loader: "css-loader",
+            options: {
+              modules: {
+                namedExport: true,
+                localIdentName: "foo__[name]__[local]",
+                exportOnlyLocals: true,
+              },
+            },
+          },
+        ],
+      },
+    ],
+  },
+  plugins: [
+    new Self({
+      filename: "[name].css",
+    }),
+  ],
+};
diff --git a/test/cases/export-only-locals-commonjs/expected/main.js b/test/cases/export-only-locals-commonjs/expected/main.js
new file mode 100644
index 0000000..72d4ede
--- /dev/null
+++ b/test/cases/export-only-locals-commonjs/expected/main.js
@@ -0,0 +1,65 @@
+/******/ (() => { // webpackBootstrap
+/******/ 	"use strict";
+/******/ 	var __webpack_modules__ = ([
+/* 0 */,
+/* 1 */
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+// extracted by mini-css-extract-plugin
+
+
+/***/ })
+/******/ 	]);
+/************************************************************************/
+/******/ 	// The module cache
+/******/ 	var __webpack_module_cache__ = {};
+/******/ 	
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/ 		// Check if module is in cache
+/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
+/******/ 		if (cachedModule !== undefined) {
+/******/ 			return cachedModule.exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = __webpack_module_cache__[moduleId] = {
+/******/ 			// no module.id needed
+/******/ 			// no module.loaded needed
+/******/ 			exports: {}
+/******/ 		};
+/******/ 	
+/******/ 		// Execute the module function
+/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/ 	
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/ 	
+/************************************************************************/
+/******/ 	/* webpack/runtime/make namespace object */
+/******/ 	(() => {
+/******/ 		// define __esModule on exports
+/******/ 		__webpack_require__.r = (exports) => {
+/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ 			}
+/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
+/******/ 		};
+/******/ 	})();
+/******/ 	
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _style_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+
+
+// eslint-disable-next-line no-console
+console.log({ aClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.aClass, bClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.bClass, cClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.cClass });
+
+})();
+
+/******/ })()
+;
\ No newline at end of file
diff --git a/test/cases/export-only-locals-commonjs/index.js b/test/cases/export-only-locals-commonjs/index.js
new file mode 100644
index 0000000..6087369
--- /dev/null
+++ b/test/cases/export-only-locals-commonjs/index.js
@@ -0,0 +1,4 @@
+import { aClass, bClass, cClass } from "./style.css";
+
+// eslint-disable-next-line no-console
+console.log({ aClass, bClass, cClass });
diff --git a/test/cases/export-only-locals-commonjs/style.css b/test/cases/export-only-locals-commonjs/style.css
new file mode 100644
index 0000000..a908540
--- /dev/null
+++ b/test/cases/export-only-locals-commonjs/style.css
@@ -0,0 +1,11 @@
+.a-class {
+  background: red;
+}
+
+.b__class {
+  color: green;
+}
+
+.cClass {
+  color: blue;
+}
diff --git a/test/cases/export-only-locals-commonjs/webpack.config.js b/test/cases/export-only-locals-commonjs/webpack.config.js
new file mode 100644
index 0000000..74b9b8e
--- /dev/null
+++ b/test/cases/export-only-locals-commonjs/webpack.config.js
@@ -0,0 +1,32 @@
+import Self from "../../../src";
+
+module.exports = {
+  entry: "./index.js",
+  module: {
+    rules: [
+      {
+        test: /\.css$/,
+        use: [
+          {
+            loader: Self.loader,
+          },
+          {
+            loader: "css-loader",
+            options: {
+              esModule: false,
+              modules: {
+                localIdentName: "foo__[name]__[local]",
+                exportOnlyLocals: true,
+              },
+            },
+          },
+        ],
+      },
+    ],
+  },
+  plugins: [
+    new Self({
+      filename: "[name].css",
+    }),
+  ],
+};
diff --git a/test/cases/export-only-locals/expected/main.js b/test/cases/export-only-locals/expected/main.js
new file mode 100644
index 0000000..72d4ede
--- /dev/null
+++ b/test/cases/export-only-locals/expected/main.js
@@ -0,0 +1,65 @@
+/******/ (() => { // webpackBootstrap
+/******/ 	"use strict";
+/******/ 	var __webpack_modules__ = ([
+/* 0 */,
+/* 1 */
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+// extracted by mini-css-extract-plugin
+
+
+/***/ })
+/******/ 	]);
+/************************************************************************/
+/******/ 	// The module cache
+/******/ 	var __webpack_module_cache__ = {};
+/******/ 	
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/ 		// Check if module is in cache
+/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
+/******/ 		if (cachedModule !== undefined) {
+/******/ 			return cachedModule.exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = __webpack_module_cache__[moduleId] = {
+/******/ 			// no module.id needed
+/******/ 			// no module.loaded needed
+/******/ 			exports: {}
+/******/ 		};
+/******/ 	
+/******/ 		// Execute the module function
+/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/ 	
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/ 	
+/************************************************************************/
+/******/ 	/* webpack/runtime/make namespace object */
+/******/ 	(() => {
+/******/ 		// define __esModule on exports
+/******/ 		__webpack_require__.r = (exports) => {
+/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ 			}
+/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
+/******/ 		};
+/******/ 	})();
+/******/ 	
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _style_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+
+
+// eslint-disable-next-line no-console
+console.log({ aClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.aClass, bClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.bClass, cClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.cClass });
+
+})();
+
+/******/ })()
+;
\ No newline at end of file
diff --git a/test/cases/export-only-locals/index.js b/test/cases/export-only-locals/index.js
new file mode 100644
index 0000000..6087369
--- /dev/null
+++ b/test/cases/export-only-locals/index.js
@@ -0,0 +1,4 @@
+import { aClass, bClass, cClass } from "./style.css";
+
+// eslint-disable-next-line no-console
+console.log({ aClass, bClass, cClass });
diff --git a/test/cases/export-only-locals/style.css b/test/cases/export-only-locals/style.css
new file mode 100644
index 0000000..a908540
--- /dev/null
+++ b/test/cases/export-only-locals/style.css
@@ -0,0 +1,11 @@
+.a-class {
+  background: red;
+}
+
+.b__class {
+  color: green;
+}
+
+.cClass {
+  color: blue;
+}
diff --git a/test/cases/export-only-locals/webpack.config.js b/test/cases/export-only-locals/webpack.config.js
new file mode 100644
index 0000000..0a8dcf6
--- /dev/null
+++ b/test/cases/export-only-locals/webpack.config.js
@@ -0,0 +1,31 @@
+import Self from "../../../src";
+
+module.exports = {
+  entry: "./index.js",
+  module: {
+    rules: [
+      {
+        test: /\.css$/,
+        use: [
+          {
+            loader: Self.loader,
+          },
+          {
+            loader: "css-loader",
+            options: {
+              modules: {
+                localIdentName: "foo__[name]__[local]",
+                exportOnlyLocals: true,
+              },
+            },
+          },
+        ],
+      },
+    ],
+  },
+  plugins: [
+    new Self({
+      filename: "[name].css",
+    }),
+  ],
+};