diff --git a/README.md b/README.md index 7d499a09..42c3e674 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,7 @@ module.exports = { - **[`publicPath`](#publicPath)** - **[`emit`](#emit)** - **[`esModule`](#esModule)** +- **[`defaultExport`](#defaultExport)** #### `publicPath` @@ -549,6 +550,60 @@ module.exports = { }; ``` +#### `defaultExport` + +Type: + +```ts +type defaultExport = boolean; +``` + +Default: `false` + +> **Note** +> +> This option will work only when you set `namedExport` to `true` in `css-loader` + +By default, `mini-css-extract-plugin` generates JS modules based on the `esModule` and `namedExport` options in `css-loader`. +Using the `esModule` and `namedExport` options will allow you to better optimize your code. +If you set `esModule: true` and `namedExport: true` for `css-loader` `mini-css-extract-plugin` will generate **only** a named export. +Our official recommendation is to use only named export for better future compatibility. +But for some applications, it is not easy to quickly rewrite the code from the default export to a named export. + +In case you need both default and named exports, you can enable this option: + +**webpack.config.js** + +```js +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +module.exports = { + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /\.css$/i, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { + defaultExport: true, + }, + }, + { + loader: "css-loader", + esModule: true, + modules: { + namedExport: true, + }, + }, + ], + }, + ], + }, +}; +``` + ## Examples ### Recommended diff --git a/src/index.js b/src/index.js index 51809608..ddfc327b 100644 --- a/src/index.js +++ b/src/index.js @@ -37,6 +37,7 @@ const { * @property {boolean} [emit] * @property {boolean} [esModule] * @property {string} [layer] + * @property {boolean} [defaultExport] */ /** diff --git a/src/loader-options.json b/src/loader-options.json index 6d10fec2..789f5045 100644 --- a/src/loader-options.json +++ b/src/loader-options.json @@ -27,6 +27,11 @@ }, "layer": { "type": "string" + }, + "defaultExport": { + "type": "boolean", + "description": "Duplicate the named export with CSS modules locals to the default export (only when `esModules: true` for css-loader).", + "link": "https://github.com/webpack-contrib/mini-css-extract-plugin#defaultexports" } } } diff --git a/src/loader.js b/src/loader.js index 35d8a9ba..2e182aa5 100644 --- a/src/loader.js +++ b/src/loader.js @@ -272,7 +272,16 @@ function pitch(request) { .map(([id, key]) => `${id} as ${JSON.stringify(key)}`) .join(", ")} }`; - return `${localsString}\n${exportsString}\n`; + const defaultExport = + typeof options.defaultExport !== "undefined" + ? options.defaultExport + : false; + + return defaultExport + ? `${localsString}\n${exportsString}\nexport default { ${identifiers + .map(([id, key]) => `${JSON.stringify(key)}: ${id}`) + .join(", ")} }\n` + : `${localsString}\n${exportsString}\n`; } return `\n${ diff --git a/test/__snapshots__/validate-loader-options.test.js.snap b/test/__snapshots__/validate-loader-options.test.js.snap index a47a0bad..5f9bdadd 100644 --- a/test/__snapshots__/validate-loader-options.test.js.snap +++ b/test/__snapshots__/validate-loader-options.test.js.snap @@ -1,5 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`validate options should throw an error on the "defaultExport" option with "1" value 1`] = ` +"Invalid options object. Mini CSS Extract Plugin Loader has been initialized using an options object that does not match the API schema. + - options.defaultExport should be a boolean. + -> Duplicate the named export with CSS modules locals to the default export (only when \`esModules: true\` for css-loader). + -> Read more at https://github.com/webpack-contrib/mini-css-extract-plugin#defaultexports" +`; + exports[`validate options should throw an error on the "esModule" option with "1" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin Loader has been initialized using an options object that does not match the API schema. - options.esModule should be a boolean. @@ -21,47 +28,47 @@ exports[`validate options should throw an error on the "publicPath" option with exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` "Invalid options object. Mini CSS Extract Plugin 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 { publicPath?, emit?, esModule?, layer? }" + object { publicPath?, emit?, esModule?, layer?, defaultExport? }" `; diff --git a/test/cases/es-named-and-default-export/expected/main.css b/test/cases/es-named-and-default-export/expected/main.css new file mode 100644 index 00000000..aa9b1569 --- /dev/null +++ b/test/cases/es-named-and-default-export/expected/main.css @@ -0,0 +1,12 @@ +.foo__style__a-class { + background: red; +} + +.foo__style__b__class { + color: green; +} + +.foo__style__cClass { + color: blue; +} + diff --git a/test/cases/es-named-and-default-export/expected/main.js b/test/cases/es-named-and-default-export/expected/main.js new file mode 100644 index 00000000..693ca53e --- /dev/null +++ b/test/cases/es-named-and-default-export/expected/main.js @@ -0,0 +1,93 @@ +/******/ (() => { // 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 */ "a-class": () => (/* binding */ _1), +/* harmony export */ b__class: () => (/* binding */ _2), +/* harmony export */ cClass: () => (/* binding */ _3), +/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) +/* harmony export */ }); +// extracted by mini-css-extract-plugin +var _1 = "foo__style__a-class"; +var _2 = "foo__style__b__class"; +var _3 = "foo__style__cClass"; + +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({ "a-class": _1, "b__class": _2, "cClass": _3 }); + + +/***/ }) +/******/ ]); +/************************************************************************/ +/******/ // 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({ css: _style_css__WEBPACK_IMPORTED_MODULE_0__["default"], aClass: _style_css__WEBPACK_IMPORTED_MODULE_0__["a-class"], bClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.b__class, cClass: _style_css__WEBPACK_IMPORTED_MODULE_0__.cClass }); + +})(); + +/******/ })() +; \ No newline at end of file diff --git a/test/cases/es-named-and-default-export/index.js b/test/cases/es-named-and-default-export/index.js new file mode 100644 index 00000000..aa114287 --- /dev/null +++ b/test/cases/es-named-and-default-export/index.js @@ -0,0 +1,8 @@ +import css, { + "a-class" as aClass, + "b__class" as bClass, + cClass, +} from "./style.css"; + +// eslint-disable-next-line no-console +console.log({ css, aClass, bClass, cClass }); diff --git a/test/cases/es-named-and-default-export/style.css b/test/cases/es-named-and-default-export/style.css new file mode 100644 index 00000000..a9085408 --- /dev/null +++ b/test/cases/es-named-and-default-export/style.css @@ -0,0 +1,11 @@ +.a-class { + background: red; +} + +.b__class { + color: green; +} + +.cClass { + color: blue; +} diff --git a/test/cases/es-named-and-default-export/webpack.config.js b/test/cases/es-named-and-default-export/webpack.config.js new file mode 100644 index 00000000..d7d364b3 --- /dev/null +++ b/test/cases/es-named-and-default-export/webpack.config.js @@ -0,0 +1,36 @@ +import Self from "../../../src"; + +module.exports = { + entry: "./index.js", + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: Self.loader, + options: { + defaultExport: true, + }, + }, + { + loader: "css-loader", + options: { + esModule: true, + modules: { + namedExport: true, + exportLocalsConvention: "asIs", + localIdentName: "foo__[name]__[local]", + }, + }, + }, + ], + }, + ], + }, + plugins: [ + new Self({ + filename: "[name].css", + }), + ], +}; diff --git a/test/validate-loader-options.test.js b/test/validate-loader-options.test.js index 299598db..e3c371dc 100644 --- a/test/validate-loader-options.test.js +++ b/test/validate-loader-options.test.js @@ -10,6 +10,10 @@ describe("validate options", () => { success: [true, false], failure: [1], }, + defaultExport: { + success: [true, false], + failure: [1], + }, unknown: { success: [], failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }], diff --git a/types/index.d.ts b/types/index.d.ts index 0765eb15..42151007 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -148,6 +148,7 @@ type PluginOptions = { * @property {boolean} [emit] * @property {boolean} [esModule] * @property {string} [layer] + * @property {boolean} [defaultExport] */ /** * @typedef {Object} PluginOptions @@ -200,6 +201,7 @@ type LoaderOptions = { emit?: boolean | undefined; esModule?: boolean | undefined; layer?: string | undefined; + defaultExport?: boolean | undefined; }; type NormalizedPluginOptions = { filename: Required["output"]["filename"];