stylelint-plugin-rhythmguard 1.4.1 → 1.5.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/CHANGELOG.md +14 -0
- package/README.md +153 -25
- package/package.json +9 -3
- package/src/cli/audit.js +161 -0
- package/src/cli/doctor.js +231 -0
- package/src/cli/index.js +38 -0
- package/src/cli/init.js +128 -0
- package/src/configs/react-tailwind.js +25 -0
- package/src/configs/react-tailwind.mjs +4 -0
- package/src/configs/tailwind.js +9 -0
- package/src/utils/token-map.js +44 -3
- package/assets/campaign-slides/scene-1.png +0 -0
- package/assets/campaign-slides/scene-2.png +0 -0
- package/assets/campaign-slides/scene-3.png +0 -0
- package/assets/campaign-slides/scene-4.png +0 -0
- package/assets/campaign-slides/scene-5.png +0 -0
- package/assets/campaign-slides/scene-6.png +0 -0
- package/assets/rhythmguard-banner.svg +0 -42
- package/assets/rhythmguard-campaign-60s-template.html +0 -322
- package/assets/rhythmguard-campaign-60s.gif +0 -0
- package/assets/rhythmguard-campaign-60s.webm +0 -0
- package/assets/rhythmguard-rules.svg +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.4.2] - 2026-02-21
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- npm package artifact is now leaner by excluding non-runtime media assets from published files.
|
|
14
|
+
- README media links now use absolute GitHub URLs so npm README rendering remains intact without bundling local media files.
|
|
15
|
+
- Added a dedicated "Drop-In for Existing Projects" path with a single install command and a single config block.
|
|
16
|
+
- Added comparison and migration guidance:
|
|
17
|
+
- `docs/COMPARISON.md` (`defensive-css` vs `logical-css` vs Rhythmguard + migration recipes)
|
|
18
|
+
- `docs/ADOPTION_DIFFS.md` (real before/after excerpts from public codebases)
|
|
19
|
+
- Added Dev.to publishing assets:
|
|
20
|
+
- `docs/DEVTO_ORIGINAL_UPDATE_NOTE_2026-02-21.md`
|
|
21
|
+
- `docs/DEVTO_CONTINUATION_2026-02-21.md`
|
|
22
|
+
|
|
9
23
|
## [1.4.1] - 2026-02-21
|
|
10
24
|
|
|
11
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-banner.svg?v=3" width="100%" alt="Rhythmguard banner showing spacing scale ruler and lint output" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# stylelint-plugin-rhythmguard
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Token governance for CSS and Tailwind. Enforce spacing scales, require design tokens, and catch arbitrary values before they ship.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
@@ -12,30 +12,53 @@ High-precision spacing governance for CSS and design systems.
|
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
[](https://nodejs.org/)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Rhythmguard enforces scale and token discipline across spacing, radius, typography, size, and motion offsets — in CSS declarations and Tailwind class strings.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Built for teams that want:
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</p>
|
|
19
|
+
- zero random spacing values in production
|
|
20
|
+
- token-first workflows with autofix migration
|
|
21
|
+
- Tailwind arbitrary value governance (`p-[13px]` → `p-[12px]`)
|
|
22
|
+
- consistent layout rhythm across components and pages
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
## Quick Start: Next.js + Tailwind
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
```bash
|
|
27
|
+
npm install --save-dev stylelint stylelint-plugin-rhythmguard
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**.stylelintrc.json:**
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**eslint.config.js** (for Tailwind class-string governance):
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import rhythmguard from 'stylelint-plugin-rhythmguard/eslint';
|
|
42
|
+
|
|
43
|
+
export default [
|
|
44
|
+
{
|
|
45
|
+
plugins: { 'rhythmguard-tailwind': rhythmguard },
|
|
46
|
+
rules: {
|
|
47
|
+
'rhythmguard-tailwind/tailwind-class-use-scale': [
|
|
48
|
+
'error',
|
|
49
|
+
{ scale: [0, 4, 8, 12, 16, 24, 32] }
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
```
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
- consistent numeric scales for radius, typography, and sizing primitives
|
|
31
|
-
- token-first spacing workflows
|
|
32
|
-
- predictable autofix behavior for large migrations
|
|
33
|
-
- consistent layout rhythm across web surfaces
|
|
56
|
+
This gives you spacing governance in both CSS files and JSX/TSX templates.
|
|
34
57
|
|
|
35
58
|
## Rule Matrix
|
|
36
59
|
|
|
37
60
|
<p align="center">
|
|
38
|
-
<img src="
|
|
61
|
+
<img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-rules.svg" width="100%" alt="Rhythmguard rule matrix visual" />
|
|
39
62
|
</p>
|
|
40
63
|
|
|
41
64
|
| Rule | What it does | Autofix |
|
|
@@ -44,15 +67,29 @@ It is built for teams that want:
|
|
|
44
67
|
| `rhythmguard/prefer-token` | Enforces token usage over raw spacing literals | Yes, with `tokenMap` |
|
|
45
68
|
| `rhythmguard/no-offscale-transform` | Enforces scale-aligned `translate*` motion offsets | Yes, nearest safe value |
|
|
46
69
|
|
|
70
|
+
## Demo
|
|
71
|
+
|
|
72
|
+
<p align="center">
|
|
73
|
+
<a href="https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/assets/rhythmguard-campaign-60s.webm">
|
|
74
|
+
<img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.gif" width="100%" alt="Rhythmguard 60-second demo" />
|
|
75
|
+
</a>
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
I built Rhythmguard after 20 years of watching teams ignore spacing scales and ship arbitrary pixel values everywhere.
|
|
79
|
+
|
|
47
80
|
## Installation
|
|
48
81
|
|
|
49
82
|
```bash
|
|
50
83
|
npm install --save-dev stylelint stylelint-plugin-rhythmguard
|
|
51
84
|
```
|
|
52
85
|
|
|
53
|
-
##
|
|
86
|
+
## Drop-In for Existing Projects (Recommended)
|
|
54
87
|
|
|
55
|
-
|
|
88
|
+
If your project already uses Stylelint, you only need one command and one config block:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install --save-dev stylelint-plugin-rhythmguard
|
|
92
|
+
```
|
|
56
93
|
|
|
57
94
|
```json
|
|
58
95
|
{
|
|
@@ -60,24 +97,34 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
|
|
|
60
97
|
}
|
|
61
98
|
```
|
|
62
99
|
|
|
63
|
-
|
|
100
|
+
## Quick Start
|
|
101
|
+
|
|
102
|
+
### Tailwind config
|
|
64
103
|
|
|
65
104
|
```json
|
|
66
105
|
{
|
|
67
|
-
"extends": ["stylelint-plugin-rhythmguard/configs/
|
|
106
|
+
"extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
|
|
68
107
|
}
|
|
69
108
|
```
|
|
70
109
|
|
|
71
|
-
|
|
110
|
+
### Recommended config
|
|
72
111
|
|
|
73
|
-
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"extends": ["stylelint-plugin-rhythmguard/configs/recommended"]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Strict config
|
|
74
119
|
|
|
75
120
|
```json
|
|
76
121
|
{
|
|
77
|
-
"extends": ["stylelint-plugin-rhythmguard/configs/
|
|
122
|
+
"extends": ["stylelint-plugin-rhythmguard/configs/strict"]
|
|
78
123
|
}
|
|
79
124
|
```
|
|
80
125
|
|
|
126
|
+
`strict` intentionally delegates transform translation enforcement to `rhythmguard/no-offscale-transform` to reduce overlapping warnings from `use-scale`.
|
|
127
|
+
|
|
81
128
|
### Expanded config
|
|
82
129
|
|
|
83
130
|
```json
|
|
@@ -108,15 +155,34 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
|
|
|
108
155
|
|
|
109
156
|
`migration` keeps on-scale numeric values temporarily while auto-building token mappings from CSS custom properties and optional Tailwind spacing config.
|
|
110
157
|
|
|
158
|
+
### React / Next.js + Tailwind config
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"extends": ["stylelint-plugin-rhythmguard/configs/react-tailwind"]
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`react-tailwind` extends the tailwind config with CSS Modules overrides (spacing + radius enforcement) and ignores Next.js build directories.
|
|
167
|
+
|
|
111
168
|
Stable shared config entry points:
|
|
112
169
|
|
|
113
170
|
- `stylelint-plugin-rhythmguard/configs/recommended`
|
|
114
171
|
- `stylelint-plugin-rhythmguard/configs/strict`
|
|
115
172
|
- `stylelint-plugin-rhythmguard/configs/tailwind`
|
|
173
|
+
- `stylelint-plugin-rhythmguard/configs/react-tailwind`
|
|
116
174
|
- `stylelint-plugin-rhythmguard/configs/expanded`
|
|
117
175
|
- `stylelint-plugin-rhythmguard/configs/logical`
|
|
118
176
|
- `stylelint-plugin-rhythmguard/configs/migration`
|
|
119
177
|
|
|
178
|
+
Framework-specific setup for Vue, Lit, Astro, and SvelteKit: [`docs/FRAMEWORKS.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/FRAMEWORKS.md)
|
|
179
|
+
|
|
180
|
+
## Comparison and Migration Recipes
|
|
181
|
+
|
|
182
|
+
- Side-by-side tool fit guide with migration snippets: [`docs/COMPARISON.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/COMPARISON.md)
|
|
183
|
+
- Real-world before/after excerpts from public repos: [`docs/ADOPTION_DIFFS.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/ADOPTION_DIFFS.md)
|
|
184
|
+
- Distribution submissions to Stylelint discovery surfaces: [`docs/DISTRIBUTION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DISTRIBUTION.md)
|
|
185
|
+
|
|
120
186
|
### Full custom setup
|
|
121
187
|
|
|
122
188
|
```json
|
|
@@ -389,7 +455,7 @@ Options:
|
|
|
389
455
|
| `mathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Restricts linting to specific 1-based argument indexes per math function |
|
|
390
456
|
| `ignoreMathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Excludes specific 1-based argument indexes per math function |
|
|
391
457
|
| `tokenMap` | `Record<string,string>` | `{}` | Enables autofix from raw value to token |
|
|
392
|
-
| `tokenMapFile` | `string` | `null` | JSON file path to merge additional token mappings |
|
|
458
|
+
| `tokenMapFile` | `string` | `null` | JSON file path to merge additional token mappings (supports flat, Style Dictionary, and W3C DTCG formats) |
|
|
393
459
|
| `tokenMapFromCssCustomProperties` | `boolean` | `false` | Auto-builds mappings from matching custom property declarations in the same stylesheet |
|
|
394
460
|
| `tokenMapFromTailwindSpacing` | `boolean` | `false` | Auto-builds mappings from `theme.spacing` and `theme.extend.spacing` in Tailwind config |
|
|
395
461
|
| `tailwindConfigPath` | `string` | `null` | Path to Tailwind config used by `tokenMapFromTailwindSpacing` (`.js`, `.cjs`, `.mjs`) |
|
|
@@ -430,6 +496,12 @@ Rhythmguard works well in Tailwind projects, but it enforces what Stylelint can
|
|
|
430
496
|
- CSS Modules (for example `*.module.css`)
|
|
431
497
|
- declarations inside `@layer` blocks
|
|
432
498
|
|
|
499
|
+
### Tailwind v4 @theme tokens
|
|
500
|
+
|
|
501
|
+
The `tailwind` config preset automatically extracts spacing tokens from Tailwind v4 `@theme` blocks and uses them for `prefer-token` enforcement. Raw values like `padding: 16px` are autofixed to `padding: var(--spacing-4)`.
|
|
502
|
+
|
|
503
|
+
See [`docs/TAILWIND.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/TAILWIND.md) for full setup.
|
|
504
|
+
|
|
433
505
|
### What Rhythmguard does not cover
|
|
434
506
|
|
|
435
507
|
- Tailwind class strings in templates/JSX/TSX, for example:
|
|
@@ -460,6 +532,18 @@ export default [
|
|
|
460
532
|
|
|
461
533
|
This rule targets arbitrary spacing utilities such as `p-[13px]`, `gap-[18px]`, `translate-x-[10px]`, and autofixes to the nearest configured scale value.
|
|
462
534
|
|
|
535
|
+
#### Supported patterns
|
|
536
|
+
|
|
537
|
+
The rule checks every string literal in your code, so it works automatically with common utility functions:
|
|
538
|
+
|
|
539
|
+
- `cn("p-[13px]")` / `cn("p-[13px]", condition && "m-[7px]")`
|
|
540
|
+
- `clsx("p-[13px]", "gap-[18px]")`
|
|
541
|
+
- `twMerge("p-[13px]", otherClasses)`
|
|
542
|
+
- `cva("base", { variants: { size: { sm: "p-[5px]" } } })`
|
|
543
|
+
- `<div className={cn("p-[13px]")} />`
|
|
544
|
+
|
|
545
|
+
No extra config needed — if the string contains an arbitrary spacing value, it gets caught and autofixed.
|
|
546
|
+
|
|
463
547
|
### Recommended stack for full Tailwind enforcement
|
|
464
548
|
|
|
465
549
|
Use both layers:
|
|
@@ -501,6 +585,35 @@ console.log(rhythmguard.presets.scales['rhythmic-4']);
|
|
|
501
585
|
console.log(Object.keys(rhythmguard.eslint.rules));
|
|
502
586
|
```
|
|
503
587
|
|
|
588
|
+
## Token File Formats
|
|
589
|
+
|
|
590
|
+
The `tokenMapFile` option supports multiple JSON formats:
|
|
591
|
+
|
|
592
|
+
**Flat token-to-value:**
|
|
593
|
+
|
|
594
|
+
```json
|
|
595
|
+
{ "--spacing-4": "16px", "--spacing-3": "12px" }
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**Style Dictionary:**
|
|
599
|
+
|
|
600
|
+
```json
|
|
601
|
+
{ "--spacing-4": { "value": "16px" } }
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**W3C DTCG (Design Token Community Group):**
|
|
605
|
+
|
|
606
|
+
```json
|
|
607
|
+
{
|
|
608
|
+
"spacing": {
|
|
609
|
+
"4": { "$value": "16px", "$type": "dimension" },
|
|
610
|
+
"2": { "$value": "8px", "$type": "dimension" }
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Nested DTCG groups are walked recursively. The key path becomes the CSS variable name: `spacing.4` → `var(--spacing-4)`. Non-length values (colors, fonts) are ignored automatically.
|
|
616
|
+
|
|
504
617
|
## Autofix Philosophy
|
|
505
618
|
|
|
506
619
|
Rhythmguard only applies deterministic fixes:
|
|
@@ -545,6 +658,21 @@ Detailed methodology and custom args are documented in [`docs/BENCHMARKING.md`](
|
|
|
545
658
|
## Article
|
|
546
659
|
|
|
547
660
|
- Dev.to: [Enforcing your spacing standards with Rhythmguard](https://dev.to/petrilahdelma/enforcing-your-spacing-standards-with-rhythmguard-a-custom-stylelint-plugin-1ojj)
|
|
661
|
+
- Original article update note (Feb 21, 2026): [`docs/DEVTO_ORIGINAL_UPDATE_NOTE_2026-02-21.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DEVTO_ORIGINAL_UPDATE_NOTE_2026-02-21.md)
|
|
662
|
+
- Continuation draft (ready to publish): [`docs/DEVTO_CONTINUATION_2026-02-21.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DEVTO_CONTINUATION_2026-02-21.md)
|
|
663
|
+
|
|
664
|
+
## Used by and Community Examples
|
|
665
|
+
|
|
666
|
+
Public codebases currently used for production migration examples:
|
|
667
|
+
|
|
668
|
+
- [PetriLahdelma/digitaltableteur-nextjs](https://github.com/PetriLahdelma/digitaltableteur-nextjs)
|
|
669
|
+
- [PetriLahdelma/digitaltableteur](https://github.com/PetriLahdelma/digitaltableteur)
|
|
670
|
+
|
|
671
|
+
Want your team listed here?
|
|
672
|
+
|
|
673
|
+
1. Open an issue with `used-by` in the title.
|
|
674
|
+
2. Include one before/after diff and your Rhythmguard config.
|
|
675
|
+
3. Add migration notes (false positives, rules enabled, rollout phase).
|
|
548
676
|
|
|
549
677
|
## Release Workflow
|
|
550
678
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Token governance for CSS and Tailwind — enforce spacing scales, require design tokens, catch arbitrary values",
|
|
5
|
+
"bin": {
|
|
6
|
+
"rhythmguard": "./src/cli/index.js"
|
|
7
|
+
},
|
|
5
8
|
"keywords": [
|
|
6
9
|
"stylelint",
|
|
7
10
|
"stylelint-plugin",
|
|
@@ -46,6 +49,10 @@
|
|
|
46
49
|
"require": "./src/configs/migration.js",
|
|
47
50
|
"import": "./src/configs/migration.mjs"
|
|
48
51
|
},
|
|
52
|
+
"./configs/react-tailwind": {
|
|
53
|
+
"require": "./src/configs/react-tailwind.js",
|
|
54
|
+
"import": "./src/configs/react-tailwind.mjs"
|
|
55
|
+
},
|
|
49
56
|
"./presets": {
|
|
50
57
|
"require": "./src/presets/index.js",
|
|
51
58
|
"import": "./src/presets/index.mjs"
|
|
@@ -68,7 +75,6 @@
|
|
|
68
75
|
}
|
|
69
76
|
},
|
|
70
77
|
"files": [
|
|
71
|
-
"assets",
|
|
72
78
|
"scales",
|
|
73
79
|
"schemas",
|
|
74
80
|
"src",
|
package/src/cli/audit.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(3);
|
|
7
|
+
const jsonMode = args.includes('--json');
|
|
8
|
+
const dir = args.find((a) => !a.startsWith('-'));
|
|
9
|
+
|
|
10
|
+
if (!dir) {
|
|
11
|
+
process.stderr.write('Usage: rhythmguard audit <dir> [--json]\n');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const resolvedDir = path.resolve(dir);
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
18
|
+
process.stderr.write(`Directory not found: ${dir}\n`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const GLOB_PATTERN = `${resolvedDir}/**/*.{css,module.css}`;
|
|
23
|
+
|
|
24
|
+
const pluginPath = path.resolve(__dirname, '..', 'index.js');
|
|
25
|
+
|
|
26
|
+
async function run() {
|
|
27
|
+
const { default: stylelint } = await import('stylelint');
|
|
28
|
+
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
result = await stylelint.lint({
|
|
32
|
+
files: GLOB_PATTERN,
|
|
33
|
+
config: {
|
|
34
|
+
plugins: [pluginPath],
|
|
35
|
+
rules: {
|
|
36
|
+
'rhythmguard/use-scale': [true, { severity: 'warning' }],
|
|
37
|
+
'rhythmguard/prefer-token': [
|
|
38
|
+
true,
|
|
39
|
+
{
|
|
40
|
+
tokenMapFromCssCustomProperties: true,
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
process.stderr.write(`Lint error: ${err.message}\n`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fileResults = result.results || [];
|
|
53
|
+
const totalFiles = fileResults.length;
|
|
54
|
+
|
|
55
|
+
const offScaleValues = {};
|
|
56
|
+
const tokenOpportunities = {};
|
|
57
|
+
let filesWithIssues = 0;
|
|
58
|
+
let totalWarnings = 0;
|
|
59
|
+
|
|
60
|
+
for (const fileResult of fileResults) {
|
|
61
|
+
const warnings = fileResult.warnings || [];
|
|
62
|
+
if (warnings.length > 0) {
|
|
63
|
+
filesWithIssues++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const warning of warnings) {
|
|
67
|
+
totalWarnings++;
|
|
68
|
+
const text = warning.text || '';
|
|
69
|
+
|
|
70
|
+
// Extract off-scale values from use-scale messages
|
|
71
|
+
// Format: Unexpected off-scale value "13px". Use scale values (nearest: 12px or 16px).
|
|
72
|
+
const offScaleMatch = text.match(
|
|
73
|
+
/Unexpected off-scale value "([^"]+)"/,
|
|
74
|
+
);
|
|
75
|
+
if (offScaleMatch) {
|
|
76
|
+
const value = offScaleMatch[1];
|
|
77
|
+
offScaleValues[value] = (offScaleValues[value] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract token opportunities from prefer-token messages
|
|
81
|
+
// Format: Unexpected raw scale value "16px". Use design tokens for scale decisions.
|
|
82
|
+
const tokenMatch = text.match(
|
|
83
|
+
/Unexpected raw scale value "([^"]+)"/,
|
|
84
|
+
);
|
|
85
|
+
if (tokenMatch) {
|
|
86
|
+
const value = tokenMatch[1];
|
|
87
|
+
tokenOpportunities[value] = (tokenOpportunities[value] || 0) + 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sortedOffScale = Object.entries(offScaleValues)
|
|
93
|
+
.sort((a, b) => b[1] - a[1])
|
|
94
|
+
.slice(0, 10);
|
|
95
|
+
|
|
96
|
+
const sortedTokenOps = Object.entries(tokenOpportunities)
|
|
97
|
+
.sort((a, b) => b[1] - a[1])
|
|
98
|
+
.slice(0, 10);
|
|
99
|
+
|
|
100
|
+
if (jsonMode) {
|
|
101
|
+
const output = {
|
|
102
|
+
directory: dir,
|
|
103
|
+
totalFiles,
|
|
104
|
+
filesWithIssues,
|
|
105
|
+
totalWarnings,
|
|
106
|
+
offScaleValues: Object.fromEntries(sortedOffScale),
|
|
107
|
+
tokenOpportunities: Object.fromEntries(sortedTokenOps),
|
|
108
|
+
};
|
|
109
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Human-readable output
|
|
114
|
+
const pct = totalFiles > 0
|
|
115
|
+
? Math.round((filesWithIssues / totalFiles) * 100)
|
|
116
|
+
: 0;
|
|
117
|
+
|
|
118
|
+
const lines = [
|
|
119
|
+
'',
|
|
120
|
+
`Rhythmguard Audit: ${dir}`,
|
|
121
|
+
'',
|
|
122
|
+
`Files scanned: ${totalFiles}`,
|
|
123
|
+
`Files with issues: ${filesWithIssues} (${pct}%)`,
|
|
124
|
+
'',
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
if (sortedOffScale.length > 0) {
|
|
128
|
+
const offScaleTotal = Object.values(offScaleValues).reduce(
|
|
129
|
+
(a, b) => a + b,
|
|
130
|
+
0,
|
|
131
|
+
);
|
|
132
|
+
lines.push(`Off-scale values: ${offScaleTotal}`);
|
|
133
|
+
for (const [value, count] of sortedOffScale) {
|
|
134
|
+
lines.push(` ${value.padEnd(10)} ×${count}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (sortedTokenOps.length > 0) {
|
|
140
|
+
const tokenTotal = Object.values(tokenOpportunities).reduce(
|
|
141
|
+
(a, b) => a + b,
|
|
142
|
+
0,
|
|
143
|
+
);
|
|
144
|
+
lines.push(`Token opportunities: ${tokenTotal}`);
|
|
145
|
+
for (const [value, count] of sortedTokenOps) {
|
|
146
|
+
lines.push(` ${value.padEnd(10)} ×${count}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push('');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (totalWarnings === 0) {
|
|
152
|
+
lines.push('No issues found. Your spacing is on scale.');
|
|
153
|
+
} else {
|
|
154
|
+
lines.push('Run "npx stylelint --fix" to auto-correct.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lines.push('');
|
|
158
|
+
process.stdout.write(lines.join('\n'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
run();
|