stylelint-plugin-rhythmguard 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,6 +65,22 @@ function isUnitStrategy(value) {
65
65
  return value === 'convert' || value === 'exact';
66
66
  }
67
67
 
68
+ function isAutoScaleLiteral(value) {
69
+ return value === 'auto';
70
+ }
71
+
72
+ function isScaleEntryOrAuto(value) {
73
+ return isAutoScaleLiteral(value) || isScaleEntry(value);
74
+ }
75
+
76
+ function isScaleSourceEntry(value) {
77
+ if (isNonEmptyString(value)) {
78
+ return true;
79
+ }
80
+
81
+ return isPlainObject(value) && (isNonEmptyString(value.path) || isNonEmptyString(value.file));
82
+ }
83
+
68
84
  function isScaleEntry(value) {
69
85
  if (typeof value === 'number') {
70
86
  return Number.isFinite(value) && value >= 0;
@@ -237,7 +253,10 @@ function validateSecondaryOptionShapes(result, ruleName, secondaryOptions, schem
237
253
  continue;
238
254
  }
239
255
 
240
- if (descriptor.expectsArray && !Array.isArray(optionValue)) {
256
+ const literalAllowed = Array.isArray(descriptor.allowLiterals)
257
+ && descriptor.allowLiterals.includes(optionValue);
258
+
259
+ if (descriptor.expectsArray && !Array.isArray(optionValue) && !literalAllowed) {
241
260
  valid = false;
242
261
  result.warn(
243
262
  `Invalid value ${stringifyOptionValue(optionValue)} for option "${optionName}" of rule "${ruleName}"`,
@@ -412,6 +431,9 @@ function resolveUnits(options) {
412
431
  }
413
432
 
414
433
  const SCALE_VALIDATION_SCHEMA = Object.freeze({
434
+ allowHairlines: Object.freeze({
435
+ entryValidator: isBoolean,
436
+ }),
415
437
  allowNegative: Object.freeze({
416
438
  entryValidator: isBoolean,
417
439
  }),
@@ -443,9 +465,17 @@ const SCALE_VALIDATION_SCHEMA = Object.freeze({
443
465
  entryValidator: isNonEmptyString,
444
466
  }),
445
467
  scale: Object.freeze({
446
- entryValidator: isScaleEntry,
468
+ allowLiterals: ['auto'],
469
+ entryValidator: isScaleEntryOrAuto,
447
470
  expectsArray: true,
448
471
  }),
472
+ scaleSources: Object.freeze({
473
+ entryValidator: isScaleSourceEntry,
474
+ expectsArray: true,
475
+ }),
476
+ tailwindConfigPath: Object.freeze({
477
+ entryValidator: isNonEmptyString,
478
+ }),
449
479
  unitStrategy: Object.freeze({
450
480
  entryValidator: isUnitStrategy,
451
481
  }),
@@ -487,6 +517,9 @@ const NO_OFFSCALE_TRANSFORM_VALIDATION_SCHEMA = Object.freeze({
487
517
  });
488
518
 
489
519
  const PREFER_TOKEN_VALIDATION_SCHEMA = Object.freeze({
520
+ allowHairlines: Object.freeze({
521
+ entryValidator: isBoolean,
522
+ }),
490
523
  allowNumericScale: Object.freeze({
491
524
  entryValidator: isBoolean,
492
525
  }),
@@ -528,7 +561,12 @@ const PREFER_TOKEN_VALIDATION_SCHEMA = Object.freeze({
528
561
  expectsObject: true,
529
562
  }),
530
563
  scale: Object.freeze({
531
- entryValidator: isScaleEntry,
564
+ allowLiterals: ['auto'],
565
+ entryValidator: isScaleEntryOrAuto,
566
+ expectsArray: true,
567
+ }),
568
+ scaleSources: Object.freeze({
569
+ entryValidator: isScaleSourceEntry,
532
570
  expectsArray: true,
533
571
  }),
534
572
  tailwindConfigPath: Object.freeze({
@@ -578,6 +616,7 @@ function buildScaleOptions(rawOptions) {
578
616
  const scaleSelection = resolveScaleSelection(options, DEFAULT_SCALE);
579
617
 
580
618
  return {
619
+ allowHairlines: options.allowHairlines !== false,
581
620
  allowNegative: options.allowNegative !== false,
582
621
  allowPercentages: options.allowPercentages !== false,
583
622
  baseFontSize:
@@ -600,6 +639,13 @@ function buildScaleOptions(rawOptions) {
600
639
  propertyGroups: normalizePropertyGroups(options.propertyGroups),
601
640
  propertyScaleOverrides: buildPropertyScaleOverrides(options.propertyScales),
602
641
  scale: scaleSelection.scale,
642
+ scaleAuto: isAutoScaleLiteral(options.scale),
643
+ scaleSources: Array.isArray(options.scaleSources) ? options.scaleSources : [],
644
+ tailwindConfigPath:
645
+ typeof options.tailwindConfigPath === 'string' && options.tailwindConfigPath.length > 0
646
+ ? options.tailwindConfigPath
647
+ : null,
648
+ tokenPatternExplicit: typeof options.tokenPattern === 'string' && options.tokenPattern.length > 0,
603
649
  tokenFunctions: Array.isArray(options.tokenFunctions)
604
650
  ? options.tokenFunctions.map((value) => String(value).toLowerCase())
605
651
  : ['var', 'theme', 'token'],
@@ -617,6 +663,7 @@ function buildTokenOptions(rawOptions) {
617
663
  const scaleSelection = resolveScaleSelection(options, DEFAULT_SCALE);
618
664
 
619
665
  return {
666
+ allowHairlines: options.allowHairlines !== false,
620
667
  allowNumericScale: options.allowNumericScale === true,
621
668
  baseFontSize:
622
669
  typeof options.baseFontSize === 'number' &&
@@ -637,6 +684,9 @@ function buildTokenOptions(rawOptions) {
637
684
  propertyGroups: normalizePropertyGroups(options.propertyGroups),
638
685
  propertyScaleOverrides: buildPropertyScaleOverrides(options.propertyScales),
639
686
  scale: scaleSelection.scale,
687
+ scaleAuto: isAutoScaleLiteral(options.scale),
688
+ scaleSources: Array.isArray(options.scaleSources) ? options.scaleSources : [],
689
+ tokenPatternExplicit: typeof options.tokenPattern === 'string' && options.tokenPattern.length > 0,
640
690
  tailwindConfigPath:
641
691
  typeof options.tailwindConfigPath === 'string' && options.tailwindConfigPath.length > 0
642
692
  ? options.tailwindConfigPath
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { parseLengthToken, toPx } = require('./length');
7
+ const { buildEffectiveTokenMap } = require('./token-map');
8
+ const { parseTokenSources } = require('./token-sources');
9
+ const { getScalePreset } = require('../presets/scales');
10
+
11
+ // Matches the audit default so lint and audit agree on what a spacing token is.
12
+ const DEFAULT_AUTO_TOKEN_PATTERN = '(^--|-)(?<!letter-)(?<!word-)(space|spacing)(-|$)';
13
+ // Tailwind v4 defines one base (`--spacing: 0.25rem`) and derives utilities by multiplying it.
14
+ const TAILWIND_BASE_TOKENS = new Set(['--spacing', '--space']);
15
+ const TAILWIND_SPACING_MULTIPLIERS = [
16
+ 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96,
17
+ ];
18
+ const FALLBACK_PRESET = 'rhythmic-4';
19
+ // Zero plus at least three distinct token values; a one- or two-token scale is worse than the default.
20
+ const MIN_INFERRED_SCALE_LENGTH = 4;
21
+ const RC_FILE = '.rhythmguardrc.json';
22
+
23
+ const sourceCache = new Map();
24
+
25
+ function pxValuesFromKeys(keys, baseFontSize) {
26
+ const values = new Set([0]);
27
+
28
+ for (const key of keys) {
29
+ const parsed = parseLengthToken(String(key));
30
+ if (!parsed) {
31
+ continue;
32
+ }
33
+
34
+ const px = toPx(Math.abs(parsed.number), parsed.unit || 'px', baseFontSize);
35
+ if (px !== null && Number.isFinite(px)) {
36
+ values.add(px);
37
+ }
38
+ }
39
+
40
+ return Array.from(values).sort((a, b) => a - b);
41
+ }
42
+
43
+ function normalizeSource(source, baseDir) {
44
+ if (typeof source === 'string') {
45
+ return { format: 'auto', path: path.resolve(baseDir, source) };
46
+ }
47
+
48
+ if (source && typeof source === 'object') {
49
+ const rawPath = source.path || source.file;
50
+ if (typeof rawPath !== 'string') {
51
+ return null;
52
+ }
53
+
54
+ return {
55
+ format: typeof source.format === 'string' ? source.format : 'auto',
56
+ path: path.resolve(source.baseDir || baseDir, rawPath),
57
+ };
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ function cacheKey(sources) {
64
+ return sources
65
+ .map((source) => {
66
+ let mtime = 'missing';
67
+ try {
68
+ mtime = String(fs.statSync(source.path).mtimeMs);
69
+ } catch {
70
+ // missing file: key still changes when it appears
71
+ }
72
+ return `${source.path}|${source.format}|${mtime}`;
73
+ })
74
+ .join('\n');
75
+ }
76
+
77
+ function scaleFromSources(sources, baseFontSize) {
78
+ const normalized = sources.map((source) => normalizeSource(source, process.cwd())).filter(Boolean);
79
+ if (normalized.length === 0) {
80
+ return null;
81
+ }
82
+
83
+ const key = `${baseFontSize}\n${cacheKey(normalized)}`;
84
+ if (sourceCache.has(key)) {
85
+ return sourceCache.get(key);
86
+ }
87
+
88
+ const parsed = parseTokenSources({ baseFontSize, sources: normalized, tokenKind: 'spacing' });
89
+ const keys = [];
90
+ for (const definition of parsed.definitions.values()) {
91
+ keys.push(...definition.normalizedValues);
92
+ }
93
+
94
+ const scale = pxValuesFromKeys(keys, baseFontSize);
95
+ const outcome = scale.length >= MIN_INFERRED_SCALE_LENGTH
96
+ ? {
97
+ files: parsed.sources.map((source) => source.file),
98
+ scale,
99
+ tokenCount: parsed.definitions.size,
100
+ warnings: parsed.warnings,
101
+ }
102
+ : null;
103
+
104
+ sourceCache.set(key, outcome);
105
+ return outcome;
106
+ }
107
+
108
+ /** Build a px scale from token definitions as produced by token-sources.js / contract.js. */
109
+ function scaleFromDefinitions(definitions, baseFontSize = 16) {
110
+ const keys = [];
111
+ const baseKeys = [];
112
+ for (const definition of definitions.values()) {
113
+ if (TAILWIND_BASE_TOKENS.has(definition.token)) {
114
+ baseKeys.push(...definition.normalizedValues);
115
+ continue;
116
+ }
117
+ keys.push(...definition.normalizedValues);
118
+ }
119
+ const scale = expandTailwindBase(pxValuesFromKeys(keys, baseFontSize), baseKeys, baseFontSize);
120
+ return scale.length >= MIN_INFERRED_SCALE_LENGTH ? scale : null;
121
+ }
122
+
123
+ /** Merge Tailwind-style base multiples into a scale when a bare --spacing/--space base is defined. */
124
+ function expandTailwindBase(scale, baseKeys, baseFontSize) {
125
+ const bases = pxValuesFromKeys(baseKeys, baseFontSize).filter((value) => value > 0);
126
+ if (bases.length === 0) {
127
+ return scale;
128
+ }
129
+ const values = new Set(scale);
130
+ for (const base of bases) {
131
+ for (const multiplier of TAILWIND_SPACING_MULTIPLIERS) {
132
+ values.add(Math.round(base * multiplier * 1000) / 1000);
133
+ }
134
+ }
135
+ return Array.from(values).sort((a, b) => a - b);
136
+ }
137
+
138
+ function rcTokenSources(cwd) {
139
+ const rcPath = path.join(cwd, RC_FILE);
140
+ if (!fs.existsSync(rcPath)) {
141
+ return [];
142
+ }
143
+
144
+ let config;
145
+ try {
146
+ config = JSON.parse(fs.readFileSync(rcPath, 'utf8'));
147
+ } catch {
148
+ return [];
149
+ }
150
+
151
+ const audit = config && typeof config === 'object' ? config.audit : null;
152
+ const sources = audit && Array.isArray(audit.tokenSources) ? audit.tokenSources : [];
153
+ const baseDir = path.dirname(rcPath);
154
+
155
+ return sources
156
+ .map((source) => normalizeSource(source, baseDir))
157
+ .filter(Boolean);
158
+ }
159
+
160
+ function scaleFromTokenMap(map, baseFontSize) {
161
+ const keys = [];
162
+ const baseKeys = [];
163
+ for (const [key, reference] of Object.entries(map)) {
164
+ const name = String(reference).match(/^var\((--[\w-]+)\)$/);
165
+ if (name && TAILWIND_BASE_TOKENS.has(name[1])) {
166
+ baseKeys.push(key);
167
+ } else {
168
+ keys.push(key);
169
+ }
170
+ }
171
+ const scale = expandTailwindBase(pxValuesFromKeys(keys, baseFontSize), baseKeys, baseFontSize);
172
+ return scale.length >= MIN_INFERRED_SCALE_LENGTH ? scale : null;
173
+ }
174
+
175
+ /**
176
+ * Resolve `scale: "auto"`. First matching source wins; sources are not merged so
177
+ * the provenance is a single file list or the stylesheet.
178
+ */
179
+ function resolveAutoScale({
180
+ baseFontSize = 16,
181
+ root,
182
+ scaleSources = [],
183
+ tailwindConfigPath = null,
184
+ tokenPattern = DEFAULT_AUTO_TOKEN_PATTERN,
185
+ } = {}) {
186
+ const fromOption = scaleFromSources(scaleSources, baseFontSize);
187
+ if (fromOption) {
188
+ return { source: 'scaleSources', ...fromOption };
189
+ }
190
+
191
+ const fromRc = scaleFromSources(rcTokenSources(process.cwd()), baseFontSize);
192
+ if (fromRc) {
193
+ return { source: 'rhythmguardrc', ...fromRc };
194
+ }
195
+
196
+ let tokenRegex;
197
+ try {
198
+ tokenRegex = new RegExp(tokenPattern);
199
+ } catch {
200
+ tokenRegex = new RegExp(DEFAULT_AUTO_TOKEN_PATTERN);
201
+ }
202
+
203
+ if (root) {
204
+ const stylesheetMap = buildEffectiveTokenMap({
205
+ options: { baseFontSize, tokenMap: {}, tokenMapFromCssCustomProperties: true },
206
+ root,
207
+ tokenRegex,
208
+ });
209
+ const scale = scaleFromTokenMap(stylesheetMap, baseFontSize);
210
+ if (scale) {
211
+ return { files: [], scale, source: 'stylesheet', tokenCount: scale.length - 1, warnings: [] };
212
+ }
213
+ }
214
+
215
+ if (tailwindConfigPath) {
216
+ const tailwindMap = buildEffectiveTokenMap({
217
+ options: {
218
+ baseFontSize,
219
+ tailwindConfigPath,
220
+ tokenMap: {},
221
+ tokenMapFromTailwindSpacing: true,
222
+ },
223
+ root,
224
+ tokenRegex,
225
+ });
226
+ const scale = scaleFromTokenMap(tailwindMap, baseFontSize);
227
+ if (scale) {
228
+ return { files: [tailwindConfigPath], scale, source: 'tailwind', tokenCount: scale.length - 1, warnings: [] };
229
+ }
230
+ }
231
+
232
+ return {
233
+ files: [],
234
+ preset: FALLBACK_PRESET,
235
+ scale: getScalePreset(FALLBACK_PRESET),
236
+ source: 'fallback',
237
+ tokenCount: 0,
238
+ warnings: [],
239
+ };
240
+ }
241
+
242
+ function autoScaleFallbackNote(inference) {
243
+ if (!inference || inference.source !== 'fallback') {
244
+ return '';
245
+ }
246
+
247
+ return `No spacing tokens were found for scale "auto"; using preset "${inference.preset}".`;
248
+ }
249
+
250
+ module.exports = {
251
+ DEFAULT_AUTO_TOKEN_PATTERN,
252
+ autoScaleFallbackNote,
253
+ resolveAutoScale,
254
+ scaleFromDefinitions,
255
+ };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { parseTokenValueLength } = require('./token-sources');
4
+
3
5
  const { spawnSync } = require('node:child_process');
4
6
  const fs = require('node:fs');
5
7
  const path = require('node:path');
@@ -43,7 +45,7 @@ function addLengthValueMapping(map, rawLength, tokenReference, baseFontSize) {
43
45
  return;
44
46
  }
45
47
 
46
- const parsed = parseLengthToken(rawLength);
48
+ const parsed = parseTokenValueLength(rawLength);
47
49
  if (!parsed) {
48
50
  return;
49
51
  }
@@ -196,7 +198,7 @@ function mergeTokenMapFromCssCustomProperties({
196
198
  return;
197
199
  }
198
200
 
199
- const parsed = parseLengthToken(decl.value);
201
+ const parsed = parseTokenValueLength(decl.value);
200
202
  if (!parsed || parsed.number === 0) {
201
203
  return;
202
204
  }
@@ -31,7 +31,8 @@ const TOKEN_KIND_PATTERNS = Object.freeze({
31
31
  radius: /^--radius-/,
32
32
  motion: /^--(?:motion|duration|delay|ease|easing)-/,
33
33
  size: /^--(?:size|width|height|container)-/,
34
- spacing: /^--(?:space|spacing)-/,
34
+ // Anchored or prefixed (--spacing-4, --lb-spacing-md, bare Tailwind v4 --spacing), never letter-/word-spacing.
35
+ spacing: /(?:^--|-)(?<!letter-)(?<!word-)(?:space|spacing)(?:-|$)/,
35
36
  typography: /^--(?:font|font-size|line-height|leading|tracking|typography)-/,
36
37
  });
37
38
 
@@ -377,6 +378,34 @@ function addDefinition(definitions, {
377
378
  definitions.set(token, entry);
378
379
  }
379
380
 
381
+ const CALC_LENGTH_TIMES_VAR = /^calc\(\s*(-?[\d.]+(?:px|rem|em)?)\s*\*\s*var\([^()]*\)\s*\)$/i;
382
+ const CALC_VAR_TIMES_LENGTH = /^calc\(\s*var\([^()]*\)\s*\*\s*(-?[\d.]+(?:px|rem|em)?)\s*\)$/i;
383
+
384
+ /**
385
+ * Parse the length a token value carries. Accepts plain lengths and the
386
+ * `calc(<length> * var(--factor))` form design systems use for scaling
387
+ * (Radix Themes: `--space-1: calc(4px * var(--scaling))`).
388
+ */
389
+ function parseTokenValueLength(value) {
390
+ if (typeof value !== 'string') {
391
+ return null;
392
+ }
393
+
394
+ const trimmed = value.trim();
395
+ const direct = parseLengthToken(trimmed);
396
+ if (direct) {
397
+ return direct;
398
+ }
399
+
400
+ const match = trimmed.match(CALC_LENGTH_TIMES_VAR) || trimmed.match(CALC_VAR_TIMES_LENGTH);
401
+ if (!match) {
402
+ return null;
403
+ }
404
+
405
+ const inner = parseLengthToken(match[1]);
406
+ return inner ? { ...inner, raw: trimmed } : null;
407
+ }
408
+
380
409
  function getNormalizedValueKeys(value, baseFontSize = 16) {
381
410
  if (value === null || value === undefined) {
382
411
  return [];
@@ -388,7 +417,7 @@ function getNormalizedValueKeys(value, baseFontSize = 16) {
388
417
  }
389
418
 
390
419
  const keys = new Set([raw]);
391
- const parsed = parseLengthToken(raw);
420
+ const parsed = parseTokenValueLength(raw);
392
421
  if (!parsed) {
393
422
  return Array.from(keys);
394
423
  }
@@ -417,4 +446,5 @@ module.exports = {
417
446
  normalizeTokenKind,
418
447
  normalizeTokenSourceFormat,
419
448
  parseTokenSources,
449
+ parseTokenValueLength,
420
450
  };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Compile-time consumer of the public declarations.
3
+ * `npm run typecheck` fails if any published export or type drifts.
4
+ * Nothing here runs; it only has to type-check.
5
+ */
6
+ import plugin, { audit, configs, eslint, presets, rules } from 'stylelint-plugin-rhythmguard';
7
+ import type {
8
+ AuditContractReport,
9
+ AuditReport,
10
+ RhythmguardRuleOptions,
11
+ RhythmguardStylelintConfig,
12
+ } from 'stylelint-plugin-rhythmguard';
13
+ import { createAuditReport, toAuditContractReport } from 'stylelint-plugin-rhythmguard/audit';
14
+ import eslintPlugin from 'stylelint-plugin-rhythmguard/eslint';
15
+ import { getScalePreset, listScalePresetNames } from 'stylelint-plugin-rhythmguard/presets';
16
+ import recommended from 'stylelint-plugin-rhythmguard/configs/recommended';
17
+ import embed from 'stylelint-plugin-rhythmguard/configs/embed';
18
+ import reactTailwind from 'stylelint-plugin-rhythmguard/configs/react-tailwind';
19
+ import useScale, { ruleName as useScaleName } from 'stylelint-plugin-rhythmguard/rules/use-scale';
20
+
21
+ const pluginConfigs: readonly RhythmguardStylelintConfig[] = [
22
+ plugin.configs.recommended,
23
+ plugin.configs.strict,
24
+ plugin.configs.tailwind,
25
+ plugin.configs.expanded,
26
+ plugin.configs.logical,
27
+ plugin.configs.migration,
28
+ plugin.configs.motion,
29
+ plugin.configs['react-tailwind'],
30
+ configs.recommended,
31
+ plugin.configs.embed,
32
+ embed,
33
+ recommended,
34
+ reactTailwind,
35
+ ];
36
+
37
+ const ruleOptions: RhythmguardRuleOptions = {
38
+ preset: 'rhythmic-4',
39
+ scale: [0, 4, 8, 12, 16],
40
+ tokenMap: { '4px': 'var(--space-1)' },
41
+ tokenMapFromCssCustomProperties: true,
42
+ };
43
+
44
+ const autoScaleOptions: RhythmguardRuleOptions = {
45
+ scale: 'auto',
46
+ scaleSources: ['./tokens.json', { path: './theme.css', format: 'css' }],
47
+ tailwindConfigPath: './tailwind.config.mjs',
48
+ };
49
+
50
+ const useScaleRule = rules['rhythmguard/use-scale'];
51
+ const fixable: boolean | undefined = useScaleRule.meta.fixable;
52
+ const docUrl: string | undefined = useScale.meta.url;
53
+ const name: string = useScaleName;
54
+
55
+ const presetNames: string[] = listScalePresetNames();
56
+ const preset = getScalePreset('rhythmic-4');
57
+ const presetsAgain: string[] = presets.listScalePresetNames();
58
+
59
+ const eslintRule = eslint.rules['tailwind-class-use-scale'];
60
+ const eslintRuleAgain = eslintPlugin.rules['tailwind-class-use-motion-scale'];
61
+
62
+ async function runAudit(): Promise<AuditContractReport> {
63
+ const report: AuditReport = await createAuditReport({ dir: './src', noConfig: true });
64
+ const viaPlugin: AuditReport = await audit.createAuditReport({ dir: './src' });
65
+ void viaPlugin;
66
+ return toAuditContractReport(report);
67
+ }
68
+
69
+ void pluginConfigs;
70
+ void ruleOptions;
71
+ void autoScaleOptions;
72
+ void fixable;
73
+ void docUrl;
74
+ void name;
75
+ void presetNames;
76
+ void preset;
77
+ void presetsAgain;
78
+ void eslintRule;
79
+ void eslintRuleAgain;
80
+ void runAudit;
package/types/audit.d.ts CHANGED
@@ -23,22 +23,29 @@ export interface AuditTokenSource {
23
23
  export interface AuditOptions {
24
24
  baseFontSize?: number;
25
25
  baselinePath?: string;
26
- config?: string;
26
+ /** Path to a `.rhythmguardrc.json`-style config. Defaults to `.rhythmguardrc.json` in `cwd`. */
27
+ configPath?: string;
27
28
  dir?: string;
28
29
  failOnNewDrift?: boolean;
29
- format?: "json" | "json-v1" | "markdown" | "text" | "html";
30
+ format?: "json" | "json-v1" | "markdown" | "text" | "html" | "github";
30
31
  ignorePath?: string;
31
32
  ignorePatterns?: string[];
32
33
  includeMotion?: boolean;
33
34
  maxFindings?: number;
34
35
  minCleanliness?: number;
35
- output?: string;
36
- scale?: Array<number | string>;
36
+ /** Skip config discovery entirely (equivalent to `--no-config`). */
37
+ noConfig?: boolean;
38
+ /** Write rendered output to this file instead of stdout (equivalent to `--output`). */
39
+ outputPath?: string;
40
+ /** Explicit scale values, or `"auto"` to infer from token sources, then scanned CSS, then the default. */
41
+ scale?: Array<number | string> | "auto";
37
42
  since?: string;
38
43
  sinceBaseline?: boolean;
39
44
  staged?: boolean;
40
45
  tokenCandidateMinCount?: number;
41
46
  tokenKind?: AuditTokenKind;
47
+ /** Format applied to `tokenSources` entries that do not declare their own. */
48
+ tokenSourceFormat?: AuditTokenSourceFormat | "auto";
42
49
  tokenSources?: AuditTokenSource[];
43
50
  writeBaseline?: boolean;
44
51
  }
@@ -81,8 +88,19 @@ export interface AuditBaselineComparison {
81
88
  [key: string]: unknown;
82
89
  }
83
90
 
91
+ export type AuditScaleSource = "default" | "explicit" | "fallback" | "scanned-css" | "token-sources";
92
+
93
+ export interface AuditScale {
94
+ /** Files the scale was derived from (token sources or scanned stylesheets). Empty for explicit, default and fallback. */
95
+ files: string[];
96
+ source: AuditScaleSource;
97
+ tokenCount: number;
98
+ values: Array<number | string>;
99
+ }
100
+
84
101
  export interface AuditReport {
85
102
  baseline?: AuditBaselineComparison | null;
103
+ scale?: AuditScale | null;
86
104
  config?: string | null;
87
105
  directory: string;
88
106
  findings: {
@@ -106,8 +124,11 @@ export interface AuditContractReport {
106
124
  motion?: unknown;
107
125
  scale: {
108
126
  cleanliness?: unknown;
127
+ files: string[];
109
128
  offScaleValues?: unknown;
129
+ source: AuditScaleSource;
110
130
  tokenOpportunities?: unknown;
131
+ values: Array<number | string> | null;
111
132
  };
112
133
  tokens?: unknown;
113
134
  };
package/types/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export type {
14
14
  RhythmguardRuleOptions,
15
15
  RhythmguardStylelintConfig,
16
16
  ScalePresetMetadata,
17
+ ScaleSource,
17
18
  ScaleValue,
18
19
  } from "./shared";
19
20
  export type {
@@ -22,6 +23,8 @@ export type {
22
23
  AuditFinding,
23
24
  AuditOptions,
24
25
  AuditReport,
26
+ AuditScale,
27
+ AuditScaleSource,
25
28
  AuditScanned,
26
29
  AuditSummary,
27
30
  AuditTokenKind,
@@ -35,10 +38,12 @@ export type { StylelintRuleModule } from "./rule";
35
38
  export interface RhythmguardPlugin extends Array<StylelintRuleModule> {
36
39
  audit: typeof auditApi;
37
40
  configs: {
41
+ embed: RhythmguardStylelintConfig;
38
42
  expanded: RhythmguardStylelintConfig;
39
43
  logical: RhythmguardStylelintConfig;
40
44
  migration: RhythmguardStylelintConfig;
41
45
  motion: RhythmguardStylelintConfig;
46
+ "react-tailwind": RhythmguardStylelintConfig;
42
47
  recommended: RhythmguardStylelintConfig;
43
48
  strict: RhythmguardStylelintConfig;
44
49
  tailwind: RhythmguardStylelintConfig;
package/types/shared.d.ts CHANGED
@@ -2,13 +2,34 @@ export type ScaleValue = number | string;
2
2
 
3
3
  export type RuleSeverity = boolean | "always" | "never";
4
4
 
5
+ export interface ScaleSource {
6
+ /** Directory that a relative `path` resolves from. Defaults to the working directory. */
7
+ baseDir?: string;
8
+ /** Alias for `path`. */
9
+ file?: string;
10
+ /** `auto` (default), `css`, `flat-json`, `style-dictionary`, or `dtcg`. */
11
+ format?: string;
12
+ path?: string;
13
+ }
14
+
5
15
  export interface RhythmguardRuleOptions {
16
+ /** Exempt non-zero lengths of one CSS pixel or less (1px, -1px, 0.5px, 0.0625rem). Default true. */
17
+ allowHairlines?: boolean;
6
18
  baseFontSize?: number;
7
19
  customScale?: ScaleValue[];
8
20
  includeMathFunctions?: boolean;
9
21
  preset?: string;
10
22
  properties?: Array<string | RegExp>;
11
- scale?: ScaleValue[];
23
+ /**
24
+ * Allowed values, or `"auto"` to infer the scale from spacing tokens: `scaleSources`,
25
+ * then `.rhythmguardrc.json` audit token sources, then the linted stylesheet's custom
26
+ * properties, then `tailwindConfigPath`, falling back to the `rhythmic-4` preset.
27
+ */
28
+ scale?: ScaleValue[] | "auto";
29
+ /** Token files consulted first when `scale` is `"auto"`. */
30
+ scaleSources?: Array<string | ScaleSource>;
31
+ /** Tailwind v3 config whose `theme.spacing` feeds `scale: "auto"` and `tokenMapFromTailwindSpacing`. */
32
+ tailwindConfigPath?: string;
12
33
  tokenMap?: Record<string, string>;
13
34
  tokenMapFile?: string;
14
35
  tokenMapFromCssCustomProperties?: boolean;