stylelint-plugin-rhythmguard 2.2.0 → 3.1.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 +39 -0
- package/CONTRIBUTING.md +66 -51
- package/README.md +6 -6
- package/SECURITY.md +2 -2
- package/package.json +21 -27
- package/scales/community/product-decimal-10.json +27 -4
- package/src/audit/args.js +7 -1
- package/src/audit/contract.js +20 -0
- package/src/audit/render-html.js +1 -0
- package/src/audit/render-markdown.js +6 -3
- package/src/audit/render-text.js +6 -3
- package/src/audit/report.js +24 -1
- package/src/audit/scan.js +140 -10
- package/src/cli/init.js +2 -3
- package/src/cli/quickstart.js +11 -3
- package/src/index.js +0 -4
- package/src/utils/scale-inference.js +108 -12
- package/src/utils/token-packages.json +48 -0
- package/src/utils/token-sources.js +282 -3
- package/types/__checks__/consumer.ts +0 -6
- package/types/audit.d.ts +13 -2
- package/types/index.d.ts +0 -4
- package/types/shared.d.ts +2 -0
- package/src/configs/expanded.js +0 -27
- package/src/configs/expanded.mjs +0 -4
- package/src/configs/logical.js +0 -16
- package/src/configs/logical.mjs +0 -4
- package/src/configs/migration.js +0 -31
- package/src/configs/migration.mjs +0 -4
- package/src/configs/react-tailwind.js +0 -25
- package/src/configs/react-tailwind.mjs +0 -4
package/src/audit/scan.js
CHANGED
|
@@ -230,8 +230,33 @@ function toPosixRelativePath(rootDir, filePath) {
|
|
|
230
230
|
return path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
function isScssFile(filePath) {
|
|
234
|
+
return filePath.endsWith('.scss');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// "CSS files" in the audit means authored stylesheets: .css always, .scss when
|
|
238
|
+
// postcss-scss can be resolved (see resolveScssSyntax).
|
|
233
239
|
function isCssFile(filePath) {
|
|
234
|
-
return filePath.endsWith('.css');
|
|
240
|
+
return filePath.endsWith('.css') || isScssFile(filePath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let scssSyntaxCache;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* postcss-scss is an optional peer. Resolve it from the audited project first,
|
|
247
|
+
* then from this package. Returns null when unavailable; SCSS files are then
|
|
248
|
+
* counted as skipped instead of failing the audit.
|
|
249
|
+
*/
|
|
250
|
+
function resolveScssSyntax() {
|
|
251
|
+
if (scssSyntaxCache !== undefined) {
|
|
252
|
+
return scssSyntaxCache;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
scssSyntaxCache = require.resolve('postcss-scss', { paths: [process.cwd(), __dirname] });
|
|
256
|
+
} catch {
|
|
257
|
+
scssSyntaxCache = null;
|
|
258
|
+
}
|
|
259
|
+
return scssSyntaxCache;
|
|
235
260
|
}
|
|
236
261
|
|
|
237
262
|
function isTemplateFile(filePath) {
|
|
@@ -240,7 +265,10 @@ function isTemplateFile(filePath) {
|
|
|
240
265
|
|
|
241
266
|
async function runStylelintAudit(cssFiles, options) {
|
|
242
267
|
if (cssFiles.length === 0) {
|
|
243
|
-
|
|
268
|
+
const empty = [];
|
|
269
|
+
empty.scssFiles = 0;
|
|
270
|
+
empty.scssSkipped = 0;
|
|
271
|
+
return empty;
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
const { default: stylelint } = await import('stylelint');
|
|
@@ -274,23 +302,52 @@ async function runStylelintAudit(cssFiles, options) {
|
|
|
274
302
|
];
|
|
275
303
|
}
|
|
276
304
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
plugins: [pluginPath],
|
|
281
|
-
rules,
|
|
282
|
-
},
|
|
283
|
-
});
|
|
305
|
+
const plainFiles = cssFiles.filter((file) => !isScssFile(file));
|
|
306
|
+
const scssFiles = cssFiles.filter(isScssFile);
|
|
307
|
+
const results = [];
|
|
284
308
|
|
|
285
|
-
|
|
309
|
+
if (plainFiles.length > 0) {
|
|
310
|
+
const result = await stylelint.lint({
|
|
311
|
+
files: plainFiles,
|
|
312
|
+
config: {
|
|
313
|
+
plugins: [pluginPath],
|
|
314
|
+
rules,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
results.push(...(result.results || []));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let scssSkipped = 0;
|
|
321
|
+
if (scssFiles.length > 0) {
|
|
322
|
+
const scssSyntax = resolveScssSyntax();
|
|
323
|
+
if (scssSyntax) {
|
|
324
|
+
const result = await stylelint.lint({
|
|
325
|
+
files: scssFiles,
|
|
326
|
+
config: {
|
|
327
|
+
customSyntax: scssSyntax,
|
|
328
|
+
plugins: [pluginPath],
|
|
329
|
+
rules,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
results.push(...(result.results || []));
|
|
333
|
+
} else {
|
|
334
|
+
scssSkipped = scssFiles.length;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
results.scssFiles = scssFiles.length;
|
|
339
|
+
results.scssSkipped = scssSkipped;
|
|
340
|
+
return results;
|
|
286
341
|
}
|
|
287
342
|
|
|
288
343
|
function collectCssFindings(fileResults) {
|
|
289
344
|
const findings = [];
|
|
345
|
+
const sources = new Map();
|
|
290
346
|
|
|
291
347
|
for (const fileResult of fileResults) {
|
|
292
348
|
for (const warning of fileResult.warnings || []) {
|
|
293
349
|
const text = warning.text || '';
|
|
350
|
+
const source = readSourceOnce(sources, fileResult.source);
|
|
294
351
|
const offScaleMatch = text.match(
|
|
295
352
|
/Unexpected (?:off-scale value|transform translation value) "([^"]+)"/,
|
|
296
353
|
);
|
|
@@ -308,6 +365,9 @@ function collectCssFindings(fileResults) {
|
|
|
308
365
|
column: warning.column || 1,
|
|
309
366
|
file: formatPath(fileResult.source),
|
|
310
367
|
line: warning.line || 1,
|
|
368
|
+
property: source === null
|
|
369
|
+
? null
|
|
370
|
+
: findDeclarationProperty(source, warning.line || 1, warning.column || 1),
|
|
311
371
|
rule: warning.rule || 'rhythmguard',
|
|
312
372
|
text,
|
|
313
373
|
type: getCssFindingType({ motionDurationMatch, motionEasingMatch, tokenMatch }),
|
|
@@ -324,6 +384,73 @@ function collectCssFindings(fileResults) {
|
|
|
324
384
|
return findings;
|
|
325
385
|
}
|
|
326
386
|
|
|
387
|
+
function readSourceOnce(cache, filePath) {
|
|
388
|
+
if (!filePath) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
if (!cache.has(filePath)) {
|
|
392
|
+
try {
|
|
393
|
+
cache.set(filePath, fs.readFileSync(filePath, 'utf8'));
|
|
394
|
+
} catch {
|
|
395
|
+
cache.set(filePath, null);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return cache.get(filePath);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const DECLARATION_BOUNDARY = new Set([';', '{', '}']);
|
|
402
|
+
const DECLARATION_HEAD_PATTERN = /^\s*(?:(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)\s*)*(--[\w-]+|[a-zA-Z][\w-]*)\s*:/;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Stylelint warnings carry a position but not the declaration node. Recover the
|
|
406
|
+
* property by walking from the warning position back to the previous declaration
|
|
407
|
+
* boundary and reading the `property:` head, skipping block and Sass line comments.
|
|
408
|
+
* Sass interpolation is blanked first so `#{...}` braces do not act as boundaries. Returns null when the position is not
|
|
409
|
+
* inside a declaration, for example inside an at-rule.
|
|
410
|
+
*/
|
|
411
|
+
function findDeclarationProperty(source, line, column) {
|
|
412
|
+
const offset = positionToOffset(source, line, column);
|
|
413
|
+
if (offset === null) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const text = source.replace(/#\{[^}]*\}/g, (match) => ' '.repeat(match.length));
|
|
418
|
+
let start = offset;
|
|
419
|
+
while (start > 0 && !DECLARATION_BOUNDARY.has(text[start - 1])) {
|
|
420
|
+
start -= 1;
|
|
421
|
+
}
|
|
422
|
+
const backward = text.slice(start, offset).match(DECLARATION_HEAD_PATTERN);
|
|
423
|
+
if (backward) {
|
|
424
|
+
return backward[1];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let end = offset;
|
|
428
|
+
while (end < text.length && !DECLARATION_BOUNDARY.has(text[end])) {
|
|
429
|
+
end += 1;
|
|
430
|
+
}
|
|
431
|
+
const forward = text.slice(offset, end).match(DECLARATION_HEAD_PATTERN);
|
|
432
|
+
return forward ? forward[1] : null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function positionToOffset(source, line, column) {
|
|
436
|
+
if (!Number.isInteger(line) || line < 1) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
let offset = 0;
|
|
440
|
+
let currentLine = 1;
|
|
441
|
+
while (currentLine < line) {
|
|
442
|
+
const newline = source.indexOf('\n', offset);
|
|
443
|
+
if (newline === -1) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
offset = newline + 1;
|
|
447
|
+
currentLine += 1;
|
|
448
|
+
}
|
|
449
|
+
const lineEnd = source.indexOf('\n', offset);
|
|
450
|
+
const lineLength = (lineEnd === -1 ? source.length : lineEnd) - offset;
|
|
451
|
+
return offset + Math.min(Math.max((column || 1) - 1, 0), lineLength);
|
|
452
|
+
}
|
|
453
|
+
|
|
327
454
|
function getCssFindingValue({
|
|
328
455
|
motionDurationMatch,
|
|
329
456
|
motionEasingMatch,
|
|
@@ -526,6 +653,7 @@ module.exports = {
|
|
|
526
653
|
collectTailwindMotionFindings,
|
|
527
654
|
createIgnoreMatchers,
|
|
528
655
|
escapeRegExp,
|
|
656
|
+
findDeclarationProperty,
|
|
529
657
|
findStringLiterals,
|
|
530
658
|
getCssFindingType,
|
|
531
659
|
getCssFindingValue,
|
|
@@ -535,6 +663,8 @@ module.exports = {
|
|
|
535
663
|
globToRegExp,
|
|
536
664
|
hasGlob,
|
|
537
665
|
isCssFile,
|
|
666
|
+
isScssFile,
|
|
667
|
+
resolveScssSyntax,
|
|
538
668
|
isPathInside,
|
|
539
669
|
isTemplateFile,
|
|
540
670
|
offsetToLineColumn,
|
package/src/cli/init.js
CHANGED
|
@@ -92,9 +92,6 @@ function detect() {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function selectProfile(stack) {
|
|
95
|
-
if (stack.nextjs && stack.tailwind) {
|
|
96
|
-
return 'react-tailwind';
|
|
97
|
-
}
|
|
98
95
|
if (stack.tailwind) {
|
|
99
96
|
return 'tailwind';
|
|
100
97
|
}
|
|
@@ -140,6 +137,8 @@ async function run() {
|
|
|
140
137
|
|
|
141
138
|
const config = {
|
|
142
139
|
extends: [`stylelint-plugin-rhythmguard/configs/${profile}`],
|
|
140
|
+
// Next.js build output is generated CSS; never lint it.
|
|
141
|
+
...(stack.nextjs ? { ignoreFiles: ['.next/**', 'out/**', 'node_modules/**'] } : {}),
|
|
143
142
|
};
|
|
144
143
|
|
|
145
144
|
const configPath = path.join(process.cwd(), '.stylelintrc.json');
|
package/src/cli/quickstart.js
CHANGED
|
@@ -14,6 +14,7 @@ const path = require('node:path');
|
|
|
14
14
|
|
|
15
15
|
const { createAuditReport } = require('../audit/report');
|
|
16
16
|
const { detect } = require('./init');
|
|
17
|
+
const { discoverTokenPackages } = require('../utils/scale-inference');
|
|
17
18
|
|
|
18
19
|
const TOKEN_FILE_PATTERN = /(^|[.-])tokens?\.json$/i;
|
|
19
20
|
const TOKEN_DIRS = ['tokens', 'design-tokens', path.join('src', 'tokens'), path.join('dist', 'tokens')];
|
|
@@ -76,13 +77,14 @@ function topEntries(counts, limit = MAX_LISTED) {
|
|
|
76
77
|
.slice(0, limit);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
function suggestedStylelintConfig({ profile, tokenFiles }) {
|
|
80
|
+
function suggestedStylelintConfig({ nextjs = false, profile, tokenFiles }) {
|
|
80
81
|
const ruleOptions = { scale: 'auto' };
|
|
81
82
|
if (tokenFiles.length > 0) {
|
|
82
83
|
ruleOptions.scaleSources = tokenFiles.map(toPosix);
|
|
83
84
|
}
|
|
84
85
|
return {
|
|
85
86
|
extends: [`stylelint-plugin-rhythmguard/configs/${profile}`],
|
|
87
|
+
...(nextjs ? { ignoreFiles: ['.next/**', 'out/**', 'node_modules/**'] } : {}),
|
|
86
88
|
rules: {
|
|
87
89
|
'rhythmguard/use-scale': [true, ruleOptions],
|
|
88
90
|
},
|
|
@@ -127,6 +129,8 @@ async function run() {
|
|
|
127
129
|
out.push(` Next.js ${stack.nextjs ? 'yes' : 'no'}`);
|
|
128
130
|
out.push(` Stylelint config ${stack.hasExistingConfig ? 'present' : 'none'}`);
|
|
129
131
|
out.push(` Token files ${tokenFiles.length > 0 ? tokenFiles.join(', ') : 'none found'}`);
|
|
132
|
+
const tokenPackages = [...new Set(discoverTokenPackages(cwd).map((source) => source.package))];
|
|
133
|
+
out.push(` Token packages ${tokenPackages.length > 0 ? tokenPackages.join(', ') : 'none installed'}`);
|
|
130
134
|
out.push(` .rhythmguardrc ${rcPresent ? 'present (its token sources are used)' : 'none'}`);
|
|
131
135
|
out.push('');
|
|
132
136
|
|
|
@@ -173,15 +177,19 @@ async function run() {
|
|
|
173
177
|
if (topValues.length > 0) {
|
|
174
178
|
out.push(` Top values ${topValues.map(([value, count]) => `${value} ×${count}`).join(', ')}`);
|
|
175
179
|
}
|
|
180
|
+
const topProperties = topEntries(report.offScaleProperties);
|
|
181
|
+
if (topProperties.length > 0) {
|
|
182
|
+
out.push(` Top properties ${topProperties.map(([property, count]) => `${property} ×${count}`).join(', ')}`);
|
|
183
|
+
}
|
|
176
184
|
const topFiles = (report.topAffectedFiles || []).slice(0, 3);
|
|
177
185
|
if (topFiles.length > 0) {
|
|
178
186
|
out.push(` Top files ${topFiles.map((entry) => `${entry.file} (${entry.count})`).join(', ')}`);
|
|
179
187
|
}
|
|
180
188
|
out.push('');
|
|
181
189
|
|
|
182
|
-
const profile = stack.tailwind ?
|
|
190
|
+
const profile = stack.tailwind ? 'tailwind' : 'recommended';
|
|
183
191
|
out.push(` Paste this into .stylelintrc.json${stack.hasExistingConfig ? ' (merge with your existing config)' : ''}:`, '');
|
|
184
|
-
out.push(JSON.stringify(suggestedStylelintConfig({ profile, tokenFiles }), null, 2).replace(/^/gm, ' '));
|
|
192
|
+
out.push(JSON.stringify(suggestedStylelintConfig({ nextjs: stack.nextjs, profile, tokenFiles }), null, 2).replace(/^/gm, ' '));
|
|
185
193
|
out.push('');
|
|
186
194
|
|
|
187
195
|
if (stack.tailwind) {
|
package/src/index.js
CHANGED
|
@@ -19,11 +19,7 @@ module.exports.configs = {
|
|
|
19
19
|
recommended: require('./configs/recommended'),
|
|
20
20
|
strict: require('./configs/strict'),
|
|
21
21
|
tailwind: require('./configs/tailwind'),
|
|
22
|
-
expanded: require('./configs/expanded'),
|
|
23
|
-
logical: require('./configs/logical'),
|
|
24
|
-
migration: require('./configs/migration'),
|
|
25
22
|
motion: require('./configs/motion'),
|
|
26
|
-
'react-tailwind': require('./configs/react-tailwind'),
|
|
27
23
|
};
|
|
28
24
|
module.exports.eslint = require('./eslint');
|
|
29
25
|
module.exports.presets = require('./presets');
|
|
@@ -5,7 +5,7 @@ const path = require('node:path');
|
|
|
5
5
|
|
|
6
6
|
const { parseLengthToken, toPx } = require('./length');
|
|
7
7
|
const { buildEffectiveTokenMap } = require('./token-map');
|
|
8
|
-
const { parseTokenSources } = require('./token-sources');
|
|
8
|
+
const { collectScssTokens, createTokenKindMatcher, parseTokenSources } = require('./token-sources');
|
|
9
9
|
const { getScalePreset } = require('../presets/scales');
|
|
10
10
|
|
|
11
11
|
// Matches the audit default so lint and audit agree on what a spacing token is.
|
|
@@ -21,6 +21,80 @@ const MIN_INFERRED_SCALE_LENGTH = 4;
|
|
|
21
21
|
const RC_FILE = '.rhythmguardrc.json';
|
|
22
22
|
|
|
23
23
|
const sourceCache = new Map();
|
|
24
|
+
const TOKEN_PACKAGES = require('./token-packages.json').packages;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Installed design-token packages that ship a spacing scale (allowlist in
|
|
28
|
+
* token-packages.json). Resolved from the project, so only what the project
|
|
29
|
+
* actually depends on is read. Returns token-source entries.
|
|
30
|
+
*/
|
|
31
|
+
function readDirectDependencies(dir) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
34
|
+
return new Set([
|
|
35
|
+
...Object.keys(pkg.dependencies || {}),
|
|
36
|
+
...Object.keys(pkg.devDependencies || {}),
|
|
37
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
38
|
+
]);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Project roots from cwd up to the repository boundary (first directory holding
|
|
46
|
+
* .git), each with the dependencies it declares directly. Hoisted monorepo
|
|
47
|
+
* installs are found by walking up; a stray global node_modules is never
|
|
48
|
+
* consulted because the walk stops at the repository.
|
|
49
|
+
*/
|
|
50
|
+
function projectRoots(cwd) {
|
|
51
|
+
const roots = [];
|
|
52
|
+
let current = path.resolve(cwd);
|
|
53
|
+
for (;;) {
|
|
54
|
+
const direct = readDirectDependencies(current);
|
|
55
|
+
if (direct) {
|
|
56
|
+
roots.push({ dir: current, direct });
|
|
57
|
+
}
|
|
58
|
+
const parent = path.dirname(current);
|
|
59
|
+
if (fs.existsSync(path.join(current, '.git')) || parent === current) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
current = parent;
|
|
63
|
+
}
|
|
64
|
+
return roots;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function discoverTokenPackages(cwd = process.cwd()) {
|
|
68
|
+
const sources = [];
|
|
69
|
+
const roots = projectRoots(cwd);
|
|
70
|
+
for (const entry of TOKEN_PACKAGES) {
|
|
71
|
+
// Only packages the project depends on directly count. A transitive
|
|
72
|
+
// tailwindcss (for example via stylelint-config-tailwindcss) must not hand
|
|
73
|
+
// a non-Tailwind project the Tailwind scale.
|
|
74
|
+
const owner = roots.find((root) => root.direct.has(entry.name));
|
|
75
|
+
if (!owner) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const root = roots
|
|
79
|
+
.map((candidate) => path.join(candidate.dir, 'node_modules', entry.name))
|
|
80
|
+
.find((dir) => fs.existsSync(path.join(dir, 'package.json')));
|
|
81
|
+
if (!root) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const file of entry.files) {
|
|
85
|
+
const resolved = path.join(root, file);
|
|
86
|
+
if (fs.existsSync(resolved)) {
|
|
87
|
+
sources.push({
|
|
88
|
+
format: 'auto',
|
|
89
|
+
package: entry.name,
|
|
90
|
+
path: resolved,
|
|
91
|
+
...(entry.tokenPattern ? { tokenPattern: entry.tokenPattern } : {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return sources;
|
|
97
|
+
}
|
|
24
98
|
|
|
25
99
|
function pxValuesFromKeys(keys, baseFontSize) {
|
|
26
100
|
const values = new Set([0]);
|
|
@@ -54,6 +128,7 @@ function normalizeSource(source, baseDir) {
|
|
|
54
128
|
return {
|
|
55
129
|
format: typeof source.format === 'string' ? source.format : 'auto',
|
|
56
130
|
path: path.resolve(source.baseDir || baseDir, rawPath),
|
|
131
|
+
...(source.tokenPattern ? { tokenPattern: source.tokenPattern } : {}),
|
|
57
132
|
};
|
|
58
133
|
}
|
|
59
134
|
|
|
@@ -69,7 +144,7 @@ function cacheKey(sources) {
|
|
|
69
144
|
} catch {
|
|
70
145
|
// missing file: key still changes when it appears
|
|
71
146
|
}
|
|
72
|
-
return `${source.path}|${source.format}|${mtime}`;
|
|
147
|
+
return `${source.path}|${source.format}|${source.tokenPattern || ''}|${mtime}`;
|
|
73
148
|
})
|
|
74
149
|
.join('\n');
|
|
75
150
|
}
|
|
@@ -86,13 +161,9 @@ function scaleFromSources(sources, baseFontSize) {
|
|
|
86
161
|
}
|
|
87
162
|
|
|
88
163
|
const parsed = parseTokenSources({ baseFontSize, sources: normalized, tokenKind: 'spacing' });
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const scale = pxValuesFromKeys(keys, baseFontSize);
|
|
95
|
-
const outcome = scale.length >= MIN_INFERRED_SCALE_LENGTH
|
|
164
|
+
// scaleFromDefinitions also expands a bare Tailwind --spacing base into its multiples.
|
|
165
|
+
const scale = scaleFromDefinitions(parsed.definitions, baseFontSize);
|
|
166
|
+
const outcome = scale
|
|
96
167
|
? {
|
|
97
168
|
files: parsed.sources.map((source) => source.file),
|
|
98
169
|
scale,
|
|
@@ -157,8 +228,27 @@ function rcTokenSources(cwd) {
|
|
|
157
228
|
.filter(Boolean);
|
|
158
229
|
}
|
|
159
230
|
|
|
160
|
-
|
|
161
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Sass variables and maps declared in the linted stylesheet itself (postcss-scss
|
|
233
|
+
* exposes them as declarations whose prop starts with `$`). Evaluated with the
|
|
234
|
+
* same collector the audit uses; component variables such as $dropdown-spacer
|
|
235
|
+
* are excluded by the anchored name rule.
|
|
236
|
+
*/
|
|
237
|
+
function sassValuesFromRoot(root) {
|
|
238
|
+
const lines = [];
|
|
239
|
+
root.walkDecls((decl) => {
|
|
240
|
+
if (typeof decl.prop === 'string' && decl.prop.startsWith('$')) {
|
|
241
|
+
lines.push(`${decl.prop}: ${decl.value};`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
if (lines.length === 0) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return collectScssTokens(lines.join('\n'), createTokenKindMatcher('spacing')).map((token) => token.value);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function scaleFromTokenMap(map, baseFontSize, extraKeys = []) {
|
|
251
|
+
const keys = [...extraKeys];
|
|
162
252
|
const baseKeys = [];
|
|
163
253
|
for (const [key, reference] of Object.entries(map)) {
|
|
164
254
|
const name = String(reference).match(/^var\((--[\w-]+)\)$/);
|
|
@@ -206,7 +296,7 @@ function resolveAutoScale({
|
|
|
206
296
|
root,
|
|
207
297
|
tokenRegex,
|
|
208
298
|
});
|
|
209
|
-
const scale = scaleFromTokenMap(stylesheetMap, baseFontSize);
|
|
299
|
+
const scale = scaleFromTokenMap(stylesheetMap, baseFontSize, sassValuesFromRoot(root));
|
|
210
300
|
if (scale) {
|
|
211
301
|
return { files: [], scale, source: 'stylesheet', tokenCount: scale.length - 1, warnings: [] };
|
|
212
302
|
}
|
|
@@ -229,6 +319,11 @@ function resolveAutoScale({
|
|
|
229
319
|
}
|
|
230
320
|
}
|
|
231
321
|
|
|
322
|
+
const fromPackages = scaleFromSources(discoverTokenPackages(process.cwd()), baseFontSize);
|
|
323
|
+
if (fromPackages) {
|
|
324
|
+
return { source: 'token-package', ...fromPackages };
|
|
325
|
+
}
|
|
326
|
+
|
|
232
327
|
return {
|
|
233
328
|
files: [],
|
|
234
329
|
preset: FALLBACK_PRESET,
|
|
@@ -250,6 +345,7 @@ function autoScaleFallbackNote(inference) {
|
|
|
250
345
|
module.exports = {
|
|
251
346
|
DEFAULT_AUTO_TOKEN_PATTERN,
|
|
252
347
|
autoScaleFallbackNote,
|
|
348
|
+
discoverTokenPackages,
|
|
253
349
|
resolveAutoScale,
|
|
254
350
|
scaleFromDefinitions,
|
|
255
351
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Design-token packages whose installed files declare a spacing scale. Used by scale: \"auto\" after the project's own stylesheets are checked. Each entry lists the files to read (relative to the package root) and, when the package does not use space/spacing names, the token pattern to match. Additions welcome; keep them to files that ship in the published package.",
|
|
3
|
+
"packages": [
|
|
4
|
+
{
|
|
5
|
+
"name": "tailwindcss",
|
|
6
|
+
"files": [
|
|
7
|
+
"theme.css"
|
|
8
|
+
],
|
|
9
|
+
"note": "Tailwind v4 base multiplier --spacing"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "@radix-ui/themes",
|
|
13
|
+
"files": [
|
|
14
|
+
"tokens.css"
|
|
15
|
+
],
|
|
16
|
+
"note": "--space-1..9 as calc(<px> * var(--scaling))"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "@mantine/core",
|
|
20
|
+
"files": [
|
|
21
|
+
"styles.css"
|
|
22
|
+
],
|
|
23
|
+
"note": "--mantine-spacing-* as calc(<rem> * var(--mantine-scale))"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "@primer/primitives",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/css/base/size/size.css"
|
|
29
|
+
],
|
|
30
|
+
"tokenPattern": "^--base-size-\\d+$",
|
|
31
|
+
"note": "Primer names its spacing ladder size"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "@shopify/polaris-tokens",
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/css/styles.css"
|
|
37
|
+
],
|
|
38
|
+
"note": "--p-space-* in rem"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "@spectrum-css/tokens",
|
|
42
|
+
"files": [
|
|
43
|
+
"dist/css/global-vars.css"
|
|
44
|
+
],
|
|
45
|
+
"note": "--spectrum-spacing-* in px"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|