css-loader 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ Then add the plugin to your `webpack` config. For example:
47
47
  **file.js**
48
48
 
49
49
  ```js
50
- import css from "file.css";
50
+ import * as css from "file.css";
51
51
  ```
52
52
 
53
53
  **webpack.config.js**
@@ -327,6 +327,17 @@ type modules =
327
327
  | "dashes-only"
328
328
  | ((name: string) => string);
329
329
  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
+ }) => Promise<void> | void;
330
341
  };
331
342
  ```
332
343
 
@@ -604,6 +615,7 @@ module.exports = {
604
615
  namedExport: true,
605
616
  exportLocalsConvention: "as-is",
606
617
  exportOnlyLocals: false,
618
+ getJSON: ({ resourcePath, imports, exports, replacements }) => {},
607
619
  },
608
620
  },
609
621
  },
@@ -1162,11 +1174,11 @@ Enables/disables ES modules named export for locals.
1162
1174
  ```js
1163
1175
  import * as styles from "./styles.css";
1164
1176
 
1177
+ // If using `exportLocalsConvention: "as-is"` (default value):
1178
+ console.log(styles["foo-baz"], styles.bar);
1179
+
1165
1180
  // If using `exportLocalsConvention: "camel-case-only"`:
1166
1181
  console.log(styles.fooBaz, styles.bar);
1167
-
1168
- // If using `exportLocalsConvention: "as-is"`:
1169
- console.log(styles["foo-baz"], styles.bar);
1170
1182
  ```
1171
1183
 
1172
1184
  You can enable a ES module named export using:
@@ -1384,6 +1396,252 @@ module.exports = {
1384
1396
  };
1385
1397
  ```
1386
1398
 
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
+ }) => Promise<void> | void;
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
+ Using `getJSON`, it's possible to output a files with all CSS module mappings.
1461
+ In the following example, we use `getJSON` to cache canonical mappings and
1462
+ add stand-ins for any composed values (through `composes`), and we use a custom plugin
1463
+ to consolidate the values and output them to a file:
1464
+
1465
+ **webpack.config.js**
1466
+
1467
+ ```js
1468
+ const path = require("path");
1469
+ const fs = require("fs");
1470
+
1471
+ const CSS_LOADER_REPLACEMENT_REGEX =
1472
+ /(___CSS_LOADER_ICSS_IMPORT_\d+_REPLACEMENT_\d+___)/g;
1473
+ const REPLACEMENT_REGEX = /___REPLACEMENT\[(.*?)]\[(.*?)]___/g;
1474
+ const IDENTIFIER_REGEX = /\[(.*?)]\[(.*?)]/;
1475
+ const replacementsMap = {};
1476
+ const canonicalValuesMap = {};
1477
+ const allExportsJson = {};
1478
+
1479
+ function generateIdentifier(resourcePath, localName) {
1480
+ return `[${resourcePath}][${localName}]`;
1481
+ }
1482
+
1483
+ function addReplacements(resourcePath, imports, exportsJson, replacements) {
1484
+ const importReplacementsMap = {};
1485
+
1486
+ // create a dict to quickly identify imports and get their absolute stand-in strings in the currently loaded file
1487
+ // e.g., { '___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___': '___REPLACEMENT[/foo/bar/baz.css][main]___' }
1488
+ importReplacementsMap[resourcePath] = replacements.reduce(
1489
+ (acc, { replacementName, importName, localName }) => {
1490
+ const replacementImportUrl = imports.find(
1491
+ (importData) => importData.importName === importName,
1492
+ ).url;
1493
+ const relativePathRe = /.*!(.*)"/;
1494
+ const [, relativePath] = replacementImportUrl.match(relativePathRe);
1495
+ const importPath = path.resolve(path.dirname(resourcePath), relativePath);
1496
+ const identifier = generateIdentifier(importPath, localName);
1497
+ return { ...acc, [replacementName]: `___REPLACEMENT${identifier}___` };
1498
+ },
1499
+ {},
1500
+ );
1501
+
1502
+ // iterate through the raw exports and add stand-in variables
1503
+ // ('___REPLACEMENT[<absolute_path>][<class_name>]___')
1504
+ // to be replaced in the plugin below
1505
+ for (const [localName, classNames] of Object.entries(exportsJson)) {
1506
+ const identifier = generateIdentifier(resourcePath, localName);
1507
+
1508
+ if (CSS_LOADER_REPLACEMENT_REGEX.test(classNames)) {
1509
+ // if there are any replacements needed in the concatenated class names,
1510
+ // add them all to the replacements map to be replaced altogether later
1511
+ replacementsMap[identifier] = classNames.replaceAll(
1512
+ CSS_LOADER_REPLACEMENT_REGEX,
1513
+ (_, replacementName) =>
1514
+ importReplacementsMap[resourcePath][replacementName],
1515
+ );
1516
+ } else {
1517
+ // otherwise, no class names need replacements so we can add them to
1518
+ // canonical values map and all exports JSON verbatim
1519
+ canonicalValuesMap[identifier] = classNames;
1520
+
1521
+ allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1522
+ allExportsJson[resourcePath][localName] = classNames;
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ function replaceReplacements(classNames) {
1528
+ return classNames.replaceAll(
1529
+ REPLACEMENT_REGEX,
1530
+ (_, resourcePath, localName) => {
1531
+ const identifier = generateIdentifier(resourcePath, localName);
1532
+
1533
+ if (identifier in canonicalValuesMap) {
1534
+ return canonicalValuesMap[identifier];
1535
+ }
1536
+
1537
+ // Recurse through other stand-in that may be imports
1538
+ const canonicalValue = replaceReplacements(replacementsMap[identifier]);
1539
+
1540
+ canonicalValuesMap[identifier] = canonicalValue;
1541
+
1542
+ return canonicalValue;
1543
+ },
1544
+ );
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
+ }
1564
+
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
+ }
1608
+ }
1609
+
1610
+ module.exports = {
1611
+ module: {
1612
+ rules: [
1613
+ {
1614
+ test: /\.css$/i,
1615
+ loader: "css-loader",
1616
+ options: { modules: { getJSON } },
1617
+ },
1618
+ ],
1619
+ },
1620
+ plugins: [
1621
+ new CssModulesJsonPlugin({
1622
+ filepath: path.resolve(__dirname, "./output.css.json"),
1623
+ }),
1624
+ ],
1625
+ };
1626
+ ```
1627
+
1628
+ 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`:
1629
+
1630
+ ```json
1631
+ {
1632
+ "foo/bar/baz.module.css": {
1633
+ "main": "D2Oy",
1634
+ "header": "thNN"
1635
+ },
1636
+ "foot/bear/bath.module.css": {
1637
+ "logo": "sqiR",
1638
+ "info": "XMyI"
1639
+ }
1640
+ }
1641
+ ```
1642
+
1643
+ This is saved to a local file named `output.css.json`.
1644
+
1387
1645
  ### `importLoaders`
1388
1646
 
1389
1647
  Type:
@@ -2033,8 +2291,8 @@ File treated as `CSS Module`.
2033
2291
  Using both `CSS Module` functionality as well as SCSS variables directly in JavaScript.
2034
2292
 
2035
2293
  ```jsx
2036
- import svars from "variables.scss";
2037
- import styles from "Component.module.scss";
2294
+ import * as svars from "variables.scss";
2295
+ import * as styles from "Component.module.scss";
2038
2296
 
2039
2297
  // Render DOM with CSS modules class name
2040
2298
  // <div className={styles.componentClass}>
package/dist/index.js CHANGED
@@ -171,5 +171,21 @@ async function loader(content, map, meta) {
171
171
  return;
172
172
  }
173
173
  const exportCode = (0, _utils.getExportCode)(exports, replacements, needToUseIcssPlugin, options, isTemplateLiteralSupported);
174
+ const {
175
+ getJSON
176
+ } = options.modules;
177
+ if (typeof getJSON === "function") {
178
+ try {
179
+ await getJSON({
180
+ resourcePath,
181
+ imports,
182
+ exports,
183
+ replacements
184
+ });
185
+ } catch (error) {
186
+ callback(error);
187
+ return;
188
+ }
189
+ }
174
190
  callback(null, `${importCode}${moduleCode}${exportCode}`);
175
191
  }
package/dist/options.json CHANGED
@@ -173,6 +173,11 @@
173
173
  "description": "Export only locals.",
174
174
  "link": "https://github.com/webpack-contrib/css-loader#exportonlylocals",
175
175
  "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"
176
181
  }
177
182
  }
178
183
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-loader",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "css loader module for webpack",
5
5
  "license": "MIT",
6
6
  "repository": "webpack-contrib/css-loader",
@@ -36,7 +36,7 @@
36
36
  "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage",
37
37
  "pretest": "npm run lint",
38
38
  "test": "npm run test:coverage",
39
- "prepare": "husky install && npm run build",
39
+ "prepare": "husky && npm run build",
40
40
  "release": "standard-version"
41
41
  },
42
42
  "files": [