stylelint-plugin-rhythmguard 3.2.0 → 3.4.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 +31 -0
  2. package/CONTRIBUTING.md +19 -3
  3. package/README.md +2 -1
  4. package/package.json +9 -8
  5. package/src/audit/args.js +103 -307
  6. package/src/audit/config.js +1 -1
  7. package/src/audit/contract.js +6 -28
  8. package/src/audit/index.js +4 -7
  9. package/src/audit/render-markdown.js +3 -0
  10. package/src/audit/render-text.js +2 -1
  11. package/src/audit/report.js +26 -19
  12. package/src/audit/scan/files.js +262 -0
  13. package/src/audit/scan/stylesheets.js +275 -0
  14. package/src/audit/scan/templates.js +175 -0
  15. package/src/cli/doctor.js +1 -1
  16. package/src/cli/quickstart.js +5 -2
  17. package/src/{utils → core}/length.js +24 -1
  18. package/src/{utils → core}/options.js +32 -108
  19. package/src/core/scale-inference.js +560 -0
  20. package/src/{utils → core}/token-packages.json +7 -0
  21. package/src/{utils → core}/token-sources.js +155 -17
  22. package/src/{utils/value-utils.js → core/value-nodes.js} +1 -17
  23. package/src/eslint/rules/tailwind-class-use-motion-scale.js +2 -2
  24. package/src/eslint/rules/tailwind-class-use-scale.js +2 -2
  25. package/src/rules/no-offscale-transform/index.js +22 -91
  26. package/src/rules/prefer-token/index.js +15 -73
  27. package/src/rules/report.js +75 -0
  28. package/src/rules/use-motion-scale/index.js +25 -42
  29. package/src/rules/use-scale/index.js +21 -94
  30. package/src/rules/validate.js +132 -0
  31. package/types/audit.d.ts +11 -0
  32. package/src/audit/scan.js +0 -676
  33. package/src/utils/scale-inference.js +0 -351
  34. /package/src/{utils/constants.js → core/css-vocabulary.js} +0 -0
  35. /package/src/{utils → core}/tailwind-class-analysis.js +0 -0
  36. /package/src/{utils → core}/tailwind-motion-analysis.js +0 -0
  37. /package/src/{utils → core}/time.js +0 -0
  38. /package/src/{utils → core}/token-map.js +0 -0
@@ -0,0 +1,560 @@
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 {
9
+ addDefinition,
10
+ collectScssTokens,
11
+ createTokenKindMatcher,
12
+ parseTokenSources,
13
+ parseTokenValueLength,
14
+ } = require('./token-sources');
15
+ const { getScalePreset } = require('../presets/scales');
16
+
17
+ // Matches the audit default so lint and audit agree on what a spacing token is.
18
+ const DEFAULT_AUTO_TOKEN_PATTERN = '(^--|-)(?<!letter-)(?<!word-)(space|spacing|spacer)(-|$)';
19
+ // Tailwind v4 defines one base (`--spacing: 0.25rem`) and derives utilities by multiplying it.
20
+ const TAILWIND_BASE_TOKENS = new Set(['--spacing', '--space']);
21
+ const TAILWIND_SPACING_MULTIPLIERS = [
22
+ 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,
23
+ ];
24
+ const FALLBACK_PRESET = 'rhythmic-4';
25
+ // Zero plus at least three distinct token values; a one- or two-token scale is worse than the default.
26
+ const MIN_INFERRED_SCALE_LENGTH = 4;
27
+ const RC_FILE = '.rhythmguardrc.json';
28
+
29
+ const sourceCache = new Map();
30
+ const TOKEN_PACKAGES = require('./token-packages.json').packages;
31
+
32
+ /**
33
+ * Installed design-token packages that ship a spacing scale (allowlist in
34
+ * token-packages.json). Resolved from the project, so only what the project
35
+ * actually depends on is read. Returns token-source entries.
36
+ */
37
+ function readDirectDependencies(dir) {
38
+ try {
39
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
40
+ return new Set([
41
+ ...Object.keys(pkg.dependencies || {}),
42
+ ...Object.keys(pkg.devDependencies || {}),
43
+ ...Object.keys(pkg.peerDependencies || {}),
44
+ ]);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Project roots from cwd up to the repository boundary (first directory holding
52
+ * .git), each with the dependencies it declares directly. Hoisted monorepo
53
+ * installs are found by walking up; a stray global node_modules is never
54
+ * consulted because the walk stops at the repository.
55
+ */
56
+ /**
57
+ * Editors and pre-commit hooks lint one file at a time, and each lint asked
58
+ * the filesystem the same questions: which package.json files sit between
59
+ * cwd and the repository, what they declare, and whether a token package is
60
+ * installed. The answers change only when one of those files changes, so
61
+ * the result is cached per cwd and revalidated by mtime, which costs a stat
62
+ * per file instead of a read, a JSON parse and a directory walk.
63
+ */
64
+ const discoveryCache = new Map();
65
+
66
+ function fileStamp(file) {
67
+ try {
68
+ return fs.statSync(file).mtimeMs;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function cachedByFiles(cacheKey, compute) {
75
+ const cached = discoveryCache.get(cacheKey);
76
+ if (cached && cached.stamps.every(([file, stamp]) => fileStamp(file) === stamp)) {
77
+ return cached.value;
78
+ }
79
+ const consulted = [];
80
+ const value = compute((file) => consulted.push([file, fileStamp(file)]));
81
+ discoveryCache.set(cacheKey, { stamps: consulted, value });
82
+ return value;
83
+ }
84
+
85
+ function projectRoots(cwd, consult = () => {}) {
86
+ const roots = [];
87
+ let current = path.resolve(cwd);
88
+ for (;;) {
89
+ consult(path.join(current, 'package.json'));
90
+ const direct = readDirectDependencies(current);
91
+ if (direct) {
92
+ roots.push({ dir: current, direct });
93
+ }
94
+ const parent = path.dirname(current);
95
+ if (fs.existsSync(path.join(current, '.git')) || parent === current) {
96
+ break;
97
+ }
98
+ current = parent;
99
+ }
100
+ return roots;
101
+ }
102
+
103
+ function discoverTokenPackages(cwd = process.cwd()) {
104
+ return cachedByFiles(`packages:${path.resolve(cwd)}`, (consult) => discoverTokenPackagesUncached(cwd, consult));
105
+ }
106
+
107
+ function discoverTokenPackagesUncached(cwd, consult) {
108
+ const sources = [];
109
+ const roots = projectRoots(cwd, consult);
110
+ for (const entry of TOKEN_PACKAGES) {
111
+ // Only packages the project depends on directly count. A transitive
112
+ // tailwindcss (for example via stylelint-config-tailwindcss) must not hand
113
+ // a non-Tailwind project the Tailwind scale.
114
+ const owner = roots.find((root) => root.direct.has(entry.name));
115
+ if (!owner) {
116
+ continue;
117
+ }
118
+ const candidates = roots.map((candidate) => path.join(candidate.dir, 'node_modules', entry.name));
119
+ for (const dir of candidates) {
120
+ consult(path.join(dir, 'package.json'));
121
+ }
122
+ const root = candidates.find((dir) => fs.existsSync(path.join(dir, 'package.json')));
123
+ if (!root) {
124
+ continue;
125
+ }
126
+ for (const file of entry.files) {
127
+ const resolved = path.join(root, file);
128
+ consult(resolved);
129
+ if (fs.existsSync(resolved)) {
130
+ sources.push({
131
+ format: 'auto',
132
+ package: entry.name,
133
+ path: resolved,
134
+ ...(entry.tokenPattern ? { tokenPattern: entry.tokenPattern } : {}),
135
+ });
136
+ }
137
+ }
138
+ }
139
+ return sources;
140
+ }
141
+
142
+ function pxValuesFromKeys(keys, baseFontSize) {
143
+ const values = new Set([0]);
144
+
145
+ for (const key of keys) {
146
+ const parsed = parseLengthToken(String(key));
147
+ if (!parsed) {
148
+ continue;
149
+ }
150
+
151
+ const px = toPx(Math.abs(parsed.number), parsed.unit || 'px', baseFontSize);
152
+ if (px !== null && Number.isFinite(px)) {
153
+ values.add(px);
154
+ }
155
+ }
156
+
157
+ return Array.from(values).sort((a, b) => a - b);
158
+ }
159
+
160
+ function normalizeSource(source, baseDir) {
161
+ if (typeof source === 'string') {
162
+ return { format: 'auto', path: path.resolve(baseDir, source) };
163
+ }
164
+
165
+ if (source && typeof source === 'object') {
166
+ const rawPath = source.path || source.file;
167
+ if (typeof rawPath !== 'string') {
168
+ return null;
169
+ }
170
+
171
+ return {
172
+ format: typeof source.format === 'string' ? source.format : 'auto',
173
+ path: path.resolve(source.baseDir || baseDir, rawPath),
174
+ ...(source.tokenPattern ? { tokenPattern: source.tokenPattern } : {}),
175
+ };
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ function cacheKey(sources) {
182
+ return sources
183
+ .map((source) => {
184
+ let mtime = 'missing';
185
+ try {
186
+ mtime = String(fs.statSync(source.path).mtimeMs);
187
+ } catch {
188
+ // missing file: key still changes when it appears
189
+ }
190
+ return `${source.path}|${source.format}|${source.tokenPattern || ''}|${mtime}`;
191
+ })
192
+ .join('\n');
193
+ }
194
+
195
+ function scaleFromSources(sources, baseFontSize) {
196
+ const normalized = sources.map((source) => normalizeSource(source, process.cwd())).filter(Boolean);
197
+ if (normalized.length === 0) {
198
+ return null;
199
+ }
200
+
201
+ const key = `${baseFontSize}\n${cacheKey(normalized)}`;
202
+ if (sourceCache.has(key)) {
203
+ return sourceCache.get(key);
204
+ }
205
+
206
+ const parsed = parseTokenSources({ baseFontSize, sources: normalized, tokenKind: 'spacing' });
207
+ // scaleFromDefinitions also expands a bare Tailwind --spacing base into its multiples.
208
+ const scale = scaleFromDefinitions(parsed.definitions, baseFontSize);
209
+ const outcome = scale
210
+ ? {
211
+ files: parsed.sources.map((source) => source.file),
212
+ scale,
213
+ tokenCount: parsed.definitions.size,
214
+ warnings: parsed.warnings,
215
+ }
216
+ : null;
217
+
218
+ sourceCache.set(key, outcome);
219
+ return outcome;
220
+ }
221
+
222
+ /**
223
+ * Build a px scale from token definitions. Root-level declarations win: when
224
+ * the definitions declared in `:root`, `html` or `@theme` form a scale on
225
+ * their own, component-local variables (`--chip-spacing: 3px` inside `.chip`)
226
+ * are left out, because they are a component's parameters, not the project's
227
+ * scale (issue #54). When the root does not carry a scale, everything counts
228
+ * and the plausibility check is the backstop. Returns the values and the
229
+ * definitions that produced them, so provenance can name only those files.
230
+ */
231
+ function inferScaleFromDefinitions(definitions, baseFontSize = 16) {
232
+ const rootOnly = new Map();
233
+ for (const [token, definition] of definitions) {
234
+ if (!definition.scopes || definition.scopes.has('root')) {
235
+ rootOnly.set(token, definition);
236
+ }
237
+ }
238
+ if (rootOnly.size > 0 && rootOnly.size < definitions.size) {
239
+ const fromRoot = scaleFromAllDefinitions(rootOnly, baseFontSize);
240
+ if (fromRoot) {
241
+ return { definitions: rootOnly, values: fromRoot };
242
+ }
243
+ }
244
+ const values = scaleFromAllDefinitions(definitions, baseFontSize);
245
+ return values ? { definitions, values } : null;
246
+ }
247
+
248
+ function scaleFromDefinitions(definitions, baseFontSize = 16) {
249
+ const inferred = inferScaleFromDefinitions(definitions, baseFontSize);
250
+ return inferred ? inferred.values : null;
251
+ }
252
+
253
+ function scaleFromAllDefinitions(definitions, baseFontSize) {
254
+ const keys = [];
255
+ const baseKeys = [];
256
+ for (const definition of definitions.values()) {
257
+ if (TAILWIND_BASE_TOKENS.has(definition.token)) {
258
+ baseKeys.push(...definition.normalizedValues);
259
+ continue;
260
+ }
261
+ keys.push(...definition.normalizedValues);
262
+ }
263
+ const scale = expandTailwindBase(pxValuesFromKeys(keys, baseFontSize), baseKeys, baseFontSize);
264
+ return scale.length >= MIN_INFERRED_SCALE_LENGTH ? scale : null;
265
+ }
266
+
267
+ /** The scope of a declaration node: the AST-side twin of customPropertyDeclarations. */
268
+ function scopeOfNode(decl) {
269
+ for (let node = decl.parent; node; node = node.parent) {
270
+ if (node.type === 'rule') {
271
+ return String(node.selector).split(',').every((selector) => ROOT_SELECTOR.test(selector.trim())) ? 'root' : 'component';
272
+ }
273
+ if (node.type === 'atrule') {
274
+ const name = String(node.name).toLowerCase();
275
+ if (name === 'theme') return 'root';
276
+ if (!TRANSPARENT_AT_RULES.has(name)) return 'component';
277
+ }
278
+ }
279
+ return 'root';
280
+ }
281
+
282
+ const ROOT_SELECTOR = /^(?::root|html|:host|:(?:where|is)\(\s*(?::root|html)\s*\))$/i;
283
+ const TRANSPARENT_AT_RULES = new Set(['media', 'supports', 'layer', 'container', 'scope', 'document']);
284
+
285
+ /** Token definitions declared in the linted stylesheet itself: custom properties with their scope, plus Sass variables and maps. */
286
+ function stylesheetDefinitions(root, tokenRegex, baseFontSize) {
287
+ const definitions = new Map();
288
+ root.walkDecls((decl) => {
289
+ const prop = decl.prop.toLowerCase();
290
+ if (!prop.startsWith('--') || !tokenRegex.test(prop)) {
291
+ return;
292
+ }
293
+ const parsed = parseTokenValueLength(decl.value);
294
+ if (!parsed || parsed.number === 0) {
295
+ return;
296
+ }
297
+ addDefinition(definitions, { baseFontSize, file: 'stylesheet', scope: scopeOfNode(decl), source: 'stylesheet', token: decl.prop, value: decl.value });
298
+ });
299
+ for (const sassToken of sassTokensFromRoot(root)) {
300
+ addDefinition(definitions, { baseFontSize, file: 'stylesheet', scope: 'root', source: 'stylesheet', token: sassToken.token, value: sassToken.value });
301
+ }
302
+ return definitions;
303
+ }
304
+
305
+ /**
306
+ * Merge Tailwind-style base multiples into a scale when a bare --spacing/--space
307
+ * base is defined. Only the first base found is expanded: a project that ships
308
+ * several theme files with different bases (shadcn/ui) has one active base at a
309
+ * time, and a union of ladders is a scale nobody designed (issue #89).
310
+ */
311
+ function expandTailwindBase(scale, baseKeys, baseFontSize) {
312
+ const base = firstPositivePx(baseKeys, baseFontSize);
313
+ if (base === null) {
314
+ return scale;
315
+ }
316
+ const values = new Set(scale);
317
+ for (const multiplier of TAILWIND_SPACING_MULTIPLIERS) {
318
+ values.add(Math.round(base * multiplier * 1000) / 1000);
319
+ }
320
+ return Array.from(values).sort((a, b) => a - b);
321
+ }
322
+
323
+ function firstPositivePx(keys, baseFontSize) {
324
+ for (const key of keys) {
325
+ const parsed = parseLengthToken(String(key));
326
+ if (!parsed) {
327
+ continue;
328
+ }
329
+ const px = toPx(Math.abs(parsed.number), parsed.unit || 'px', baseFontSize);
330
+ if (px !== null && Number.isFinite(px) && px > 0) {
331
+ return px;
332
+ }
333
+ }
334
+ return null;
335
+ }
336
+
337
+ function rcTokenSources(cwd) {
338
+ const rcPath = path.join(cwd, RC_FILE);
339
+ return cachedByFiles(`rc:${rcPath}`, (consult) => {
340
+ consult(rcPath);
341
+ return rcTokenSourcesUncached(rcPath);
342
+ });
343
+ }
344
+
345
+ function rcTokenSourcesUncached(rcPath) {
346
+ if (!fs.existsSync(rcPath)) {
347
+ return [];
348
+ }
349
+
350
+ let config;
351
+ try {
352
+ config = JSON.parse(fs.readFileSync(rcPath, 'utf8'));
353
+ } catch {
354
+ return [];
355
+ }
356
+
357
+ const audit = config && typeof config === 'object' ? config.audit : null;
358
+ const sources = audit && Array.isArray(audit.tokenSources) ? audit.tokenSources : [];
359
+ const baseDir = path.dirname(rcPath);
360
+
361
+ return sources
362
+ .map((source) => normalizeSource(source, baseDir))
363
+ .filter(Boolean);
364
+ }
365
+
366
+ /**
367
+ * Sass variables and maps declared in the linted stylesheet itself (postcss-scss
368
+ * exposes them as declarations whose prop starts with `$`). Evaluated with the
369
+ * same collector the audit uses; component variables such as $dropdown-spacer
370
+ * are excluded by the anchored name rule.
371
+ */
372
+ function sassTokensFromRoot(root) {
373
+ const lines = [];
374
+ root.walkDecls((decl) => {
375
+ if (typeof decl.prop === 'string' && decl.prop.startsWith('$')) {
376
+ lines.push(`${decl.prop}: ${decl.value};`);
377
+ }
378
+ });
379
+ if (lines.length === 0) {
380
+ return [];
381
+ }
382
+ return collectScssTokens(lines.join('\n'), createTokenKindMatcher('spacing'));
383
+ }
384
+
385
+ function scaleFromTokenMap(map, baseFontSize, extraKeys = []) {
386
+ const keys = [...extraKeys];
387
+ const baseKeys = [];
388
+ for (const [key, reference] of Object.entries(map)) {
389
+ const name = String(reference).match(/^var\((--[\w-]+)\)$/);
390
+ if (name && TAILWIND_BASE_TOKENS.has(name[1])) {
391
+ baseKeys.push(key);
392
+ } else {
393
+ keys.push(key);
394
+ }
395
+ }
396
+ const scale = expandTailwindBase(pxValuesFromKeys(keys, baseFontSize), baseKeys, baseFontSize);
397
+ return scale.length >= MIN_INFERRED_SCALE_LENGTH ? scale : null;
398
+ }
399
+
400
+ /**
401
+ * Resolve `scale: "auto"`. First matching source wins; sources are not merged so
402
+ * the provenance is a single file list or the stylesheet.
403
+ */
404
+ function resolveAutoScale({
405
+ baseFontSize = 16,
406
+ root,
407
+ scaleSources = [],
408
+ tailwindConfigPath = null,
409
+ tokenPattern = DEFAULT_AUTO_TOKEN_PATTERN,
410
+ } = {}) {
411
+ const fromOption = scaleFromSources(scaleSources, baseFontSize);
412
+ if (fromOption) {
413
+ return { source: 'scaleSources', ...fromOption };
414
+ }
415
+
416
+ const fromRc = scaleFromSources(rcTokenSources(process.cwd()), baseFontSize);
417
+ if (fromRc) {
418
+ return { source: 'rhythmguardrc', ...fromRc };
419
+ }
420
+
421
+ let tokenRegex;
422
+ try {
423
+ tokenRegex = new RegExp(tokenPattern);
424
+ } catch {
425
+ tokenRegex = new RegExp(DEFAULT_AUTO_TOKEN_PATTERN);
426
+ }
427
+
428
+ let rejected = null;
429
+ if (root) {
430
+ const scale = scaleFromDefinitions(stylesheetDefinitions(root, tokenRegex, baseFontSize), baseFontSize);
431
+ if (scale) {
432
+ const assessment = assessScale({ source: 'stylesheet', values: scale });
433
+ if (assessment.plausible) {
434
+ return { files: [], scale, source: 'stylesheet', tokenCount: scale.length - 1, warnings: [] };
435
+ }
436
+ rejected = { reasons: assessment.reasons, source: 'stylesheet', values: scale };
437
+ }
438
+ }
439
+
440
+ if (tailwindConfigPath) {
441
+ const tailwindMap = buildEffectiveTokenMap({
442
+ options: {
443
+ baseFontSize,
444
+ tailwindConfigPath,
445
+ tokenMap: {},
446
+ tokenMapFromTailwindSpacing: true,
447
+ },
448
+ root,
449
+ tokenRegex,
450
+ });
451
+ const scale = scaleFromTokenMap(tailwindMap, baseFontSize);
452
+ if (scale) {
453
+ return { files: [tailwindConfigPath], scale, source: 'tailwind', tokenCount: scale.length - 1, warnings: [] };
454
+ }
455
+ }
456
+
457
+ const fromPackages = scaleFromSources(discoverTokenPackages(process.cwd()), baseFontSize);
458
+ if (fromPackages) {
459
+ return { source: 'token-package', ...fromPackages };
460
+ }
461
+
462
+ return fallbackInference(rejected);
463
+ }
464
+
465
+ const TRUSTED_SCALE_SOURCES = new Set(['scaleSources', 'rhythmguardrc', 'token-sources', 'tailwind', 'token-package', 'explicit', 'default']);
466
+ const TOKEN_FILE_PATTERN = /(token|variable|spacing|space|theme|primitive|global|scale|layout)/i;
467
+ const LADDER_STEPS = [2, 3, 4, 5, 8];
468
+ const MIN_ASSESSED_STEPS = 3;
469
+ // A scale with more steps than this must be a near-perfect ladder; otherwise it
470
+ // is a list of every value a codebase happens to use (Semi Design: 42 steps).
471
+ const MAX_LOOSE_STEPS = 24;
472
+
473
+ /**
474
+ * Inference can pick up component-local variables (`--chip-spacing: 3px`) and
475
+ * assemble a "scale" nobody designed. Explicit sources are trusted. An inferred
476
+ * scale is plausible when it has at least three positive steps, is mostly whole
477
+ * pixels, mostly shares a common step (2, 3, 4, 5 or 8), and, past two dozen
478
+ * steps, is a near-perfect ladder. When the source
479
+ * files are known, a set that comes mostly from component files must be a
480
+ * near-perfect ladder to pass. Fallback is never the project's scale.
481
+ */
482
+ function assessScale({ files = null, source, values = [] } = {}) {
483
+ if (source === 'fallback') {
484
+ return { plausible: false, reasons: ['fallback'] };
485
+ }
486
+ if (TRUSTED_SCALE_SOURCES.has(source)) {
487
+ return { plausible: true, reasons: [] };
488
+ }
489
+ const positives = values.map(Number).filter((value) => Number.isFinite(value) && value > 0);
490
+ const integers = positives.filter(Number.isInteger);
491
+ const integerShare = positives.length ? integers.length / positives.length : 0;
492
+ const coherence = positives.length
493
+ ? Math.max(...LADDER_STEPS.map((step) => integers.filter((value) => value % step === 0).length / positives.length))
494
+ : 0;
495
+ const reasons = [];
496
+ if (positives.length < MIN_ASSESSED_STEPS) reasons.push('fewer than three steps');
497
+ if (integerShare < 0.8) reasons.push('fractional values');
498
+ if (coherence < 0.7) reasons.push('no common step');
499
+ if (positives.length > MAX_LOOSE_STEPS && coherence < 0.9) reasons.push('too many steps');
500
+ if (Array.isArray(files) && files.length > 0) {
501
+ const tokenFileShare = files.filter((file) => TOKEN_FILE_PATTERN.test(file)).length / files.length;
502
+ if (tokenFileShare < 0.5 && coherence < 0.9) reasons.push('sources are component files');
503
+ }
504
+ return { plausible: reasons.length === 0, reasons };
505
+ }
506
+
507
+ function fallbackInference(rejected = null) {
508
+ return {
509
+ files: [],
510
+ preset: FALLBACK_PRESET,
511
+ ...(rejected ? { rejected } : {}),
512
+ scale: getScalePreset(FALLBACK_PRESET),
513
+ source: 'fallback',
514
+ tokenCount: 0,
515
+ warnings: [],
516
+ };
517
+ }
518
+
519
+ /**
520
+ * Apply `scale: "auto"` to built rule options: the inferred scale replaces the
521
+ * placeholder and the inference is kept for the fallback note. Options with
522
+ * an explicit scale pass through untouched.
523
+ */
524
+ function withResolvedScale(options, root) {
525
+ if (!options.scaleAuto) {
526
+ return options;
527
+ }
528
+ const inference = resolveAutoScale({
529
+ baseFontSize: options.baseFontSize,
530
+ root,
531
+ scaleSources: options.scaleSources,
532
+ tailwindConfigPath: options.tailwindConfigPath,
533
+ tokenPattern: options.tokenPatternExplicit ? options.tokenPattern : DEFAULT_AUTO_TOKEN_PATTERN,
534
+ });
535
+ options.scale = inference.scale;
536
+ options.scaleInference = inference;
537
+ return options;
538
+ }
539
+
540
+ function autoScaleFallbackNote(inference) {
541
+ if (!inference || inference.source !== 'fallback') {
542
+ return '';
543
+ }
544
+ if (inference.rejected) {
545
+ return `The spacing tokens found do not form a spacing scale (${inference.rejected.reasons.join(', ')}); using preset "${inference.preset}".`;
546
+ }
547
+
548
+ return `No spacing tokens were found for scale "auto"; using preset "${inference.preset}".`;
549
+ }
550
+
551
+ module.exports = {
552
+ DEFAULT_AUTO_TOKEN_PATTERN,
553
+ assessScale,
554
+ autoScaleFallbackNote,
555
+ discoverTokenPackages,
556
+ inferScaleFromDefinitions,
557
+ resolveAutoScale,
558
+ scaleFromDefinitions,
559
+ withResolvedScale,
560
+ };
@@ -43,6 +43,13 @@
43
43
  "dist/css/global-vars.css"
44
44
  ],
45
45
  "note": "--spectrum-spacing-* in px"
46
+ },
47
+ {
48
+ "name": "@carbon/layout",
49
+ "files": [
50
+ "scss/generated/_spacing.scss"
51
+ ],
52
+ "note": "$spacing-01..13 in rem, Sass variables; Carbon's styles package forwards them from here"
46
53
  }
47
54
  ]
48
55
  }