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/CHANGELOG.md b/CHANGELOG.md index a2b512ec..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) @@ -27,6 +34,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 2c490431..4c8b8bbe 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** @@ -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[]; + }) => Promise | void; }; ``` @@ -604,6 +615,7 @@ module.exports = { namedExport: true, exportLocalsConvention: "as-is", exportOnlyLocals: false, + getJSON: ({ resourcePath, imports, exports, replacements }) => {}, }, }, }, @@ -1162,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: @@ -1384,6 +1396,252 @@ module.exports = { }; ``` +##### `getJSON` + +Type: + +```ts +type getJSON = ({ + resourcePath, + imports, + exports, + replacements, +}: { + resourcePath: string; + imports: object[]; + exports: object[]; + replacements: object[]; +}) => Promise | void; +``` + +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" +} +``` + +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 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) => + 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) => { + key[0] = path + .relative(compiler.context, key[0]) + .replace(/\\/g, "/"); + + return key; + }), + ), + null, + 2, + ), + "utf8", + ); + }); + } +} + +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { modules: { getJSON } }, + }, + ], + }, + plugins: [ + new CssModulesJsonPlugin({ + filepath: path.resolve(__dirname, "./output.css.json"), + }), + ], +}; +``` + +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: @@ -2033,8 +2291,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 //
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 65d45335..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", @@ -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": [ diff --git a/src/index.js b/src/index.js index 886a831f..c1137b12 100644 --- a/src/index.js +++ b/src/index.js @@ -273,5 +273,17 @@ 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..8300e020 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`] = ` @@ -23651,6 +23787,145 @@ exports[`"modules" option should work with the \`exportGlobals\` option (the \`m exports[`"modules" option should work with the \`exportGlobals\` option (the \`mode\` option is \`pure\`): warnings 1`] = `[]`; +exports[`"modules" option should work with the \`getJSON\` option and resolve all classes: errors 1`] = `[]`; + +exports[`"modules" option should work with the \`getJSON\` option and resolve all classes: locals 1`] = ` +{ + "modules/composes/alias-1.css": { + "imported-alias-2": "Lg5UPByIZH1XWiASCk_q", + "imported-alias-3": "QllkotlwlKJ4pFhiIzqP", + }, + "modules/composes/alias.css": { + "imported-alias": "dnhKs1AYKq4KodZdfzcx", + }, + "modules/composes/multiple.css": { + "class": "BwiLdQraIwYyRAA53QEQ RsClSIMkfTMmUvwYT4aD OdpZEdUc2oHF96Xqdoba A3lCTIjOyIaMw91SUTt_ dnhKs1AYKq4KodZdfzcx Lg5UPByIZH1XWiASCk_q QllkotlwlKJ4pFhiIzqP global-class global-class-1 global-class-2", + "class-1": "OdpZEdUc2oHF96Xqdoba", + "class-2": "A3lCTIjOyIaMw91SUTt_", + "class-other": "DemABT8Zz2xVnnu848uO RsClSIMkfTMmUvwYT4aD OdpZEdUc2oHF96Xqdoba", + "other-class": "RsClSIMkfTMmUvwYT4aD", + }, +} +`; + +exports[`"modules" option should work with the \`getJSON\` option and resolve all classes: 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]!./alias.css"; +import ___CSS_LOADER_ICSS_IMPORT_1___, * as ___CSS_LOADER_ICSS_IMPORT_1____NAMED___ from "-!../../../../src/index.js??ruleSet[1].rules[0].use[0]!./alias-1.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); +___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_1___, "", true); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \`.RsClSIMkfTMmUvwYT4aD { + color: red; +} + +.OdpZEdUc2oHF96Xqdoba { + color: blue; +} + +.A3lCTIjOyIaMw91SUTt_ { + color: blue; +} + +.global-class { + padding: 10px; +} + +.global-class-1 { + padding: 10px; +} + +.global-class-2 { + padding: 10px; +} + +.BwiLdQraIwYyRAA53QEQ { + color: gainsboro; +} + +.DemABT8Zz2xVnnu848uO { +} +\`, ""]); +// Exports +var _1 = \`RsClSIMkfTMmUvwYT4aD\`; +export { _1 as "other-class" }; +var _2 = \`OdpZEdUc2oHF96Xqdoba\`; +export { _2 as "class-1" }; +var _3 = \`A3lCTIjOyIaMw91SUTt_\`; +export { _3 as "class-2" }; +var _4 = \`BwiLdQraIwYyRAA53QEQ RsClSIMkfTMmUvwYT4aD OdpZEdUc2oHF96Xqdoba A3lCTIjOyIaMw91SUTt_ \${___CSS_LOADER_ICSS_IMPORT_0____NAMED___["imported-alias"]} \${___CSS_LOADER_ICSS_IMPORT_1____NAMED___["imported-alias-2"]} \${___CSS_LOADER_ICSS_IMPORT_1____NAMED___["imported-alias-3"]} global-class global-class-1 global-class-2\`; +export { _4 as "class" }; +var _5 = \`DemABT8Zz2xVnnu848uO RsClSIMkfTMmUvwYT4aD OdpZEdUc2oHF96Xqdoba\`; +export { _5 as "class-other" }; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work with the \`getJSON\` option and resolve all classes: result 1`] = ` +[ + [ + "../../src/index.js??ruleSet[1].rules[0].use[0]!./modules/composes/alias.css", + ".dnhKs1AYKq4KodZdfzcx { + display: table; +} +", + "", + ], + [ + "../../src/index.js??ruleSet[1].rules[0].use[0]!./modules/composes/alias-1.css", + ".Lg5UPByIZH1XWiASCk_q { + background: red; +} + +.QllkotlwlKJ4pFhiIzqP { + background: red; +} +", + "", + ], + [ + "./modules/composes/multiple.css", + ".RsClSIMkfTMmUvwYT4aD { + color: red; +} + +.OdpZEdUc2oHF96Xqdoba { + color: blue; +} + +.A3lCTIjOyIaMw91SUTt_ { + color: blue; +} + +.global-class { + padding: 10px; +} + +.global-class-1 { + padding: 10px; +} + +.global-class-2 { + padding: 10px; +} + +.BwiLdQraIwYyRAA53QEQ { + color: gainsboro; +} + +.DemABT8Zz2xVnnu848uO { +} +", + "", + ], +] +`; + +exports[`"modules" option should work with the \`getJSON\` option and resolve all classes: warnings 1`] = `[]`; + exports[`"modules" option show work when the "mode" option is function and return "icss" value, case "duplicate-export": errors 1`] = `[]`; exports[`"modules" option show work when the "mode" option is function and return "icss" value, case "duplicate-export": 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/get-json.js b/test/helpers/get-json.js new file mode 100644 index 00000000..60ab2291 --- /dev/null +++ b/test/helpers/get-json.js @@ -0,0 +1,144 @@ +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 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) => + 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/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..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, @@ -12,8 +14,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 +2602,59 @@ describe('"modules" option', () => { expect(getWarnings(stats)).toMatchSnapshot("warnings"); 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", { + 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: {