From 6f439299838eab7e6fb18f6e9f47b9dee2208463 Mon Sep 17 00:00:00 2001 From: Marcel Laverdet Date: Tue, 23 Jan 2024 11:41:11 -0600 Subject: [PATCH] feat: support named exports with any characters --- CHANGELOG.md | 6 ++--- README.md | 22 ++++++++-------- src/utils.js | 25 +++++++++++-------- .../__snapshots__/modules-option.test.js.snap | 13 ++++++++++ .../namedExport/exportsAs/exportsAs.css | 7 ++++++ .../modules/namedExport/exportsAs/index.js | 4 +++ test/modules-option.test.js | 18 +++++++++++++ 7 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 test/fixtures/modules/namedExport/exportsAs/exportsAs.css create mode 100644 test/fixtures/modules/namedExport/exportsAs/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ef2e59..8b9095ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. See [standa ### Bug Fixes -* css nesting support +* css nesting support * `@scope` at-rule support ## [6.9.0](https://github.com/webpack-contrib/css-loader/compare/v6.8.1...v6.9.0) (2024-01-09) @@ -170,7 +170,7 @@ All notable changes to this project will be documented in this file. See [standa * `new URL()` syntax used for `url()`, only when the `esModule` option is enabled (enabled by default), it means you can bundle CSS for libraries * [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) are handling in `url()`, it means you can register loaders for them, [example](https://webpack.js.org/configuration/module/#rulescheme) * aliases with `false` value for `url()` now generate empty data URI (i.e. `data:0,`), only when the `esModule` option is enabled (enabled by default) -* `[ext]` placeholder don't need `.` (dot) before for the `localIdentName` option, i.e. please change `.[ext]` on `[ext]` (no dot before) +* `[ext]` placeholder don't need `.` (dot) before for the `localIdentName` option, i.e. please change `.[ext]` on `[ext]` (no dot before) * `[folder]` placeholder was removed without replacement for the `localIdentName` option, please use a custom function if you need complex logic * `[emoji]` placeholder was removed without replacement for the `localIdentName` option, please use a custom function if you need complex logic * the `localIdentHashPrefix` was removed in favor the `localIdentHashSalt` option @@ -189,7 +189,7 @@ All notable changes to this project will be documented in this file. See [standa ### Notes -* **we strongly recommend not to add `.css` to `resolve.extensions`, it reduces performance and in most cases it is simply not necessary, alternative you can set resolve options [by dependency](https://webpack.js.org/configuration/resolve/#resolvebydependency)** +* **we strongly recommend not to add `.css` to `resolve.extensions`, it reduces performance and in most cases it is simply not necessary, alternative you can set resolve options [by dependency](https://webpack.js.org/configuration/resolve/#resolvebydependency)** ### [5.2.7](https://github.com/webpack-contrib/css-loader/compare/v5.2.6...v5.2.7) (2021-07-13) diff --git a/README.md b/README.md index 71331ea7..33df7048 100644 --- a/README.md +++ b/README.md @@ -1119,11 +1119,15 @@ Enables/disables ES modules named export for locals. > **Warning** > -> Names of locals are converted to camelcase, i.e. the `exportLocalsConvention` option has `camelCaseOnly` value by default. +> Names of locals are converted to camelcase, i.e. the `exportLocalsConvention` option has +> `camelCaseOnly` value by default. You can set this back to any other valid option but selectors +> which are not valid JavaScript identifiers may run into problems which do not implement the entire +> modules specification. > **Warning** > -> It is not allowed to use JavaScript reserved words in css class names. +> It is not allowed to use JavaScript reserved words in css class names unless +> `exportLocalsConvention` is `"asIs"`. **styles.css** @@ -1139,9 +1143,11 @@ Enables/disables ES modules named export for locals. **index.js** ```js -import { fooBaz, bar } from "./styles.css"; +import * as styles from "./styles.css"; -console.log(fooBaz, bar); +console.log(styles.fooBaz, styles.bar); +// or if using `exportLocalsConvention: "asIs"`: +console.log(styles["foo-baz"], styles.bar); ``` You can enable a ES module named export using: @@ -1224,10 +1230,6 @@ Style of exported class names. By default, the exported JSON keys mirror the class names (i.e `asIs` value). -> **Warning** -> -> Only `camelCaseOnly` value allowed if you set the `namedExport` value to `true`. - | Name | Type | Description | | :-------------------: | :------: | :----------------------------------------------------------------------------------------------- | | **`'asIs'`** | `string` | Class names will be exported as is. | @@ -1739,7 +1741,7 @@ With the help of the `/* webpackIgnore: true */`comment, it is possible to disab .class { /* Disabled url handling for the first url in the 'background' declaration */ color: red; - background: + background: /* webpackIgnore: true */ url("./url/img.png"), url("./url/img.png"); } @@ -1755,7 +1757,7 @@ With the help of the `/* webpackIgnore: true */`comment, it is possible to disab /* Disabled url handling for the second url in the 'background' declaration */ color: red; background: url("./url/img.png"), - /* webpackIgnore: true */ + /* webpackIgnore: true */ url("./url/img.png"); } diff --git a/src/utils.js b/src/utils.js index 80d6673c..8ce299fd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -594,6 +594,7 @@ function getModulesOptions(rawOptions, exportType, loaderContext) { : "asIs", exportOnlyLocals: false, ...rawModulesOptions, + useExportsAs: rawModulesOptions.exportLocalsConvention === "asIs", }; let exportLocalsConventionType; @@ -679,6 +680,7 @@ function getModulesOptions(rawOptions, exportType, loaderContext) { if ( typeof exportLocalsConventionType === "string" && + exportLocalsConventionType !== "asIs" && exportLocalsConventionType !== "camelCaseOnly" && exportLocalsConventionType !== "dashesOnly" ) { @@ -1158,6 +1160,7 @@ function getExportCode( if (icssPluginUsed) { let localsCode = ""; + let identifierId = 0; const addExportToLocalsCode = (names, value) => { const normalizedNames = Array.isArray(names) @@ -1165,22 +1168,24 @@ function getExportCode( : new Set([names]); for (const name of normalizedNames) { + const serializedValue = isTemplateLiteralSupported + ? convertToTemplateLiteral(value) + : JSON.stringify(value); if (options.modules.namedExport) { - localsCode += `export var ${name} = ${ - isTemplateLiteralSupported - ? convertToTemplateLiteral(value) - : JSON.stringify(value) - };\n`; + if (options.modules.useExportsAs) { + identifierId += 1; + const id = `_${identifierId.toString(16)}`; + localsCode += `var ${id} = ${serializedValue};\n`; + localsCode += `export { ${id} as ${JSON.stringify(name)} };\n`; + } else { + localsCode += `export var ${name} = ${serializedValue};\n`; + } } else { if (localsCode) { localsCode += `,\n`; } - localsCode += `\t${JSON.stringify(name)}: ${ - isTemplateLiteralSupported - ? convertToTemplateLiteral(value) - : JSON.stringify(value) - }`; + localsCode += `\t${JSON.stringify(name)}: ${serializedValue}`; } } }; diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index 5ec97206..15d944d7 100644 --- a/test/__snapshots__/modules-option.test.js.snap +++ b/test/__snapshots__/modules-option.test.js.snap @@ -8801,6 +8801,19 @@ Object { exports[`"modules" option should work with "exportOnlyLocals" and "esModule" with "true" value options: warnings 1`] = `Array []`; +exports[`"modules" option should work with "exportOnlyLocals" and "exportLocalsConvention": "asIs": errors 1`] = `Array []`; + +exports[`"modules" option should work with "exportOnlyLocals" and "exportLocalsConvention": "asIs": module 1`] = ` +"// Exports +var _1 = \`Sl3D7kVfPwS7_QdqSTVq\`; +export { _1 as \\"class\\" }; +var _2 = \`tHyHTECdn65WISyToGeV\`; +export { _2 as \\"class-name\\" }; +" +`; + +exports[`"modules" option should work with "exportOnlyLocals" and "exportLocalsConvention": "asIs": warnings 1`] = `Array []`; + exports[`"modules" option should work with "exportOnlyLocals" and "namedExport" option: errors 1`] = `Array []`; exports[`"modules" option should work with "exportOnlyLocals" and "namedExport" option: module 1`] = ` diff --git a/test/fixtures/modules/namedExport/exportsAs/exportsAs.css b/test/fixtures/modules/namedExport/exportsAs/exportsAs.css new file mode 100644 index 00000000..c89328e4 --- /dev/null +++ b/test/fixtures/modules/namedExport/exportsAs/exportsAs.css @@ -0,0 +1,7 @@ +:local(.class) { + color: red; +} + +:local(.class-name) { + color: red; +} diff --git a/test/fixtures/modules/namedExport/exportsAs/index.js b/test/fixtures/modules/namedExport/exportsAs/index.js new file mode 100644 index 00000000..557ae21c --- /dev/null +++ b/test/fixtures/modules/namedExport/exportsAs/index.js @@ -0,0 +1,4 @@ +import * as css from './exportsAs.css'; + +export const _class = css['class']; +export const _className = css['class-name']; diff --git a/test/modules-option.test.js b/test/modules-option.test.js index 4f2f1bf9..99bacdbd 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -1837,6 +1837,24 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it('should work with "exportOnlyLocals" and "exportLocalsConvention": "asIs"', async () => { + const compiler = getCompiler("./modules/namedExport/exportsAs/index.js", { + esModule: true, + modules: { + namedExport: true, + exportLocalsConvention: "asIs", + exportOnlyLocals: true, + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource("./modules/namedExport/exportsAs/exportsAs.css", stats) + ).toMatchSnapshot("module"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats, true)).toMatchSnapshot("errors"); + }); + it('should work with "url" and "namedExport"', async () => { const compiler = getCompiler("./modules/url/source.js", { modules: {