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.
Files changed (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +5 -1
  3. package/package.json +1 -1
  4. package/src/audit/baseline.js +184 -27
  5. package/src/audit/codemod.js +20 -6
  6. package/src/audit/config.js +8 -29
  7. package/src/audit/contract.js +96 -10
  8. package/src/audit/render-github.js +4 -0
  9. package/src/audit/render-html.js +7 -0
  10. package/src/audit/render-markdown.js +27 -0
  11. package/src/audit/render-text.js +23 -1
  12. package/src/audit/report.js +58 -11
  13. package/src/audit/scan/files.js +137 -25
  14. package/src/audit/scan/stylesheets.js +67 -34
  15. package/src/audit/scan/templates.js +22 -7
  16. package/src/audit/token-chains.js +18 -7
  17. package/src/cli/doctor.js +48 -7
  18. package/src/cli/fix.js +13 -16
  19. package/src/cli/init.js +136 -23
  20. package/src/cli/quickstart.js +34 -8
  21. package/src/core/fs-cache.js +6 -0
  22. package/src/core/options.js +7 -1
  23. package/src/core/scale-inference.js +99 -68
  24. package/src/core/tailwind-class-analysis.js +24 -14
  25. package/src/core/tailwind-motion-analysis.js +12 -3
  26. package/src/core/token-index.js +5 -2
  27. package/src/core/token-source-entries.js +64 -0
  28. package/src/core/token-sources.js +361 -69
  29. package/src/core/value-nodes.js +280 -54
  30. package/src/eslint/literal-spans.js +94 -9
  31. package/src/eslint/rules/tailwind-class-use-scale.js +39 -11
  32. package/src/rules/no-offscale-transform/index.js +33 -13
  33. package/src/rules/prefer-token/index.js +39 -18
  34. package/src/rules/use-motion-scale/index.js +68 -5
  35. package/src/rules/use-scale/index.js +50 -36
  36. package/types/__checks__/eslint-config.ts +37 -0
  37. package/types/audit.d.ts +10 -2
  38. package/types/shared.d.ts +21 -3
@@ -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 = fs.readFileSync(filePath, 'utf8');
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
- const totalFiles = cssFiles.length + templateFiles.length;
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 = totalFiles > 0
274
- ? Math.max(0, Math.round(((totalFiles - filesWithIssues) / totalFiles) * 100))
275
- : 100;
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: cssFiles.length,
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: cssFiles.length,
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: { type: 'object' },
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: { type: 'object' },
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
  ));
@@ -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
  };
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('node:fs');
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 stylelintFindings = collectCssFindings(cssResults);
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
- applyBaselineComparison(report, parsed.baselinePath);
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
- if (parsed.writeBaseline) {
131
- writeBaseline(report, parsed.baselinePath);
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 = fs.readFileSync(filePath, 'utf8');
224
+ text = readSource(filePath);
178
225
  } catch {
179
226
  continue;
180
227
  }