stylelint-plugin-rhythmguard 3.7.0 → 3.7.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/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.7.1] - 2026-09-17
10
+
11
+ ### Fixed
12
+
13
+ - Sass maps declared through a merge helper are read: `$spacers: defaults((...), $spacers)` (Bootstrap v6), `map.merge($base, (...))` and `map-merge((...), (...))` contribute the merged map, later keys winning. The reader also follows Sass assignment rules now: a later plain assignment replaces an earlier `!default`, a later `!default` does not. Before, the first assignment always won, so Bootstrap v6's `$spacers: () !default;` hid the map that followed it and `scale: "auto"` fell back. ([#143](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/143))
14
+
15
+ ### Changed
16
+
17
+ - The quiet benchmark has a `bootstrap-v6` row, the branch its maintainers asked to see (twbs/bootstrap#42907); the manifest accepts a `ref` so a row can pin a branch other than the default.
18
+ - The release verify matrix and the post-publish smoke are back on the self-hosted farm. The repository variable `RELEASE_ON_HOSTED=true` moves a release onto GitHub-hosted runners while the farm is down, without a workflow edit or a re-cut; the release checklist explains when to use it.
19
+
9
20
  ## [3.7.0] - 2026-09-17
10
21
 
11
22
  ### Added
package/CONTRIBUTING.md CHANGED
@@ -106,7 +106,7 @@ The maintainer aims to respond to issues and PRs within a week. Small PRs merge
106
106
 
107
107
  ## Releases
108
108
 
109
- Maintainer-run. Bump `package.json` and `CHANGELOG.md`, create a GitHub release, and `release.yml` verifies the matrix and publishes through npm trusted publishing (OIDC), on GitHub-hosted runners so a release does not depend on the self-hosted farm. Provenance is automatic. The checklist is in [`docs/RELEASE_CHECKLIST.md`](./docs/RELEASE_CHECKLIST.md).
109
+ Maintainer-run. Bump `package.json` and `CHANGELOG.md`, create a GitHub release, and `release.yml` verifies on the self-hosted matrix and publishes through npm trusted publishing (OIDC) from a GitHub-hosted job. The checklist covers cutting a release while the farm is down. Provenance is automatic. The checklist is in [`docs/RELEASE_CHECKLIST.md`](./docs/RELEASE_CHECKLIST.md).
110
110
 
111
111
  ## Benchmarking performance
112
112
 
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-banner.svg?v=10" width="100%" alt="Rhythmguard: stable local evidence for design system drift" />
2
+ <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-banner.svg?v=11" width="100%" alt="Rhythmguard: stable local evidence for design system drift" />
3
3
  </p>
4
4
 
5
5
  # stylelint-plugin-rhythmguard
@@ -7,7 +7,7 @@
7
7
  Nobody chose 13px. Rhythmguard catches off-scale spacing in CSS and Tailwind class strings, tells you the nearest steps on your scale, and snaps to them or to your tokens when you ask.
8
8
 
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/petrilahdelma/stylelint-plugin-rhythmguard/ci.yml?branch=main&label=ci)](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
10
- [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard?label=npm&color=1f6feb&r=370)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
10
+ [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard?label=npm&color=1f6feb&r=371)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
11
11
  [![npm downloads](https://img.shields.io/npm/dm/stylelint-plugin-rhythmguard.svg)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-white.svg)](./LICENSE)
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "description": "Nobody chose 13px. Catches off-scale spacing in CSS and Tailwind class strings and snaps it to your scale or tokens. Stylelint rules, an ESLint companion, and an audit CLI.",
5
5
  "bin": {
6
6
  "rhythmguard": "src/cli/index.js"
@@ -153,7 +153,7 @@
153
153
  "devDependencies": {
154
154
  "@eslint/js": "^9.39.5",
155
155
  "@types/node": "^22.20.1",
156
- "c8": "^10.1.3",
156
+ "c8": "^12.0.0",
157
157
  "eslint": "^9.39.5",
158
158
  "postcss-scss": "^4.0.9",
159
159
  "stylelint": "^16.15.0",
@@ -456,15 +456,67 @@ function parseScssDeclarations(source) {
456
456
  }
457
457
 
458
458
  startPattern.lastIndex = index;
459
+ const isDefault = /!default\b/.test(value);
459
460
  const cleaned = value.replace(/!(default|global)\b/g, '').trim();
460
- if (cleaned && !declarations.has(name)) {
461
+ // Sass semantics at the top level: `!default` assigns only when the variable is
462
+ // still unset, a plain assignment always wins. Bootstrap v6 declares
463
+ // `$spacers: () !default;` and then `$spacers: defaults((...), $spacers);`.
464
+ if (cleaned && (!declarations.has(name) || !isDefault)) {
461
465
  declarations.set(name, cleaned);
462
466
  }
463
467
  }
464
468
 
469
+ for (const [name, raw] of declarations) {
470
+ const unwrapped = unwrapScssMapCall(raw, declarations);
471
+ if (unwrapped) {
472
+ declarations.set(name, unwrapped);
473
+ }
474
+ }
475
+
465
476
  return declarations;
466
477
  }
467
478
 
479
+ /**
480
+ * A map declared through a merge helper, read as the map it produces:
481
+ * `defaults((...), $spacers)` (Bootstrap v6), `map.merge($a, (...))`,
482
+ * `map-merge((...), (...))`. Map-literal arguments and variables that hold a
483
+ * map are merged left to right with later keys winning, as Sass does; anything
484
+ * else (an unset `!default` placeholder, an expression) contributes nothing.
485
+ */
486
+ const SCSS_MAP_MERGE_CALL = /^(?:defaults|map\.merge|map-merge)\s*\(([\s\S]*)\)$/;
487
+
488
+ function unwrapScssMapCall(raw, declarations) {
489
+ const match = raw.match(SCSS_MAP_MERGE_CALL);
490
+ if (!match) {
491
+ return null;
492
+ }
493
+ const entries = new Map();
494
+ for (const argument of splitTopLevel(match[1], ',')) {
495
+ const text = argument.trim();
496
+ let mapLiteral = null;
497
+ if (isScssMap(text)) {
498
+ mapLiteral = text;
499
+ } else if (/^\$[\w-]+$/.test(text)) {
500
+ const referenced = declarations.get(text.slice(1));
501
+ if (referenced && isScssMap(referenced)) {
502
+ mapLiteral = referenced;
503
+ }
504
+ }
505
+ if (!mapLiteral) {
506
+ continue;
507
+ }
508
+ for (const entry of splitTopLevel(mapLiteral.slice(1, -1), ',')) {
509
+ const pair = splitTopLevel(entry, ':');
510
+ if (pair.length < 2) continue;
511
+ entries.set(pair[0].trim(), pair.slice(1).join(':').trim());
512
+ }
513
+ }
514
+ if (entries.size === 0) {
515
+ return null;
516
+ }
517
+ return `(${[...entries].map(([key, expression]) => `${key}: ${expression}`).join(', ')})`;
518
+ }
519
+
468
520
  function isScssMap(raw) {
469
521
  return raw.startsWith('(') && raw.endsWith(')') && /:/.test(raw);
470
522
  }