Skip to content

Commit a1f5f03

Browse files
committed
fix: update README.md with fuller example of outputting CSS modules mapping to file
1 parent 0cdd520 commit a1f5f03

File tree

8 files changed

+586
-190
lines changed

8 files changed

+586
-190
lines changed

README.md

+230-19
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,17 @@ type modules =
327327
| "dashesOnly"
328328
| ((name: string) => string);
329329
exportOnlyLocals: boolean;
330-
getJSON: (resourcePath: string, json: object) => any;
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;
331341
};
332342
```
333343

@@ -593,7 +603,7 @@ module.exports = {
593603
namedExport: true,
594604
exportLocalsConvention: "camelCase",
595605
exportOnlyLocals: false,
596-
getJSON: (resourcePath, json) => {},
606+
getJSON: ({ resourcePath, imports, exports, replacements }) => {},
597607
},
598608
},
599609
},
@@ -1379,34 +1389,59 @@ module.exports = {
13791389
Type:
13801390

13811391
```ts
1382-
type getJSON = (resourcePath: string, json: object) => any;
1392+
type getJSON = ({
1393+
resourcePath,
1394+
imports,
1395+
exports,
1396+
replacements,
1397+
}: {
1398+
resourcePath: string;
1399+
imports: object[];
1400+
exports: object[];
1401+
replacements: object[];
1402+
}) => any;
13831403
```
13841404

13851405
Default: `undefined`
13861406

1387-
Enables a callback to output the CSS modules mapping JSON. The callback is invoked with two arguments:
1407+
Enables a callback to output the CSS modules mapping JSON. The callback is invoked with an object containing the following:
13881408

1389-
- `resourcePath`: the absolutely path of the original resource, e.g., /foo/bar/baz.css
1390-
- `json`: the CSS modules map object, e.g.,
1409+
- `resourcePath`: the absolute path of the original resource, e.g., `/foo/bar/baz.module.css`
13911410

1411+
- `imports`: an array of import objects with data about import types and file paths, e.g.,
1412+
1413+
```json
1414+
[
1415+
{
1416+
"type": "icss_import",
1417+
"importName": "___CSS_LOADER_ICSS_IMPORT_0___",
1418+
"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\"",
1419+
"icss": true,
1420+
"index": 0
1421+
}
1422+
]
13921423
```
1393-
/* baz.css */
13941424

1395-
.a {
1396-
background-color: aliceblue;
1397-
}
1425+
(Note that this will include all imports, not just those relevant to CSS modules.)
13981426

1399-
.b {
1400-
background-color: burlywood;
1401-
}
1427+
- `exports`: an array of export objects with exported names and values, e.g.,
1428+
1429+
```json
1430+
[
1431+
{
1432+
"name": "main",
1433+
"value": "D2Oy"
1434+
}
1435+
]
14021436
```
14031437

1404-
`json` will be something like the following (depending on your other `modules` settings):
1438+
- `replacements`: an array of import replacement objects used for linking `imports` and `exports`, e.g.,
14051439

1406-
```
1440+
```json
14071441
{
1408-
"a": "a__uRkh1",
1409-
"b": "b__pjFcy"
1442+
"replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___",
1443+
"importName": "___CSS_LOADER_ICSS_IMPORT_0___",
1444+
"localName": "main"
14101445
}
14111446
```
14121447

@@ -1422,8 +1457,13 @@ module.exports = {
14221457
loader: "css-loader",
14231458
options: {
14241459
modules: {
1425-
getJSON: (resourcePath, json) => {
1460+
getJSON: ({ resourcePath, exports }) => {
14261461
// synchronously write a .json mapping file in the same directory as the resource
1462+
const exportsJson = exports.reduce(
1463+
(acc, { name, value }) => ({ ...acc, [name]: value }),
1464+
{}
1465+
);
1466+
14271467
const outputPath = path.resolve(
14281468
path.dirname(resourcePath),
14291469
`${path.basename(resourcePath)}.json`
@@ -1448,7 +1488,12 @@ module.exports = {
14481488
loader: "css-loader",
14491489
options: {
14501490
modules: {
1451-
getJSON: async (resourcePath, json) => {
1491+
getJSON: async ({ resourcePath, exports }) => {
1492+
const exportsJson = exports.reduce(
1493+
(acc, { name, value }) => ({ ...acc, [name]: value }),
1494+
{}
1495+
);
1496+
14521497
const outputPath = path.resolve(
14531498
path.dirname(resourcePath),
14541499
`${path.basename(resourcePath)}.json`
@@ -1465,6 +1510,172 @@ module.exports = {
14651510
};
14661511
```
14671512

1513+
Using `getJSON`, it's possible to output a files with all CSS module mappings.
1514+
In the following example, we use `getJSON` to cache canonical mappings and
1515+
add stand-ins for any composed values (through `composes`), and we use a custom plugin
1516+
to consolidate the values and output them to a file:
1517+
1518+
```js
1519+
const CSS_LOADER_REPLACEMENT_REGEX =
1520+
/(___CSS_LOADER_ICSS_IMPORT_\d+_REPLACEMENT_\d+___)/g;
1521+
const REPLACEMENT_REGEX = /___REPLACEMENT\[(.*?)\]\[(.*?)\]___/g;
1522+
const IDENTIFIER_REGEX = /\[(.*?)\]\[(.*?)\]/;
1523+
const replacementsMap = {};
1524+
const canonicalValuesMap = {};
1525+
const allExportsJson = {};
1526+
1527+
function generateIdentifier(resourcePath, localName) {
1528+
return `[${resourcePath}][${localName}]`;
1529+
}
1530+
1531+
function addReplacements(resourcePath, imports, exportsJson, replacements) {
1532+
const importReplacementsMap = {};
1533+
1534+
// create a dict to quickly identify imports and get their absolute stand-in strings in the currently loaded file
1535+
// e.g., { '___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___': '___REPLACEMENT[/foo/bar/baz.css][main]___' }
1536+
importReplacementsMap[resourcePath] = replacements.reduce(
1537+
(acc, { replacementName, importName, localName }) => {
1538+
const replacementImportUrl = imports.find(
1539+
(importData) => importData.importName === importName
1540+
).url;
1541+
const relativePathRe = /.*!(.*)"/;
1542+
const [, relativePath] = replacementImportUrl.match(relativePathRe);
1543+
const importPath = path.resolve(path.dirname(resourcePath), relativePath);
1544+
const identifier = generateIdentifier(importPath, localName);
1545+
return { ...acc, [replacementName]: `___REPLACEMENT${identifier}___` };
1546+
},
1547+
{}
1548+
);
1549+
1550+
// iterate through the raw exports and add stand-in variables
1551+
// ('___REPLACEMENT[<absolute_path>][<class_name>]___')
1552+
// to be replaced in the plugin below
1553+
for (const [localName, classNames] of Object.entries(exportsJson)) {
1554+
const identifier = generateIdentifier(resourcePath, localName);
1555+
1556+
if (CSS_LOADER_REPLACEMENT_REGEX.test(classNames)) {
1557+
// if there are any replacements needed in the concatenated class names,
1558+
// add them all to the replacements map to be replaced altogether later
1559+
replacementsMap[identifier] = classNames.replaceAll(
1560+
CSS_LOADER_REPLACEMENT_REGEX,
1561+
(_, replacementName) => {
1562+
return importReplacementsMap[resourcePath][replacementName];
1563+
}
1564+
);
1565+
} else {
1566+
// otherwise, no class names need replacements so we can add them to
1567+
// canonical values map and all exports JSON verbatim
1568+
canonicalValuesMap[identifier] = classNames;
1569+
1570+
allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1571+
allExportsJson[resourcePath][localName] = classNames;
1572+
}
1573+
}
1574+
}
1575+
1576+
function replaceReplacements(classNames) {
1577+
const adjustedClassNames = classNames.replaceAll(
1578+
REPLACEMENT_REGEX,
1579+
(_, resourcePath, localName) => {
1580+
const identifier = generateIdentifier(resourcePath, localName);
1581+
if (identifier in canonicalValuesMap) {
1582+
return canonicalValuesMap[identifier];
1583+
}
1584+
1585+
// recurse through other stand-in that may be imports
1586+
const canonicalValue = replaceReplacements(replacementsMap[identifier]);
1587+
canonicalValuesMap[identifier] = canonicalValue;
1588+
return canonicalValue;
1589+
}
1590+
);
1591+
1592+
return adjustedClassNames;
1593+
}
1594+
1595+
module.exports = {
1596+
module: {
1597+
rules: [
1598+
{
1599+
test: /\.css$/i,
1600+
loader: "css-loader",
1601+
options: {
1602+
modules: {
1603+
getJSON: ({ resourcePath, imports, exports, replacements }) => {
1604+
const exportsJson = exports.reduce(
1605+
(acc, { name, value }) => ({ ...acc, [name]: value }),
1606+
{}
1607+
);
1608+
1609+
if (replacements.length > 0) {
1610+
// replacements present --> add stand-in values for absolute paths and local names,
1611+
// which will be resolved to their canonical values in the plugin below
1612+
addReplacements(
1613+
resourcePath,
1614+
imports,
1615+
exportsJson,
1616+
replacements
1617+
);
1618+
} else {
1619+
// no replacements present --> add to canonicalValuesMap verbatim
1620+
// since all values here are canonical/don't need resolution
1621+
for (const [key, value] of Object.entries(exportsJson)) {
1622+
const id = `[${resourcePath}][${key}]`;
1623+
1624+
canonicalValuesMap[id] = value;
1625+
}
1626+
1627+
allExportsJson[resourcePath] = exportsJson;
1628+
}
1629+
},
1630+
},
1631+
},
1632+
},
1633+
],
1634+
},
1635+
plugins: [
1636+
{
1637+
apply(compiler) {
1638+
compiler.hooks.done.tap("CssModulesJsonPlugin", () => {
1639+
for (const [identifier, classNames] of Object.entries(
1640+
replacementsMap
1641+
)) {
1642+
const adjustedClassNames = replaceReplacements(classNames);
1643+
replacementsMap[identifier] = adjustedClassNames;
1644+
const [, resourcePath, localName] =
1645+
identifier.match(IDENTIFIER_REGEX);
1646+
allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1647+
allExportsJson[resourcePath][localName] = adjustedClassNames;
1648+
}
1649+
1650+
fs.writeFileSync(
1651+
"./output.css.json",
1652+
JSON.stringify(allExportsJson, null, 2),
1653+
"utf8"
1654+
);
1655+
});
1656+
},
1657+
},
1658+
],
1659+
};
1660+
```
1661+
1662+
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`:
1663+
1664+
```json
1665+
{
1666+
"/foo/bar/baz.module.css": {
1667+
"main": "D2Oy",
1668+
"header": "thNN"
1669+
},
1670+
"/foot/bear/bath.module.css": {
1671+
"logo": "sqiR",
1672+
"info": "XMyI"
1673+
}
1674+
}
1675+
```
1676+
1677+
This is saved to a local file named `output.css.json`.
1678+
14681679
### `importLoaders`
14691680

14701681
Type:

src/index.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,7 @@ export default async function loader(content, map, meta) {
277277
const { getJSON } = options.modules;
278278
if (typeof getJSON === "function") {
279279
try {
280-
const json = exports.reduce((acc, { name, value }) => {
281-
return { ...acc, [name]: value };
282-
}, {});
283-
284-
await getJSON(resourcePath, json);
280+
await getJSON({ resourcePath, imports, exports, replacements });
285281
} catch (error) {
286282
callback(error);
287283

0 commit comments

Comments
 (0)