Skip to content

Commit af834b4

Browse files
authored
feat: added the getJSON option to output CSS modules mapping (#1577)
1 parent fd18587 commit af834b4

File tree

12 files changed

+549
-20
lines changed

12 files changed

+549
-20
lines changed

README.md

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

13891693
Type:

src/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,5 +273,16 @@ export default async function loader(content, map, meta) {
273273
isTemplateLiteralSupported,
274274
);
275275

276+
const { getJSON } = options.modules;
277+
if (typeof getJSON === "function") {
278+
try {
279+
await getJSON({ resourcePath, imports, exports, replacements });
280+
} catch (error) {
281+
callback(error);
282+
283+
return;
284+
}
285+
}
286+
276287
callback(null, `${importCode}${moduleCode}${exportCode}`);
277288
}

src/options.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@
173173
"description": "Export only locals.",
174174
"link": "https://github.com/webpack-contrib/css-loader#exportonlylocals",
175175
"type": "boolean"
176+
},
177+
"getJSON": {
178+
"description": "Allows outputting of CSS modules mapping through a callback.",
179+
"link": "https://github.com/webpack-contrib/css-loader#getJSON",
180+
"instanceof": "Function"
176181
}
177182
}
178183
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy