stylelint-plugin-rhythmguard 3.11.0 → 3.12.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 +56 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/audit/baseline.js +184 -27
- package/src/audit/codemod.js +20 -6
- package/src/audit/config.js +8 -29
- package/src/audit/contract.js +96 -10
- package/src/audit/render-github.js +4 -0
- package/src/audit/render-html.js +7 -0
- package/src/audit/render-markdown.js +27 -0
- package/src/audit/render-text.js +23 -1
- package/src/audit/report.js +58 -11
- package/src/audit/scan/files.js +137 -25
- package/src/audit/scan/stylesheets.js +67 -34
- package/src/audit/scan/templates.js +22 -7
- package/src/audit/token-chains.js +18 -7
- package/src/cli/doctor.js +48 -7
- package/src/cli/fix.js +13 -16
- package/src/cli/init.js +136 -23
- package/src/cli/quickstart.js +34 -8
- package/src/core/fs-cache.js +6 -0
- package/src/core/options.js +7 -1
- package/src/core/scale-inference.js +99 -68
- package/src/core/tailwind-class-analysis.js +24 -14
- package/src/core/tailwind-motion-analysis.js +12 -3
- package/src/core/token-index.js +5 -2
- package/src/core/token-source-entries.js +64 -0
- package/src/core/token-sources.js +361 -69
- package/src/core/value-nodes.js +280 -54
- package/src/eslint/literal-spans.js +94 -9
- package/src/eslint/rules/tailwind-class-use-scale.js +39 -11
- package/src/rules/no-offscale-transform/index.js +33 -13
- package/src/rules/prefer-token/index.js +39 -18
- package/src/rules/use-motion-scale/index.js +68 -5
- package/src/rules/use-scale/index.js +50 -36
- package/types/__checks__/eslint-config.ts +37 -0
- package/types/audit.d.ts +10 -2
- package/types/shared.d.ts +21 -3
package/src/audit/contract.js
CHANGED
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
collectCssTokens,
|
|
7
7
|
createTokenKindMatcher,
|
|
8
8
|
getNormalizedValueKeys,
|
|
9
|
+
hasLineComments,
|
|
9
10
|
} = require('../core/token-sources');
|
|
10
11
|
const { formatPath } = require('./shared');
|
|
11
12
|
|
|
@@ -15,6 +16,7 @@ function collectTokenContract({
|
|
|
15
16
|
cssFindings,
|
|
16
17
|
externalTokenDefinitions,
|
|
17
18
|
minCandidateCount,
|
|
19
|
+
readSource = (filePath) => fs.readFileSync(filePath, 'utf8'),
|
|
18
20
|
tailwindFindings,
|
|
19
21
|
tokenKind,
|
|
20
22
|
tokenSourceReports,
|
|
@@ -28,7 +30,7 @@ function collectTokenContract({
|
|
|
28
30
|
for (const filePath of cssFiles) {
|
|
29
31
|
let source;
|
|
30
32
|
try {
|
|
31
|
-
source =
|
|
33
|
+
source = readSource(filePath);
|
|
32
34
|
} catch {
|
|
33
35
|
continue;
|
|
34
36
|
}
|
|
@@ -97,7 +99,7 @@ function collectTokenContract({
|
|
|
97
99
|
|
|
98
100
|
/** Custom properties and Sass tokens declared in one stylesheet, added to the definitions map. */
|
|
99
101
|
function collectTokenDefinitions(source, file, definitions, matchesKind, baseFontSize) {
|
|
100
|
-
for (const { scope, token, value } of collectCssTokens(source, matchesKind)) {
|
|
102
|
+
for (const { scope, token, value } of collectCssTokens(source, matchesKind, { lineComments: hasLineComments(file) })) {
|
|
101
103
|
addDefinition(definitions, { baseFontSize, file, scope, source: file, token, value });
|
|
102
104
|
}
|
|
103
105
|
}
|
|
@@ -231,6 +233,8 @@ function buildReport({
|
|
|
231
233
|
externalTokenDefinitions,
|
|
232
234
|
includeMotion,
|
|
233
235
|
motionFindings,
|
|
236
|
+
parseErrorFindings = [],
|
|
237
|
+
readSource,
|
|
234
238
|
scale,
|
|
235
239
|
scanScope,
|
|
236
240
|
scssFiles = 0,
|
|
@@ -267,18 +271,24 @@ function buildReport({
|
|
|
267
271
|
...tailwindFindings.map((finding) => finding.file),
|
|
268
272
|
])).slice(0, 10);
|
|
269
273
|
|
|
270
|
-
|
|
274
|
+
// A stylesheet skipped for want of postcss-scss, or excluded by the project's
|
|
275
|
+
// .stylelintignore, was not audited: it is neither scanned nor clean.
|
|
276
|
+
const cssFilesScanned = Math.max(0, cssFiles.length - scssSkipped - stylelintIgnored);
|
|
277
|
+
const totalFiles = cssFilesScanned + templateFiles.length;
|
|
271
278
|
const totalWarnings = cssFindings.length + motionFindings.length + tailwindFindings.length;
|
|
272
279
|
const filesWithIssues = issueFiles.size;
|
|
273
|
-
const scaleCleanliness =
|
|
274
|
-
|
|
275
|
-
:
|
|
280
|
+
const scaleCleanliness = Math.floor(computeScaleCleanliness({
|
|
281
|
+
filesWithIssues,
|
|
282
|
+
parseErrorFiles: countFiles(parseErrorFindings),
|
|
283
|
+
totalFiles,
|
|
284
|
+
}));
|
|
276
285
|
const tokenContract = collectTokenContract({
|
|
277
286
|
baseFontSize,
|
|
278
287
|
cssFiles,
|
|
279
288
|
cssFindings,
|
|
280
289
|
externalTokenDefinitions,
|
|
281
290
|
minCandidateCount: tokenCandidateMinCount,
|
|
291
|
+
readSource,
|
|
282
292
|
tailwindFindings,
|
|
283
293
|
tokenKind,
|
|
284
294
|
tokenSourceReports,
|
|
@@ -286,12 +296,13 @@ function buildReport({
|
|
|
286
296
|
|
|
287
297
|
return {
|
|
288
298
|
config,
|
|
289
|
-
cssFilesScanned
|
|
299
|
+
cssFilesScanned,
|
|
290
300
|
directory: dir,
|
|
291
301
|
filesWithIssues,
|
|
292
302
|
findings: {
|
|
293
303
|
css: cssFindings,
|
|
294
304
|
motion: motionFindings,
|
|
305
|
+
parseErrors: parseErrorFindings,
|
|
295
306
|
tailwind: tailwindFindings,
|
|
296
307
|
},
|
|
297
308
|
formatVersion: 5,
|
|
@@ -306,7 +317,7 @@ function buildReport({
|
|
|
306
317
|
scaleCleanliness,
|
|
307
318
|
scanScope,
|
|
308
319
|
scanned: {
|
|
309
|
-
cssFiles:
|
|
320
|
+
cssFiles: cssFilesScanned,
|
|
310
321
|
scssFiles,
|
|
311
322
|
scssSkipped,
|
|
312
323
|
stylelintIgnored,
|
|
@@ -317,6 +328,7 @@ function buildReport({
|
|
|
317
328
|
cssWarnings: cssFindings.length,
|
|
318
329
|
filesWithIssues,
|
|
319
330
|
motionFindings: motionFindings.length,
|
|
331
|
+
parseErrors: parseErrorFindings.length,
|
|
320
332
|
rawValueMatches: tokenContract.summary.rawValueMatches,
|
|
321
333
|
missingTokens: tokenContract.summary.missingTokens,
|
|
322
334
|
rawValueCandidates: tokenContract.summary.rawValueCandidates,
|
|
@@ -338,6 +350,36 @@ function buildReport({
|
|
|
338
350
|
};
|
|
339
351
|
}
|
|
340
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The share of audited files with no finding, unrounded. A file that failed to
|
|
355
|
+
* parse was not audited, so it is out of the denominator. The report shows the
|
|
356
|
+
* floor of this, so it never reads 100% while a file has a finding, and the
|
|
357
|
+
* --min-cleanliness gate compares this exact value.
|
|
358
|
+
*/
|
|
359
|
+
function computeScaleCleanliness({ filesWithIssues, parseErrorFiles = 0, totalFiles }) {
|
|
360
|
+
const auditedFiles = totalFiles - parseErrorFiles;
|
|
361
|
+
if (auditedFiles <= 0) {
|
|
362
|
+
return 100;
|
|
363
|
+
}
|
|
364
|
+
return Math.max(0, ((auditedFiles - filesWithIssues) * 100) / auditedFiles);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The unrounded cleanliness of a built report. */
|
|
368
|
+
function exactScaleCleanliness(report) {
|
|
369
|
+
if (!Number.isFinite(report.totalFiles) || !Number.isFinite(report.filesWithIssues)) {
|
|
370
|
+
return report.scaleCleanliness;
|
|
371
|
+
}
|
|
372
|
+
return computeScaleCleanliness({
|
|
373
|
+
filesWithIssues: report.filesWithIssues,
|
|
374
|
+
parseErrorFiles: countFiles((report.findings && report.findings.parseErrors) || []),
|
|
375
|
+
totalFiles: report.totalFiles,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function countFiles(findings) {
|
|
380
|
+
return new Set(findings.map((finding) => finding.file)).size;
|
|
381
|
+
}
|
|
382
|
+
|
|
341
383
|
function countByValue(values) {
|
|
342
384
|
const counts = {};
|
|
343
385
|
|
|
@@ -395,10 +437,52 @@ const AUDIT_JSON_SCHEMA = Object.freeze({
|
|
|
395
437
|
baseline: { type: ['object', 'null'] },
|
|
396
438
|
command: { type: 'object' },
|
|
397
439
|
contracts: { type: 'object' },
|
|
398
|
-
findings: {
|
|
440
|
+
findings: {
|
|
441
|
+
additionalProperties: true,
|
|
442
|
+
properties: {
|
|
443
|
+
css: { type: 'array' },
|
|
444
|
+
motion: { type: 'array' },
|
|
445
|
+
parseErrors: {
|
|
446
|
+
description: 'Stylesheets that could not be parsed and so were not audited. Not counted as findings or in cleanliness.',
|
|
447
|
+
items: {
|
|
448
|
+
additionalProperties: true,
|
|
449
|
+
properties: {
|
|
450
|
+
column: { type: 'integer' },
|
|
451
|
+
file: { type: 'string' },
|
|
452
|
+
line: { type: 'integer' },
|
|
453
|
+
rule: { const: 'CssSyntaxError' },
|
|
454
|
+
text: { type: 'string' },
|
|
455
|
+
type: { const: 'parse-error' },
|
|
456
|
+
},
|
|
457
|
+
required: ['file', 'text', 'type'],
|
|
458
|
+
type: 'object',
|
|
459
|
+
},
|
|
460
|
+
type: 'array',
|
|
461
|
+
},
|
|
462
|
+
tailwind: { type: 'array' },
|
|
463
|
+
},
|
|
464
|
+
type: 'object',
|
|
465
|
+
},
|
|
399
466
|
scanned: { type: 'object' },
|
|
400
467
|
schemaVersion: { const: '2.0' },
|
|
401
|
-
summary: {
|
|
468
|
+
summary: {
|
|
469
|
+
additionalProperties: true,
|
|
470
|
+
properties: {
|
|
471
|
+
parseErrors: {
|
|
472
|
+
description: 'Number of stylesheets that could not be parsed.',
|
|
473
|
+
minimum: 0,
|
|
474
|
+
type: 'integer',
|
|
475
|
+
},
|
|
476
|
+
scaleCleanliness: {
|
|
477
|
+
description: 'Percentage of audited files with no finding, rounded down.',
|
|
478
|
+
maximum: 100,
|
|
479
|
+
minimum: 0,
|
|
480
|
+
type: 'integer',
|
|
481
|
+
},
|
|
482
|
+
totalFindings: { minimum: 0, type: 'integer' },
|
|
483
|
+
},
|
|
484
|
+
type: 'object',
|
|
485
|
+
},
|
|
402
486
|
},
|
|
403
487
|
required: ['schemaVersion', 'command', 'summary', 'scanned', 'contracts', 'findings'],
|
|
404
488
|
title: 'Rhythmguard Audit Report',
|
|
@@ -414,8 +498,10 @@ module.exports = {
|
|
|
414
498
|
collectTokenContract,
|
|
415
499
|
collectTokenDefinitions,
|
|
416
500
|
collectTokenUses,
|
|
501
|
+
computeScaleCleanliness,
|
|
417
502
|
countByValue,
|
|
418
503
|
createValueToTokensMap,
|
|
504
|
+
exactScaleCleanliness,
|
|
419
505
|
mapTokenEntries,
|
|
420
506
|
sortCountMap,
|
|
421
507
|
sumCounts,
|
|
@@ -7,6 +7,10 @@ function renderGithub(report) {
|
|
|
7
7
|
const lines = report.baseline
|
|
8
8
|
? [`::notice title=Rhythmguard audit::Since baseline: ${report.baseline.resolvedFindingsCount} resolved, ${report.baseline.newFindingsCount} new`]
|
|
9
9
|
: [];
|
|
10
|
+
// A stylesheet that failed to parse was not audited: an error on its line.
|
|
11
|
+
lines.push(...((report.findings && report.findings.parseErrors) || []).map((finding) =>
|
|
12
|
+
`::error file=${escapeGithubProperty(finding.file)},line=${finding.line || 1},col=${finding.column || 1},title=${escapeGithubProperty(finding.rule || 'CssSyntaxError')}::${escapeGithubData(`Not audited: ${finding.text || ''}`)}`,
|
|
13
|
+
));
|
|
10
14
|
lines.push(...findings.map((finding) =>
|
|
11
15
|
`::warning file=${escapeGithubProperty(finding.file)},line=${finding.line || 1},col=${finding.column || 1},title=${escapeGithubProperty(finding.rule || 'rhythmguard')}::${escapeGithubData(finding.text || '')}`,
|
|
12
16
|
));
|
package/src/audit/render-html.js
CHANGED
|
@@ -33,7 +33,9 @@ function renderHtml(report) {
|
|
|
33
33
|
metricHtml('Scale cleanliness', `${report.scaleCleanliness}%`),
|
|
34
34
|
metricHtml('CSS files', report.cssFilesScanned),
|
|
35
35
|
metricHtml('Template files', report.templateFilesScanned),
|
|
36
|
+
...(parseErrorRows(report).length > 0 ? [metricHtml('Parse errors (not audited)', parseErrorRows(report).length)] : []),
|
|
36
37
|
'</section>',
|
|
38
|
+
renderHtmlTable('Parse Errors (not audited)', ['File', 'Error'], parseErrorRows(report)),
|
|
37
39
|
renderHtmlTable('CSS Off-Scale Properties', ['Property', 'Count'], Object.entries(report.offScaleProperties || {})),
|
|
38
40
|
renderHtmlTable('Top Affected Files', ['File', 'Findings'], report.topAffectedFiles.map(({ file, count }) => [file, count])),
|
|
39
41
|
renderHtmlTable('Token Contract Sources', ['File', 'Format', 'Tokens'], report.tokenContract.sources.map((source) => [
|
|
@@ -52,6 +54,11 @@ function renderHtml(report) {
|
|
|
52
54
|
return `${lines.join('\n')}\n`;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function parseErrorRows(report) {
|
|
58
|
+
return ((report.findings && report.findings.parseErrors) || [])
|
|
59
|
+
.map((finding) => [`${finding.file}:${finding.line}:${finding.column}`, finding.text]);
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
function metricHtml(label, value) {
|
|
56
63
|
return `<div class="metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
57
64
|
}
|
|
@@ -28,6 +28,7 @@ function renderMarkdown(report) {
|
|
|
28
28
|
...(report.scanned && report.scanned.scssFiles > 0 ? [`| SCSS files scanned | ${report.scanned.scssFiles - report.scanned.scssSkipped} |`] : []),
|
|
29
29
|
...(report.scanned && report.scanned.scssSkipped > 0 ? [`| SCSS files skipped (install postcss-scss) | ${report.scanned.scssSkipped} |`] : []),
|
|
30
30
|
...(report.scanned && report.scanned.stylelintIgnored > 0 ? [`| Stylesheets ignored by .stylelintignore (not audited) | ${report.scanned.stylelintIgnored} |`] : []),
|
|
31
|
+
...(parseErrors(report).length > 0 ? [`| Stylesheets that failed to parse (not audited) | ${parseErrors(report).length} |`] : []),
|
|
31
32
|
`| Files with issues | ${report.filesWithIssues} |`,
|
|
32
33
|
`| Total findings | ${report.totalWarnings} |`,
|
|
33
34
|
`| Scale cleanliness | ${report.scaleCleanliness}% |`,
|
|
@@ -57,6 +58,7 @@ function renderMarkdown(report) {
|
|
|
57
58
|
appendTokenChainsMarkdown(lines, report.tokenContract.chains);
|
|
58
59
|
appendTokenLaddersMarkdown(lines, report.tokenContract.ladders);
|
|
59
60
|
appendBaselineMarkdown(lines, report);
|
|
61
|
+
appendParseErrorsMarkdown(lines, report);
|
|
60
62
|
|
|
61
63
|
if (report.topAffectedFiles.length > 0) {
|
|
62
64
|
lines.push('## Top Affected Files');
|
|
@@ -82,6 +84,9 @@ function renderMarkdown(report) {
|
|
|
82
84
|
|
|
83
85
|
lines.push('## Recommended Next Actions');
|
|
84
86
|
lines.push('');
|
|
87
|
+
if (parseErrors(report).length > 0) {
|
|
88
|
+
lines.push('- Fix the parse errors above: those stylesheets were not audited.');
|
|
89
|
+
}
|
|
85
90
|
if (report.totalWarnings === 0) {
|
|
86
91
|
lines.push('- Keep Rhythmguard in CI to prevent new drift.');
|
|
87
92
|
} else {
|
|
@@ -94,6 +99,27 @@ function renderMarkdown(report) {
|
|
|
94
99
|
return `${lines.join('\n')}\n`;
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
function parseErrors(report) {
|
|
103
|
+
return (report.findings && report.findings.parseErrors) || [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function appendParseErrorsMarkdown(lines, report) {
|
|
107
|
+
const errors = parseErrors(report);
|
|
108
|
+
if (errors.length === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
lines.push('## Parse Errors');
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push('These stylesheets could not be parsed, so they were not audited. They are not counted as findings or in scale cleanliness.');
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('| File | Error |');
|
|
116
|
+
lines.push('| --- | --- |');
|
|
117
|
+
for (const finding of errors) {
|
|
118
|
+
lines.push(`| \`${escapeMarkdown(`${finding.file}:${finding.line}:${finding.column}`)}\` | ${escapeMarkdown(finding.text)} |`);
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
|
|
97
123
|
function appendTokenChainsMarkdown(lines, chains) {
|
|
98
124
|
if (!chains || chains.summary.total === 0) {
|
|
99
125
|
return;
|
|
@@ -295,6 +321,7 @@ function appendMarkdownCounts(lines, title, counts, label = 'Value') {
|
|
|
295
321
|
module.exports = {
|
|
296
322
|
appendBaselineMarkdown,
|
|
297
323
|
appendMarkdownCounts,
|
|
324
|
+
appendParseErrorsMarkdown,
|
|
298
325
|
appendTokenContractMarkdown,
|
|
299
326
|
renderMarkdown,
|
|
300
327
|
};
|
package/src/audit/render-text.js
CHANGED
|
@@ -22,6 +22,7 @@ function renderText(report) {
|
|
|
22
22
|
...(report.scanned && report.scanned.scssFiles > 0 ? [` SCSS files scanned ${String(report.scanned.scssFiles - report.scanned.scssSkipped).padStart(4)}`] : []),
|
|
23
23
|
...(report.scanned && report.scanned.scssSkipped > 0 ? [` SCSS files skipped ${String(report.scanned.scssSkipped).padStart(4)} (install postcss-scss to audit them)`] : []),
|
|
24
24
|
...(report.scanned && report.scanned.stylelintIgnored > 0 ? [` Ignored by .stylelintignore ${String(report.scanned.stylelintIgnored).padStart(4)} (not audited)`] : []),
|
|
25
|
+
...(parseErrors(report).length > 0 ? [` Parse errors ${String(parseErrors(report).length).padStart(4)} (not audited)`] : []),
|
|
25
26
|
` Files with issues ${String(report.filesWithIssues).padStart(4)}`,
|
|
26
27
|
` Scale cleanliness ${scoreBar(report.scaleCleanliness)} ${report.scaleCleanliness}%`,
|
|
27
28
|
];
|
|
@@ -57,6 +58,7 @@ function renderText(report) {
|
|
|
57
58
|
lines.push('');
|
|
58
59
|
}
|
|
59
60
|
appendBaselineText(lines, report);
|
|
61
|
+
appendParseErrorsText(lines, report);
|
|
60
62
|
|
|
61
63
|
if (report.topAffectedFiles.length > 0) {
|
|
62
64
|
lines.push(' ── TOP AFFECTED FILES ──');
|
|
@@ -68,7 +70,9 @@ function renderText(report) {
|
|
|
68
70
|
lines.push('');
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
if (report.totalWarnings === 0) {
|
|
73
|
+
if (report.totalWarnings === 0 && parseErrors(report).length > 0) {
|
|
74
|
+
lines.push(` ! No findings in the stylesheets that parsed; ${parseErrors(report).length} could not be parsed and were not audited.`);
|
|
75
|
+
} else if (report.totalWarnings === 0) {
|
|
72
76
|
lines.push(' ✓ No issues found. Your design-system rhythm is clean.');
|
|
73
77
|
} else {
|
|
74
78
|
lines.push(' → Run "npx stylelint --fix" for CSS declaration fixes.');
|
|
@@ -79,6 +83,23 @@ function renderText(report) {
|
|
|
79
83
|
return `${lines.join('\n')}\n`;
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
function parseErrors(report) {
|
|
87
|
+
return (report.findings && report.findings.parseErrors) || [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function appendParseErrorsText(lines, report) {
|
|
91
|
+
const errors = parseErrors(report);
|
|
92
|
+
if (errors.length === 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
lines.push(` ── PARSE ERRORS ── ${errors.length} not audited`);
|
|
96
|
+
lines.push('');
|
|
97
|
+
for (const finding of errors) {
|
|
98
|
+
lines.push(` ${finding.file}:${finding.line}:${finding.column} ${finding.text}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
function appendTokenContractText(lines, tokenContract) {
|
|
83
104
|
const {
|
|
84
105
|
conflictingTokens,
|
|
@@ -184,6 +205,7 @@ function appendHistogram(lines, title, counts = {}) {
|
|
|
184
205
|
module.exports = {
|
|
185
206
|
appendBaselineText,
|
|
186
207
|
appendHistogram,
|
|
208
|
+
appendParseErrorsText,
|
|
187
209
|
appendTokenContractText,
|
|
188
210
|
renderText,
|
|
189
211
|
};
|
package/src/audit/report.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const path = require('node:path');
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
6
|
createTokenKindMatcher,
|
|
@@ -27,7 +27,7 @@ const {
|
|
|
27
27
|
const { applyDecisions, buildDecisionPlan } = require('./decisions');
|
|
28
28
|
const { collectDeclarations, resolveTokenChains, toChainsContract } = require('./token-chains');
|
|
29
29
|
const { resolveTokenLadders, toLaddersContract } = require('./token-ladders');
|
|
30
|
-
const { assertDirectory, getScanFiles } = require('./scan/files');
|
|
30
|
+
const { assertDirectory, getScanFiles, readSourceFromDisk } = require('./scan/files');
|
|
31
31
|
const { collectCssFindings, runStylelintAudit } = require('./scan/stylesheets');
|
|
32
32
|
const { collectTailwindFindings, collectTailwindMotionFindings } = require('./scan/templates');
|
|
33
33
|
|
|
@@ -39,6 +39,8 @@ async function createAuditReport(options) {
|
|
|
39
39
|
const scanFiles = getScanFiles(resolvedDir, ignorePatterns, parsed);
|
|
40
40
|
|
|
41
41
|
const { cssFiles, scanScope, templateFiles } = scanFiles;
|
|
42
|
+
// A --staged run reads each changed file as the index holds it.
|
|
43
|
+
const readSource = scanFiles.readSource || readSourceFromDisk;
|
|
42
44
|
|
|
43
45
|
const tokenSourceResult = parseTokenSources({
|
|
44
46
|
baseFontSize: parsed.baseFontSize,
|
|
@@ -46,23 +48,34 @@ async function createAuditReport(options) {
|
|
|
46
48
|
tokenKind: parsed.tokenKind,
|
|
47
49
|
});
|
|
48
50
|
|
|
51
|
+
// The spacing scale always reads spacing tokens; --token-kind shapes the token contract only (issue #260).
|
|
52
|
+
const spacingSourceResult = parsed.tokenKind === 'spacing'
|
|
53
|
+
? tokenSourceResult
|
|
54
|
+
: parseTokenSources({ baseFontSize: parsed.baseFontSize, sources: parsed.tokenSources, tokenKind: 'spacing' });
|
|
55
|
+
|
|
49
56
|
const scale = resolveAuditScale({
|
|
50
57
|
baseFontSize: parsed.baseFontSize,
|
|
51
58
|
cssFiles,
|
|
59
|
+
readSource,
|
|
52
60
|
requested: parsed.scale,
|
|
53
|
-
tokenSourceResult,
|
|
61
|
+
tokenSourceResult: spacingSourceResult,
|
|
54
62
|
});
|
|
55
63
|
|
|
56
64
|
const lintOptions = {
|
|
57
65
|
baseFontSize: parsed.baseFontSize,
|
|
58
66
|
checkMathBounds: parsed.checkMathBounds,
|
|
59
67
|
includeMotion: parsed.includeMotion,
|
|
68
|
+
readSource: scanFiles.readSource,
|
|
60
69
|
scale: scale.values,
|
|
61
70
|
scales: parsed.scales,
|
|
62
71
|
};
|
|
63
72
|
|
|
64
73
|
const cssResults = await runStylelintAudit(cssFiles, lintOptions);
|
|
65
|
-
const
|
|
74
|
+
const collectedCssFindings = collectCssFindings(cssResults, readSource);
|
|
75
|
+
// A stylesheet that failed to parse was not audited; it is reported apart
|
|
76
|
+
// and is neither a finding nor a clean file.
|
|
77
|
+
const parseErrorFindings = collectedCssFindings.filter((finding) => finding.type === 'parse-error');
|
|
78
|
+
const stylelintFindings = collectedCssFindings.filter((finding) => finding.type !== 'parse-error');
|
|
66
79
|
const decided = applyDecisions({
|
|
67
80
|
baseFontSize: parsed.baseFontSize,
|
|
68
81
|
cssFindings: stylelintFindings.filter((finding) => !finding.type.startsWith('motion-')),
|
|
@@ -84,6 +97,8 @@ async function createAuditReport(options) {
|
|
|
84
97
|
externalTokenDefinitions: tokenSourceResult.definitions,
|
|
85
98
|
includeMotion: parsed.includeMotion,
|
|
86
99
|
motionFindings,
|
|
100
|
+
parseErrorFindings,
|
|
101
|
+
readSource,
|
|
87
102
|
scale,
|
|
88
103
|
scanScope,
|
|
89
104
|
scssFiles: cssResults.scssFiles || 0,
|
|
@@ -100,11 +115,13 @@ async function createAuditReport(options) {
|
|
|
100
115
|
const declarations = collectDeclarations({
|
|
101
116
|
cssFiles,
|
|
102
117
|
externalDefinitions: tokenSourceResult.definitions,
|
|
118
|
+
readSource,
|
|
103
119
|
skipFile: (file) => NON_AUTHORED_SEGMENT.test(file),
|
|
104
120
|
});
|
|
105
121
|
report.tokenContract.chains = toChainsContract(resolveTokenChains({
|
|
106
122
|
baseFontSize: parsed.baseFontSize,
|
|
107
123
|
declarations,
|
|
124
|
+
references: collectDeclarations({ cssFiles: [], externalDefinitions: chainDefinitions(parsed) }),
|
|
108
125
|
scale: scale.values,
|
|
109
126
|
}));
|
|
110
127
|
report.tokenContract.ladders = toLaddersContract(resolveTokenLadders({
|
|
@@ -123,17 +140,47 @@ async function createAuditReport(options) {
|
|
|
123
140
|
scale: scale.values,
|
|
124
141
|
});
|
|
125
142
|
|
|
126
|
-
if (parsed.sinceBaseline) {
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
if (parsed.sinceBaseline || parsed.writeBaseline) {
|
|
144
|
+
const unauditedFiles = [
|
|
145
|
+
...(cssResults.unauditedFiles || []),
|
|
146
|
+
...parseErrorFindings.map((finding) => path.resolve(process.cwd(), finding.file)),
|
|
147
|
+
];
|
|
148
|
+
const unaudited = new Set(unauditedFiles);
|
|
149
|
+
const baselineScope = {
|
|
150
|
+
auditedFiles: [...cssFiles, ...templateFiles].filter((file) => !unaudited.has(file)),
|
|
151
|
+
scanRoot: resolvedDir,
|
|
152
|
+
scoped: scanScope.mode !== 'full',
|
|
153
|
+
unauditedFiles,
|
|
154
|
+
};
|
|
129
155
|
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
if (parsed.sinceBaseline) {
|
|
157
|
+
applyBaselineComparison(report, parsed.baselinePath, baselineScope);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (parsed.writeBaseline) {
|
|
161
|
+
writeBaseline(report, parsed.baselinePath, baselineScope);
|
|
162
|
+
}
|
|
132
163
|
}
|
|
133
164
|
|
|
134
165
|
return report;
|
|
135
166
|
}
|
|
136
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Every custom property a chain can end in: all tokens in the token sources,
|
|
170
|
+
* whatever --token-kind says and whatever name pattern a source sets, plus
|
|
171
|
+
* installed token packages (issue #259). A chain through `--size-8` or a
|
|
172
|
+
* package's `--space-2` resolves instead of reading "not declared"; these are
|
|
173
|
+
* looked up only, never reported as chains of their own.
|
|
174
|
+
*/
|
|
175
|
+
function chainDefinitions(parsed) {
|
|
176
|
+
const unfiltered = (source) => (source && typeof source === 'object' ? { ...source, tokenPattern: null } : source);
|
|
177
|
+
return parseTokenSources({
|
|
178
|
+
baseFontSize: parsed.baseFontSize,
|
|
179
|
+
sources: [...parsed.tokenSources, ...discoverTokenPackages(process.cwd())].map(unfiltered),
|
|
180
|
+
tokenKind: 'all',
|
|
181
|
+
}).definitions;
|
|
182
|
+
}
|
|
183
|
+
|
|
137
184
|
const NON_AUTHORED_SEGMENT = /(^|\/)(test|tests|__tests__|spec|specs|fixtures?|__fixtures__|__snapshots__|vendor|third[-_]?party|storybook-static)(\/|$)/i;
|
|
138
185
|
|
|
139
186
|
/**
|
|
@@ -141,7 +188,7 @@ const NON_AUTHORED_SEGMENT = /(^|\/)(test|tests|__tests__|spec|specs|fixtures?|_
|
|
|
141
188
|
* "auto": external token sources, then spacing custom properties across the scanned
|
|
142
189
|
* CSS, then the default scale. First source that yields values wins.
|
|
143
190
|
*/
|
|
144
|
-
function resolveAuditScale({ baseFontSize, cssFiles, requested, tokenSourceResult }) {
|
|
191
|
+
function resolveAuditScale({ baseFontSize, cssFiles, readSource = readSourceFromDisk, requested, tokenSourceResult }) {
|
|
145
192
|
if (Array.isArray(requested)) {
|
|
146
193
|
return {
|
|
147
194
|
files: [],
|
|
@@ -174,7 +221,7 @@ function resolveAuditScale({ baseFontSize, cssFiles, requested, tokenSourceResul
|
|
|
174
221
|
}
|
|
175
222
|
let text;
|
|
176
223
|
try {
|
|
177
|
-
text =
|
|
224
|
+
text = readSource(filePath);
|
|
178
225
|
} catch {
|
|
179
226
|
continue;
|
|
180
227
|
}
|