stylelint-plugin-rhythmguard 1.6.1 → 1.7.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 +10 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/cli/audit.js +711 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.7.0] - 2026-05-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added audit baseline workflows with `--write-baseline`, `--since-baseline`, and `--fail-on-new-drift` for legacy-safe CI adoption.
|
|
14
|
+
- Added changed-only audit scopes with `--staged` and `--since <git-ref>`.
|
|
15
|
+
- Added CI threshold gates with `--max-findings` and `--min-cleanliness`.
|
|
16
|
+
- Added `.rhythmguardignore` / `--ignore-path` support for reusable root-relative scan pruning.
|
|
17
|
+
- Added token contract reporting for spacing tokens used but missing, tokens defined but unused, and repeated raw value candidates.
|
|
18
|
+
|
|
9
19
|
## [1.6.1] - 2026-05-23
|
|
10
20
|
|
|
11
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -90,9 +90,12 @@ npx rhythmguard audit ./src
|
|
|
90
90
|
npx rhythmguard audit ./src --format markdown
|
|
91
91
|
npx rhythmguard audit ./src --json
|
|
92
92
|
npx rhythmguard audit . --ignore "apps/legacy/**" --ignore "vendor/**"
|
|
93
|
+
npx rhythmguard audit ./src --write-baseline
|
|
94
|
+
npx rhythmguard audit ./src --since-baseline --fail-on-new-drift
|
|
95
|
+
npx rhythmguard audit ./src --staged --max-findings 0
|
|
93
96
|
```
|
|
94
97
|
|
|
95
|
-
The report covers authored CSS declarations
|
|
98
|
+
The report covers authored CSS declarations, Tailwind arbitrary spacing values in common template/source files, and token-contract drift such as missing spacing tokens, unused spacing tokens, and repeated raw values that deserve token review. Scan paths are scoped to the directory argument. Use `--ignore`, `.rhythmguardignore`, or `--ignore-path` for generated or legacy subtrees, then add baselines and CI thresholds when you are ready to gate new drift. Markdown output is PR-ready for UX developers, UX designers, and design-system owners:
|
|
96
99
|
|
|
97
100
|
```md
|
|
98
101
|
# Rhythmguard Design-System Audit
|
|
@@ -104,6 +107,7 @@ The report covers authored CSS declarations and Tailwind arbitrary spacing value
|
|
|
104
107
|
| Files with issues | 12 |
|
|
105
108
|
| Total findings | 52 |
|
|
106
109
|
| Scale cleanliness | 91% |
|
|
110
|
+
| New findings | 3 |
|
|
107
111
|
```
|
|
108
112
|
|
|
109
113
|
## Installation
|
package/package.json
CHANGED
package/src/cli/audit.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
3
4
|
const fs = require('node:fs');
|
|
4
5
|
const path = require('node:path');
|
|
5
6
|
|
|
@@ -11,6 +12,10 @@ const pluginPath = path.resolve(__dirname, '..', 'index.js');
|
|
|
11
12
|
|
|
12
13
|
const DEFAULT_SCALE = [0, 4, 8, 12, 16, 24, 32];
|
|
13
14
|
const DEFAULT_BASE_FONT_SIZE = 16;
|
|
15
|
+
const DEFAULT_BASELINE_PATH = '.rhythmguard-baseline.json';
|
|
16
|
+
const DEFAULT_IGNORE_PATH = '.rhythmguardignore';
|
|
17
|
+
const DEFAULT_TOKEN_PATTERN = '^--spac(e|ing)-';
|
|
18
|
+
const DEFAULT_TOKEN_CANDIDATE_MIN_COUNT = 2;
|
|
14
19
|
const VALID_FORMATS = new Set(['text', 'json', 'markdown']);
|
|
15
20
|
const SKIP_DIRS = new Set([
|
|
16
21
|
'.git',
|
|
@@ -45,17 +50,37 @@ Options:
|
|
|
45
50
|
--json Alias for --format json
|
|
46
51
|
--markdown Alias for --format markdown
|
|
47
52
|
--ignore <pattern> Exclude root-relative path/glob (repeatable, comma-separated)
|
|
53
|
+
--ignore-path <file> Load ignore patterns from file (default: .rhythmguardignore when present)
|
|
54
|
+
--baseline <file> Baseline file path (default: .rhythmguard-baseline.json)
|
|
55
|
+
--write-baseline [file] Write current findings as a baseline
|
|
56
|
+
--since-baseline [file] Compare current findings against a baseline
|
|
57
|
+
--fail-on-new-drift Exit 1 when --since-baseline finds new drift
|
|
58
|
+
--max-findings <number> Exit 1 when total findings exceed this count
|
|
59
|
+
--min-cleanliness <percent> Exit 1 when scale cleanliness is lower than this percent
|
|
60
|
+
--since <git-ref> Scan only changed files since a git ref
|
|
61
|
+
--staged Scan only staged files
|
|
62
|
+
--token-candidate-min-count <n> Minimum repeated raw value count for token candidates (default: 2)
|
|
48
63
|
--scale <values> Comma-separated scale values (default: 0,4,8,12,16,24,32)
|
|
49
64
|
--base-font-size <number> px base for rem/em conversion (default: 16)
|
|
50
65
|
`;
|
|
51
66
|
|
|
52
67
|
function parseArgs(argv) {
|
|
53
68
|
const parsed = {
|
|
69
|
+
baselinePath: DEFAULT_BASELINE_PATH,
|
|
54
70
|
baseFontSize: DEFAULT_BASE_FONT_SIZE,
|
|
55
71
|
dir: null,
|
|
72
|
+
failOnNewDrift: false,
|
|
56
73
|
format: 'text',
|
|
74
|
+
ignorePath: DEFAULT_IGNORE_PATH,
|
|
57
75
|
ignorePatterns: [],
|
|
76
|
+
maxFindings: null,
|
|
77
|
+
minCleanliness: null,
|
|
58
78
|
scale: DEFAULT_SCALE,
|
|
79
|
+
since: null,
|
|
80
|
+
sinceBaseline: false,
|
|
81
|
+
staged: false,
|
|
82
|
+
tokenCandidateMinCount: DEFAULT_TOKEN_CANDIDATE_MIN_COUNT,
|
|
83
|
+
writeBaseline: false,
|
|
59
84
|
};
|
|
60
85
|
|
|
61
86
|
for (let index = 0; index < argv.length; index++) {
|
|
@@ -86,6 +111,110 @@ function parseArgs(argv) {
|
|
|
86
111
|
continue;
|
|
87
112
|
}
|
|
88
113
|
|
|
114
|
+
if (arg === '--ignore-path') {
|
|
115
|
+
parsed.ignorePath = parsePathOption(argv[++index], '--ignore-path');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (arg.startsWith('--ignore-path=')) {
|
|
120
|
+
parsed.ignorePath = parsePathOption(arg.slice('--ignore-path='.length), '--ignore-path');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (arg === '--baseline') {
|
|
125
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--baseline');
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (arg.startsWith('--baseline=')) {
|
|
130
|
+
parsed.baselinePath = parsePathOption(arg.slice('--baseline='.length), '--baseline');
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (arg === '--write-baseline') {
|
|
135
|
+
parsed.writeBaseline = true;
|
|
136
|
+
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
137
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--write-baseline');
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (arg.startsWith('--write-baseline=')) {
|
|
143
|
+
parsed.writeBaseline = true;
|
|
144
|
+
parsed.baselinePath = parsePathOption(arg.slice('--write-baseline='.length), '--write-baseline');
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (arg === '--since-baseline') {
|
|
149
|
+
parsed.sinceBaseline = true;
|
|
150
|
+
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
151
|
+
parsed.baselinePath = parsePathOption(argv[++index], '--since-baseline');
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (arg.startsWith('--since-baseline=')) {
|
|
157
|
+
parsed.sinceBaseline = true;
|
|
158
|
+
parsed.baselinePath = parsePathOption(arg.slice('--since-baseline='.length), '--since-baseline');
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (arg === '--fail-on-new-drift') {
|
|
163
|
+
parsed.failOnNewDrift = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (arg === '--max-findings') {
|
|
168
|
+
parsed.maxFindings = parseNonNegativeInteger(argv[++index], '--max-findings');
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (arg.startsWith('--max-findings=')) {
|
|
173
|
+
parsed.maxFindings = parseNonNegativeInteger(arg.slice('--max-findings='.length), '--max-findings');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (arg === '--min-cleanliness') {
|
|
178
|
+
parsed.minCleanliness = parsePercentage(argv[++index], '--min-cleanliness');
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (arg.startsWith('--min-cleanliness=')) {
|
|
183
|
+
parsed.minCleanliness = parsePercentage(
|
|
184
|
+
arg.slice('--min-cleanliness='.length),
|
|
185
|
+
'--min-cleanliness',
|
|
186
|
+
);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (arg === '--since') {
|
|
191
|
+
parsed.since = parsePathOption(argv[++index], '--since');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (arg.startsWith('--since=')) {
|
|
196
|
+
parsed.since = parsePathOption(arg.slice('--since='.length), '--since');
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (arg === '--staged') {
|
|
201
|
+
parsed.staged = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (arg === '--token-candidate-min-count') {
|
|
206
|
+
parsed.tokenCandidateMinCount = parsePositiveInteger(argv[++index], '--token-candidate-min-count');
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (arg.startsWith('--token-candidate-min-count=')) {
|
|
211
|
+
parsed.tokenCandidateMinCount = parsePositiveInteger(
|
|
212
|
+
arg.slice('--token-candidate-min-count='.length),
|
|
213
|
+
'--token-candidate-min-count',
|
|
214
|
+
);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
89
218
|
if (arg === '--format') {
|
|
90
219
|
parsed.format = String(argv[++index] || '').toLowerCase();
|
|
91
220
|
continue;
|
|
@@ -128,9 +257,52 @@ function parseArgs(argv) {
|
|
|
128
257
|
throw new Error(`Invalid format "${parsed.format}". Expected text, json, or markdown.`);
|
|
129
258
|
}
|
|
130
259
|
|
|
260
|
+
if (parsed.since && parsed.staged) {
|
|
261
|
+
throw new Error('Use either --since or --staged, not both.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (parsed.failOnNewDrift && !parsed.sinceBaseline) {
|
|
265
|
+
throw new Error('--fail-on-new-drift requires --since-baseline.');
|
|
266
|
+
}
|
|
267
|
+
|
|
131
268
|
return parsed;
|
|
132
269
|
}
|
|
133
270
|
|
|
271
|
+
function parsePathOption(raw, optionName) {
|
|
272
|
+
if (!raw) {
|
|
273
|
+
throw new Error(`Missing value for ${optionName}.`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return String(raw);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseNonNegativeInteger(raw, optionName) {
|
|
280
|
+
const value = Number(raw);
|
|
281
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
282
|
+
throw new Error(`${optionName} must be a non-negative integer.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parsePositiveInteger(raw, optionName) {
|
|
289
|
+
const value = Number(raw);
|
|
290
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
291
|
+
throw new Error(`${optionName} must be a positive integer.`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parsePercentage(raw, optionName) {
|
|
298
|
+
const value = Number(raw);
|
|
299
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
300
|
+
throw new Error(`${optionName} must be a number between 0 and 100.`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
|
|
134
306
|
function parseIgnorePatterns(raw) {
|
|
135
307
|
if (!raw) {
|
|
136
308
|
throw new Error('Missing value for --ignore.');
|
|
@@ -205,6 +377,27 @@ function assertDirectory(dir) {
|
|
|
205
377
|
return resolvedDir;
|
|
206
378
|
}
|
|
207
379
|
|
|
380
|
+
function loadIgnorePatterns(ignorePath) {
|
|
381
|
+
if (!ignorePath) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const resolvedPath = path.resolve(process.cwd(), ignorePath);
|
|
386
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
387
|
+
if (ignorePath === DEFAULT_IGNORE_PATH) {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
throw new Error(`Ignore file not found: ${ignorePath}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return fs.readFileSync(resolvedPath, 'utf8')
|
|
394
|
+
.split(/\r?\n/)
|
|
395
|
+
.map((line) => line.trim())
|
|
396
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
397
|
+
.map((line) => normalizeIgnorePattern(line))
|
|
398
|
+
.filter(Boolean);
|
|
399
|
+
}
|
|
400
|
+
|
|
208
401
|
function walkFiles(rootDir, ignorePatterns = []) {
|
|
209
402
|
const cssFiles = [];
|
|
210
403
|
const templateFiles = [];
|
|
@@ -238,11 +431,96 @@ function walkFiles(rootDir, ignorePatterns = []) {
|
|
|
238
431
|
return { cssFiles, templateFiles };
|
|
239
432
|
}
|
|
240
433
|
|
|
434
|
+
function getScanFiles(rootDir, ignorePatterns, parsed) {
|
|
435
|
+
if (parsed.staged || parsed.since) {
|
|
436
|
+
return getGitChangedScanFiles(rootDir, ignorePatterns, parsed);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
...walkFiles(rootDir, ignorePatterns),
|
|
441
|
+
scanScope: {
|
|
442
|
+
mode: 'full',
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function getGitChangedScanFiles(rootDir, ignorePatterns, parsed) {
|
|
448
|
+
const args = parsed.staged
|
|
449
|
+
? ['diff', '--name-only', '--cached', '--diff-filter=ACMR', '--']
|
|
450
|
+
: ['diff', '--name-only', '--diff-filter=ACMR', parsed.since, '--'];
|
|
451
|
+
let output = '';
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
output = execFileSync('git', args, {
|
|
455
|
+
cwd: process.cwd(),
|
|
456
|
+
encoding: 'utf8',
|
|
457
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
458
|
+
});
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const stderr = err.stderr ? String(err.stderr).trim() : err.message;
|
|
461
|
+
throw new Error(`Unable to read changed files from git: ${stderr}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const cssFiles = [];
|
|
465
|
+
const templateFiles = [];
|
|
466
|
+
const ignoreMatchers = createIgnoreMatchers(ignorePatterns);
|
|
467
|
+
const seen = new Set();
|
|
468
|
+
const changedFiles = output.split(/\r?\n/)
|
|
469
|
+
.map((filePath) => filePath.trim())
|
|
470
|
+
.filter(Boolean);
|
|
471
|
+
|
|
472
|
+
for (const filePath of changedFiles) {
|
|
473
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
474
|
+
|
|
475
|
+
if (!isPathInside(rootDir, fullPath) || seen.has(fullPath) || !fs.existsSync(fullPath)) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const stat = fs.statSync(fullPath);
|
|
480
|
+
if (!stat.isFile()) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const relativePath = toPosixRelativePath(rootDir, fullPath);
|
|
485
|
+
if (shouldIgnoreRelativeFile(relativePath, ignoreMatchers)) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
seen.add(fullPath);
|
|
490
|
+
if (isCssFile(fullPath)) {
|
|
491
|
+
cssFiles.push(fullPath);
|
|
492
|
+
} else if (isTemplateFile(fullPath)) {
|
|
493
|
+
templateFiles.push(fullPath);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
cssFiles,
|
|
499
|
+
scanScope: {
|
|
500
|
+
changedFiles: changedFiles.length,
|
|
501
|
+
mode: parsed.staged ? 'staged' : 'since',
|
|
502
|
+
since: parsed.since,
|
|
503
|
+
},
|
|
504
|
+
templateFiles,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function isPathInside(rootDir, filePath) {
|
|
509
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
510
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
511
|
+
}
|
|
512
|
+
|
|
241
513
|
function shouldIgnorePath(relativePath, entry, ignoreMatchers) {
|
|
242
514
|
return ignoreMatchers.some((matcher) => matcher.test(relativePath))
|
|
243
515
|
|| (entry.isDirectory() && SKIP_DIRS.has(entry.name));
|
|
244
516
|
}
|
|
245
517
|
|
|
518
|
+
function shouldIgnoreRelativeFile(relativePath, ignoreMatchers) {
|
|
519
|
+
const segments = relativePath.split('/');
|
|
520
|
+
return segments.some((segment) => SKIP_DIRS.has(segment))
|
|
521
|
+
|| ignoreMatchers.some((matcher) => matcher.test(relativePath));
|
|
522
|
+
}
|
|
523
|
+
|
|
246
524
|
function createIgnoreMatchers(patterns) {
|
|
247
525
|
const variants = new Set();
|
|
248
526
|
|
|
@@ -483,12 +761,151 @@ function offsetToLineColumn(lineStarts, offset) {
|
|
|
483
761
|
};
|
|
484
762
|
}
|
|
485
763
|
|
|
764
|
+
function collectTokenContract(cssFiles, cssFindings, tailwindFindings, minCandidateCount) {
|
|
765
|
+
const definitions = new Map();
|
|
766
|
+
const uses = new Map();
|
|
767
|
+
const rawValues = new Map();
|
|
768
|
+
const rawValueLocations = new Set();
|
|
769
|
+
|
|
770
|
+
for (const filePath of cssFiles) {
|
|
771
|
+
let source = '';
|
|
772
|
+
try {
|
|
773
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
774
|
+
} catch {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const file = formatPath(filePath);
|
|
779
|
+
collectTokenDefinitions(source, file, definitions);
|
|
780
|
+
collectTokenUses(source, file, uses);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
for (const finding of cssFindings) {
|
|
784
|
+
addRawValue(rawValues, rawValueLocations, finding.value, finding);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
for (const finding of tailwindFindings) {
|
|
788
|
+
addRawValue(rawValues, rawValueLocations, finding.rawValue, finding);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const definedTokens = mapTokenEntries(definitions);
|
|
792
|
+
const usedTokens = mapTokenEntries(uses);
|
|
793
|
+
const missingTokens = usedTokens.filter(({ token }) => !definitions.has(token));
|
|
794
|
+
const unusedTokens = definedTokens.filter(({ token }) => !uses.has(token));
|
|
795
|
+
const rawValueCandidates = Array.from(rawValues.entries())
|
|
796
|
+
.map(([value, entry]) => ({
|
|
797
|
+
count: entry.count,
|
|
798
|
+
files: Array.from(entry.files).sort(),
|
|
799
|
+
value,
|
|
800
|
+
}))
|
|
801
|
+
.filter(({ count }) => count >= minCandidateCount)
|
|
802
|
+
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
definedTokens,
|
|
806
|
+
missingTokens,
|
|
807
|
+
rawValueCandidates,
|
|
808
|
+
summary: {
|
|
809
|
+
definedTokens: definedTokens.length,
|
|
810
|
+
missingTokens: missingTokens.length,
|
|
811
|
+
rawValueCandidates: rawValueCandidates.length,
|
|
812
|
+
unusedTokens: unusedTokens.length,
|
|
813
|
+
usedTokens: usedTokens.length,
|
|
814
|
+
},
|
|
815
|
+
unusedTokens,
|
|
816
|
+
usedTokens,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function collectTokenDefinitions(source, file, definitions) {
|
|
821
|
+
const declarationPattern = /(--[\w-]+)\s*:\s*([^;{}]+)/g;
|
|
822
|
+
let match;
|
|
823
|
+
|
|
824
|
+
while ((match = declarationPattern.exec(source)) !== null) {
|
|
825
|
+
const token = match[1];
|
|
826
|
+
if (!isSpacingToken(token)) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const entry = definitions.get(token) || {
|
|
831
|
+
files: new Set(),
|
|
832
|
+
token,
|
|
833
|
+
values: new Set(),
|
|
834
|
+
};
|
|
835
|
+
entry.files.add(file);
|
|
836
|
+
entry.values.add(match[2].trim());
|
|
837
|
+
definitions.set(token, entry);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function collectTokenUses(source, file, uses) {
|
|
842
|
+
const varPattern = /var\(\s*(--[\w-]+)/g;
|
|
843
|
+
let match;
|
|
844
|
+
|
|
845
|
+
while ((match = varPattern.exec(source)) !== null) {
|
|
846
|
+
const token = match[1];
|
|
847
|
+
if (!isSpacingToken(token)) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const entry = uses.get(token) || {
|
|
852
|
+
files: new Set(),
|
|
853
|
+
token,
|
|
854
|
+
};
|
|
855
|
+
entry.files.add(file);
|
|
856
|
+
uses.set(token, entry);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function isSpacingToken(token) {
|
|
861
|
+
return new RegExp(DEFAULT_TOKEN_PATTERN).test(token);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function addRawValue(rawValues, rawValueLocations, value, finding) {
|
|
865
|
+
if (!value) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const locationKey = [
|
|
870
|
+
finding.file || '',
|
|
871
|
+
finding.line || '',
|
|
872
|
+
finding.column || '',
|
|
873
|
+
value,
|
|
874
|
+
].join('\u001f');
|
|
875
|
+
if (rawValueLocations.has(locationKey)) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
rawValueLocations.add(locationKey);
|
|
879
|
+
|
|
880
|
+
const entry = rawValues.get(value) || {
|
|
881
|
+
count: 0,
|
|
882
|
+
files: new Set(),
|
|
883
|
+
};
|
|
884
|
+
entry.count += 1;
|
|
885
|
+
if (finding.file) {
|
|
886
|
+
entry.files.add(finding.file);
|
|
887
|
+
}
|
|
888
|
+
rawValues.set(value, entry);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function mapTokenEntries(entries) {
|
|
892
|
+
return Array.from(entries.values())
|
|
893
|
+
.map((entry) => ({
|
|
894
|
+
files: Array.from(entry.files).sort(),
|
|
895
|
+
token: entry.token,
|
|
896
|
+
values: entry.values ? Array.from(entry.values).sort() : undefined,
|
|
897
|
+
}))
|
|
898
|
+
.sort((a, b) => a.token.localeCompare(b.token));
|
|
899
|
+
}
|
|
900
|
+
|
|
486
901
|
function buildReport({
|
|
487
902
|
cssFiles,
|
|
488
903
|
cssFindings,
|
|
489
904
|
dir,
|
|
905
|
+
scanScope,
|
|
490
906
|
templateFiles,
|
|
491
907
|
tailwindFindings,
|
|
908
|
+
tokenCandidateMinCount,
|
|
492
909
|
}) {
|
|
493
910
|
const offScaleValues = countByValue(cssFindings
|
|
494
911
|
.filter((finding) => finding.type === 'off-scale' && finding.value)
|
|
@@ -513,6 +930,12 @@ function buildReport({
|
|
|
513
930
|
const scaleCleanliness = totalFiles > 0
|
|
514
931
|
? Math.max(0, Math.round(((totalFiles - filesWithIssues) / totalFiles) * 100))
|
|
515
932
|
: 100;
|
|
933
|
+
const tokenContract = collectTokenContract(
|
|
934
|
+
cssFiles,
|
|
935
|
+
cssFindings,
|
|
936
|
+
tailwindFindings,
|
|
937
|
+
tokenCandidateMinCount,
|
|
938
|
+
);
|
|
516
939
|
|
|
517
940
|
return {
|
|
518
941
|
cssFilesScanned: cssFiles.length,
|
|
@@ -522,9 +945,10 @@ function buildReport({
|
|
|
522
945
|
css: cssFindings,
|
|
523
946
|
tailwind: tailwindFindings,
|
|
524
947
|
},
|
|
525
|
-
formatVersion:
|
|
948
|
+
formatVersion: 3,
|
|
526
949
|
offScaleValues: Object.fromEntries(sortCountMap(offScaleValues).slice(0, 10)),
|
|
527
950
|
scaleCleanliness,
|
|
951
|
+
scanScope,
|
|
528
952
|
scanned: {
|
|
529
953
|
cssFiles: cssFiles.length,
|
|
530
954
|
templateFiles: templateFiles.length,
|
|
@@ -533,13 +957,17 @@ function buildReport({
|
|
|
533
957
|
summary: {
|
|
534
958
|
cssWarnings: cssFindings.length,
|
|
535
959
|
filesWithIssues,
|
|
960
|
+
missingTokens: tokenContract.summary.missingTokens,
|
|
961
|
+
rawValueCandidates: tokenContract.summary.rawValueCandidates,
|
|
536
962
|
scaleCleanliness,
|
|
537
963
|
tailwindArbitrarySpacing: tailwindFindings.length,
|
|
538
964
|
tokenOpportunities: sumCounts(tokenOpportunities),
|
|
539
965
|
totalFindings: totalWarnings,
|
|
966
|
+
unusedTokens: tokenContract.summary.unusedTokens,
|
|
540
967
|
},
|
|
541
968
|
tailwindArbitraryValues: Object.fromEntries(sortCountMap(tailwindArbitraryValues).slice(0, 10)),
|
|
542
969
|
templateFilesScanned: templateFiles.length,
|
|
970
|
+
tokenContract,
|
|
543
971
|
tokenOpportunities: Object.fromEntries(sortCountMap(tokenOpportunities).slice(0, 10)),
|
|
544
972
|
topAffectedFiles: topAffectedFiles.map(([file, count]) => ({ count, file })),
|
|
545
973
|
totalFiles,
|
|
@@ -567,6 +995,105 @@ function sumCounts(counts) {
|
|
|
567
995
|
return Object.values(counts).reduce((total, count) => total + count, 0);
|
|
568
996
|
}
|
|
569
997
|
|
|
998
|
+
function applyBaselineComparison(report, baselinePath) {
|
|
999
|
+
const resolvedPath = path.resolve(process.cwd(), baselinePath);
|
|
1000
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1001
|
+
throw new Error(`Baseline file not found: ${baselinePath}`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const baseline = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
1005
|
+
const baselineFindings = Array.isArray(baseline.findings) ? baseline.findings : [];
|
|
1006
|
+
const baselineKeys = new Set(baselineFindings.map((finding) => finding.key || createFindingKey(finding)));
|
|
1007
|
+
const currentFindings = getAllFindings(report);
|
|
1008
|
+
const currentKeys = new Set(currentFindings.map(createFindingKey));
|
|
1009
|
+
const newFindings = currentFindings.filter((finding) => !baselineKeys.has(createFindingKey(finding)));
|
|
1010
|
+
const resolvedFindings = baselineFindings.filter((finding) => !currentKeys.has(finding.key || createFindingKey(finding)));
|
|
1011
|
+
|
|
1012
|
+
report.baseline = {
|
|
1013
|
+
baselineFindings: baselineFindings.length,
|
|
1014
|
+
file: formatPath(resolvedPath),
|
|
1015
|
+
newFindings,
|
|
1016
|
+
newFindingsCount: newFindings.length,
|
|
1017
|
+
resolvedFindingsCount: resolvedFindings.length,
|
|
1018
|
+
};
|
|
1019
|
+
report.summary.newFindings = newFindings.length;
|
|
1020
|
+
report.summary.resolvedFindings = resolvedFindings.length;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function writeBaseline(report, baselinePath) {
|
|
1024
|
+
const resolvedPath = path.resolve(process.cwd(), baselinePath);
|
|
1025
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
1026
|
+
fs.writeFileSync(
|
|
1027
|
+
resolvedPath,
|
|
1028
|
+
`${JSON.stringify({
|
|
1029
|
+
createdAt: new Date().toISOString(),
|
|
1030
|
+
directory: report.directory,
|
|
1031
|
+
findings: getAllFindings(report).map(toBaselineFinding),
|
|
1032
|
+
formatVersion: 1,
|
|
1033
|
+
summary: {
|
|
1034
|
+
scaleCleanliness: report.scaleCleanliness,
|
|
1035
|
+
totalFindings: report.totalWarnings,
|
|
1036
|
+
},
|
|
1037
|
+
}, null, 2)}\n`,
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
report.baselineWritten = {
|
|
1041
|
+
file: formatPath(resolvedPath),
|
|
1042
|
+
findings: report.totalWarnings,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function getAllFindings(report) {
|
|
1047
|
+
return [
|
|
1048
|
+
...report.findings.css,
|
|
1049
|
+
...report.findings.tailwind,
|
|
1050
|
+
];
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function toBaselineFinding(finding) {
|
|
1054
|
+
return {
|
|
1055
|
+
column: finding.column,
|
|
1056
|
+
file: finding.file,
|
|
1057
|
+
key: createFindingKey(finding),
|
|
1058
|
+
line: finding.line,
|
|
1059
|
+
rule: finding.rule,
|
|
1060
|
+
text: finding.text,
|
|
1061
|
+
token: finding.token,
|
|
1062
|
+
type: finding.type,
|
|
1063
|
+
value: finding.value || finding.rawValue,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function createFindingKey(finding) {
|
|
1068
|
+
return [
|
|
1069
|
+
finding.rule || '',
|
|
1070
|
+
finding.type || '',
|
|
1071
|
+
finding.file || '',
|
|
1072
|
+
finding.line || '',
|
|
1073
|
+
finding.column || '',
|
|
1074
|
+
finding.value || finding.rawValue || finding.token || '',
|
|
1075
|
+
finding.text || '',
|
|
1076
|
+
].join('\u001f');
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function getAuditFailures(report, parsed) {
|
|
1080
|
+
const failures = [];
|
|
1081
|
+
|
|
1082
|
+
if (parsed.maxFindings !== null && report.totalWarnings > parsed.maxFindings) {
|
|
1083
|
+
failures.push(`total findings ${report.totalWarnings} exceeds --max-findings ${parsed.maxFindings}`);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (parsed.minCleanliness !== null && report.scaleCleanliness < parsed.minCleanliness) {
|
|
1087
|
+
failures.push(`scale cleanliness ${report.scaleCleanliness}% is below --min-cleanliness ${parsed.minCleanliness}%`);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (parsed.failOnNewDrift && report.baseline && report.baseline.newFindingsCount > 0) {
|
|
1091
|
+
failures.push(`new drift found: ${report.baseline.newFindingsCount} finding(s) not present in baseline`);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return failures;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
570
1097
|
function renderText(report) {
|
|
571
1098
|
const lines = [
|
|
572
1099
|
'',
|
|
@@ -584,6 +1111,8 @@ function renderText(report) {
|
|
|
584
1111
|
appendHistogram(lines, 'CSS OFF-SCALE VALUES', report.offScaleValues);
|
|
585
1112
|
appendHistogram(lines, 'CSS TOKEN OPPORTUNITIES', report.tokenOpportunities);
|
|
586
1113
|
appendHistogram(lines, 'TAILWIND CLASS-STRING DRIFT', report.tailwindArbitraryValues);
|
|
1114
|
+
appendTokenContractText(lines, report.tokenContract);
|
|
1115
|
+
appendBaselineText(lines, report);
|
|
587
1116
|
|
|
588
1117
|
if (report.topAffectedFiles.length > 0) {
|
|
589
1118
|
lines.push(' ── TOP AFFECTED FILES ──');
|
|
@@ -606,6 +1135,55 @@ function renderText(report) {
|
|
|
606
1135
|
return `${lines.join('\n')}\n`;
|
|
607
1136
|
}
|
|
608
1137
|
|
|
1138
|
+
function appendTokenContractText(lines, tokenContract) {
|
|
1139
|
+
const { missingTokens, rawValueCandidates, unusedTokens } = tokenContract;
|
|
1140
|
+
if (missingTokens.length === 0 && rawValueCandidates.length === 0 && unusedTokens.length === 0) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
lines.push(' ── TOKEN CONTRACT ──');
|
|
1145
|
+
lines.push('');
|
|
1146
|
+
|
|
1147
|
+
if (missingTokens.length > 0) {
|
|
1148
|
+
lines.push(` Missing tokens ${missingTokens.length}`);
|
|
1149
|
+
for (const entry of missingTokens.slice(0, 5)) {
|
|
1150
|
+
lines.push(` ${entry.token} (${truncate(entry.files.join(', '), 42)})`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (unusedTokens.length > 0) {
|
|
1155
|
+
lines.push(` Defined but unused ${unusedTokens.length}`);
|
|
1156
|
+
for (const entry of unusedTokens.slice(0, 5)) {
|
|
1157
|
+
lines.push(` ${entry.token}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (rawValueCandidates.length > 0) {
|
|
1162
|
+
lines.push(` Repeated raw values ${rawValueCandidates.length}`);
|
|
1163
|
+
for (const entry of rawValueCandidates.slice(0, 5)) {
|
|
1164
|
+
lines.push(` ${entry.value.padEnd(14)} ${entry.count}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
lines.push('');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function appendBaselineText(lines, report) {
|
|
1172
|
+
if (report.baseline) {
|
|
1173
|
+
lines.push(' ── BASELINE COMPARISON ──');
|
|
1174
|
+
lines.push('');
|
|
1175
|
+
lines.push(` Baseline findings ${String(report.baseline.baselineFindings).padStart(4)}`);
|
|
1176
|
+
lines.push(` New findings ${String(report.baseline.newFindingsCount).padStart(4)}`);
|
|
1177
|
+
lines.push(` Resolved findings ${String(report.baseline.resolvedFindingsCount).padStart(4)}`);
|
|
1178
|
+
lines.push('');
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (report.baselineWritten) {
|
|
1182
|
+
lines.push(` Baseline written ${report.baselineWritten.file}`);
|
|
1183
|
+
lines.push('');
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
609
1187
|
function appendHistogram(lines, title, counts) {
|
|
610
1188
|
const entries = sortCountMap(counts);
|
|
611
1189
|
const total = sumCounts(counts);
|
|
@@ -638,12 +1216,19 @@ function renderMarkdown(report) {
|
|
|
638
1216
|
`| Files with issues | ${report.filesWithIssues} |`,
|
|
639
1217
|
`| Total findings | ${report.totalWarnings} |`,
|
|
640
1218
|
`| Scale cleanliness | ${report.scaleCleanliness}% |`,
|
|
641
|
-
'',
|
|
642
1219
|
];
|
|
643
1220
|
|
|
1221
|
+
if (report.baseline) {
|
|
1222
|
+
lines.push(`| New findings | ${report.baseline.newFindingsCount} |`);
|
|
1223
|
+
lines.push(`| Resolved findings | ${report.baseline.resolvedFindingsCount} |`);
|
|
1224
|
+
}
|
|
1225
|
+
lines.push('');
|
|
1226
|
+
|
|
644
1227
|
appendMarkdownCounts(lines, 'CSS Off-Scale Values', report.offScaleValues);
|
|
645
1228
|
appendMarkdownCounts(lines, 'CSS Token Opportunities', report.tokenOpportunities);
|
|
646
1229
|
appendMarkdownCounts(lines, 'Tailwind Class-String Drift', report.tailwindArbitraryValues);
|
|
1230
|
+
appendTokenContractMarkdown(lines, report.tokenContract);
|
|
1231
|
+
appendBaselineMarkdown(lines, report);
|
|
647
1232
|
|
|
648
1233
|
if (report.topAffectedFiles.length > 0) {
|
|
649
1234
|
lines.push('## Top Affected Files');
|
|
@@ -681,6 +1266,81 @@ function renderMarkdown(report) {
|
|
|
681
1266
|
return `${lines.join('\n')}\n`;
|
|
682
1267
|
}
|
|
683
1268
|
|
|
1269
|
+
function appendTokenContractMarkdown(lines, tokenContract) {
|
|
1270
|
+
const { missingTokens, rawValueCandidates, unusedTokens } = tokenContract;
|
|
1271
|
+
if (missingTokens.length === 0 && rawValueCandidates.length === 0 && unusedTokens.length === 0) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
lines.push('## Token Contract');
|
|
1276
|
+
lines.push('');
|
|
1277
|
+
|
|
1278
|
+
if (missingTokens.length > 0) {
|
|
1279
|
+
lines.push('### Tokens Used But Missing');
|
|
1280
|
+
lines.push('');
|
|
1281
|
+
lines.push('| Token | Files |');
|
|
1282
|
+
lines.push('| --- | --- |');
|
|
1283
|
+
for (const entry of missingTokens.slice(0, 10)) {
|
|
1284
|
+
lines.push(`| \`${escapeMarkdown(entry.token)}\` | \`${escapeMarkdown(entry.files.join(', '))}\` |`);
|
|
1285
|
+
}
|
|
1286
|
+
lines.push('');
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (unusedTokens.length > 0) {
|
|
1290
|
+
lines.push('### Tokens Defined But Unused');
|
|
1291
|
+
lines.push('');
|
|
1292
|
+
lines.push('| Token | Value |');
|
|
1293
|
+
lines.push('| --- | --- |');
|
|
1294
|
+
for (const entry of unusedTokens.slice(0, 10)) {
|
|
1295
|
+
lines.push(`| \`${escapeMarkdown(entry.token)}\` | \`${escapeMarkdown((entry.values || []).join(', ') || 'n/a')}\` |`);
|
|
1296
|
+
}
|
|
1297
|
+
lines.push('');
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (rawValueCandidates.length > 0) {
|
|
1301
|
+
lines.push('### Repeated Raw Value Candidates');
|
|
1302
|
+
lines.push('');
|
|
1303
|
+
lines.push('| Value | Count | Files |');
|
|
1304
|
+
lines.push('| --- | ---: | --- |');
|
|
1305
|
+
for (const entry of rawValueCandidates.slice(0, 10)) {
|
|
1306
|
+
lines.push(`| \`${escapeMarkdown(entry.value)}\` | ${entry.count} | \`${escapeMarkdown(entry.files.join(', '))}\` |`);
|
|
1307
|
+
}
|
|
1308
|
+
lines.push('');
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function appendBaselineMarkdown(lines, report) {
|
|
1313
|
+
if (!report.baseline && !report.baselineWritten) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
lines.push('## Baseline');
|
|
1318
|
+
lines.push('');
|
|
1319
|
+
|
|
1320
|
+
if (report.baseline) {
|
|
1321
|
+
lines.push('| Metric | Value |');
|
|
1322
|
+
lines.push('| --- | ---: |');
|
|
1323
|
+
lines.push(`| Baseline findings | ${report.baseline.baselineFindings} |`);
|
|
1324
|
+
lines.push(`| New findings | ${report.baseline.newFindingsCount} |`);
|
|
1325
|
+
lines.push(`| Resolved findings | ${report.baseline.resolvedFindingsCount} |`);
|
|
1326
|
+
lines.push('');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (report.baseline && report.baseline.newFindings.length > 0) {
|
|
1330
|
+
lines.push('| New finding | Location |');
|
|
1331
|
+
lines.push('| --- | --- |');
|
|
1332
|
+
for (const finding of report.baseline.newFindings.slice(0, 10)) {
|
|
1333
|
+
lines.push(`| \`${escapeMarkdown(finding.text)}\` | \`${escapeMarkdown(`${finding.file}:${finding.line}`)}\` |`);
|
|
1334
|
+
}
|
|
1335
|
+
lines.push('');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (report.baselineWritten) {
|
|
1339
|
+
lines.push(`Baseline written: \`${escapeMarkdown(report.baselineWritten.file)}\``);
|
|
1340
|
+
lines.push('');
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
684
1344
|
function appendMarkdownCounts(lines, title, counts) {
|
|
685
1345
|
const entries = sortCountMap(counts);
|
|
686
1346
|
|
|
@@ -741,7 +1401,26 @@ async function run() {
|
|
|
741
1401
|
}
|
|
742
1402
|
|
|
743
1403
|
const resolvedDir = assertDirectory(parsed.dir);
|
|
744
|
-
|
|
1404
|
+
let ignorePatterns;
|
|
1405
|
+
try {
|
|
1406
|
+
ignorePatterns = [
|
|
1407
|
+
...loadIgnorePatterns(parsed.ignorePath),
|
|
1408
|
+
...parsed.ignorePatterns,
|
|
1409
|
+
];
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
process.stderr.write(`${err.message}\n`);
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
let scanFiles;
|
|
1416
|
+
try {
|
|
1417
|
+
scanFiles = getScanFiles(resolvedDir, ignorePatterns, parsed);
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
process.stderr.write(`${err.message}\n`);
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const { cssFiles, scanScope, templateFiles } = scanFiles;
|
|
745
1424
|
const options = {
|
|
746
1425
|
baseFontSize: parsed.baseFontSize,
|
|
747
1426
|
scale: parsed.scale,
|
|
@@ -759,21 +1438,50 @@ async function run() {
|
|
|
759
1438
|
cssFiles,
|
|
760
1439
|
cssFindings: collectCssFindings(cssResults),
|
|
761
1440
|
dir: parsed.dir,
|
|
1441
|
+
scanScope,
|
|
762
1442
|
tailwindFindings: collectTailwindFindings(templateFiles, options),
|
|
763
1443
|
templateFiles,
|
|
1444
|
+
tokenCandidateMinCount: parsed.tokenCandidateMinCount,
|
|
764
1445
|
});
|
|
765
1446
|
|
|
1447
|
+
try {
|
|
1448
|
+
if (parsed.sinceBaseline) {
|
|
1449
|
+
applyBaselineComparison(report, parsed.baselinePath);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (parsed.writeBaseline) {
|
|
1453
|
+
writeBaseline(report, parsed.baselinePath);
|
|
1454
|
+
}
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
process.stderr.write(`${err.message}\n`);
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const auditFailures = getAuditFailures(report, parsed);
|
|
1461
|
+
|
|
766
1462
|
if (parsed.format === 'json') {
|
|
767
1463
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
1464
|
+
finish(auditFailures);
|
|
768
1465
|
return;
|
|
769
1466
|
}
|
|
770
1467
|
|
|
771
1468
|
if (parsed.format === 'markdown') {
|
|
772
1469
|
process.stdout.write(renderMarkdown(report));
|
|
1470
|
+
finish(auditFailures);
|
|
773
1471
|
return;
|
|
774
1472
|
}
|
|
775
1473
|
|
|
776
1474
|
process.stdout.write(renderText(report));
|
|
1475
|
+
finish(auditFailures);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function finish(auditFailures) {
|
|
1479
|
+
if (auditFailures.length === 0) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
process.stderr.write(`Audit failed: ${auditFailures.join('; ')}\n`);
|
|
1484
|
+
process.exitCode = 1;
|
|
777
1485
|
}
|
|
778
1486
|
|
|
779
1487
|
run();
|