stylelint-plugin-rhythmguard 3.5.0 → 3.6.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 +18 -0
- package/CONTRIBUTING.md +7 -4
- package/README.md +8 -10
- package/SECURITY.md +3 -2
- package/package.json +2 -1
- package/src/audit/args.js +1 -0
- package/src/audit/baseline.js +55 -11
- package/src/audit/codemod.js +129 -0
- package/src/audit/config.js +5 -0
- package/src/audit/contract.js +1 -0
- package/src/audit/decisions.js +105 -0
- package/src/audit/render-github.js +5 -2
- package/src/audit/render-markdown.js +31 -0
- package/src/audit/render-text.js +11 -0
- package/src/audit/report.js +30 -2
- package/src/audit/scan/stylesheets.js +1 -0
- package/src/audit/shared.js +2 -0
- package/src/audit/token-chains.js +157 -0
- package/src/cli/audit.js +5 -0
- package/src/cli/fix.js +151 -0
- package/src/cli/index.js +4 -0
- package/src/core/decisions.js +121 -0
- package/src/core/fs-cache.js +36 -0
- package/src/core/options.js +12 -0
- package/src/core/scale-inference.js +34 -37
- package/src/core/token-index.js +72 -0
- package/src/core/token-sources.js +3 -2
- package/src/rules/no-offscale-transform/index.js +24 -4
- package/src/rules/prefer-token/index.js +3 -19
- package/src/rules/report.js +6 -0
- package/src/rules/use-scale/index.js +33 -5
- package/types/audit.d.ts +31 -0
- package/types/shared.d.ts +4 -0
|
@@ -26,6 +26,8 @@ const FALLBACK_PRESET = 'rhythmic-4';
|
|
|
26
26
|
const MIN_INFERRED_SCALE_LENGTH = 4;
|
|
27
27
|
const RC_FILE = '.rhythmguardrc.json';
|
|
28
28
|
|
|
29
|
+
const { cachedByFiles } = require('./fs-cache');
|
|
30
|
+
|
|
29
31
|
const sourceCache = new Map();
|
|
30
32
|
const TOKEN_PACKAGES = require('./token-packages.json').packages;
|
|
31
33
|
|
|
@@ -53,35 +55,6 @@ function readDirectDependencies(dir) {
|
|
|
53
55
|
* installs are found by walking up; a stray global node_modules is never
|
|
54
56
|
* consulted because the walk stops at the repository.
|
|
55
57
|
*/
|
|
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
58
|
function projectRoots(cwd, consult = () => {}) {
|
|
86
59
|
const roots = [];
|
|
87
60
|
let current = path.resolve(cwd);
|
|
@@ -192,21 +165,27 @@ function cacheKey(sources) {
|
|
|
192
165
|
.join('\n');
|
|
193
166
|
}
|
|
194
167
|
|
|
195
|
-
|
|
168
|
+
/** Parse token sources once per (sources, base font size, file mtimes). */
|
|
169
|
+
function parseSourcesCached(sources, baseFontSize) {
|
|
196
170
|
const normalized = sources.map((source) => normalizeSource(source, process.cwd())).filter(Boolean);
|
|
197
171
|
if (normalized.length === 0) {
|
|
198
172
|
return null;
|
|
199
173
|
}
|
|
200
|
-
|
|
201
174
|
const key = `${baseFontSize}\n${cacheKey(normalized)}`;
|
|
202
|
-
if (sourceCache.has(key)) {
|
|
203
|
-
|
|
175
|
+
if (!sourceCache.has(key)) {
|
|
176
|
+
sourceCache.set(key, parseTokenSources({ baseFontSize, sources: normalized, tokenKind: 'spacing' }));
|
|
204
177
|
}
|
|
178
|
+
return sourceCache.get(key);
|
|
179
|
+
}
|
|
205
180
|
|
|
206
|
-
|
|
181
|
+
function scaleFromSources(sources, baseFontSize) {
|
|
182
|
+
const parsed = parseSourcesCached(sources, baseFontSize);
|
|
183
|
+
if (!parsed) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
207
186
|
// scaleFromDefinitions also expands a bare Tailwind --spacing base into its multiples.
|
|
208
187
|
const scale = scaleFromDefinitions(parsed.definitions, baseFontSize);
|
|
209
|
-
|
|
188
|
+
return scale
|
|
210
189
|
? {
|
|
211
190
|
files: parsed.sources.map((source) => source.file),
|
|
212
191
|
scale,
|
|
@@ -214,9 +193,26 @@ function scaleFromSources(sources, baseFontSize) {
|
|
|
214
193
|
warnings: parsed.warnings,
|
|
215
194
|
}
|
|
216
195
|
: null;
|
|
196
|
+
}
|
|
217
197
|
|
|
218
|
-
|
|
219
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Every spacing token definition visible from a stylesheet, for fixes that
|
|
200
|
+
* write tokens: the stylesheet's own declarations, then `scaleSources`,
|
|
201
|
+
* `.rhythmguardrc.json` token sources and installed token packages. The same
|
|
202
|
+
* places scale inference reads, in the same order.
|
|
203
|
+
*/
|
|
204
|
+
function collectTokenDefinitions({ baseFontSize = 16, root, scaleSources = [], tokenRegex }) {
|
|
205
|
+
const definitions = root ? stylesheetDefinitions(root, tokenRegex, baseFontSize) : new Map();
|
|
206
|
+
const sources = [...scaleSources, ...rcTokenSources(process.cwd()), ...discoverTokenPackages(process.cwd())];
|
|
207
|
+
const parsed = parseSourcesCached(sources, baseFontSize);
|
|
208
|
+
if (parsed) {
|
|
209
|
+
for (const [token, definition] of parsed.definitions) {
|
|
210
|
+
if (!definitions.has(token)) {
|
|
211
|
+
definitions.set(token, definition);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return definitions;
|
|
220
216
|
}
|
|
221
217
|
|
|
222
218
|
/**
|
|
@@ -552,6 +548,7 @@ module.exports = {
|
|
|
552
548
|
DEFAULT_AUTO_TOKEN_PATTERN,
|
|
553
549
|
assessScale,
|
|
554
550
|
autoScaleFallbackNote,
|
|
551
|
+
collectTokenDefinitions,
|
|
555
552
|
discoverTokenPackages,
|
|
556
553
|
inferScaleFromDefinitions,
|
|
557
554
|
resolveAutoScale,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { fixedLengthValue, numbersEqual, toPx } = require('./length');
|
|
4
|
+
const { parseTokenValueLength } = require('./token-sources');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which token a fix may write for a length. Strict on purpose: the token must
|
|
8
|
+
* hold the same px value in the same unit as the literal being fixed, it must
|
|
9
|
+
* be a custom property (a Sass variable is not valid in CSS output), and it
|
|
10
|
+
* must be the only candidate. A fix that guesses a token is worse than a fix
|
|
11
|
+
* that writes a number, so anything ambiguous falls back to the literal.
|
|
12
|
+
*/
|
|
13
|
+
function tokenIndexFromDefinitions(definitions, baseFontSize = 16) {
|
|
14
|
+
const index = [];
|
|
15
|
+
for (const definition of definitions.values()) {
|
|
16
|
+
for (const raw of definition.values) {
|
|
17
|
+
const parsed = parseTokenValueLength(String(raw).trim());
|
|
18
|
+
if (!parsed) continue;
|
|
19
|
+
const unit = parsed.unit || 'px';
|
|
20
|
+
const px = toPx(Math.abs(parsed.number), unit, baseFontSize);
|
|
21
|
+
if (px === null || !Number.isFinite(px) || px === 0) continue;
|
|
22
|
+
index.push({ px, raw: String(raw).trim(), token: definition.token, unit });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return index;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function tokenForLength(index, px, unit) {
|
|
29
|
+
const wanted = unit || 'px';
|
|
30
|
+
const candidates = new Set();
|
|
31
|
+
for (const entry of index) {
|
|
32
|
+
if (entry.token.startsWith('--') && entry.unit === wanted && numbersEqual(entry.px, Math.abs(px))) {
|
|
33
|
+
candidates.add(entry.token);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (candidates.size !== 1) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return `var(${[...candidates][0]})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A negative token reference that is still valid where it is written. */
|
|
43
|
+
function negateReplacement(replacement) {
|
|
44
|
+
if (!replacement || replacement.startsWith('-')) {
|
|
45
|
+
return replacement;
|
|
46
|
+
}
|
|
47
|
+
if (replacement.startsWith('$') || replacement.startsWith('@')) {
|
|
48
|
+
return `-${replacement}`;
|
|
49
|
+
}
|
|
50
|
+
return `calc(-1 * ${replacement})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The replacement text a length rule writes: the matching token when the
|
|
55
|
+
* options ask for one and exactly one qualifies, otherwise the literal.
|
|
56
|
+
*/
|
|
57
|
+
function replacementFor(parsedLength, nearestPx, options) {
|
|
58
|
+
if (options.fixWith === 'token' && options.tokenIndex) {
|
|
59
|
+
const token = tokenForLength(options.tokenIndex, nearestPx, parsedLength.unit || 'px');
|
|
60
|
+
if (token) {
|
|
61
|
+
return parsedLength.number < 0 ? negateReplacement(token) : token;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return fixedLengthValue(parsedLength, nearestPx, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
negateReplacement,
|
|
69
|
+
replacementFor,
|
|
70
|
+
tokenForLength,
|
|
71
|
+
tokenIndexFromDefinitions,
|
|
72
|
+
};
|
|
@@ -792,8 +792,9 @@ function addDefinition(definitions, {
|
|
|
792
792
|
definitions.set(token, entry);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
-
|
|
796
|
-
const
|
|
795
|
+
// The factor must carry a unit: `calc(4px * var(--scaling))` is a length, `calc(var(--x) * 2)` is a multiplier.
|
|
796
|
+
const CALC_LENGTH_TIMES_VAR = /^calc\(\s*(-?[\d.]+(?:px|rem|em))\s*\*\s*var\([^()]*\)\s*\)$/i;
|
|
797
|
+
const CALC_VAR_TIMES_LENGTH = /^calc\(\s*var\([^()]*\)\s*\*\s*(-?[\d.]+(?:px|rem|em))\s*\)$/i;
|
|
797
798
|
|
|
798
799
|
/**
|
|
799
800
|
* Parse the length a token value carries. Accepts plain lengths and the
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const stylelint = require('stylelint');
|
|
4
4
|
const valueParser = require('postcss-value-parser');
|
|
5
5
|
const {
|
|
6
|
-
fixedLengthValue,
|
|
7
6
|
formatLength,
|
|
8
7
|
isHairlineLength,
|
|
9
8
|
nearestScaleValues,
|
|
@@ -23,12 +22,15 @@ const {
|
|
|
23
22
|
} = require('../../core/value-nodes');
|
|
24
23
|
|
|
25
24
|
const {
|
|
25
|
+
collectTokenDefinitions,
|
|
26
26
|
withResolvedScale,
|
|
27
27
|
} = require('../../core/scale-inference');
|
|
28
28
|
|
|
29
29
|
const { validatePrimary, validateNoOffscaleTransformSecondaryOptions } = require('../validate');
|
|
30
30
|
|
|
31
|
-
const { reportInvalidPreset, reportValueNode } = require('../report');
|
|
31
|
+
const { reportInvalidPreset, reportProblem, reportValueNode } = require('../report');
|
|
32
|
+
const { decisionFor, loadRcDecisions } = require('../../core/decisions');
|
|
33
|
+
const { replacementFor, tokenIndexFromDefinitions } = require('../../core/token-index');
|
|
32
34
|
|
|
33
35
|
const ruleName = 'rhythmguard/no-offscale-transform';
|
|
34
36
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
@@ -57,8 +59,20 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
57
59
|
|
|
58
60
|
const options = buildScaleOptions(secondaryOptions);
|
|
59
61
|
reportInvalidPreset(options, { message: messages.invalidPreset, result, root, ruleName });
|
|
62
|
+
try {
|
|
63
|
+
options.decisions = options.readDecisions ? loadRcDecisions(process.cwd(), { baseFontSize: options.baseFontSize }) : [];
|
|
64
|
+
} catch (error) {
|
|
65
|
+
reportProblem(error.message, { result, root, ruleName });
|
|
66
|
+
options.decisions = [];
|
|
67
|
+
}
|
|
60
68
|
|
|
61
69
|
withResolvedScale(options, root);
|
|
70
|
+
if (options.fixWith === 'token') {
|
|
71
|
+
options.tokenIndex = tokenIndexFromDefinitions(
|
|
72
|
+
collectTokenDefinitions({ baseFontSize: options.baseFontSize, root, scaleSources: options.scaleSources, tokenRegex: new RegExp(options.tokenPattern) }),
|
|
73
|
+
options.baseFontSize,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
62
76
|
|
|
63
77
|
const getScaleStateForProperty = createPropertyScaleResolver(options);
|
|
64
78
|
|
|
@@ -117,6 +131,12 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
117
131
|
return;
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
const decidedPx = toPx(Math.abs(parsedLength.number), parsedLength.unit, options.baseFontSize);
|
|
135
|
+
const decision = decidedPx === null ? null : decisionFor(decidedPx, prop, options.decisions);
|
|
136
|
+
if (decision && (decision.decision === 'adopt' || decision.decision === 'allow')) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
120
140
|
if (options.unitStrategy === 'exact') {
|
|
121
141
|
const unit = parsedLength.unit || 'px';
|
|
122
142
|
const unitScale = scaleByUnit.get(unit);
|
|
@@ -136,7 +156,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
const fixedValue = options.fixToScale
|
|
139
|
-
?
|
|
159
|
+
? replacementFor(parsedLength, nearest.nearest, options)
|
|
140
160
|
: null;
|
|
141
161
|
|
|
142
162
|
report(node, nearest, unit, fixedValue);
|
|
@@ -160,7 +180,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
const fixedValue = options.fixToScale
|
|
163
|
-
?
|
|
183
|
+
? replacementFor(parsedLength, nearest.nearest, options)
|
|
164
184
|
: null;
|
|
165
185
|
|
|
166
186
|
report(node, nearest, 'px', fixedValue);
|
|
@@ -31,6 +31,8 @@ const {
|
|
|
31
31
|
const { createTokenRegex, reportInvalidPreset, reportValueNode } = require('../report');
|
|
32
32
|
const { validatePrimary, validatePreferTokenSecondaryOptions } = require('../validate');
|
|
33
33
|
|
|
34
|
+
const { negateReplacement } = require('../../core/token-index');
|
|
35
|
+
|
|
34
36
|
const ruleName = 'rhythmguard/prefer-token';
|
|
35
37
|
|
|
36
38
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
@@ -41,25 +43,7 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
function applyNegativeToken(replacement, parsedLength) {
|
|
44
|
-
|
|
45
|
-
return replacement;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (replacement.startsWith('-')) {
|
|
49
|
-
return replacement;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
replacement.startsWith('var(') ||
|
|
54
|
-
replacement.startsWith('theme(') ||
|
|
55
|
-
replacement.startsWith('token(') ||
|
|
56
|
-
replacement.startsWith('$') ||
|
|
57
|
-
replacement.startsWith('@')
|
|
58
|
-
) {
|
|
59
|
-
return `-${replacement}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return `calc(${replacement} * -1)`;
|
|
46
|
+
return !replacement || parsedLength.number >= 0 ? replacement : negateReplacement(replacement);
|
|
63
47
|
}
|
|
64
48
|
|
|
65
49
|
function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
|
package/src/rules/report.js
CHANGED
|
@@ -68,8 +68,14 @@ function reportInvalidPreset(options, { message, result, root, ruleName }) {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** A configuration problem that is not about one node: reported once, on the root. */
|
|
72
|
+
function reportProblem(message, { result, root, ruleName }) {
|
|
73
|
+
stylelint.utils.report({ message, node: root, result, ruleName });
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
module.exports = {
|
|
72
77
|
createTokenRegex,
|
|
73
78
|
reportInvalidPreset,
|
|
79
|
+
reportProblem,
|
|
74
80
|
reportValueNode,
|
|
75
81
|
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const stylelint = require('stylelint');
|
|
4
4
|
const valueParser = require('postcss-value-parser');
|
|
5
5
|
const {
|
|
6
|
-
fixedLengthValue,
|
|
7
6
|
formatLength,
|
|
8
7
|
isHairlineLength,
|
|
9
8
|
nearestScaleValues,
|
|
@@ -27,10 +26,13 @@ const {
|
|
|
27
26
|
|
|
28
27
|
const {
|
|
29
28
|
autoScaleFallbackNote,
|
|
29
|
+
collectTokenDefinitions,
|
|
30
30
|
withResolvedScale,
|
|
31
31
|
} = require('../../core/scale-inference');
|
|
32
32
|
|
|
33
|
-
const { createTokenRegex, reportInvalidPreset, reportValueNode } = require('../report');
|
|
33
|
+
const { createTokenRegex, reportInvalidPreset, reportProblem, reportValueNode } = require('../report');
|
|
34
|
+
const { decisionFor, loadRcDecisions } = require('../../core/decisions');
|
|
35
|
+
const { replacementFor, tokenIndexFromDefinitions } = require('../../core/token-index');
|
|
34
36
|
const { validatePrimary, validateUseScaleSecondaryOptions } = require('../validate');
|
|
35
37
|
|
|
36
38
|
const ruleName = 'rhythmguard/use-scale';
|
|
@@ -42,6 +44,19 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
|
42
44
|
`Unexpected off-scale value "${value}". Use scale values (nearest: ${lower} or ${upper}).${note ? ` ${note}` : ''}`,
|
|
43
45
|
});
|
|
44
46
|
|
|
47
|
+
/** Decisions from .rhythmguardrc.json; an invalid section is reported once and ignored. */
|
|
48
|
+
function readDecisions(options, { result, root }) {
|
|
49
|
+
if (!options.readDecisions) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return loadRcDecisions(process.cwd(), { baseFontSize: options.baseFontSize });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
reportProblem(error.message, { result, root, ruleName });
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
function checkLengthValue({
|
|
46
61
|
decl,
|
|
47
62
|
node,
|
|
@@ -90,6 +105,12 @@ function checkLengthValue({
|
|
|
90
105
|
return false;
|
|
91
106
|
}
|
|
92
107
|
|
|
108
|
+
const decidedPx = toPx(Math.abs(parsedLength.number), parsedLength.unit, options.baseFontSize);
|
|
109
|
+
const decision = decidedPx === null ? null : decisionFor(decidedPx, decl.prop, options.decisions);
|
|
110
|
+
if (decision && (decision.decision === 'adopt' || decision.decision === 'allow')) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
if (options.unitStrategy === 'exact') {
|
|
94
115
|
const unit = parsedLength.unit || 'px';
|
|
95
116
|
const unitScale = scaleByUnit.get(unit);
|
|
@@ -110,7 +131,7 @@ function checkLengthValue({
|
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
const fixedValue = options.fixToScale
|
|
113
|
-
?
|
|
134
|
+
? replacementFor(parsedLength, nearest.nearest, options)
|
|
114
135
|
: null;
|
|
115
136
|
|
|
116
137
|
report(node.value, decl, node, nearest, fixedValue, unit);
|
|
@@ -134,7 +155,7 @@ function checkLengthValue({
|
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
const fixedValue = options.fixToScale
|
|
137
|
-
?
|
|
158
|
+
? replacementFor(parsedLength, nearest.nearest, options)
|
|
138
159
|
: null;
|
|
139
160
|
|
|
140
161
|
report(node.value, decl, node, nearest, fixedValue, 'px');
|
|
@@ -160,10 +181,17 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
160
181
|
|
|
161
182
|
const options = buildScaleOptions(secondaryOptions);
|
|
162
183
|
reportInvalidPreset(options, { message: messages.invalidPreset, result, root, ruleName });
|
|
163
|
-
|
|
184
|
+
options.decisions = readDecisions(options, { result, root });
|
|
164
185
|
withResolvedScale(options, root);
|
|
165
186
|
|
|
166
187
|
const tokenRegex = createTokenRegex(options.tokenPattern, result, ruleName);
|
|
188
|
+
if (options.fixWith === 'token') {
|
|
189
|
+
options.tokenIndex = tokenIndexFromDefinitions(
|
|
190
|
+
collectTokenDefinitions({ baseFontSize: options.baseFontSize, root, scaleSources: options.scaleSources, tokenRegex }),
|
|
191
|
+
options.baseFontSize,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
167
195
|
let fallbackNote = autoScaleFallbackNote(options.scaleInference);
|
|
168
196
|
const getScaleStateForProperty = createPropertyScaleResolver(options);
|
|
169
197
|
|
package/types/audit.d.ts
CHANGED
|
@@ -98,6 +98,33 @@ export interface AuditBaselineComparison {
|
|
|
98
98
|
|
|
99
99
|
export type AuditScaleSource = "default" | "explicit" | "fallback" | "scanned-css" | "token-package" | "token-sources";
|
|
100
100
|
|
|
101
|
+
export type AuditDecisionKind = "adopt" | "allow" | "snap" | "undecided";
|
|
102
|
+
|
|
103
|
+
/** One entry of the `decisions` section of `.rhythmguardrc.json`. */
|
|
104
|
+
export interface AuditDecision {
|
|
105
|
+
value: string;
|
|
106
|
+
decision: AuditDecisionKind;
|
|
107
|
+
/** For `adopt`: the token the value should become. */
|
|
108
|
+
as?: string;
|
|
109
|
+
/** For `allow`: property names or `prefix-*` patterns the allowance is limited to. */
|
|
110
|
+
properties?: string[];
|
|
111
|
+
reason?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface AuditDecisionSummary {
|
|
115
|
+
adopt: number;
|
|
116
|
+
allow: number;
|
|
117
|
+
snap: number;
|
|
118
|
+
undecided: number;
|
|
119
|
+
suppressed: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One line of `rhythmguard audit --plan`: a decision entry with what the audit saw. */
|
|
123
|
+
export interface AuditDecisionPlanEntry extends AuditDecision {
|
|
124
|
+
count: number;
|
|
125
|
+
nearest: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
export interface AuditScaleRejected {
|
|
102
129
|
files?: string[];
|
|
103
130
|
/** Why the inferred set was not accepted as a scale, for example "no common step". */
|
|
@@ -117,6 +144,8 @@ export interface AuditScale {
|
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
export interface AuditReport {
|
|
147
|
+
decisions?: AuditDecisionSummary | null;
|
|
148
|
+
decisionPlan?: AuditDecisionPlanEntry[];
|
|
120
149
|
baseline?: AuditBaselineComparison | null;
|
|
121
150
|
scale?: AuditScale | null;
|
|
122
151
|
config?: string | null;
|
|
@@ -143,6 +172,8 @@ export interface AuditContractReport {
|
|
|
143
172
|
scanScope: string;
|
|
144
173
|
};
|
|
145
174
|
contracts: {
|
|
175
|
+
/** Counts per decision kind and how many findings the decisions suppressed; null when the config has no decisions. */
|
|
176
|
+
decisions: AuditDecisionSummary | null;
|
|
146
177
|
motion?: unknown;
|
|
147
178
|
scale: {
|
|
148
179
|
cleanliness?: unknown;
|
package/types/shared.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface ScaleSource {
|
|
|
17
17
|
export interface RhythmguardRuleOptions {
|
|
18
18
|
/** Exempt non-zero lengths of one CSS pixel or less (1px, -1px, 0.5px, 0.0625rem). Default true. */
|
|
19
19
|
allowHairlines?: boolean;
|
|
20
|
+
/** What autofix writes: the nearest literal (default) or, with "token", `var(--name)` when exactly one custom property holds that value in the same unit. */
|
|
21
|
+
fixWith?: "value" | "token";
|
|
22
|
+
/** Read the `decisions` section of `.rhythmguardrc.json` (default true). The audit sets false and applies decisions itself. */
|
|
23
|
+
decisions?: boolean;
|
|
20
24
|
baseFontSize?: number;
|
|
21
25
|
customScale?: ScaleValue[];
|
|
22
26
|
includeMathFunctions?: boolean;
|