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: { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy