From af834b43b375f336108d74ff7bd9ed13bc79200a Mon Sep 17 00:00:00 2001 From: Stephen Kao Date: Mon, 8 Apr 2024 13:06:27 -0400 Subject: [PATCH] feat: added the `getJSON` option to output CSS modules mapping (#1577) --- README.md | 304 ++++++++++++++++++ src/index.js | 11 + src/options.json | 5 + .../__snapshots__/modules-option.test.js.snap | 136 ++++++++ .../validate-options.test.js.snap | 44 ++- .../modules/getJSON/composeSource.css | 7 + test/fixtures/modules/getJSON/source.css | 11 + test/fixtures/modules/getJSON/source.js | 5 + test/helpers/normalizeErrors.js | 2 +- test/modules-option.test.js | 33 +- test/url-option.test.js | 6 +- test/validate-options.test.js | 5 + 12 files changed, 549 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/modules/getJSON/composeSource.css create mode 100644 test/fixtures/modules/getJSON/source.css create mode 100644 test/fixtures/modules/getJSON/source.js diff --git a/README.md b/README.md index 2c490431..54b0a30a 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,17 @@ type modules = | "dashes-only" | ((name: string) => string); exportOnlyLocals: boolean; + getJSON: ({ + resourcePath, + imports, + exports, + replacements, + }: { + resourcePath: string; + imports: object[]; + exports: object[]; + replacements: object[]; + }) => any; }; ``` @@ -604,6 +615,7 @@ module.exports = { namedExport: true, exportLocalsConvention: "as-is", exportOnlyLocals: false, + getJSON: ({ resourcePath, imports, exports, replacements }) => {}, }, }, }, @@ -1384,6 +1396,298 @@ module.exports = { }; ``` +##### `getJSON` + +Type: + +```ts +type getJSON = ({ + resourcePath, + imports, + exports, + replacements, +}: { + resourcePath: string; + imports: object[]; + exports: object[]; + replacements: object[]; +}) => any; +``` + +Default: `undefined` + +Enables a callback to output the CSS modules mapping JSON. The callback is invoked with an object containing the following: + +- `resourcePath`: the absolute path of the original resource, e.g., `/foo/bar/baz.module.css` + +- `imports`: an array of import objects with data about import types and file paths, e.g., + +```json +[ + { + "type": "icss_import", + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "url": "\"-!../../../../../node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[4].use[1]!../../../../../node_modules/postcss-loader/dist/cjs.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../baz.module.css\"", + "icss": true, + "index": 0 + } +] +``` + +(Note that this will include all imports, not just those relevant to CSS modules.) + +- `exports`: an array of export objects with exported names and values, e.g., + +```json +[ + { + "name": "main", + "value": "D2Oy" + } +] +``` + +- `replacements`: an array of import replacement objects used for linking `imports` and `exports`, e.g., + +```json +{ + "replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___", + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "localName": "main" +} +``` + +**webpack.config.js** + +```js +// supports a synchronous callback +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + getJSON: ({ resourcePath, exports }) => { + // synchronously write a .json mapping file in the same directory as the resource + const exportsJson = exports.reduce( + (acc, { name, value }) => ({ ...acc, [name]: value }), + {}, + ); + + const outputPath = path.resolve( + path.dirname(resourcePath), + `${path.basename(resourcePath)}.json`, + ); + + const fs = require("fs"); + fs.writeFileSync(outputPath, JSON.stringify(json)); + }, + }, + }, + }, + ], + }, +}; + +// supports an asynchronous callback +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + getJSON: async ({ resourcePath, exports }) => { + const exportsJson = exports.reduce( + (acc, { name, value }) => ({ ...acc, [name]: value }), + {}, + ); + + const outputPath = path.resolve( + path.dirname(resourcePath), + `${path.basename(resourcePath)}.json`, + ); + + const fsp = require("fs/promises"); + await fsp.writeFile(outputPath, JSON.stringify(json)); + }, + }, + }, + }, + ], + }, +}; +``` + +Using `getJSON`, it's possible to output a files with all CSS module mappings. +In the following example, we use `getJSON` to cache canonical mappings and +add stand-ins for any composed values (through `composes`), and we use a custom plugin +to consolidate the values and output them to a file: + +```js +const CSS_LOADER_REPLACEMENT_REGEX = + /(___CSS_LOADER_ICSS_IMPORT_\d+_REPLACEMENT_\d+___)/g; +const REPLACEMENT_REGEX = /___REPLACEMENT\[(.*?)\]\[(.*?)\]___/g; +const IDENTIFIER_REGEX = /\[(.*?)\]\[(.*?)\]/; +const replacementsMap = {}; +const canonicalValuesMap = {}; +const allExportsJson = {}; + +function generateIdentifier(resourcePath, localName) { + return `[${resourcePath}][${localName}]`; +} + +function addReplacements(resourcePath, imports, exportsJson, replacements) { + const importReplacementsMap = {}; + + // create a dict to quickly identify imports and get their absolute stand-in strings in the currently loaded file + // e.g., { '___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___': '___REPLACEMENT[/foo/bar/baz.css][main]___' } + importReplacementsMap[resourcePath] = replacements.reduce( + (acc, { replacementName, importName, localName }) => { + const replacementImportUrl = imports.find( + (importData) => importData.importName === importName, + ).url; + const relativePathRe = /.*!(.*)"/; + const [, relativePath] = replacementImportUrl.match(relativePathRe); + const importPath = path.resolve(path.dirname(resourcePath), relativePath); + const identifier = generateIdentifier(importPath, localName); + return { ...acc, [replacementName]: `___REPLACEMENT${identifier}___` }; + }, + {}, + ); + + // iterate through the raw exports and add stand-in variables + // ('___REPLACEMENT[][]___') + // to be replaced in the plugin below + for (const [localName, classNames] of Object.entries(exportsJson)) { + const identifier = generateIdentifier(resourcePath, localName); + + if (CSS_LOADER_REPLACEMENT_REGEX.test(classNames)) { + // if there are any replacements needed in the concatenated class names, + // add them all to the replacements map to be replaced altogether later + replacementsMap[identifier] = classNames.replaceAll( + CSS_LOADER_REPLACEMENT_REGEX, + (_, replacementName) => { + return importReplacementsMap[resourcePath][replacementName]; + }, + ); + } else { + // otherwise, no class names need replacements so we can add them to + // canonical values map and all exports JSON verbatim + canonicalValuesMap[identifier] = classNames; + + allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; + allExportsJson[resourcePath][localName] = classNames; + } + } +} + +function replaceReplacements(classNames) { + const adjustedClassNames = classNames.replaceAll( + REPLACEMENT_REGEX, + (_, resourcePath, localName) => { + const identifier = generateIdentifier(resourcePath, localName); + if (identifier in canonicalValuesMap) { + return canonicalValuesMap[identifier]; + } + + // recurse through other stand-in that may be imports + const canonicalValue = replaceReplacements(replacementsMap[identifier]); + canonicalValuesMap[identifier] = canonicalValue; + return canonicalValue; + }, + ); + + return adjustedClassNames; +} + +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + loader: "css-loader", + options: { + modules: { + getJSON: ({ resourcePath, imports, exports, replacements }) => { + const exportsJson = exports.reduce( + (acc, { name, value }) => ({ ...acc, [name]: value }), + {}, + ); + + if (replacements.length > 0) { + // replacements present --> add stand-in values for absolute paths and local names, + // which will be resolved to their canonical values in the plugin below + addReplacements( + resourcePath, + imports, + exportsJson, + replacements, + ); + } else { + // no replacements present --> add to canonicalValuesMap verbatim + // since all values here are canonical/don't need resolution + for (const [key, value] of Object.entries(exportsJson)) { + const id = `[${resourcePath}][${key}]`; + + canonicalValuesMap[id] = value; + } + + allExportsJson[resourcePath] = exportsJson; + } + }, + }, + }, + }, + ], + }, + plugins: [ + { + apply(compiler) { + compiler.hooks.done.tap("CssModulesJsonPlugin", () => { + for (const [identifier, classNames] of Object.entries( + replacementsMap, + )) { + const adjustedClassNames = replaceReplacements(classNames); + replacementsMap[identifier] = adjustedClassNames; + const [, resourcePath, localName] = + identifier.match(IDENTIFIER_REGEX); + allExportsJson[resourcePath] = allExportsJson[resourcePath] || {}; + allExportsJson[resourcePath][localName] = adjustedClassNames; + } + + fs.writeFileSync( + "./output.css.json", + JSON.stringify(allExportsJson, null, 2), + "utf8", + ); + }); + }, + }, + ], +}; +``` + +In the above, all import aliases are replaced with `___REPLACEMENT[][]___` in `getJSON`, and they're resolved in the custom plugin. All CSS mappings are contained in `allExportsJson`: + +```json +{ + "/foo/bar/baz.module.css": { + "main": "D2Oy", + "header": "thNN" + }, + "/foot/bear/bath.module.css": { + "logo": "sqiR", + "info": "XMyI" + } +} +``` + +This is saved to a local file named `output.css.json`. + ### `importLoaders` Type: diff --git a/src/index.js b/src/index.js index 886a831f..25262bee 100644 --- a/src/index.js +++ b/src/index.js @@ -273,5 +273,16 @@ export default async function loader(content, map, meta) { isTemplateLiteralSupported, ); + const { getJSON } = options.modules; + if (typeof getJSON === "function") { + try { + await getJSON({ resourcePath, imports, exports, replacements }); + } catch (error) { + callback(error); + + return; + } + } + callback(null, `${importCode}${moduleCode}${exportCode}`); } diff --git a/src/options.json b/src/options.json index b8667f03..22773c7b 100644 --- a/src/options.json +++ b/src/options.json @@ -173,6 +173,11 @@ "description": "Export only locals.", "link": "https://github.com/webpack-contrib/css-loader#exportonlylocals", "type": "boolean" + }, + "getJSON": { + "description": "Allows outputting of CSS modules mapping through a callback.", + "link": "https://github.com/webpack-contrib/css-loader#getJSON", + "instanceof": "Function" } } } diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index 1352de14..2a2c2a44 100644 --- a/test/__snapshots__/modules-option.test.js.snap +++ b/test/__snapshots__/modules-option.test.js.snap @@ -1101,6 +1101,142 @@ exports[`"modules" option should emit warning when localIdentName is emoji: erro exports[`"modules" option should emit warning when localIdentName is emoji: warnings 1`] = `[]`; +exports[`"modules" option should invoke the custom getJSON function if provided: args 1`] = ` +[ + [ + { + "exports": [ + { + "name": "a", + "value": "RT7ktT7mB7tfBR25sJDZ ___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___", + }, + { + "name": "b", + "value": "IZmhTnK9CIeu6ww6Zjbv ___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_1___", + }, + ], + "imports": [ + { + "importName": "___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___", + "url": ""../../../../src/runtime/noSourceMaps.js"", + }, + { + "importName": "___CSS_LOADER_API_IMPORT___", + "type": "api_import", + "url": ""../../../../src/runtime/api.js"", + }, + { + "icss": true, + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "index": 0, + "type": "icss_import", + "url": ""-!../../../../src/index.js??ruleSet[1].rules[0].use[0]!./composeSource.css"", + }, + ], + "replacements": [ + { + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "localName": "composedA", + "replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___", + }, + { + "importName": "___CSS_LOADER_ICSS_IMPORT_0___", + "localName": "composedB", + "replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_1___", + }, + ], + "resourcePath": "/test/fixtures/modules/getJSON/source.css", + }, + ], + [ + { + "exports": [ + { + "name": "composedA", + "value": "mm3SuQiO3doywWWliORs", + }, + { + "name": "composedB", + "value": "hFeFcgvjCoj_9RRA4E59 mm3SuQiO3doywWWliORs", + }, + ], + "imports": [ + { + "importName": "___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___", + "url": ""../../../../src/runtime/noSourceMaps.js"", + }, + { + "importName": "___CSS_LOADER_API_IMPORT___", + "type": "api_import", + "url": ""../../../../src/runtime/api.js"", + }, + ], + "replacements": [], + "resourcePath": "/test/fixtures/modules/getJSON/composeSource.css", + }, + ], +] +`; + +exports[`"modules" option should invoke the custom getJSON function if provided: errors 1`] = `[]`; + +exports[`"modules" option should invoke the custom getJSON function if provided: module 1`] = ` +"// Imports +import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../../../../src/runtime/noSourceMaps.js"; +import ___CSS_LOADER_API_IMPORT___ from "../../../../src/runtime/api.js"; +import ___CSS_LOADER_ICSS_IMPORT_0___, * as ___CSS_LOADER_ICSS_IMPORT_0____NAMED___ from "-!../../../../src/index.js??ruleSet[1].rules[0].use[0]!./composeSource.css"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___); +___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, "", true); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \`.RT7ktT7mB7tfBR25sJDZ { + + background-color: aliceblue; +} + +.IZmhTnK9CIeu6ww6Zjbv { + + background-color: blanchedalmond; +} +\`, ""]); +// Exports +export var a = \`RT7ktT7mB7tfBR25sJDZ \${___CSS_LOADER_ICSS_IMPORT_0____NAMED___["composedA"]}\`; +export var b = \`IZmhTnK9CIeu6ww6Zjbv \${___CSS_LOADER_ICSS_IMPORT_0____NAMED___["composedB"]}\`; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should invoke the custom getJSON function if provided: result 1`] = ` +[ + [ + "../../src/index.js??ruleSet[1].rules[0].use[0]!./modules/getJSON/composeSource.css", + ".mm3SuQiO3doywWWliORs { + height: 200px; +} + +.hFeFcgvjCoj_9RRA4E59 { +} +", + "", + ], + [ + "./modules/getJSON/source.css", + ".RT7ktT7mB7tfBR25sJDZ { + + background-color: aliceblue; +} + +.IZmhTnK9CIeu6ww6Zjbv { + + background-color: blanchedalmond; +} +", + "", + ], +] +`; + +exports[`"modules" option should invoke the custom getJSON function if provided: warnings 1`] = `[]`; + exports[`"modules" option should keep order: errors 1`] = `[]`; exports[`"modules" option should keep order: module 1`] = ` diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index da19f1c1..b1a0ac65 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -85,7 +85,7 @@ exports[`validate options should throw an error on the "importLoaders" option wi exports[`validate options should throw an error on the "modules" option with "{"auto":"invalid"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -109,7 +109,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"exportLocalsConvention":"unknown"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -130,6 +130,20 @@ exports[`validate options should throw an error on the "modules" option with "{" -> Read more at https://github.com/webpack-contrib/css-loader#exportonlylocals" `; +exports[`validate options should throw an error on the "modules" option with "{"getJSON":"invalid"}" value 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options.modules.getJSON should be an instance of function. + -> Allows outputting of CSS modules mapping through a callback. + -> Read more at https://github.com/webpack-contrib/css-loader#getJSON" +`; + +exports[`validate options should throw an error on the "modules" option with "{"getJSON":true}" value 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options.modules.getJSON should be an instance of function. + -> Allows outputting of CSS modules mapping through a callback. + -> Read more at https://github.com/webpack-contrib/css-loader#getJSON" +`; + exports[`validate options should throw an error on the "modules" option with "{"getLocalIdent":[]}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules.getLocalIdent should be an instance of function. @@ -161,7 +175,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"localIdentRegExp":true}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -177,7 +191,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":"globals"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -194,7 +208,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":"locals"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -211,7 +225,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":"pures"}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -228,7 +242,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "{"mode":true}" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -252,7 +266,7 @@ exports[`validate options should throw an error on the "modules" option with "{" exports[`validate options should throw an error on the "modules" option with "globals" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -260,13 +274,13 @@ exports[`validate options should throw an error on the "modules" option with "gl * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "modules" option with "locals" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -274,13 +288,13 @@ exports[`validate options should throw an error on the "modules" option with "lo * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "modules" option with "pures" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -288,13 +302,13 @@ exports[`validate options should throw an error on the "modules" option with "pu * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "modules" option with "true" value 1`] = ` "Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. - options.modules should be one of these: - boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? } + boolean | "local" | "global" | "pure" | "icss" | object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? } -> Allows to enable/disable CSS Modules or ICSS and setup configuration. -> Read more at https://github.com/webpack-contrib/css-loader#modules Details: @@ -302,7 +316,7 @@ exports[`validate options should throw an error on the "modules" option with "tr * options.modules should be one of these: "local" | "global" | "pure" | "icss" * options.modules should be an object: - object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals? }" + object { auto?, mode?, localIdentName?, localIdentContext?, localIdentHashSalt?, localIdentHashFunction?, localIdentHashDigest?, localIdentHashDigestLength?, hashStrategy?, localIdentRegExp?, getLocalIdent?, namedExport?, exportGlobals?, exportLocalsConvention?, exportOnlyLocals?, getJSON? }" `; exports[`validate options should throw an error on the "sourceMap" option with "true" value 1`] = ` diff --git a/test/fixtures/modules/getJSON/composeSource.css b/test/fixtures/modules/getJSON/composeSource.css new file mode 100644 index 00000000..8303fe70 --- /dev/null +++ b/test/fixtures/modules/getJSON/composeSource.css @@ -0,0 +1,7 @@ +.composedA { + height: 200px; +} + +.composedB { + composes: composedA; +} diff --git a/test/fixtures/modules/getJSON/source.css b/test/fixtures/modules/getJSON/source.css new file mode 100644 index 00000000..7007a4e7 --- /dev/null +++ b/test/fixtures/modules/getJSON/source.css @@ -0,0 +1,11 @@ +.a { + composes: composedA from "./composeSource.css"; + + background-color: aliceblue; +} + +.b { + composes: composedB from "./composeSource.css"; + + background-color: blanchedalmond; +} diff --git a/test/fixtures/modules/getJSON/source.js b/test/fixtures/modules/getJSON/source.js new file mode 100644 index 00000000..1996779e --- /dev/null +++ b/test/fixtures/modules/getJSON/source.js @@ -0,0 +1,5 @@ +import css from './source.css'; + +__export__ = css; + +export default css; diff --git a/test/helpers/normalizeErrors.js b/test/helpers/normalizeErrors.js index e3ef68ab..04ec5673 100644 --- a/test/helpers/normalizeErrors.js +++ b/test/helpers/normalizeErrors.js @@ -1,6 +1,6 @@ import stripAnsi from "strip-ansi"; -function removeCWD(str) { +export function removeCWD(str) { const isWin = process.platform === "win32"; let cwd = process.cwd(); diff --git a/test/modules-option.test.js b/test/modules-option.test.js index a1314ea1..48716f02 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -12,8 +12,10 @@ import { getWarnings, readAsset, } from "./helpers/index"; +import { removeCWD } from "./helpers/normalizeErrors"; -const testCasesPath = path.join(__dirname, "fixtures/modules/tests-cases"); +const modulesFixturesPath = path.join(__dirname, "fixtures/modules"); +const testCasesPath = path.join(modulesFixturesPath, "tests-cases"); const testCases = fs.readdirSync(testCasesPath); jest.setTimeout(60000); @@ -2598,4 +2600,33 @@ describe('"modules" option', () => { expect(getWarnings(stats)).toMatchSnapshot("warnings"); expect(getErrors(stats)).toMatchSnapshot("errors"); }); + + it("should invoke the custom getJSON function if provided", async () => { + const getJSONSpy = jest.fn(); + const compiler = getCompiler("./modules/getJSON/source.js", { + modules: { + // need to wrap Jest spy since it doesn't pass ajv validation on its own + getJSON: (...args) => getJSONSpy(...args), + }, + }); + const stats = await compile(compiler); + + const args = getJSONSpy.mock.calls.map((arg) => [ + { + ...arg[0], + // resourcePaths are absolute so we need to make them relative for snapshots + resourcePath: removeCWD(arg[0].resourcePath), + }, + ]); + expect(args).toMatchSnapshot("args"); + + expect( + getModuleSource("./modules/getJSON/source.css", stats), + ).toMatchSnapshot("module"); + expect(getExecutedCode("main.bundle.js", compiler, stats)).toMatchSnapshot( + "result", + ); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); }); diff --git a/test/url-option.test.js b/test/url-option.test.js index f322d6c2..69e56efa 100644 --- a/test/url-option.test.js +++ b/test/url-option.test.js @@ -86,15 +86,15 @@ describe('"url" option', () => { it("should work with url.filter", async () => { const compiler = getCompiler("./url/url.js", { url: { - filter: (url, resourcePath) => { + filter: (_url, resourcePath) => { expect(typeof resourcePath === "string").toBe(true); - if (url.startsWith("/guide/img")) { + if (_url.startsWith("/guide/img")) { return false; } // Don't handle `img.png` - if (url.includes("img.png")) { + if (_url.includes("img.png")) { return false; } diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 235d250a..b9785653 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -56,6 +56,9 @@ describe("validate options", () => { { namedExport: false }, { exportOnlyLocals: true }, { exportOnlyLocals: false }, + { + getJSON: (resourcePath) => resourcePath, + }, ], failure: [ "true", @@ -76,6 +79,8 @@ describe("validate options", () => { { exportLocalsConvention: "unknown" }, { namedExport: "invalid" }, { exportOnlyLocals: "invalid" }, + { getJSON: true }, + { getJSON: "invalid" }, ], }, sourceMap: {