stylelint-plugin-rhythmguard 2.0.1 → 2.2.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 +42 -0
- package/CONTRIBUTING.md +24 -5
- package/README.md +45 -768
- package/package.json +15 -6
- package/src/audit/args.js +449 -0
- package/src/audit/baseline.js +114 -0
- package/src/audit/config.js +217 -0
- package/src/audit/contract.js +422 -0
- package/src/audit/render-github.js +39 -0
- package/src/audit/render-html.js +78 -0
- package/src/audit/render-markdown.js +227 -0
- package/src/audit/render-text.js +168 -0
- package/src/audit/render-utils.js +41 -0
- package/src/audit/report.js +189 -0
- package/src/audit/scan.js +546 -0
- package/src/audit/shared.js +108 -0
- package/src/cli/audit.js +22 -2213
- package/src/cli/index.js +11 -5
- package/src/cli/init.js +5 -1
- package/src/cli/quickstart.js +203 -0
- package/src/configs/embed.js +24 -0
- package/src/configs/embed.mjs +4 -0
- package/src/eslint/rules/tailwind-class-use-motion-scale.js +1 -0
- package/src/eslint/rules/tailwind-class-use-scale.js +1 -0
- package/src/index.js +2 -0
- package/src/rules/no-offscale-transform/index.js +23 -1
- package/src/rules/prefer-token/index.js +29 -1
- package/src/rules/use-motion-scale/index.js +1 -1
- package/src/rules/use-scale/index.js +35 -5
- package/src/utils/length.js +16 -0
- package/src/utils/options.js +53 -3
- package/src/utils/scale-inference.js +255 -0
- package/src/utils/token-map.js +4 -2
- package/src/utils/token-sources.js +32 -2
- package/types/__checks__/consumer.ts +80 -0
- package/types/audit.d.ts +25 -4
- package/types/index.d.ts +5 -0
- package/types/shared.d.ts +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Token governance for CSS and Tailwind — enforce spacing scales, require design tokens, catch arbitrary values",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rhythmguard": "src/cli/index.js"
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"require": "./src/configs/recommended.js",
|
|
33
33
|
"import": "./src/configs/recommended.mjs"
|
|
34
34
|
},
|
|
35
|
+
"./configs/embed": {
|
|
36
|
+
"types": "./types/config.d.ts",
|
|
37
|
+
"require": "./src/configs/embed.js",
|
|
38
|
+
"import": "./src/configs/embed.mjs"
|
|
39
|
+
},
|
|
35
40
|
"./configs/strict": {
|
|
36
41
|
"types": "./types/config.d.ts",
|
|
37
42
|
"require": "./src/configs/strict.js",
|
|
@@ -118,19 +123,21 @@
|
|
|
118
123
|
"scripts": {
|
|
119
124
|
"bench:perf": "node scripts/bench/compare.mjs",
|
|
120
125
|
"bench:perf:fix": "node scripts/bench/compare.mjs --fix",
|
|
126
|
+
"bench:quiet": "node scripts/bench/quiet.mjs",
|
|
121
127
|
"lint": "eslint .",
|
|
122
128
|
"scales:add": "node scripts/scales/add-scale.mjs",
|
|
123
129
|
"scales:validate": "node scripts/scales/validate-community-scales.mjs",
|
|
124
|
-
"test:pack-smoke": "node scripts/ci/pack-smoke.mjs",
|
|
125
|
-
"test:npm-smoke": "node scripts/ci/npm-registry-smoke.mjs --package stylelint-plugin-rhythmguard --version latest",
|
|
126
130
|
"test": "node --test test/*.test.js",
|
|
127
131
|
"test:compat-floor": "node --test test/floor-compat.test.js",
|
|
132
|
+
"test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js",
|
|
133
|
+
"test:npm-smoke": "node scripts/ci/npm-registry-smoke.mjs --package stylelint-plugin-rhythmguard --version latest",
|
|
134
|
+
"test:pack-smoke": "node scripts/ci/pack-smoke.mjs",
|
|
128
135
|
"test:watch": "node --test --watch test/*.test.js",
|
|
129
|
-
"
|
|
136
|
+
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
130
137
|
},
|
|
131
138
|
"repository": {
|
|
132
139
|
"type": "git",
|
|
133
|
-
"url": "https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard"
|
|
140
|
+
"url": "git+https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard.git"
|
|
134
141
|
},
|
|
135
142
|
"homepage": "https://github.com/petrilahdelma/stylelint-plugin-rhythmguard#readme",
|
|
136
143
|
"bugs": {
|
|
@@ -147,13 +154,15 @@
|
|
|
147
154
|
},
|
|
148
155
|
"devDependencies": {
|
|
149
156
|
"@eslint/js": "^9.22.0",
|
|
157
|
+
"@types/node": "^22.20.1",
|
|
150
158
|
"c8": "^10.1.3",
|
|
151
159
|
"eslint": "^9.22.0",
|
|
152
160
|
"geist": "^1.7.0",
|
|
153
161
|
"postcss-value-parser": "^4.2.0",
|
|
154
162
|
"stylelint": "^16.15.0",
|
|
163
|
+
"stylelint-plugin-logical-css": "^2.0.2",
|
|
155
164
|
"stylelint-scales": "^5.0.0",
|
|
156
|
-
"
|
|
165
|
+
"typescript": "^5.9.3"
|
|
157
166
|
},
|
|
158
167
|
"engines": {
|
|
159
168
|
"node": ">=18.18.0"
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
normalizeTokenKind,
|
|
5
|
+
normalizeTokenSourceFormat,
|
|
6
|
+
} = require('../utils/token-sources');
|
|
7
|
+
const {
|
|
8
|
+
VALID_FORMATS,
|
|
9
|
+
createDefaultAuditOptions,
|
|
10
|
+
normalizeIgnorePattern,
|
|
11
|
+
} = require('./shared');
|
|
12
|
+
|
|
13
|
+
const HELP = `Usage: rhythmguard audit <dir> [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--format <text|json|json-v1|markdown|html|github> Output format (default: text)
|
|
17
|
+
github = GitHub Actions workflow-command annotations
|
|
18
|
+
--json Alias for --format json
|
|
19
|
+
--markdown Alias for --format markdown
|
|
20
|
+
--schema Print the audit JSON schema and exit
|
|
21
|
+
--output <file> Write json, markdown, or html output to a file
|
|
22
|
+
--config <file> Load audit config (default: .rhythmguardrc.json when present)
|
|
23
|
+
--no-config Ignore .rhythmguardrc.json discovery
|
|
24
|
+
--ignore <pattern> Exclude root-relative path/glob (repeatable, comma-separated)
|
|
25
|
+
--ignore-path <file> Load ignore patterns from file (default: .rhythmguardignore when present)
|
|
26
|
+
--baseline <file> Baseline file path (default: .rhythmguard-baseline.json)
|
|
27
|
+
--write-baseline [file] Write current findings as a baseline
|
|
28
|
+
--since-baseline [file] Compare current findings against a baseline
|
|
29
|
+
--fail-on-new-drift Exit 1 when --since-baseline finds new drift
|
|
30
|
+
--max-findings <number> Exit 1 when total findings exceed this count
|
|
31
|
+
--min-cleanliness <percent> Exit 1 when scale cleanliness is lower than this percent
|
|
32
|
+
--since <git-ref> Scan only changed files since a git ref
|
|
33
|
+
--staged Scan only staged files
|
|
34
|
+
--include-motion Include opt-in motion duration/easing drift
|
|
35
|
+
--token-source <file> External token source (repeatable, comma-separated)
|
|
36
|
+
--token-source-format <format> Token source format: auto, css, flat-json, style-dictionary, dtcg (default: auto)
|
|
37
|
+
--token-kind <kind> Token kind: spacing, radius, typography, size, motion, all (default: spacing)
|
|
38
|
+
--token-candidate-min-count <n> Minimum repeated raw value count for token candidates (default: 2)
|
|
39
|
+
--scale <values|auto> Comma-separated scale values (default: 0,4,8,12,16,24,32);
|
|
40
|
+
auto infers the scale from token sources, then scanned CSS
|
|
41
|
+
--base-font-size <number> px base for rem/em conversion (default: 16)
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const parsed = createDefaultAuditOptions();
|
|
46
|
+
|
|
47
|
+
for (let index = 0; index < argv.length; index++) {
|
|
48
|
+
const arg = argv[index];
|
|
49
|
+
|
|
50
|
+
if (arg === '--help' || arg === '-h') {
|
|
51
|
+
parsed.help = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (arg === '--schema') {
|
|
56
|
+
parsed.schema = true;
|
|
57
|
+
parsed.format = 'json';
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (arg === '--json') {
|
|
62
|
+
parsed.format = 'json';
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (arg === '--markdown') {
|
|
67
|
+
parsed.format = 'markdown';
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (arg === '--output') {
|
|
72
|
+
parsed.outputPath = parsePathOption(argv[++index], '--output');
|
|
73
|
+
parsed.cliOptions.add('outputPath');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (arg.startsWith('--output=')) {
|
|
78
|
+
parsed.outputPath = parsePathOption(arg.slice('--output='.length), '--output');
|
|
79
|
+
parsed.cliOptions.add('outputPath');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (arg === '--ignore') {
|
|
84
|
+
parsed.ignorePatterns.push(...parseIgnorePatterns(argv[++index]));
|
|
85
|
+
parsed.cliOptions.add('ignore');
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (arg.startsWith('--ignore=')) {
|
|
90
|
+
parsed.ignorePatterns.push(...parseIgnorePatterns(arg.slice('--ignore='.length)));
|
|
91
|
+
parsed.cliOptions.add('ignore');
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (arg === '--config') {
|
|
96
|
+
parsed.configPath = parsePathOption(argv[++index], '--config');
|
|
97
|
+
parsed.configExplicit = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (arg.startsWith('--config=')) {
|
|
102
|
+
parsed.configPath = parsePathOption(arg.slice('--config='.length), '--config');
|
|
103
|
+
parsed.configExplicit = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arg === '--no-config') {
|
|
108
|
+
parsed.noConfig = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (arg === '--ignore-path') {
|
|
113
|
+
parsed.ignorePath = parsePathOption(argv[++index], '--ignore-path');
|
|
114
|
+
parsed.cliOptions.add('ignorePath');
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (arg.startsWith('--ignore-path=')) {
|
|
119
|
+
parsed.ignorePath = parsePathOption(arg.slice('--ignore-path='.length), '--ignore-path');
|
|
120
|
+
parsed.cliOptions.add('ignorePath');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (arg === '--baseline') {
|
|
125
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--baseline');
|
|
126
|
+
parsed.cliOptions.add('baselinePath');
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (arg.startsWith('--baseline=')) {
|
|
131
|
+
parsed.baselinePath = parsePathOption(arg.slice('--baseline='.length), '--baseline');
|
|
132
|
+
parsed.cliOptions.add('baselinePath');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (arg === '--write-baseline') {
|
|
137
|
+
parsed.writeBaseline = true;
|
|
138
|
+
parsed.cliOptions.add('writeBaseline');
|
|
139
|
+
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
140
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--write-baseline');
|
|
141
|
+
parsed.cliOptions.add('baselinePath');
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (arg.startsWith('--write-baseline=')) {
|
|
147
|
+
parsed.writeBaseline = true;
|
|
148
|
+
parsed.baselinePath = parsePathOption(arg.slice('--write-baseline='.length), '--write-baseline');
|
|
149
|
+
parsed.cliOptions.add('writeBaseline');
|
|
150
|
+
parsed.cliOptions.add('baselinePath');
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (arg === '--since-baseline') {
|
|
155
|
+
parsed.sinceBaseline = true;
|
|
156
|
+
parsed.cliOptions.add('sinceBaseline');
|
|
157
|
+
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
158
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--since-baseline');
|
|
159
|
+
parsed.cliOptions.add('baselinePath');
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (arg.startsWith('--since-baseline=')) {
|
|
165
|
+
parsed.sinceBaseline = true;
|
|
166
|
+
parsed.baselinePath = parsePathOption(arg.slice('--since-baseline='.length), '--since-baseline');
|
|
167
|
+
parsed.cliOptions.add('sinceBaseline');
|
|
168
|
+
parsed.cliOptions.add('baselinePath');
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (arg === '--fail-on-new-drift') {
|
|
173
|
+
parsed.failOnNewDrift = true;
|
|
174
|
+
parsed.cliOptions.add('failOnNewDrift');
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (arg === '--max-findings') {
|
|
179
|
+
parsed.maxFindings = parseNonNegativeInteger(argv[++index], '--max-findings');
|
|
180
|
+
parsed.cliOptions.add('maxFindings');
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (arg.startsWith('--max-findings=')) {
|
|
185
|
+
parsed.maxFindings = parseNonNegativeInteger(arg.slice('--max-findings='.length), '--max-findings');
|
|
186
|
+
parsed.cliOptions.add('maxFindings');
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (arg === '--min-cleanliness') {
|
|
191
|
+
parsed.minCleanliness = parsePercentage(argv[++index], '--min-cleanliness');
|
|
192
|
+
parsed.cliOptions.add('minCleanliness');
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (arg.startsWith('--min-cleanliness=')) {
|
|
197
|
+
parsed.minCleanliness = parsePercentage(
|
|
198
|
+
arg.slice('--min-cleanliness='.length),
|
|
199
|
+
'--min-cleanliness',
|
|
200
|
+
);
|
|
201
|
+
parsed.cliOptions.add('minCleanliness');
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (arg === '--since') {
|
|
206
|
+
parsed.since = parsePathOption(argv[++index], '--since');
|
|
207
|
+
parsed.cliOptions.add('since');
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (arg.startsWith('--since=')) {
|
|
212
|
+
parsed.since = parsePathOption(arg.slice('--since='.length), '--since');
|
|
213
|
+
parsed.cliOptions.add('since');
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (arg === '--staged') {
|
|
218
|
+
parsed.staged = true;
|
|
219
|
+
parsed.cliOptions.add('staged');
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (arg === '--include-motion') {
|
|
224
|
+
parsed.includeMotion = true;
|
|
225
|
+
parsed.cliOptions.add('includeMotion');
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (arg === '--token-source') {
|
|
230
|
+
parsed.tokenSources.push(...parseTokenSourcePaths(argv[++index]));
|
|
231
|
+
parsed.cliOptions.add('tokenSources');
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (arg.startsWith('--token-source=')) {
|
|
236
|
+
parsed.tokenSources.push(...parseTokenSourcePaths(arg.slice('--token-source='.length)));
|
|
237
|
+
parsed.cliOptions.add('tokenSources');
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (arg === '--token-source-format') {
|
|
242
|
+
parsed.tokenSourceFormat = normalizeTokenSourceFormat(argv[++index]);
|
|
243
|
+
parsed.cliOptions.add('tokenSourceFormat');
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (arg.startsWith('--token-source-format=')) {
|
|
248
|
+
parsed.tokenSourceFormat = normalizeTokenSourceFormat(arg.slice('--token-source-format='.length));
|
|
249
|
+
parsed.cliOptions.add('tokenSourceFormat');
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (arg === '--token-kind') {
|
|
254
|
+
parsed.tokenKind = normalizeTokenKind(argv[++index]);
|
|
255
|
+
parsed.cliOptions.add('tokenKind');
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (arg.startsWith('--token-kind=')) {
|
|
260
|
+
parsed.tokenKind = normalizeTokenKind(arg.slice('--token-kind='.length));
|
|
261
|
+
parsed.cliOptions.add('tokenKind');
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (arg === '--token-candidate-min-count') {
|
|
266
|
+
parsed.tokenCandidateMinCount = parsePositiveInteger(argv[++index], '--token-candidate-min-count');
|
|
267
|
+
parsed.cliOptions.add('tokenCandidateMinCount');
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (arg.startsWith('--token-candidate-min-count=')) {
|
|
272
|
+
parsed.tokenCandidateMinCount = parsePositiveInteger(
|
|
273
|
+
arg.slice('--token-candidate-min-count='.length),
|
|
274
|
+
'--token-candidate-min-count',
|
|
275
|
+
);
|
|
276
|
+
parsed.cliOptions.add('tokenCandidateMinCount');
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (arg === '--format') {
|
|
281
|
+
parsed.format = String(argv[++index] || '').toLowerCase();
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (arg.startsWith('--format=')) {
|
|
286
|
+
parsed.format = arg.slice('--format='.length).toLowerCase();
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (arg === '--scale') {
|
|
291
|
+
parsed.scale = parseScale(argv[++index]);
|
|
292
|
+
parsed.cliOptions.add('scale');
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (arg.startsWith('--scale=')) {
|
|
297
|
+
parsed.scale = parseScale(arg.slice('--scale='.length));
|
|
298
|
+
parsed.cliOptions.add('scale');
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (arg === '--base-font-size') {
|
|
303
|
+
parsed.baseFontSize = parseBaseFontSize(argv[++index]);
|
|
304
|
+
parsed.cliOptions.add('baseFontSize');
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (arg.startsWith('--base-font-size=')) {
|
|
309
|
+
parsed.baseFontSize = parseBaseFontSize(arg.slice('--base-font-size='.length));
|
|
310
|
+
parsed.cliOptions.add('baseFontSize');
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!arg.startsWith('-') && !parsed.dir) {
|
|
315
|
+
parsed.dir = arg;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!VALID_FORMATS.has(parsed.format)) {
|
|
323
|
+
throw new Error(`Invalid format "${parsed.format}". Expected text, json, json-v1, markdown, html, or github.`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (parsed.since && parsed.staged) {
|
|
327
|
+
throw new Error('Use either --since or --staged, not both.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (parsed.failOnNewDrift && !parsed.sinceBaseline) {
|
|
331
|
+
throw new Error('--fail-on-new-drift requires --since-baseline.');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return parsed;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parsePathOption(raw, optionName) {
|
|
338
|
+
if (!raw) {
|
|
339
|
+
throw new Error(`Missing value for ${optionName}.`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return String(raw);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseNonNegativeInteger(raw, optionName) {
|
|
346
|
+
const value = Number(raw);
|
|
347
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
348
|
+
throw new Error(`${optionName} must be a non-negative integer.`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function parsePositiveInteger(raw, optionName) {
|
|
355
|
+
const value = Number(raw);
|
|
356
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
357
|
+
throw new Error(`${optionName} must be a positive integer.`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parsePercentage(raw, optionName) {
|
|
364
|
+
const value = Number(raw);
|
|
365
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
366
|
+
throw new Error(`${optionName} must be a number between 0 and 100.`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function parseIgnorePatterns(raw) {
|
|
373
|
+
if (!raw) {
|
|
374
|
+
throw new Error('Missing value for --ignore.');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const patterns = String(raw).split(',')
|
|
378
|
+
.map((pattern) => normalizeIgnorePattern(pattern))
|
|
379
|
+
.filter(Boolean);
|
|
380
|
+
|
|
381
|
+
if (patterns.length === 0) {
|
|
382
|
+
throw new Error('--ignore must include at least one pattern.');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return patterns;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function parseTokenSourcePaths(raw) {
|
|
389
|
+
if (!raw) {
|
|
390
|
+
throw new Error('Missing value for --token-source.');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const sources = String(raw).split(',')
|
|
394
|
+
.map((sourcePath) => sourcePath.trim())
|
|
395
|
+
.filter(Boolean);
|
|
396
|
+
|
|
397
|
+
if (sources.length === 0) {
|
|
398
|
+
throw new Error('--token-source must include at least one file.');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return sources;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
function parseScale(raw) {
|
|
406
|
+
if (!raw) {
|
|
407
|
+
throw new Error('Missing value for --scale.');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (raw.trim().toLowerCase() === 'auto') {
|
|
411
|
+
return 'auto';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const scale = raw.split(',')
|
|
415
|
+
.map((part) => part.trim())
|
|
416
|
+
.filter(Boolean)
|
|
417
|
+
.map((part) => {
|
|
418
|
+
const numeric = Number(part);
|
|
419
|
+
return Number.isFinite(numeric) && !/[a-z%]/i.test(part) ? numeric : part;
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
if (scale.length === 0) {
|
|
423
|
+
throw new Error('Scale must include at least one value.');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return scale;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parseBaseFontSize(raw) {
|
|
430
|
+
const value = Number(raw);
|
|
431
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
432
|
+
throw new Error('--base-font-size must be a positive number.');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = {
|
|
439
|
+
HELP,
|
|
440
|
+
parseArgs,
|
|
441
|
+
parseBaseFontSize,
|
|
442
|
+
parseIgnorePatterns,
|
|
443
|
+
parseNonNegativeInteger,
|
|
444
|
+
parsePathOption,
|
|
445
|
+
parsePercentage,
|
|
446
|
+
parsePositiveInteger,
|
|
447
|
+
parseScale,
|
|
448
|
+
parseTokenSourcePaths,
|
|
449
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { formatPath } = require('./shared');
|
|
6
|
+
|
|
7
|
+
function applyBaselineComparison(report, baselinePath) {
|
|
8
|
+
const resolvedPath = path.resolve(process.cwd(), baselinePath);
|
|
9
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
10
|
+
throw new Error(`Baseline file not found: ${baselinePath}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const baseline = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
14
|
+
const baselineFindings = Array.isArray(baseline.findings) ? baseline.findings : [];
|
|
15
|
+
const baselineKeys = new Set(baselineFindings.map((finding) => finding.key || createFindingKey(finding)));
|
|
16
|
+
const currentFindings = getAllFindings(report);
|
|
17
|
+
const currentKeys = new Set(currentFindings.map(createFindingKey));
|
|
18
|
+
const newFindings = currentFindings.filter((finding) => !baselineKeys.has(createFindingKey(finding)));
|
|
19
|
+
const resolvedFindings = baselineFindings.filter((finding) => !currentKeys.has(finding.key || createFindingKey(finding)));
|
|
20
|
+
|
|
21
|
+
report.baseline = {
|
|
22
|
+
baselineFindings: baselineFindings.length,
|
|
23
|
+
file: formatPath(resolvedPath),
|
|
24
|
+
newFindings,
|
|
25
|
+
newFindingsCount: newFindings.length,
|
|
26
|
+
resolvedFindingsCount: resolvedFindings.length,
|
|
27
|
+
};
|
|
28
|
+
report.summary.newFindings = newFindings.length;
|
|
29
|
+
report.summary.resolvedFindings = resolvedFindings.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeBaseline(report, baselinePath) {
|
|
33
|
+
const resolvedPath = path.resolve(process.cwd(), baselinePath);
|
|
34
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
resolvedPath,
|
|
37
|
+
`${JSON.stringify({
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
directory: report.directory,
|
|
40
|
+
findings: getAllFindings(report).map(toBaselineFinding),
|
|
41
|
+
formatVersion: 1,
|
|
42
|
+
summary: {
|
|
43
|
+
scaleCleanliness: report.scaleCleanliness,
|
|
44
|
+
totalFindings: report.totalWarnings,
|
|
45
|
+
},
|
|
46
|
+
}, null, 2)}\n`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
report.baselineWritten = {
|
|
50
|
+
file: formatPath(resolvedPath),
|
|
51
|
+
findings: report.totalWarnings,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getAllFindings(report) {
|
|
56
|
+
return [
|
|
57
|
+
...report.findings.css,
|
|
58
|
+
...report.findings.motion,
|
|
59
|
+
...report.findings.tailwind,
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toBaselineFinding(finding) {
|
|
64
|
+
return {
|
|
65
|
+
column: finding.column,
|
|
66
|
+
file: finding.file,
|
|
67
|
+
key: createFindingKey(finding),
|
|
68
|
+
line: finding.line,
|
|
69
|
+
rule: finding.rule,
|
|
70
|
+
text: finding.text,
|
|
71
|
+
token: finding.token,
|
|
72
|
+
type: finding.type,
|
|
73
|
+
value: finding.value || finding.rawValue,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createFindingKey(finding) {
|
|
78
|
+
return [
|
|
79
|
+
finding.rule || '',
|
|
80
|
+
finding.type || '',
|
|
81
|
+
finding.file || '',
|
|
82
|
+
finding.line || '',
|
|
83
|
+
finding.column || '',
|
|
84
|
+
finding.value || finding.rawValue || finding.token || '',
|
|
85
|
+
finding.text || '',
|
|
86
|
+
].join('\u001f');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getAuditFailures(report, parsed) {
|
|
90
|
+
const failures = [];
|
|
91
|
+
|
|
92
|
+
if (parsed.maxFindings !== null && report.totalWarnings > parsed.maxFindings) {
|
|
93
|
+
failures.push(`total findings ${report.totalWarnings} exceeds --max-findings ${parsed.maxFindings}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (parsed.minCleanliness !== null && report.scaleCleanliness < parsed.minCleanliness) {
|
|
97
|
+
failures.push(`scale cleanliness ${report.scaleCleanliness}% is below --min-cleanliness ${parsed.minCleanliness}%`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (parsed.failOnNewDrift && report.baseline && report.baseline.newFindingsCount > 0) {
|
|
101
|
+
failures.push(`new drift found: ${report.baseline.newFindingsCount} finding(s) not present in baseline`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return failures;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
applyBaselineComparison,
|
|
109
|
+
createFindingKey,
|
|
110
|
+
getAllFindings,
|
|
111
|
+
getAuditFailures,
|
|
112
|
+
toBaselineFinding,
|
|
113
|
+
writeBaseline,
|
|
114
|
+
};
|