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 +11 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/core/token-sources.js +53 -1
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)
|
|
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=
|
|
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
|
[](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
|
|
10
|
-
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
11
11
|
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "3.7.
|
|
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": "^
|
|
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
|
-
|
|
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
|
}
|