css-loader 7.0.0 → 7.1.1

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
  },
@@ -1144,7 +1156,7 @@ Enables/disables ES modules named export for locals.
1144
1156
 
1145
1157
  > **Warning**
1146
1158
  >
1147
- > It is not allowed to use the `default` reserved word in css classes.
1159
+ > Because it is not allowed to use the `default` class in CSS when the `namedExport` is `true` (since ECMA modules have a reserved keyword `default` for default export), it will be automatically renamed to the `_default` class.
1148
1160
 
1149
1161
  **styles.css**
1150
1162
 
@@ -1155,6 +1167,9 @@ Enables/disables ES modules named export for locals.
1155
1167
  .bar {
1156
1168
  color: blue;
1157
1169
  }
1170
+ .default {
1171
+ color: green;
1172
+ }
1158
1173
  ```
1159
1174
 
1160
1175
  **index.js**
@@ -1162,11 +1177,14 @@ Enables/disables ES modules named export for locals.
1162
1177
  ```js
1163
1178
  import * as styles from "./styles.css";
1164
1179
 
1180
+ // If using `exportLocalsConvention: "as-is"` (default value):
1181
+ console.log(styles["foo-baz"], styles.bar);
1182
+
1165
1183
  // If using `exportLocalsConvention: "camel-case-only"`:
1166
1184
  console.log(styles.fooBaz, styles.bar);
1167
1185
 
1168
- // If using `exportLocalsConvention: "as-is"`:
1169
- console.log(styles["foo-baz"], styles.bar);
1186
+ // For the `default` classname
1187
+ console.log(styles["_default"]);
1170
1188
  ```
1171
1189
 
1172
1190
  You can enable a ES module named export using:
@@ -1384,6 +1402,252 @@ module.exports = {
1384
1402
  };
1385
1403
  ```
1386
1404
 
1405
+ ##### `getJSON`
1406
+
1407
+ Type:
1408
+
1409
+ ```ts
1410
+ type getJSON = ({
1411
+ resourcePath,
1412
+ imports,
1413
+ exports,
1414
+ replacements,
1415
+ }: {
1416
+ resourcePath: string;
1417
+ imports: object[];
1418
+ exports: object[];
1419
+ replacements: object[];
1420
+ }) => Promise<void> | void;
1421
+ ```
1422
+
1423
+ Default: `undefined`
1424
+
1425
+ Enables a callback to output the CSS modules mapping JSON. The callback is invoked with an object containing the following:
1426
+
1427
+ - `resourcePath`: the absolute path of the original resource, e.g., `/foo/bar/baz.module.css`
1428
+
1429
+ - `imports`: an array of import objects with data about import types and file paths, e.g.,
1430
+
1431
+ ```json
1432
+ [
1433
+ {
1434
+ "type": "icss_import",
1435
+ "importName": "___CSS_LOADER_ICSS_IMPORT_0___",
1436
+ "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\"",
1437
+ "icss": true,
1438
+ "index": 0
1439
+ }
1440
+ ]
1441
+ ```
1442
+
1443
+ (Note that this will include all imports, not just those relevant to CSS modules.)
1444
+
1445
+ - `exports`: an array of export objects with exported names and values, e.g.,
1446
+
1447
+ ```json
1448
+ [
1449
+ {
1450
+ "name": "main",
1451
+ "value": "D2Oy"
1452
+ }
1453
+ ]
1454
+ ```
1455
+
1456
+ - `replacements`: an array of import replacement objects used for linking `imports` and `exports`, e.g.,
1457
+
1458
+ ```json
1459
+ {
1460
+ "replacementName": "___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___",
1461
+ "importName": "___CSS_LOADER_ICSS_IMPORT_0___",
1462
+ "localName": "main"
1463
+ }
1464
+ ```
1465
+
1466
+ Using `getJSON`, it's possible to output a files with all CSS module mappings.
1467
+ In the following example, we use `getJSON` to cache canonical mappings and
1468
+ add stand-ins for any composed values (through `composes`), and we use a custom plugin
1469
+ to consolidate the values and output them to a file:
1470
+
1471
+ **webpack.config.js**
1472
+
1473
+ ```js
1474
+ const path = require("path");
1475
+ const fs = require("fs");
1476
+
1477
+ const CSS_LOADER_REPLACEMENT_REGEX =
1478
+ /(___CSS_LOADER_ICSS_IMPORT_\d+_REPLACEMENT_\d+___)/g;
1479
+ const REPLACEMENT_REGEX = /___REPLACEMENT\[(.*?)]\[(.*?)]___/g;
1480
+ const IDENTIFIER_REGEX = /\[(.*?)]\[(.*?)]/;
1481
+ const replacementsMap = {};
1482
+ const canonicalValuesMap = {};
1483
+ const allExportsJson = {};
1484
+
1485
+ function generateIdentifier(resourcePath, localName) {
1486
+ return `[${resourcePath}][${localName}]`;
1487
+ }
1488
+
1489
+ function addReplacements(resourcePath, imports, exportsJson, replacements) {
1490
+ const importReplacementsMap = {};
1491
+
1492
+ // create a dict to quickly identify imports and get their absolute stand-in strings in the currently loaded file
1493
+ // e.g., { '___CSS_LOADER_ICSS_IMPORT_0_REPLACEMENT_0___': '___REPLACEMENT[/foo/bar/baz.css][main]___' }
1494
+ importReplacementsMap[resourcePath] = replacements.reduce(
1495
+ (acc, { replacementName, importName, localName }) => {
1496
+ const replacementImportUrl = imports.find(
1497
+ (importData) => importData.importName === importName,
1498
+ ).url;
1499
+ const relativePathRe = /.*!(.*)"/;
1500
+ const [, relativePath] = replacementImportUrl.match(relativePathRe);
1501
+ const importPath = path.resolve(path.dirname(resourcePath), relativePath);
1502
+ const identifier = generateIdentifier(importPath, localName);
1503
+ return { ...acc, [replacementName]: `___REPLACEMENT${identifier}___` };
1504
+ },
1505
+ {},
1506
+ );
1507
+
1508
+ // iterate through the raw exports and add stand-in variables
1509
+ // ('___REPLACEMENT[<absolute_path>][<class_name>]___')
1510
+ // to be replaced in the plugin below
1511
+ for (const [localName, classNames] of Object.entries(exportsJson)) {
1512
+ const identifier = generateIdentifier(resourcePath, localName);
1513
+
1514
+ if (CSS_LOADER_REPLACEMENT_REGEX.test(classNames)) {
1515
+ // if there are any replacements needed in the concatenated class names,
1516
+ // add them all to the replacements map to be replaced altogether later
1517
+ replacementsMap[identifier] = classNames.replaceAll(
1518
+ CSS_LOADER_REPLACEMENT_REGEX,
1519
+ (_, replacementName) =>
1520
+ importReplacementsMap[resourcePath][replacementName],
1521
+ );
1522
+ } else {
1523
+ // otherwise, no class names need replacements so we can add them to
1524
+ // canonical values map and all exports JSON verbatim
1525
+ canonicalValuesMap[identifier] = classNames;
1526
+
1527
+ allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1528
+ allExportsJson[resourcePath][localName] = classNames;
1529
+ }
1530
+ }
1531
+ }
1532
+
1533
+ function replaceReplacements(classNames) {
1534
+ return classNames.replaceAll(
1535
+ REPLACEMENT_REGEX,
1536
+ (_, resourcePath, localName) => {
1537
+ const identifier = generateIdentifier(resourcePath, localName);
1538
+
1539
+ if (identifier in canonicalValuesMap) {
1540
+ return canonicalValuesMap[identifier];
1541
+ }
1542
+
1543
+ // Recurse through other stand-in that may be imports
1544
+ const canonicalValue = replaceReplacements(replacementsMap[identifier]);
1545
+
1546
+ canonicalValuesMap[identifier] = canonicalValue;
1547
+
1548
+ return canonicalValue;
1549
+ },
1550
+ );
1551
+ }
1552
+
1553
+ function getJSON({ resourcePath, imports, exports, replacements }) {
1554
+ const exportsJson = exports.reduce((acc, { name, value }) => {
1555
+ return { ...acc, [name]: value };
1556
+ }, {});
1557
+
1558
+ if (replacements.length > 0) {
1559
+ // replacements present --> add stand-in values for absolute paths and local names,
1560
+ // which will be resolved to their canonical values in the plugin below
1561
+ addReplacements(resourcePath, imports, exportsJson, replacements);
1562
+ } else {
1563
+ // no replacements present --> add to canonicalValuesMap verbatim
1564
+ // since all values here are canonical/don't need resolution
1565
+ for (const [key, value] of Object.entries(exportsJson)) {
1566
+ const id = `[${resourcePath}][${key}]`;
1567
+
1568
+ canonicalValuesMap[id] = value;
1569
+ }
1570
+
1571
+ allExportsJson[resourcePath] = exportsJson;
1572
+ }
1573
+ }
1574
+
1575
+ class CssModulesJsonPlugin {
1576
+ constructor(options) {
1577
+ this.options = options;
1578
+ }
1579
+
1580
+ // eslint-disable-next-line class-methods-use-this
1581
+ apply(compiler) {
1582
+ compiler.hooks.emit.tap("CssModulesJsonPlugin", () => {
1583
+ for (const [identifier, classNames] of Object.entries(replacementsMap)) {
1584
+ const adjustedClassNames = replaceReplacements(classNames);
1585
+
1586
+ replacementsMap[identifier] = adjustedClassNames;
1587
+
1588
+ const [, resourcePath, localName] = identifier.match(IDENTIFIER_REGEX);
1589
+
1590
+ allExportsJson[resourcePath] = allExportsJson[resourcePath] || {};
1591
+ allExportsJson[resourcePath][localName] = adjustedClassNames;
1592
+ }
1593
+
1594
+ fs.writeFileSync(
1595
+ this.options.filepath,
1596
+ JSON.stringify(
1597
+ // Make path to be relative to `context` (your project root)
1598
+ Object.fromEntries(
1599
+ Object.entries(allExportsJson).map((key) => {
1600
+ key[0] = path
1601
+ .relative(compiler.context, key[0])
1602
+ .replace(/\\/g, "/");
1603
+
1604
+ return key;
1605
+ }),
1606
+ ),
1607
+ null,
1608
+ 2,
1609
+ ),
1610
+ "utf8",
1611
+ );
1612
+ });
1613
+ }
1614
+ }
1615
+
1616
+ module.exports = {
1617
+ module: {
1618
+ rules: [
1619
+ {
1620
+ test: /\.css$/i,
1621
+ loader: "css-loader",
1622
+ options: { modules: { getJSON } },
1623
+ },
1624
+ ],
1625
+ },
1626
+ plugins: [
1627
+ new CssModulesJsonPlugin({
1628
+ filepath: path.resolve(__dirname, "./output.css.json"),
1629
+ }),
1630
+ ],
1631
+ };
1632
+ ```
1633
+
1634
+ 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`:
1635
+
1636
+ ```json
1637
+ {
1638
+ "foo/bar/baz.module.css": {
1639
+ "main": "D2Oy",
1640
+ "header": "thNN"
1641
+ },
1642
+ "foot/bear/bath.module.css": {
1643
+ "logo": "sqiR",
1644
+ "info": "XMyI"
1645
+ }
1646
+ }
1647
+ ```
1648
+
1649
+ This is saved to a local file named `output.css.json`.
1650
+
1387
1651
  ### `importLoaders`
1388
1652
 
1389
1653
  Type:
@@ -2033,8 +2297,8 @@ File treated as `CSS Module`.
2033
2297
  Using both `CSS Module` functionality as well as SCSS variables directly in JavaScript.
2034
2298
 
2035
2299
  ```jsx
2036
- import svars from "variables.scss";
2037
- import styles from "Component.module.scss";
2300
+ import * as svars from "variables.scss";
2301
+ import * as styles from "Component.module.scss";
2038
2302
 
2039
2303
  // Render DOM with CSS modules class name
2040
2304
  // <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/dist/utils.js CHANGED
@@ -845,9 +845,12 @@ function getExportCode(exports, replacements, icssPluginUsed, options, isTemplat
845
845
  let identifierId = 0;
846
846
  const addExportToLocalsCode = (names, value) => {
847
847
  const normalizedNames = Array.isArray(names) ? new Set(names) : new Set([names]);
848
- for (const name of normalizedNames) {
848
+ for (let name of normalizedNames) {
849
849
  const serializedValue = isTemplateLiteralSupported ? convertToTemplateLiteral(value) : JSON.stringify(value);
850
850
  if (options.modules.namedExport) {
851
+ if (name === "default") {
852
+ name = `_${name}`;
853
+ }
851
854
  if (!validIdentifier.test(name) || keywords.has(name)) {
852
855
  identifierId += 1;
853
856
  const id = `_${identifierId.toString(16)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-loader",
3
- "version": "7.0.0",
3
+ "version": "7.1.1",
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": [