@@ -337,7 +337,7 @@ type modules =
337337 imports: object [];
338338 exports: object [];
339339 replacements: object [];
340- }) => any ;
340+ }) => Promise < void > | void ;
341341 };
342342```
343343
@@ -1411,7 +1411,7 @@ type getJSON = ({
14111411 imports: object [];
14121412 exports: object [];
14131413 replacements: object [];
1414- }) => any ;
1414+ }) => Promise < void > | void ;
14151415```
14161416
14171417Default: ` undefined `
@@ -1457,81 +1457,21 @@ Enables a callback to output the CSS modules mapping JSON. The callback is invok
14571457}
14581458```
14591459
1460- ** webpack.config.js**
1461-
1462- ``` js
1463- // supports a synchronous callback
1464- module .exports = {
1465- module: {
1466- rules: [
1467- {
1468- test: / \. css$ / i ,
1469- loader: " css-loader" ,
1470- options: {
1471- modules: {
1472- getJSON : ({ resourcePath, exports }) => {
1473- // synchronously write a .json mapping file in the same directory as the resource
1474- const exportsJson = exports .reduce (
1475- (acc , { name, value }) => ({ ... acc, [name]: value }),
1476- {},
1477- );
1478-
1479- const outputPath = path .resolve (
1480- path .dirname (resourcePath),
1481- ` ${ path .basename (resourcePath)} .json` ,
1482- );
1483-
1484- const fs = require (" fs" );
1485- fs .writeFileSync (outputPath, JSON .stringify (json));
1486- },
1487- },
1488- },
1489- },
1490- ],
1491- },
1492- };
1493-
1494- // supports an asynchronous callback
1495- module .exports = {
1496- module: {
1497- rules: [
1498- {
1499- test: / \. css$ / i ,
1500- loader: " css-loader" ,
1501- options: {
1502- modules: {
1503- getJSON: async ({ resourcePath, exports }) => {
1504- const exportsJson = exports .reduce (
1505- (acc , { name, value }) => ({ ... acc, [name]: value }),
1506- {},
1507- );
1508-
1509- const outputPath = path .resolve (
1510- path .dirname (resourcePath),
1511- ` ${ path .basename (resourcePath)} .json` ,
1512- );
1513-
1514- const fsp = require (" fs/promises" );
1515- await fsp .writeFile (outputPath, JSON .stringify (json));
1516- },
1517- },
1518- },
1519- },
1520- ],
1521- },
1522- };
1523- ```
1524-
15251460Using ` getJSON ` , it's possible to output a files with all CSS module mappings.
15261461In the following example, we use ` getJSON ` to cache canonical mappings and
15271462add stand-ins for any composed values (through ` composes ` ), and we use a custom plugin
15281463to consolidate the values and output them to a file:
15291464
1465+ ** webpack.config.js**
1466+
15301467``` js
1468+ const path = require (" path" );
1469+ const fs = require (" fs" );
1470+
15311471const CSS_LOADER_REPLACEMENT_REGEX =
15321472 / (___CSS_LOADER_ICSS_IMPORT_\d + _REPLACEMENT_\d + ___)/ g ;
1533- const REPLACEMENT_REGEX = / ___REPLACEMENT\[ (. *? )\ ]\[ (. *? )\ ] ___/ g ;
1534- const IDENTIFIER_REGEX = / \[ (. *? )\ ]\[ (. *? )\ ]/ ;
1473+ const REPLACEMENT_REGEX = / ___REPLACEMENT\[ (. *? )]\[ (. *? )]___/ g ;
1474+ const IDENTIFIER_REGEX = / \[ (. *? )]\[ (. *? )]/ ;
15351475const replacementsMap = {};
15361476const canonicalValuesMap = {};
15371477const allExportsJson = {};
@@ -1570,9 +1510,8 @@ function addReplacements(resourcePath, imports, exportsJson, replacements) {
15701510 // add them all to the replacements map to be replaced altogether later
15711511 replacementsMap[identifier] = classNames .replaceAll (
15721512 CSS_LOADER_REPLACEMENT_REGEX ,
1573- (_ , replacementName ) => {
1574- return importReplacementsMap[resourcePath][replacementName];
1575- },
1513+ (_ , replacementName ) =>
1514+ importReplacementsMap[resourcePath][replacementName],
15761515 );
15771516 } else {
15781517 // otherwise, no class names need replacements so we can add them to
@@ -1586,22 +1525,86 @@ function addReplacements(resourcePath, imports, exportsJson, replacements) {
15861525}
15871526
15881527function replaceReplacements (classNames ) {
1589- const adjustedClassNames = classNames .replaceAll (
1528+ return classNames .replaceAll (
15901529 REPLACEMENT_REGEX ,
15911530 (_ , resourcePath , localName ) => {
15921531 const identifier = generateIdentifier (resourcePath, localName);
1532+
15931533 if (identifier in canonicalValuesMap) {
15941534 return canonicalValuesMap[identifier];
15951535 }
15961536
1597- // recurse through other stand-in that may be imports
1537+ // Recurse through other stand-in that may be imports
15981538 const canonicalValue = replaceReplacements (replacementsMap[identifier]);
1539+
15991540 canonicalValuesMap[identifier] = canonicalValue;
1541+
16001542 return canonicalValue;
16011543 },
16021544 );
1545+ }
1546+
1547+ function getJSON ({ resourcePath, imports, exports , replacements }) {
1548+ const exportsJson = exports .reduce ((acc , { name, value }) => {
1549+ return { ... acc, [name]: value };
1550+ }, {});
1551+
1552+ if (replacements .length > 0 ) {
1553+ // replacements present --> add stand-in values for absolute paths and local names,
1554+ // which will be resolved to their canonical values in the plugin below
1555+ addReplacements (resourcePath, imports, exportsJson, replacements);
1556+ } else {
1557+ // no replacements present --> add to canonicalValuesMap verbatim
1558+ // since all values here are canonical/don't need resolution
1559+ for (const [key , value ] of Object .entries (exportsJson)) {
1560+ const id = ` [${ resourcePath} ][${ key} ]` ;
1561+
1562+ canonicalValuesMap[id] = value;
1563+ }
16031564
1604- return adjustedClassNames;
1565+ allExportsJson[resourcePath] = exportsJson;
1566+ }
1567+ }
1568+
1569+ class CssModulesJsonPlugin {
1570+ constructor (options ) {
1571+ this .options = options;
1572+ }
1573+
1574+ // eslint-disable-next-line class-methods-use-this
1575+ apply (compiler ) {
1576+ compiler .hooks .emit .tap (" CssModulesJsonPlugin" , () => {
1577+ for (const [identifier , classNames ] of Object .entries (replacementsMap)) {
1578+ const adjustedClassNames = replaceReplacements (classNames);
1579+
1580+ replacementsMap[identifier] = adjustedClassNames;
1581+
1582+ const [, resourcePath , localName ] = identifier .match (IDENTIFIER_REGEX );
1583+
1584+ allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1585+ allExportsJson[resourcePath][localName] = adjustedClassNames;
1586+ }
1587+
1588+ fs .writeFileSync (
1589+ this .options .filepath ,
1590+ JSON .stringify (
1591+ // Make path to be relative to `context` (your project root)
1592+ Object .fromEntries (
1593+ Object .entries (allExportsJson).map ((key ) => {
1594+ key[0 ] = path
1595+ .relative (compiler .context , key[0 ])
1596+ .replace (/ \\ / g , " /" );
1597+
1598+ return key;
1599+ }),
1600+ ),
1601+ null ,
1602+ 2 ,
1603+ ),
1604+ " utf8" ,
1605+ );
1606+ });
1607+ }
16051608}
16061609
16071610module .exports = {
@@ -1610,63 +1613,14 @@ module.exports = {
16101613 {
16111614 test: / \. css$ / i ,
16121615 loader: " css-loader" ,
1613- options: {
1614- modules: {
1615- getJSON : ({ resourcePath, imports, exports , replacements }) => {
1616- const exportsJson = exports .reduce (
1617- (acc , { name, value }) => ({ ... acc, [name]: value }),
1618- {},
1619- );
1620-
1621- if (replacements .length > 0 ) {
1622- // replacements present --> add stand-in values for absolute paths and local names,
1623- // which will be resolved to their canonical values in the plugin below
1624- addReplacements (
1625- resourcePath,
1626- imports,
1627- exportsJson,
1628- replacements,
1629- );
1630- } else {
1631- // no replacements present --> add to canonicalValuesMap verbatim
1632- // since all values here are canonical/don't need resolution
1633- for (const [key , value ] of Object .entries (exportsJson)) {
1634- const id = ` [${ resourcePath} ][${ key} ]` ;
1635-
1636- canonicalValuesMap[id] = value;
1637- }
1638-
1639- allExportsJson[resourcePath] = exportsJson;
1640- }
1641- },
1642- },
1643- },
1616+ options: { modules: { getJSON } },
16441617 },
16451618 ],
16461619 },
16471620 plugins: [
1648- {
1649- apply (compiler ) {
1650- compiler .hooks .done .tap (" CssModulesJsonPlugin" , () => {
1651- for (const [identifier , classNames ] of Object .entries (
1652- replacementsMap,
1653- )) {
1654- const adjustedClassNames = replaceReplacements (classNames);
1655- replacementsMap[identifier] = adjustedClassNames;
1656- const [, resourcePath , localName ] =
1657- identifier .match (IDENTIFIER_REGEX );
1658- allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1659- allExportsJson[resourcePath][localName] = adjustedClassNames;
1660- }
1661-
1662- fs .writeFileSync (
1663- " ./output.css.json" ,
1664- JSON .stringify (allExportsJson, null , 2 ),
1665- " utf8" ,
1666- );
1667- });
1668- },
1669- },
1621+ new CssModulesJsonPlugin ({
1622+ filepath: path .resolve (__dirname , " ./output.css.json" ),
1623+ }),
16701624 ],
16711625};
16721626```
@@ -1675,11 +1629,11 @@ In the above, all import aliases are replaced with `___REPLACEMENT[<resourcePath
16751629
16761630``` json
16771631{
1678- "/ foo/bar/baz.module.css" : {
1632+ "foo/bar/baz.module.css" : {
16791633 "main" : " D2Oy" ,
16801634 "header" : " thNN"
16811635 },
1682- "/ foot/bear/bath.module.css" : {
1636+ "foot/bear/bath.module.css" : {
16831637 "logo" : " sqiR" ,
16841638 "info" : " XMyI"
16851639 }
0 commit comments