@@ -327,6 +327,17 @@ type modules =
327327 | " dashes-only"
328328 | ((name : string ) => string );
329329 exportOnlyLocals: boolean ;
330+ getJSON: ({
331+ resourcePath,
332+ imports,
333+ exports,
334+ replacements,
335+ }: {
336+ resourcePath: string ;
337+ imports: object [];
338+ exports: object [];
339+ replacements: object [];
340+ }) => any ;
330341 };
331342```
332343
@@ -604,6 +615,7 @@ module.exports = {
604615 namedExport: true ,
605616 exportLocalsConvention: " as-is" ,
606617 exportOnlyLocals: false ,
618+ getJSON : ({ resourcePath, imports, exports , replacements }) => {},
607619 },
608620 },
609621 },
@@ -1384,6 +1396,298 @@ module.exports = {
13841396};
13851397```
13861398
1399+ ##### ` getJSON `
1400+
1401+ Type:
1402+
1403+ ``` ts
1404+ type getJSON = ({
1405+ resourcePath,
1406+ imports,
1407+ exports,
1408+ replacements,
1409+ }: {
1410+ resourcePath: string ;
1411+ imports: object [];
1412+ exports: object [];
1413+ replacements: object [];
1414+ }) => any ;
1415+ ```
1416+
1417+ Default: ` undefined `
1418+
1419+ Enables a callback to output the CSS modules mapping JSON. The callback is invoked with an object containing the following:
1420+
1421+ - ` resourcePath ` : the absolute path of the original resource, e.g., ` /foo/bar/baz.module.css `
1422+
1423+ - ` imports ` : an array of import objects with data about import types and file paths, e.g.,
1424+
1425+ ``` json
1426+ [
1427+ {
1428+ "type" : " icss_import" ,
1429+ "importName" : " ___CSS_LOADER_ICSS_IMPORT_0___" ,
1430+ "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\" " ,
1431+ "icss" : true ,
1432+ "index" : 0
1433+ }
1434+ ]
1435+ ```
1436+
1437+ (Note that this will include all imports, not just those relevant to CSS modules.)
1438+
1439+ - ` exports ` : an array of export objects with exported names and values, e.g.,
1440+
1441+ ``` json
1442+ [
1443+ {
1444+ "name" : " main" ,
1445+ "value" : " D2Oy"
1446+ }
1447+ ]
1448+ ```
1449+
1450+ - ` replacements ` : an array of import replacement objects used for linking ` imports ` and ` exports ` , e.g.,
1451+
1452+ ``` json
1453+ {
1454+ "replacementName" : " ___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___" ,
1455+ "importName" : " ___CSS_LOADER_ICSS_IMPORT_0___" ,
1456+ "localName" : " main"
1457+ }
1458+ ```
1459+
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+
1525+ Using ` getJSON ` , it's possible to output a files with all CSS module mappings.
1526+ In the following example, we use ` getJSON ` to cache canonical mappings and
1527+ add stand-ins for any composed values (through ` composes ` ), and we use a custom plugin
1528+ to consolidate the values and output them to a file:
1529+
1530+ ``` js
1531+ const CSS_LOADER_REPLACEMENT_REGEX =
1532+ / (___CSS_LOADER_ICSS_IMPORT_\d + _REPLACEMENT_\d + ___)/ g ;
1533+ const REPLACEMENT_REGEX = / ___REPLACEMENT\[ (. *? )\]\[ (. *? )\] ___/ g ;
1534+ const IDENTIFIER_REGEX = / \[ (. *? )\]\[ (. *? )\] / ;
1535+ const replacementsMap = {};
1536+ const canonicalValuesMap = {};
1537+ const allExportsJson = {};
1538+
1539+ function generateIdentifier (resourcePath , localName ) {
1540+ return ` [${ resourcePath} ][${ localName} ]` ;
1541+ }
1542+
1543+ function addReplacements (resourcePath , imports , exportsJson , replacements ) {
1544+ const importReplacementsMap = {};
1545+
1546+ // create a dict to quickly identify imports and get their absolute stand-in strings in the currently loaded file
1547+ // e.g., { '___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___': '___REPLACEMENT[/foo/bar/baz.css][main]___' }
1548+ importReplacementsMap[resourcePath] = replacements .reduce (
1549+ (acc , { replacementName, importName, localName }) => {
1550+ const replacementImportUrl = imports .find (
1551+ (importData ) => importData .importName === importName,
1552+ ).url ;
1553+ const relativePathRe = / . * !(. * )"/ ;
1554+ const [, relativePath ] = replacementImportUrl .match (relativePathRe);
1555+ const importPath = path .resolve (path .dirname (resourcePath), relativePath);
1556+ const identifier = generateIdentifier (importPath, localName);
1557+ return { ... acc, [replacementName]: ` ___REPLACEMENT${ identifier} ___` };
1558+ },
1559+ {},
1560+ );
1561+
1562+ // iterate through the raw exports and add stand-in variables
1563+ // ('___REPLACEMENT[<absolute_path>][<class_name>]___')
1564+ // to be replaced in the plugin below
1565+ for (const [localName , classNames ] of Object .entries (exportsJson)) {
1566+ const identifier = generateIdentifier (resourcePath, localName);
1567+
1568+ if (CSS_LOADER_REPLACEMENT_REGEX .test (classNames)) {
1569+ // if there are any replacements needed in the concatenated class names,
1570+ // add them all to the replacements map to be replaced altogether later
1571+ replacementsMap[identifier] = classNames .replaceAll (
1572+ CSS_LOADER_REPLACEMENT_REGEX ,
1573+ (_ , replacementName ) => {
1574+ return importReplacementsMap[resourcePath][replacementName];
1575+ },
1576+ );
1577+ } else {
1578+ // otherwise, no class names need replacements so we can add them to
1579+ // canonical values map and all exports JSON verbatim
1580+ canonicalValuesMap[identifier] = classNames;
1581+
1582+ allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1583+ allExportsJson[resourcePath][localName] = classNames;
1584+ }
1585+ }
1586+ }
1587+
1588+ function replaceReplacements (classNames ) {
1589+ const adjustedClassNames = classNames .replaceAll (
1590+ REPLACEMENT_REGEX ,
1591+ (_ , resourcePath , localName ) => {
1592+ const identifier = generateIdentifier (resourcePath, localName);
1593+ if (identifier in canonicalValuesMap) {
1594+ return canonicalValuesMap[identifier];
1595+ }
1596+
1597+ // recurse through other stand-in that may be imports
1598+ const canonicalValue = replaceReplacements (replacementsMap[identifier]);
1599+ canonicalValuesMap[identifier] = canonicalValue;
1600+ return canonicalValue;
1601+ },
1602+ );
1603+
1604+ return adjustedClassNames;
1605+ }
1606+
1607+ module .exports = {
1608+ module: {
1609+ rules: [
1610+ {
1611+ test: / \. css$ / i ,
1612+ 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+ },
1644+ },
1645+ ],
1646+ },
1647+ 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+ },
1670+ ],
1671+ };
1672+ ```
1673+
1674+ 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 ` :
1675+
1676+ ``` json
1677+ {
1678+ "/foo/bar/baz.module.css" : {
1679+ "main" : " D2Oy" ,
1680+ "header" : " thNN"
1681+ },
1682+ "/foot/bear/bath.module.css" : {
1683+ "logo" : " sqiR" ,
1684+ "info" : " XMyI"
1685+ }
1686+ }
1687+ ```
1688+
1689+ This is saved to a local file named ` output.css.json ` .
1690+
13871691### ` importLoaders `
13881692
13891693Type:
0 commit comments