stylelint-plugin-rhythmguard 2.0.1 → 2.1.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 +21 -0
- package/CONTRIBUTING.md +23 -5
- package/README.md +39 -773
- package/package.json +9 -6
- package/src/audit/args.js +444 -0
- package/src/audit/baseline.js +114 -0
- package/src/audit/config.js +207 -0
- package/src/audit/contract.js +417 -0
- package/src/audit/render-github.js +39 -0
- package/src/audit/render-html.js +78 -0
- package/src/audit/render-markdown.js +216 -0
- package/src/audit/render-text.js +162 -0
- package/src/audit/render-utils.js +41 -0
- package/src/audit/report.js +115 -0
- package/src/audit/scan.js +546 -0
- package/src/audit/shared.js +108 -0
- package/src/cli/audit.js +22 -2213
- 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 +1 -0
- package/src/rules/no-offscale-transform/index.js +1 -1
- package/src/rules/prefer-token/index.js +1 -1
- package/src/rules/use-motion-scale/index.js +1 -1
- package/src/rules/use-scale/index.js +1 -1
- package/types/__checks__/consumer.ts +70 -0
- package/types/audit.d.ts +9 -3
- package/types/index.d.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.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"
|
|
@@ -121,16 +121,17 @@
|
|
|
121
121
|
"lint": "eslint .",
|
|
122
122
|
"scales:add": "node scripts/scales/add-scale.mjs",
|
|
123
123
|
"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
124
|
"test": "node --test test/*.test.js",
|
|
127
125
|
"test:compat-floor": "node --test test/floor-compat.test.js",
|
|
126
|
+
"test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js",
|
|
127
|
+
"test:npm-smoke": "node scripts/ci/npm-registry-smoke.mjs --package stylelint-plugin-rhythmguard --version latest",
|
|
128
|
+
"test:pack-smoke": "node scripts/ci/pack-smoke.mjs",
|
|
128
129
|
"test:watch": "node --test --watch test/*.test.js",
|
|
129
|
-
"
|
|
130
|
+
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
130
131
|
},
|
|
131
132
|
"repository": {
|
|
132
133
|
"type": "git",
|
|
133
|
-
"url": "https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard"
|
|
134
|
+
"url": "git+https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard.git"
|
|
134
135
|
},
|
|
135
136
|
"homepage": "https://github.com/petrilahdelma/stylelint-plugin-rhythmguard#readme",
|
|
136
137
|
"bugs": {
|
|
@@ -147,13 +148,15 @@
|
|
|
147
148
|
},
|
|
148
149
|
"devDependencies": {
|
|
149
150
|
"@eslint/js": "^9.22.0",
|
|
151
|
+
"@types/node": "^22.20.1",
|
|
150
152
|
"c8": "^10.1.3",
|
|
151
153
|
"eslint": "^9.22.0",
|
|
152
154
|
"geist": "^1.7.0",
|
|
153
155
|
"postcss-value-parser": "^4.2.0",
|
|
154
156
|
"stylelint": "^16.15.0",
|
|
157
|
+
"stylelint-plugin-logical-css": "^2.0.2",
|
|
155
158
|
"stylelint-scales": "^5.0.0",
|
|
156
|
-
"
|
|
159
|
+
"typescript": "^5.9.3"
|
|
157
160
|
},
|
|
158
161
|
"engines": {
|
|
159
162
|
"node": ">=18.18.0"
|
|
@@ -0,0 +1,444 @@
|
|
|
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> Comma-separated scale values (default: 0,4,8,12,16,24,32)
|
|
40
|
+
--base-font-size <number> px base for rem/em conversion (default: 16)
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const parsed = createDefaultAuditOptions();
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < argv.length; index++) {
|
|
47
|
+
const arg = argv[index];
|
|
48
|
+
|
|
49
|
+
if (arg === '--help' || arg === '-h') {
|
|
50
|
+
parsed.help = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (arg === '--schema') {
|
|
55
|
+
parsed.schema = true;
|
|
56
|
+
parsed.format = 'json';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (arg === '--json') {
|
|
61
|
+
parsed.format = 'json';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (arg === '--markdown') {
|
|
66
|
+
parsed.format = 'markdown';
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arg === '--output') {
|
|
71
|
+
parsed.outputPath = parsePathOption(argv[++index], '--output');
|
|
72
|
+
parsed.cliOptions.add('outputPath');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (arg.startsWith('--output=')) {
|
|
77
|
+
parsed.outputPath = parsePathOption(arg.slice('--output='.length), '--output');
|
|
78
|
+
parsed.cliOptions.add('outputPath');
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (arg === '--ignore') {
|
|
83
|
+
parsed.ignorePatterns.push(...parseIgnorePatterns(argv[++index]));
|
|
84
|
+
parsed.cliOptions.add('ignore');
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (arg.startsWith('--ignore=')) {
|
|
89
|
+
parsed.ignorePatterns.push(...parseIgnorePatterns(arg.slice('--ignore='.length)));
|
|
90
|
+
parsed.cliOptions.add('ignore');
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (arg === '--config') {
|
|
95
|
+
parsed.configPath = parsePathOption(argv[++index], '--config');
|
|
96
|
+
parsed.configExplicit = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (arg.startsWith('--config=')) {
|
|
101
|
+
parsed.configPath = parsePathOption(arg.slice('--config='.length), '--config');
|
|
102
|
+
parsed.configExplicit = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (arg === '--no-config') {
|
|
107
|
+
parsed.noConfig = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (arg === '--ignore-path') {
|
|
112
|
+
parsed.ignorePath = parsePathOption(argv[++index], '--ignore-path');
|
|
113
|
+
parsed.cliOptions.add('ignorePath');
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (arg.startsWith('--ignore-path=')) {
|
|
118
|
+
parsed.ignorePath = parsePathOption(arg.slice('--ignore-path='.length), '--ignore-path');
|
|
119
|
+
parsed.cliOptions.add('ignorePath');
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (arg === '--baseline') {
|
|
124
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--baseline');
|
|
125
|
+
parsed.cliOptions.add('baselinePath');
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (arg.startsWith('--baseline=')) {
|
|
130
|
+
parsed.baselinePath = parsePathOption(arg.slice('--baseline='.length), '--baseline');
|
|
131
|
+
parsed.cliOptions.add('baselinePath');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (arg === '--write-baseline') {
|
|
136
|
+
parsed.writeBaseline = true;
|
|
137
|
+
parsed.cliOptions.add('writeBaseline');
|
|
138
|
+
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
139
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--write-baseline');
|
|
140
|
+
parsed.cliOptions.add('baselinePath');
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (arg.startsWith('--write-baseline=')) {
|
|
146
|
+
parsed.writeBaseline = true;
|
|
147
|
+
parsed.baselinePath = parsePathOption(arg.slice('--write-baseline='.length), '--write-baseline');
|
|
148
|
+
parsed.cliOptions.add('writeBaseline');
|
|
149
|
+
parsed.cliOptions.add('baselinePath');
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (arg === '--since-baseline') {
|
|
154
|
+
parsed.sinceBaseline = true;
|
|
155
|
+
parsed.cliOptions.add('sinceBaseline');
|
|
156
|
+
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
157
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--since-baseline');
|
|
158
|
+
parsed.cliOptions.add('baselinePath');
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (arg.startsWith('--since-baseline=')) {
|
|
164
|
+
parsed.sinceBaseline = true;
|
|
165
|
+
parsed.baselinePath = parsePathOption(arg.slice('--since-baseline='.length), '--since-baseline');
|
|
166
|
+
parsed.cliOptions.add('sinceBaseline');
|
|
167
|
+
parsed.cliOptions.add('baselinePath');
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (arg === '--fail-on-new-drift') {
|
|
172
|
+
parsed.failOnNewDrift = true;
|
|
173
|
+
parsed.cliOptions.add('failOnNewDrift');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (arg === '--max-findings') {
|
|
178
|
+
parsed.maxFindings = parseNonNegativeInteger(argv[++index], '--max-findings');
|
|
179
|
+
parsed.cliOptions.add('maxFindings');
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (arg.startsWith('--max-findings=')) {
|
|
184
|
+
parsed.maxFindings = parseNonNegativeInteger(arg.slice('--max-findings='.length), '--max-findings');
|
|
185
|
+
parsed.cliOptions.add('maxFindings');
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (arg === '--min-cleanliness') {
|
|
190
|
+
parsed.minCleanliness = parsePercentage(argv[++index], '--min-cleanliness');
|
|
191
|
+
parsed.cliOptions.add('minCleanliness');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (arg.startsWith('--min-cleanliness=')) {
|
|
196
|
+
parsed.minCleanliness = parsePercentage(
|
|
197
|
+
arg.slice('--min-cleanliness='.length),
|
|
198
|
+
'--min-cleanliness',
|
|
199
|
+
);
|
|
200
|
+
parsed.cliOptions.add('minCleanliness');
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (arg === '--since') {
|
|
205
|
+
parsed.since = parsePathOption(argv[++index], '--since');
|
|
206
|
+
parsed.cliOptions.add('since');
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (arg.startsWith('--since=')) {
|
|
211
|
+
parsed.since = parsePathOption(arg.slice('--since='.length), '--since');
|
|
212
|
+
parsed.cliOptions.add('since');
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (arg === '--staged') {
|
|
217
|
+
parsed.staged = true;
|
|
218
|
+
parsed.cliOptions.add('staged');
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (arg === '--include-motion') {
|
|
223
|
+
parsed.includeMotion = true;
|
|
224
|
+
parsed.cliOptions.add('includeMotion');
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (arg === '--token-source') {
|
|
229
|
+
parsed.tokenSources.push(...parseTokenSourcePaths(argv[++index]));
|
|
230
|
+
parsed.cliOptions.add('tokenSources');
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (arg.startsWith('--token-source=')) {
|
|
235
|
+
parsed.tokenSources.push(...parseTokenSourcePaths(arg.slice('--token-source='.length)));
|
|
236
|
+
parsed.cliOptions.add('tokenSources');
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (arg === '--token-source-format') {
|
|
241
|
+
parsed.tokenSourceFormat = normalizeTokenSourceFormat(argv[++index]);
|
|
242
|
+
parsed.cliOptions.add('tokenSourceFormat');
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (arg.startsWith('--token-source-format=')) {
|
|
247
|
+
parsed.tokenSourceFormat = normalizeTokenSourceFormat(arg.slice('--token-source-format='.length));
|
|
248
|
+
parsed.cliOptions.add('tokenSourceFormat');
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (arg === '--token-kind') {
|
|
253
|
+
parsed.tokenKind = normalizeTokenKind(argv[++index]);
|
|
254
|
+
parsed.cliOptions.add('tokenKind');
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (arg.startsWith('--token-kind=')) {
|
|
259
|
+
parsed.tokenKind = normalizeTokenKind(arg.slice('--token-kind='.length));
|
|
260
|
+
parsed.cliOptions.add('tokenKind');
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (arg === '--token-candidate-min-count') {
|
|
265
|
+
parsed.tokenCandidateMinCount = parsePositiveInteger(argv[++index], '--token-candidate-min-count');
|
|
266
|
+
parsed.cliOptions.add('tokenCandidateMinCount');
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (arg.startsWith('--token-candidate-min-count=')) {
|
|
271
|
+
parsed.tokenCandidateMinCount = parsePositiveInteger(
|
|
272
|
+
arg.slice('--token-candidate-min-count='.length),
|
|
273
|
+
'--token-candidate-min-count',
|
|
274
|
+
);
|
|
275
|
+
parsed.cliOptions.add('tokenCandidateMinCount');
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (arg === '--format') {
|
|
280
|
+
parsed.format = String(argv[++index] || '').toLowerCase();
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (arg.startsWith('--format=')) {
|
|
285
|
+
parsed.format = arg.slice('--format='.length).toLowerCase();
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (arg === '--scale') {
|
|
290
|
+
parsed.scale = parseScale(argv[++index]);
|
|
291
|
+
parsed.cliOptions.add('scale');
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (arg.startsWith('--scale=')) {
|
|
296
|
+
parsed.scale = parseScale(arg.slice('--scale='.length));
|
|
297
|
+
parsed.cliOptions.add('scale');
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (arg === '--base-font-size') {
|
|
302
|
+
parsed.baseFontSize = parseBaseFontSize(argv[++index]);
|
|
303
|
+
parsed.cliOptions.add('baseFontSize');
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (arg.startsWith('--base-font-size=')) {
|
|
308
|
+
parsed.baseFontSize = parseBaseFontSize(arg.slice('--base-font-size='.length));
|
|
309
|
+
parsed.cliOptions.add('baseFontSize');
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!arg.startsWith('-') && !parsed.dir) {
|
|
314
|
+
parsed.dir = arg;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!VALID_FORMATS.has(parsed.format)) {
|
|
322
|
+
throw new Error(`Invalid format "${parsed.format}". Expected text, json, json-v1, markdown, html, or github.`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (parsed.since && parsed.staged) {
|
|
326
|
+
throw new Error('Use either --since or --staged, not both.');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (parsed.failOnNewDrift && !parsed.sinceBaseline) {
|
|
330
|
+
throw new Error('--fail-on-new-drift requires --since-baseline.');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return parsed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function parsePathOption(raw, optionName) {
|
|
337
|
+
if (!raw) {
|
|
338
|
+
throw new Error(`Missing value for ${optionName}.`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return String(raw);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function parseNonNegativeInteger(raw, optionName) {
|
|
345
|
+
const value = Number(raw);
|
|
346
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
347
|
+
throw new Error(`${optionName} must be a non-negative integer.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parsePositiveInteger(raw, optionName) {
|
|
354
|
+
const value = Number(raw);
|
|
355
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
356
|
+
throw new Error(`${optionName} must be a positive integer.`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function parsePercentage(raw, optionName) {
|
|
363
|
+
const value = Number(raw);
|
|
364
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
365
|
+
throw new Error(`${optionName} must be a number between 0 and 100.`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function parseIgnorePatterns(raw) {
|
|
372
|
+
if (!raw) {
|
|
373
|
+
throw new Error('Missing value for --ignore.');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const patterns = String(raw).split(',')
|
|
377
|
+
.map((pattern) => normalizeIgnorePattern(pattern))
|
|
378
|
+
.filter(Boolean);
|
|
379
|
+
|
|
380
|
+
if (patterns.length === 0) {
|
|
381
|
+
throw new Error('--ignore must include at least one pattern.');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return patterns;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function parseTokenSourcePaths(raw) {
|
|
388
|
+
if (!raw) {
|
|
389
|
+
throw new Error('Missing value for --token-source.');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const sources = String(raw).split(',')
|
|
393
|
+
.map((sourcePath) => sourcePath.trim())
|
|
394
|
+
.filter(Boolean);
|
|
395
|
+
|
|
396
|
+
if (sources.length === 0) {
|
|
397
|
+
throw new Error('--token-source must include at least one file.');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return sources;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
function parseScale(raw) {
|
|
405
|
+
if (!raw) {
|
|
406
|
+
throw new Error('Missing value for --scale.');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const scale = raw.split(',')
|
|
410
|
+
.map((part) => part.trim())
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.map((part) => {
|
|
413
|
+
const numeric = Number(part);
|
|
414
|
+
return Number.isFinite(numeric) && !/[a-z%]/i.test(part) ? numeric : part;
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (scale.length === 0) {
|
|
418
|
+
throw new Error('Scale must include at least one value.');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return scale;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function parseBaseFontSize(raw) {
|
|
425
|
+
const value = Number(raw);
|
|
426
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
427
|
+
throw new Error('--base-font-size must be a positive number.');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = {
|
|
434
|
+
HELP,
|
|
435
|
+
parseArgs,
|
|
436
|
+
parseBaseFontSize,
|
|
437
|
+
parseIgnorePatterns,
|
|
438
|
+
parseNonNegativeInteger,
|
|
439
|
+
parsePathOption,
|
|
440
|
+
parsePercentage,
|
|
441
|
+
parsePositiveInteger,
|
|
442
|
+
parseScale,
|
|
443
|
+
parseTokenSourcePaths,
|
|
444
|
+
};
|
|
@@ -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
|
+
};
|