css-loader
Advanced tools
Comparing version 7.0.0 to 7.1.0
@@ -174,3 +174,19 @@ "use strict"; | ||
const exportCode = (0, _utils.getExportCode)(exports, replacements, needToUseIcssPlugin, options, 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}`); | ||
} |
@@ -176,2 +176,7 @@ { | ||
"type": "boolean" | ||
}, | ||
"getJSON": { | ||
"description": "Allows outputting of CSS modules mapping through a callback.", | ||
"link": "https://github.com/webpack-contrib/css-loader#getJSON", | ||
"instanceof": "Function" | ||
} | ||
@@ -178,0 +183,0 @@ } |
{ | ||
"name": "css-loader", | ||
"version": "7.0.0", | ||
"version": "7.1.0", | ||
"description": "css loader module for webpack", | ||
@@ -39,3 +39,3 @@ "license": "MIT", | ||
"test": "npm run test:coverage", | ||
"prepare": "husky install && npm run build", | ||
"prepare": "husky && npm run build", | ||
"release": "standard-version" | ||
@@ -42,0 +42,0 @@ }, |
270
README.md
@@ -50,3 +50,3 @@ <div align="center"> | ||
```js | ||
import css from "file.css"; | ||
import * as css from "file.css"; | ||
``` | ||
@@ -331,2 +331,13 @@ | ||
exportOnlyLocals: boolean; | ||
getJSON: ({ | ||
resourcePath, | ||
imports, | ||
exports, | ||
replacements, | ||
}: { | ||
resourcePath: string; | ||
imports: object[]; | ||
exports: object[]; | ||
replacements: object[]; | ||
}) => Promise<void> | void; | ||
}; | ||
@@ -609,2 +620,3 @@ ``` | ||
exportOnlyLocals: false, | ||
getJSON: ({ resourcePath, imports, exports, replacements }) => {}, | ||
}, | ||
@@ -1168,7 +1180,7 @@ }, | ||
// 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); | ||
``` | ||
@@ -1391,2 +1403,248 @@ | ||
##### `getJSON` | ||
Type: | ||
```ts | ||
type getJSON = ({ | ||
resourcePath, | ||
imports, | ||
exports, | ||
replacements, | ||
}: { | ||
resourcePath: string; | ||
imports: object[]; | ||
exports: object[]; | ||
replacements: object[]; | ||
}) => Promise<void> | 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[<absolute_path>][<class_name>]___') | ||
// 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[<resourcePath>][<localName>]___` 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` | ||
@@ -2041,4 +2299,4 @@ | ||
```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"; | ||
@@ -2045,0 +2303,0 @@ // Render DOM with CSS modules class name |
140449
2282
2329