From fd18587c1b6d689e3e3a3cc3e6c9fe52f5080181 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:55:16 +0300 Subject: [PATCH 1/5] chore: husky migration (#1584) --- .husky/commit-msg | 3 --- .husky/pre-commit | 3 --- package.json | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.husky/commit-msg b/.husky/commit-msg index e8511eae..fd2bf708 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx --no-install commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit index d37daa07..041c660c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx --no-install lint-staged diff --git a/package.json b/package.json index 65d45335..0187e7f8 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", "pretest": "npm run lint", "test": "npm run test:coverage", - "prepare": "husky install && npm run build", + "prepare": "husky && npm run build", "release": "standard-version" }, "files": [ From af834b43b375f336108d74ff7bd9ed13bc79200a Mon Sep 17 00:00:00 2001 From: Stephen Kao Date: Mon, 8 Apr 2024 13:06:27 -0400 Subject: [PATCH 2/5] feat: added the `getJSON` option to output CSS modules mapping (#1577) --- README.md | 304 ++++++++++++++++++ src/index.js | 11 + src/options.json | 5 + .../__snapshots__/modules-option.test.js.snap | 136 ++++++++ .../validate-options.test.js.snap | 44 ++- .../modules/getJSON/composeSource.css | 7 + test/fixtures/modules/getJSON/source.css | 11 + test/fixtures/modules/getJSON/source.js | 5 + test/helpers/normalizeErrors.js | 2 +- test/modules-option.test.js | 33 +- test/url-option.test.js | 6 +- test/validate-options.test.js | 5 + 12 files changed, 549 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/modules/getJSON/composeSource.css create mode 100644 test/fixtures/modules/getJSON/source.css create mode 100644 test/fixtures/modules/getJSON/source.js diff --git a/README.md b/README.md index 2c490431..54b0a30a 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,17 @@ type modules = | "dashes-only" | ((name: string) => string); exportOnlyLocals: boolean; + getJSON: ({ + resourcePath, + imports, + exports, + replacements, + }: { + resourcePath: string; + imports: object[]; + exports: object[]; + replacements: object[]; + }) => any; }; ``` @@ -604,6 +615,7 @@ module.exports = { namedExport: true, exportLocalsConvention: "as-is", exportOnlyLocals: false, + getJSON: ({ resourcePath, imports, exports, replacements }) => {}, }, }, }, @@ -1384,6 +1396,298 @@ module.exports = { }; ``` +##### `getJSON` + +Type: + +```ts +type getJSON = ({ + resourcePath, + imports, + exports, + replacements, +}: { + resourcePath: string; + imports: object[]; + exports: object[]; + replacements: object[]; +}) => any; +``` + +Default: `undefined` + +Enables a callback to output the CSS modules mapping JSON. The callback is invoked with an object containing the following: + +- `resourcePath`: the absolute path of the original resource, e.g., `/foo/bar/baz.module.css` + +- `imports`: an array of import objects with data about import types and file paths, e.g., + +```json +[ + { + "type": "icss_import", + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "url": "\"-!../../../../../node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[4].use[1]!../../../../../node_modules/postcss-loader/dist/cjs.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../baz.module.css\"", + "icss": true, + "index": 0 + } +] +``` + +(Note that this will include all imports, not just those relevant to CSS modules.) + +- `exports`: an array of export objects with exported names and values, e.g., + +```json +[ + { + "name": "main", + "value": "D2Oy" + } +] +``` + +- `replacements`: an array of import replacement objects used for linking `imports` and `exports`, e.g., + +```json +{ + "replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___", + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "localName": "main" +} +``` + +**webpack.config.js** + +```js +// supports a synchronous callback +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + getJSON: ({ resourcePath, exports }) => { + // synchronously write a .json mapping file in the same directory as the resource + const exportsJson = exports.reduce( + (acc, { name, value }) => ({ ...acc, [name]: value }), + {}, + ); + + const outputPath = path.resolve( + path.dirname(resourcePath), + `${path.basename(resourcePath)}.json`, + ); + + const fs = require("fs"); + fs.writeFileSync(outputPath, JSON.stringify(json)); + }, + }, + }, + }, + ], + }, +}; + +// supports an asynchronous callback +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + getJSON: async ({ resourcePath, exports }) => { + const exportsJson = exports.reduce( + (acc, { name, value }) => ({ ...acc, [name]: value }), + {}, + ); + + const outputPath = path.resolve( + path.dirname(resourcePath), + `${path.basename(resourcePath)}.json`, + ); + + const fsp = require("fs/promises"); + await fsp.writeFile(outputPath, JSON.stringify(json)); + }, + }, + }, + }, + ], + }, +}; +``` + +Using `getJSON`, it's possible to output a files with all CSS module mappings. +In the following example, we use `getJSON` to cache canonical mappings and +add stand-ins for any composed values (through `composes`), and we use a custom plugin +to consolidate the values and output them to a file: + +```js +const CSS_LOADER_REPLACEMENT_REGEX = + /(___CSS_LOADER_ICSS_IMPORT_\d+_REPLACEMENT_\d+___)/g; +const REPLACEMENT_REGEX = /___REPLACEMENT\[(.*?)\]\[(.*?)\]___/g; +const IDENTIFIER_REGEX = /\[(.*?)\]\[(.*?)\]/; +const replacementsMap = {}; +const canonicalValuesMap = {}; +const allExportsJson = {}; + +function generateIdentifier(resourcePath, localName) { + return `[${resourcePath}][${localName}]`; +} + +function addReplacements(resourcePath, imports, exportsJson, replacements) { + const importReplacementsMap = {}; + + // create a dict to quickly identify imports and get their absolute stand-in strings in the currently loaded file + // e.g., { '___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___': '___REPLACEMENT[/foo/bar/baz.css][main]___' } + importReplacementsMap[resourcePath] = replacements.reduce( + (acc, { replacementName, importName, localName }) => { + const replacementImportUrl = imports.find( + (importData) => importData.importName === importName, + ).url; + const relativePathRe = /.*!(.*)"/; + const [, relativePath] = replacementImportUrl.match(relativePathRe); + const importPath = path.resolve(path.dirname(resourcePath), relativePath); + const identifier = generateIdentifier(importPath, localName); + return { ...acc, [replacementName]: `___REPLACEMENT${identifier}___` }; + }, + {}, + ); + + // iterate through the raw exports and add stand-in variables + // ('___REPLACEMENT[][]___') + // to be replaced in the plugin below + for (const [localName, classNames] of Object.entries(exportsJson)) { + const identifier = generateIdentifier(resourcePath, localName); + + if (CSS_LOADER_REPLACEMENT_REGEX.test(classNames)) { + // if there are any replacements needed in the concatenated class names, + // add them all to the replacements map to be replaced altogether later + replacementsMap[identifier] = classNames.replaceAll( + CSS_LOADER_REPLACEMENT_REGEX, + (_, replacementName) => { + return importReplacementsMap[resourcePath][replacementName]; + }, + ); + } else { + // otherwise, no class names need replacements so we can add them to + // canonical values map and all exports JSON verbatim + canonicalValuesMap[identifier] = classNames; + + allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; + allExportsJson[resourcePath][localName] = classNames; + } + } +} + +function replaceReplacements(classNames) { + const adjustedClassNames = classNames.replaceAll( + REPLACEMENT_REGEX, + (_, resourcePath, localName) => { + const identifier = generateIdentifier(resourcePath, localName); + if (identifier in canonicalValuesMap) { + return canonicalValuesMap[identifier]; + } + + // recurse through other stand-in that may be imports + const canonicalValue = replaceReplacements(replacementsMap[identifier]); + canonicalValuesMap[identifier] = canonicalValue; + return canonicalValue; + }, + ); + + return adjustedClassNames; +} + +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + getJSON: ({ resourcePath, imports, exports, replacements }) => { + const exportsJson = exports.reduce( + (acc, { name, value }) => ({ ...acc, [name]: value }), + {}, + ); + + if (replacements.length > 0) { + // replacements present --> add stand-in values for absolute paths and local names, + // which will be resolved to their canonical values in the plugin below + addReplacements( + resourcePath, + imports, + exportsJson, + replacements, + ); + } else { + // no replacements present --> add to canonicalValuesMap verbatim + // since all values here are canonical/don't need resolution + for (const [key, value] of Object.entries(exportsJson)) { + const id = `[${resourcePath}][${key}]`; + + canonicalValuesMap[id] = value; + } + + allExportsJson[resourcePath] = exportsJson; + } + }, + }, + }, + }, + ], + }, + plugins: [ + { + apply(compiler) { + compiler.hooks.done.tap("CssModulesJsonPlugin", () => { + for (const [identifier, classNames] of Object.entries( + replacementsMap, + )) { + const adjustedClassNames = replaceReplacements(classNames); + replacementsMap[identifier] = adjustedClassNames; + const [, resourcePath, localName] = + identifier.match(IDENTIFIER_REGEX); + allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; + allExportsJson[resourcePath][localName] = adjustedClassNames; + } + + fs.writeFileSync( + "./output.css.json", + JSON.stringify(allExportsJson, null, 2), + "utf8", + ); + }); + }, + }, + ], +}; +``` + +In the above, all import aliases are replaced with `___REPLACEMENT[][]___` in `getJSON`, and they're resolved in the custom plugin. All CSS mappings are contained in `allExportsJson`: + +```json +{ + "/foo/bar/baz.module.css": { + "main": "D2Oy", + "header": "thNN" + }, + "/foot/bear/bath.module.css": { + "logo": "sqiR", + "info": "XMyI" + } +} +``` + +This is saved to a local file named `output.css.json`. + ### `importLoaders` Type: diff --git a/src/index.js b/src/index.js index 886a831f..25262bee 100644 --- a/src/index.js +++ b/src/index.js @@ -273,5 +273,16 @@ export default async function loader(content, map, meta) { isTemplateLiteralSupported, ); + const { getJSON } = options.modules; + if (typeof getJSON === "function") { + try { + await getJSON({ resourcePath, imports, exports, replacements }); + } catch (error) { + callback(error); + + return; + } + } + callback(null, `${importCode}${moduleCode}${exportCode}`); } diff --git a/src/options.json b/src/options.json index b8667f03..22773c7b 100644 --- a/src/options.json +++ b/src/options.json @@ -173,6 +173,11 @@ "description": "Export only locals.", "link": "https://github.com/webpack-contrib/css-loader#exportonlylocals", "type": "boolean" + }, + "getJSON": { + "description": "Allows outputting of CSS modules mapping through a callback.", + "link": "https://github.com/webpack-contrib/css-loader#getJSON", + "instanceof": "Function" } } } diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index 1352de14..2a2c2a44 100644 --- a/test/__snapshots__/modules-option.test.js.snap +++ b/test/__snapshots__/modules-option.test.js.snap @@ -1101,6 +1101,142 @@ exports[`"modules" option should emit warning when localIdentName is emoji: erro exports[`"modules" option should emit warning when localIdentName is emoji: warnings 1`] = `[]`; +exports[`"modules" option should invoke the custom getJSON function if provided: args 1`] = ` +[ + [ + { + "exports": [ + { + "name": "a", + "value": "RT7ktT7mB7tfBR25sJDZ ___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___", + }, + { + "name": "b", + "value": "IZmhTnK9CIeu6ww6Zjbv ___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_1___", + }, + ], + "imports": [ + { + "importName": "___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___", + "url": ""../../../../src/runtime/noSourceMaps.js"", + }, + { + "importName": "___CSS_LOADER_API_IMPORT___", + "type": "api_import", + "url": ""../../../../src/runtime/api.js"", + }, + { + "icss": true, + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "index": 0, + "type": "icss_import", + "url": ""-!../../../../src/index.js??ruleSet[1].rules[0].use[0]!./composeSource.css"", + }, + ], + "replacements": [ + { + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "localName": "composedA", + "replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___", + }, + { + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "localName": "composedB", + "replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_1___", + }, + ], + "resourcePath": "/test/fixtures/modules/getJSON/source.css", + }, + ], + [ + { + "exports": [ + { + "name": "composedA", + "value": "mm3SuQiO3doywWWliORs", + }, + { + "name": "composedB", + "value": "hFeFcgvjCoj_9RRA4E59 mm3SuQiO3doywWWliORs", + }, + ], + "imports": [ + { + "importName": "___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___", + "url": ""../../../../src/runtime/noSourceMaps.js"", + }, + { + "importName": "___CSS_LOADER_API_IMPORT___", + "type": "api_import", + "url": ""../../../../src/runtime/api.js"", + }, + ], + "replacements": [], + "resourcePath": "/test/fixtures/modules/getJSON/composeSource.css", + }, + ], +] +`; + +exports[`"modules" option should invoke the custom getJSON function if provided: errors 1`] = `[]`; + +exports[`"modules" option should invoke the custom getJSON function if provided: module 1`] = ` +"// Imports +import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../../../../src/runtime/noSourceMaps.js"; +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??ruleSet[1].rules[0].use[0]!./composeSource.css"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___); +___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, "", true); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \`.RT7ktT7mB7tfBR25sJDZ { + + background-color: aliceblue; +} + +.IZmhTnK9CIeu6ww6Zjbv { + + background-color: blanchedalmond; +} +\`, ""]); +// Exports +export var a = \`RT7ktT7mB7tfBR25sJDZ \${___CSS_LOADER_ICSS_IMPORT_0____NAMED___["composedA"]}\`; +export var b = \`IZmhTnK9CIeu6ww6Zjbv \${___CSS_LOADER_ICSS_IMPORT_0____NAMED___["composedB"]}\`; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should invoke the custom getJSON function if provided: result 1`] = ` +[ + [ + "../../src/index.js??ruleSet[1].rules[0].use[0]!./modules/getJSON/composeSource.css", + ".mm3SuQiO3doywWWliORs { + height: 200px; +} + +.hFeFcgvjCoj_9RRA4E59 { +} +", + "", + ], + [ + "./modules/getJSON/source.css", + ".RT7ktT7mB7tfBR25sJDZ { + + background-color: aliceblue; +} + +.IZmhTnK9CIeu6ww6Zjbv { + + background-color: blanchedalmond; +} +", + "", + ], +] +`; + +exports[`"modules" option should invoke the custom getJSON function if provided: warnings 1`] = `[]`; + exports[`"modules" option should keep order: errors 1`] = `[]`; exports[`"modules" option should keep order: module 1`] = ` diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index da19f1c1..b1a0ac65 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -85,7 +85,7 @@ exports[`validate options should throw an error on the "importLoaders" option wi exports[`validate options should throw an error on the "modules" option with "{"auto":"invalid"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -109,7 +109,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"exportLocalsConvention":"unknown"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -130,6 +130,20 @@ exports[`validate options should throw an error on the "modules" option with "{" -> Read more at https://github.com/webpack-contrib/css-loader#exportonlylocals" `; +exports[`validate options should throw an error on the "modules" option with "{"getJSON":"invalid"}" value 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options.modules.getJSON should be an instance of function. + -> Allows outputting of CSS modules mapping through a callback. + -> Read more at https://github.com/webpack-contrib/css-loader#getJSON" +`; + +exports[`validate options should throw an error on the "modules" option with "{"getJSON":true}" value 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options.modules.getJSON should be an instance of function. + -> Allows outputting of CSS modules mapping through a callback. + -> Read more at https://github.com/webpack-contrib/css-loader#getJSON" +`; + exports[`validate options should throw an error on the "modules" option with "{"getLocalIdent":[]}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules.getLocalIdent should be an instance of function. @@ -161,7 +175,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"localIdentRegExp":true}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -177,7 +191,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":"globals"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -194,7 +208,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":"locals"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -211,7 +225,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":"pures"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -228,7 +242,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":true}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -252,7 +266,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "globals" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -260,13 +274,13 @@ exports[`validate options should throw an error on the "modules" option with "gl * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "modules" option with "locals" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -274,13 +288,13 @@ exports[`validate options should throw an error on the "modules" option with "lo * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "modules" option with "pures" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -288,13 +302,13 @@ exports[`validate options should throw an error on the "modules" option with "pu * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "modules" option with "true" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -302,7 +316,7 @@ exports[`validate options should throw an error on the "modules" option with "tr * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "sourceMap" option with "true" value 1`] = ` diff --git a/test/fixtures/modules/getJSON/composeSource.css b/test/fixtures/modules/getJSON/composeSource.css new file mode 100644 index 00000000..8303fe70 --- /dev/null +++ b/test/fixtures/modules/getJSON/composeSource.css @@ -0,0 +1,7 @@ +.composedA { + height: 200px; +} + +.composedB { + composes: composedA; +} diff --git a/test/fixtures/modules/getJSON/source.css b/test/fixtures/modules/getJSON/source.css new file mode 100644 index 00000000..7007a4e7 --- /dev/null +++ b/test/fixtures/modules/getJSON/source.css @@ -0,0 +1,11 @@ +.a { + composes: composedA from "./composeSource.css"; + + background-color: aliceblue; +} + +.b { + composes: composedB from "./composeSource.css"; + + background-color: blanchedalmond; +} diff --git a/test/fixtures/modules/getJSON/source.js b/test/fixtures/modules/getJSON/source.js new file mode 100644 index 00000000..1996779e --- /dev/null +++ b/test/fixtures/modules/getJSON/source.js @@ -0,0 +1,5 @@ +import css from './source.css'; + +__export__ = css; + +export default css; diff --git a/test/helpers/normalizeErrors.js b/test/helpers/normalizeErrors.js index e3ef68ab..04ec5673 100644 --- a/test/helpers/normalizeErrors.js +++ b/test/helpers/normalizeErrors.js @@ -1,6 +1,6 @@ import stripAnsi from "strip-ansi"; -function removeCWD(str) { +export function removeCWD(str) { const isWin = process.platform === "win32"; let cwd = process.cwd(); diff --git a/test/modules-option.test.js b/test/modules-option.test.js index a1314ea1..48716f02 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -12,8 +12,10 @@ import { getWarnings, readAsset, } from "./helpers/index"; +import { removeCWD } from "./helpers/normalizeErrors"; -const testCasesPath = path.join(__dirname, "fixtures/modules/tests-cases"); +const modulesFixturesPath = path.join(__dirname, "fixtures/modules"); +const testCasesPath = path.join(modulesFixturesPath, "tests-cases"); const testCases = fs.readdirSync(testCasesPath); jest.setTimeout(60000); @@ -2598,4 +2600,33 @@ describe('"modules" option', () => { expect(getWarnings(stats)).toMatchSnapshot("warnings"); expect(getErrors(stats)).toMatchSnapshot("errors"); }); + + it("should invoke the custom getJSON function if provided", async () => { + const getJSONSpy = jest.fn(); + const compiler = getCompiler("./modules/getJSON/source.js", { + modules: { + // need to wrap Jest spy since it doesn't pass ajv validation on its own + getJSON: (...args) => getJSONSpy(...args), + }, + }); + const stats = await compile(compiler); + + const args = getJSONSpy.mock.calls.map((arg) => [ + { + ...arg[0], + // resourcePaths are absolute so we need to make them relative for snapshots + resourcePath: removeCWD(arg[0].resourcePath), + }, + ]); + expect(args).toMatchSnapshot("args"); + + expect( + getModuleSource("./modules/getJSON/source.css", stats), + ).toMatchSnapshot("module"); + expect(getExecutedCode("main.bundle.js", compiler, stats)).toMatchSnapshot( + "result", + ); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); }); diff --git a/test/url-option.test.js b/test/url-option.test.js index f322d6c2..69e56efa 100644 --- a/test/url-option.test.js +++ b/test/url-option.test.js @@ -86,15 +86,15 @@ describe('"url" option', () => { it("should work with url.filter", async () => { const compiler = getCompiler("./url/url.js", { url: { - filter: (url, resourcePath) => { + filter: (_url, resourcePath) => { expect(typeof resourcePath === "string").toBe(true); - if (url.startsWith("/guide/img")) { + if (_url.startsWith("/guide/img")) { return false; } // Don't handle `img.png` - if (url.includes("img.png")) { + if (_url.includes("img.png")) { return false; } diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 235d250a..b9785653 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -56,6 +56,9 @@ describe("validate options", () => { { namedExport: false }, { exportOnlyLocals: true }, { exportOnlyLocals: false }, + { + getJSON: (resourcePath) => resourcePath, + }, ], failure: [ "true", @@ -76,6 +79,8 @@ describe("validate options", () => { { exportLocalsConvention: "unknown" }, { namedExport: "invalid" }, { exportOnlyLocals: "invalid" }, + { getJSON: true }, + { getJSON: "invalid" }, ], }, sourceMap: { From 9c165a4b9152d1bb5d8738f9b7775907f5483295 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:07:07 +0300 Subject: [PATCH 3/5] docs: update migration guide (#1586) --- CHANGELOG.md | 25 +++++++++++++++++++++++++ README.md | 12 ++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b512ec..672a0734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,31 @@ import * as style from "./style.css"; console.log(style.myClass); ``` +To restore 6.x behavior, please use: + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + namedExport: false, + exportLocalsConvention: 'as-is', + // + // or, if you prefer camelcase style + // + // exportLocalsConvention: 'camel-case-only' + }, + }, + }, + ], + }, +}; +``` + * The `modules.exportLocalsConvention` has the value `as-is` when the `modules.namedExport` option is `true` and you don't specify a value * Minimum supported webpack version is `5.27.0` * Minimum supported Node.js version is `18.12.0` diff --git a/README.md b/README.md index 54b0a30a..eeeb73ca 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Then add the plugin to your `webpack` config. For example: **file.js** ```js -import css from "file.css"; +import * as css from "file.css"; ``` **webpack.config.js** @@ -1174,11 +1174,11 @@ Enables/disables ES modules named export for locals. ```js import * as styles from "./styles.css"; +// If using `exportLocalsConvention: "as-is"` (default value): +console.log(styles["foo-baz"], styles.bar); + // If using `exportLocalsConvention: "camel-case-only"`: console.log(styles.fooBaz, styles.bar); - -// If using `exportLocalsConvention: "as-is"`: -console.log(styles["foo-baz"], styles.bar); ``` You can enable a ES module named export using: @@ -2337,8 +2337,8 @@ File treated as `CSS Module`. Using both `CSS Module` functionality as well as SCSS variables directly in JavaScript. ```jsx -import svars from "variables.scss"; -import styles from "Component.module.scss"; +import * as svars from "variables.scss"; +import * as styles from "Component.module.scss"; // Render DOM with CSS modules class name //
From 15f793d4fae5bd6addf84a8fce50470af9bf5129 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Mon, 8 Apr 2024 21:08:49 +0300 Subject: [PATCH 4/5] docs: update logic (#1587) --- README.md | 214 +++++++----------- src/index.js | 1 + .../__snapshots__/modules-option.test.js.snap | 139 ++++++++++++ test/helpers/get-json.js | 144 ++++++++++++ test/modules-option.test.js | 28 +++ 5 files changed, 396 insertions(+), 130 deletions(-) create mode 100644 test/helpers/get-json.js diff --git a/README.md b/README.md index eeeb73ca..4c8b8bbe 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ type modules = imports: object[]; exports: object[]; replacements: object[]; - }) => any; + }) => Promise | void; }; ``` @@ -1411,7 +1411,7 @@ type getJSON = ({ imports: object[]; exports: object[]; replacements: object[]; -}) => any; +}) => Promise | void; ``` Default: `undefined` @@ -1457,81 +1457,21 @@ Enables a callback to output the CSS modules mapping JSON. The callback is invok } ``` -**webpack.config.js** - -```js -// supports a synchronous callback -module.exports = { - module: { - rules: [ - { - test: /\.css$/i, - loader: "css-loader", - options: { - modules: { - getJSON: ({ resourcePath, exports }) => { - // synchronously write a .json mapping file in the same directory as the resource - const exportsJson = exports.reduce( - (acc, { name, value }) => ({ ...acc, [name]: value }), - {}, - ); - - const outputPath = path.resolve( - path.dirname(resourcePath), - `${path.basename(resourcePath)}.json`, - ); - - const fs = require("fs"); - fs.writeFileSync(outputPath, JSON.stringify(json)); - }, - }, - }, - }, - ], - }, -}; - -// supports an asynchronous callback -module.exports = { - module: { - rules: [ - { - test: /\.css$/i, - loader: "css-loader", - options: { - modules: { - getJSON: async ({ resourcePath, exports }) => { - const exportsJson = exports.reduce( - (acc, { name, value }) => ({ ...acc, [name]: value }), - {}, - ); - - const outputPath = path.resolve( - path.dirname(resourcePath), - `${path.basename(resourcePath)}.json`, - ); - - const fsp = require("fs/promises"); - await fsp.writeFile(outputPath, JSON.stringify(json)); - }, - }, - }, - }, - ], - }, -}; -``` - Using `getJSON`, it's possible to output a files with all CSS module mappings. In the following example, we use `getJSON` to cache canonical mappings and add stand-ins for any composed values (through `composes`), and we use a custom plugin to consolidate the values and output them to a file: +**webpack.config.js** + ```js +const path = require("path"); +const fs = require("fs"); + const CSS_LOADER_REPLACEMENT_REGEX = /(___CSS_LOADER_ICSS_IMPORT_\d+_REPLACEMENT_\d+___)/g; -const REPLACEMENT_REGEX = /___REPLACEMENT\[(.*?)\]\[(.*?)\]___/g; -const IDENTIFIER_REGEX = /\[(.*?)\]\[(.*?)\]/; +const REPLACEMENT_REGEX = /___REPLACEMENT\[(.*?)]\[(.*?)]___/g; +const IDENTIFIER_REGEX = /\[(.*?)]\[(.*?)]/; const replacementsMap = {}; const canonicalValuesMap = {}; const allExportsJson = {}; @@ -1570,9 +1510,8 @@ function addReplacements(resourcePath, imports, exportsJson, replacements) { // add them all to the replacements map to be replaced altogether later replacementsMap[identifier] = classNames.replaceAll( CSS_LOADER_REPLACEMENT_REGEX, - (_, replacementName) => { - return importReplacementsMap[resourcePath][replacementName]; - }, + (_, replacementName) => + importReplacementsMap[resourcePath][replacementName], ); } else { // otherwise, no class names need replacements so we can add them to @@ -1586,22 +1525,86 @@ function addReplacements(resourcePath, imports, exportsJson, replacements) { } function replaceReplacements(classNames) { - const adjustedClassNames = classNames.replaceAll( + return classNames.replaceAll( REPLACEMENT_REGEX, (_, resourcePath, localName) => { const identifier = generateIdentifier(resourcePath, localName); + if (identifier in canonicalValuesMap) { return canonicalValuesMap[identifier]; } - // recurse through other stand-in that may be imports + // Recurse through other stand-in that may be imports const canonicalValue = replaceReplacements(replacementsMap[identifier]); + canonicalValuesMap[identifier] = canonicalValue; + return canonicalValue; }, ); +} + +function getJSON({ resourcePath, imports, exports, replacements }) { + const exportsJson = exports.reduce((acc, { name, value }) => { + return { ...acc, [name]: value }; + }, {}); + + if (replacements.length > 0) { + // replacements present --> add stand-in values for absolute paths and local names, + // which will be resolved to their canonical values in the plugin below + addReplacements(resourcePath, imports, exportsJson, replacements); + } else { + // no replacements present --> add to canonicalValuesMap verbatim + // since all values here are canonical/don't need resolution + for (const [key, value] of Object.entries(exportsJson)) { + const id = `[${resourcePath}][${key}]`; + + canonicalValuesMap[id] = value; + } - return adjustedClassNames; + allExportsJson[resourcePath] = exportsJson; + } +} + +class CssModulesJsonPlugin { + constructor(options) { + this.options = options; + } + + // eslint-disable-next-line class-methods-use-this + apply(compiler) { + compiler.hooks.emit.tap("CssModulesJsonPlugin", () => { + for (const [identifier, classNames] of Object.entries(replacementsMap)) { + const adjustedClassNames = replaceReplacements(classNames); + + replacementsMap[identifier] = adjustedClassNames; + + const [, resourcePath, localName] = identifier.match(IDENTIFIER_REGEX); + + allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; + allExportsJson[resourcePath][localName] = adjustedClassNames; + } + + fs.writeFileSync( + this.options.filepath, + JSON.stringify( + // Make path to be relative to `context` (your project root) + Object.fromEntries( + Object.entries(allExportsJson).map((key) => { + key[0] = path + .relative(compiler.context, key[0]) + .replace(/\\/g, "/"); + + return key; + }), + ), + null, + 2, + ), + "utf8", + ); + }); + } } module.exports = { @@ -1610,63 +1613,14 @@ module.exports = { { test: /\.css$/i, loader: "css-loader", - options: { - modules: { - getJSON: ({ resourcePath, imports, exports, replacements }) => { - const exportsJson = exports.reduce( - (acc, { name, value }) => ({ ...acc, [name]: value }), - {}, - ); - - if (replacements.length > 0) { - // replacements present --> add stand-in values for absolute paths and local names, - // which will be resolved to their canonical values in the plugin below - addReplacements( - resourcePath, - imports, - exportsJson, - replacements, - ); - } else { - // no replacements present --> add to canonicalValuesMap verbatim - // since all values here are canonical/don't need resolution - for (const [key, value] of Object.entries(exportsJson)) { - const id = `[${resourcePath}][${key}]`; - - canonicalValuesMap[id] = value; - } - - allExportsJson[resourcePath] = exportsJson; - } - }, - }, - }, + options: { modules: { getJSON } }, }, ], }, plugins: [ - { - apply(compiler) { - compiler.hooks.done.tap("CssModulesJsonPlugin", () => { - for (const [identifier, classNames] of Object.entries( - replacementsMap, - )) { - const adjustedClassNames = replaceReplacements(classNames); - replacementsMap[identifier] = adjustedClassNames; - const [, resourcePath, localName] = - identifier.match(IDENTIFIER_REGEX); - allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; - allExportsJson[resourcePath][localName] = adjustedClassNames; - } - - fs.writeFileSync( - "./output.css.json", - JSON.stringify(allExportsJson, null, 2), - "utf8", - ); - }); - }, - }, + new CssModulesJsonPlugin({ + filepath: path.resolve(__dirname, "./output.css.json"), + }), ], }; ``` @@ -1675,11 +1629,11 @@ In the above, all import aliases are replaced with `___REPLACEMENT[ { + const replacementImportUrl = imports.find( + (importData) => importData.importName === importName, + ).url; + const relativePathRe = /.*!(.*)"/; + const [, relativePath] = replacementImportUrl.match(relativePathRe); + const importPath = path.resolve(path.dirname(resourcePath), relativePath); + const identifier = generateIdentifier(importPath, localName); + return { ...acc, [replacementName]: `___REPLACEMENT${identifier}___` }; + }, + {}, + ); + + // iterate through the raw exports and add stand-in variables + // ('___REPLACEMENT[][]___') + // to be replaced in the plugin below + for (const [localName, classNames] of Object.entries(exportsJson)) { + const identifier = generateIdentifier(resourcePath, localName); + + if (CSS_LOADER_REPLACEMENT_REGEX.test(classNames)) { + // if there are any replacements needed in the concatenated class names, + // add them all to the replacements map to be replaced altogether later + replacementsMap[identifier] = classNames.replaceAll( + CSS_LOADER_REPLACEMENT_REGEX, + (_, replacementName) => + importReplacementsMap[resourcePath][replacementName], + ); + } else { + // otherwise, no class names need replacements so we can add them to + // canonical values map and all exports JSON verbatim + canonicalValuesMap[identifier] = classNames; + + allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; + allExportsJson[resourcePath][localName] = classNames; + } + } +} + +function replaceReplacements(classNames) { + return classNames.replaceAll( + REPLACEMENT_REGEX, + (_, resourcePath, localName) => { + const identifier = generateIdentifier(resourcePath, localName); + + if (identifier in canonicalValuesMap) { + return canonicalValuesMap[identifier]; + } + + // Recurse through other stand-in that may be imports + const canonicalValue = replaceReplacements(replacementsMap[identifier]); + + canonicalValuesMap[identifier] = canonicalValue; + + return canonicalValue; + }, + ); +} + +function getJSON({ resourcePath, imports, exports, replacements }) { + const exportsJson = exports.reduce((acc, { name, value }) => { + return { ...acc, [name]: value }; + }, {}); + + if (replacements.length > 0) { + // replacements present --> add stand-in values for absolute paths and local names, + // which will be resolved to their canonical values in the plugin below + addReplacements(resourcePath, imports, exportsJson, replacements); + } else { + // no replacements present --> add to canonicalValuesMap verbatim + // since all values here are canonical/don't need resolution + for (const [key, value] of Object.entries(exportsJson)) { + const id = `[${resourcePath}][${key}]`; + + canonicalValuesMap[id] = value; + } + + allExportsJson[resourcePath] = exportsJson; + } +} + +class CssModulesJsonPlugin { + constructor(options) { + this.options = options; + } + + // eslint-disable-next-line class-methods-use-this + apply(compiler) { + compiler.hooks.emit.tap("CssModulesJsonPlugin", () => { + for (const [identifier, classNames] of Object.entries(replacementsMap)) { + const adjustedClassNames = replaceReplacements(classNames); + + replacementsMap[identifier] = adjustedClassNames; + + const [, resourcePath, localName] = identifier.match(IDENTIFIER_REGEX); + + allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; + allExportsJson[resourcePath][localName] = adjustedClassNames; + } + + fs.writeFileSync( + this.options.filepath, + JSON.stringify( + // Make path to be relative to `context` (your project root) + Object.fromEntries( + Object.entries(allExportsJson).map((key) => { + // eslint-disable-next-line no-param-reassign + key[0] = path + .relative(compiler.context, key[0]) + .replace(/\\/g, "/"); + + return key; + }), + ), + null, + 2, + ), + "utf8", + ); + }); + } +} + +module.exports = { getJSON, CssModulesJsonPlugin }; diff --git a/test/modules-option.test.js b/test/modules-option.test.js index 48716f02..c17f55dd 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -3,6 +3,8 @@ import fs from "fs"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; +import { getJSON, CssModulesJsonPlugin } from "./helpers/get-json"; + import { compile, getCompiler, @@ -2601,6 +2603,32 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it("should work with the `getJSON` option and resolve all classes", async () => { + const compiler = getCompiler("./modules/composes/multiple.js", { + modules: { getJSON }, + }); + + fs.mkdirSync(path.resolve(__dirname, "./outputs/"), { recursive: true }); + + const filepath = path.resolve(__dirname, "./outputs/modules.css.json"); + + new CssModulesJsonPlugin({ filepath }).apply(compiler); + + const stats = await compile(compiler); + + expect(JSON.parse(fs.readFileSync(filepath, "utf8"))).toMatchSnapshot( + "locals", + ); + expect( + getModuleSource("./modules/composes/multiple.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 invoke the custom getJSON function if provided", async () => { const getJSONSpy = jest.fn(); const compiler = getCompiler("./modules/getJSON/source.js", { From b162e252eef254d6c8271dad1751690ac4214c34 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Mon, 8 Apr 2024 21:09:04 +0300 Subject: [PATCH 5/5] chore(release): 7.1.0 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 672a0734..87f91b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.1.0](https://github.com/webpack-contrib/css-loader/compare/v7.0.0...v7.1.0) (2024-04-08) + + +### Features + +* added the `getJSON` option to output CSS modules mapping ([#1577](https://github.com/webpack-contrib/css-loader/issues/1577)) ([af834b4](https://github.com/webpack-contrib/css-loader/commit/af834b43b375f336108d74ff7bd9ed13bc79200a)) + ## [7.0.0](https://github.com/webpack-contrib/css-loader/compare/v6.11.0...v7.0.0) (2024-04-04) diff --git a/package-lock.json b/package-lock.json index c183ec93..9e429408 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "css-loader", - "version": "7.0.0", + "version": "7.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "css-loader", - "version": "7.0.0", + "version": "7.1.0", "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", diff --git a/package.json b/package.json index 0187e7f8..de1f1be5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "css-loader", - "version": "7.0.0", + "version": "7.1.0", "description": "css loader module for webpack", "license": "MIT", "repository": "webpack-contrib/css-loader",