stylelint-plugin-rhythmguard 3.5.0 → 3.7.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 +31 -0
- package/CONTRIBUTING.md +8 -5
- package/README.md +8 -10
- package/SECURITY.md +3 -2
- package/agents/claude-code/SKILL.md +1 -1
- package/agents/copilot/copilot-instructions.md +1 -1
- package/agents/cursor/rhythmguard.mdc +1 -1
- 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/scan/templates.js +2 -4
- 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/length.js +1 -0
- package/src/core/options.js +40 -0
- package/src/core/scale-inference.js +34 -37
- package/src/core/tailwind-class-analysis.js +59 -3
- package/src/core/token-index.js +82 -0
- package/src/core/token-map.js +50 -13
- package/src/core/token-sources.js +3 -2
- package/src/eslint/rules/tailwind-class-use-motion-scale.js +6 -2
- package/src/eslint/rules/tailwind-class-use-scale.js +13 -13
- package/src/rules/no-offscale-transform/index.js +27 -6
- package/src/rules/prefer-token/index.js +20 -29
- package/src/rules/report.js +6 -0
- package/src/rules/use-motion-scale/index.js +12 -8
- package/src/rules/use-scale/index.js +35 -6
- package/types/audit.d.ts +31 -0
- package/types/shared.d.ts +6 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { MAX_NOTE_LENGTH, normalizeNote, withNote } = require('../../core/options');
|
|
3
4
|
const { formatTime } = require('../../core/time');
|
|
4
5
|
const { createTailwindMotionAnalyzer } = require('../../core/tailwind-motion-analysis');
|
|
5
6
|
|
|
@@ -51,7 +52,7 @@ function maybeCheckNodeText(node, sourceCode, context, analyzer, allowFix) {
|
|
|
51
52
|
? formatTime(analysis.nearest.upper, 'ms')
|
|
52
53
|
: 'n/a';
|
|
53
54
|
context.report({
|
|
54
|
-
message: buildMessage(analysis, segment, lower, upper),
|
|
55
|
+
message: withNote(buildMessage(analysis, segment, lower, upper), analyzer.note),
|
|
55
56
|
node,
|
|
56
57
|
fix:
|
|
57
58
|
fixedText && analysis.reason === 'duration'
|
|
@@ -95,13 +96,16 @@ module.exports = {
|
|
|
95
96
|
},
|
|
96
97
|
type: 'array',
|
|
97
98
|
},
|
|
99
|
+
note: { maxLength: MAX_NOTE_LENGTH, minLength: 1, type: 'string' },
|
|
98
100
|
},
|
|
99
101
|
type: 'object',
|
|
100
102
|
},
|
|
101
103
|
],
|
|
102
104
|
},
|
|
103
105
|
create(context) {
|
|
104
|
-
const
|
|
106
|
+
const option = (context.options && context.options[0]) || {};
|
|
107
|
+
const analyzer = createTailwindMotionAnalyzer(option);
|
|
108
|
+
analyzer.note = normalizeNote(option.note);
|
|
105
109
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
106
110
|
|
|
107
111
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const { createTailwindClassAnalyzer } = require('../../core/tailwind-class-analysis');
|
|
3
|
+
const { MAX_NOTE_LENGTH, normalizeNote, withNote } = require('../../core/options');
|
|
4
|
+
const { createTailwindClassAnalyzer, offScaleClassMessage } = require('../../core/tailwind-class-analysis');
|
|
5
5
|
|
|
6
6
|
const RULE_NAME = 'tailwind-class-use-scale';
|
|
7
7
|
|
|
@@ -44,17 +44,8 @@ function maybeCheckNodeText(node, sourceCode, context, analyzer, allowFix) {
|
|
|
44
44
|
: null;
|
|
45
45
|
|
|
46
46
|
for (const { analysis, segment } of findings) {
|
|
47
|
-
const lower = analysis.nearest
|
|
48
|
-
? formatLength(analysis.nearest.lower, 'px')
|
|
49
|
-
: 'n/a';
|
|
50
|
-
const upper = analysis.nearest
|
|
51
|
-
? formatLength(analysis.nearest.upper, 'px')
|
|
52
|
-
: 'n/a';
|
|
53
47
|
context.report({
|
|
54
|
-
message:
|
|
55
|
-
analysis.reason === 'negative'
|
|
56
|
-
? `Unexpected Tailwind arbitrary spacing value "${segment.token}". Negative values are disabled for this rule.`
|
|
57
|
-
: `Unexpected Tailwind arbitrary spacing value "${segment.token}". Use scale values (nearest: ${lower} or ${upper}).`,
|
|
48
|
+
message: withNote(offScaleClassMessage(segment.token, analysis), analyzer.note),
|
|
58
49
|
node,
|
|
59
50
|
fix:
|
|
60
51
|
fixedText && analysis.reason !== 'negative'
|
|
@@ -77,6 +68,7 @@ module.exports = {
|
|
|
77
68
|
properties: {
|
|
78
69
|
allowNegative: { type: 'boolean' },
|
|
79
70
|
baseFontSize: { type: 'number' },
|
|
71
|
+
note: { maxLength: MAX_NOTE_LENGTH, minLength: 1, type: 'string' },
|
|
80
72
|
scale: {
|
|
81
73
|
items: {
|
|
82
74
|
anyOf: [
|
|
@@ -86,6 +78,12 @@ module.exports = {
|
|
|
86
78
|
},
|
|
87
79
|
type: 'array',
|
|
88
80
|
},
|
|
81
|
+
spacingUnit: {
|
|
82
|
+
anyOf: [
|
|
83
|
+
{ exclusiveMinimum: true, minimum: 0, type: 'number' },
|
|
84
|
+
{ enum: [false] },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
89
87
|
units: {
|
|
90
88
|
items: { type: 'string' },
|
|
91
89
|
type: 'array',
|
|
@@ -96,7 +94,9 @@ module.exports = {
|
|
|
96
94
|
],
|
|
97
95
|
},
|
|
98
96
|
create(context) {
|
|
99
|
-
const
|
|
97
|
+
const option = (context.options && context.options[0]) || {};
|
|
98
|
+
const analyzer = createTailwindClassAnalyzer(option);
|
|
99
|
+
analyzer.note = normalizeNote(option.note);
|
|
100
100
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
101
101
|
|
|
102
102
|
return {
|
|
@@ -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,19 +22,22 @@ 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, tokenHoldsNote, tokenIndexFromDefinitions } = require('../../core/token-index');
|
|
32
34
|
|
|
33
35
|
const ruleName = 'rhythmguard/no-offscale-transform';
|
|
34
36
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
35
37
|
invalidPreset: (presetName, presetNames) =>
|
|
36
38
|
`Unknown scale preset "${presetName}". Available presets: ${presetNames.join(', ')}.`,
|
|
37
|
-
rejected: (value, lower, upper) =>
|
|
38
|
-
`Unexpected transform translation value "${value}". Use scale values (nearest: ${lower} or ${upper})
|
|
39
|
+
rejected: (value, lower, upper, note = '') =>
|
|
40
|
+
`Unexpected transform translation value "${value}". Use scale values (nearest: ${lower} or ${upper}).${note ? ` ${note}` : ''}`,
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
const ruleFunction = (primary, secondaryOptions) => {
|
|
@@ -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
|
|
|
@@ -79,6 +93,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
79
93
|
node.value,
|
|
80
94
|
formatLength(nearest.lower, nearestUnit),
|
|
81
95
|
formatLength(nearest.upper, nearestUnit),
|
|
96
|
+
[tokenHoldsNote(fixedValue, formatLength(nearest.nearest, nearestUnit)), options.note].filter(Boolean).join(' '),
|
|
82
97
|
),
|
|
83
98
|
node,
|
|
84
99
|
replacement: fixedValue,
|
|
@@ -117,6 +132,12 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
117
132
|
return;
|
|
118
133
|
}
|
|
119
134
|
|
|
135
|
+
const decidedPx = toPx(Math.abs(parsedLength.number), parsedLength.unit, options.baseFontSize);
|
|
136
|
+
const decision = decidedPx === null ? null : decisionFor(decidedPx, prop, options.decisions);
|
|
137
|
+
if (decision && (decision.decision === 'adopt' || decision.decision === 'allow')) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
if (options.unitStrategy === 'exact') {
|
|
121
142
|
const unit = parsedLength.unit || 'px';
|
|
122
143
|
const unitScale = scaleByUnit.get(unit);
|
|
@@ -136,7 +157,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
const fixedValue = options.fixToScale
|
|
139
|
-
?
|
|
160
|
+
? replacementFor(parsedLength, nearest.nearest, options)
|
|
140
161
|
: null;
|
|
141
162
|
|
|
142
163
|
report(node, nearest, unit, fixedValue);
|
|
@@ -160,7 +181,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
const fixedValue = options.fixToScale
|
|
163
|
-
?
|
|
184
|
+
? replacementFor(parsedLength, nearest.nearest, options)
|
|
164
185
|
: null;
|
|
165
186
|
|
|
166
187
|
report(node, nearest, 'px', fixedValue);
|
|
@@ -31,45 +31,34 @@ 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, {
|
|
37
39
|
invalidPreset: (presetName, presetNames) =>
|
|
38
40
|
`Unknown scale preset "${presetName}". Available presets: ${presetNames.join(', ')}.`,
|
|
39
|
-
rejected: (value) =>
|
|
40
|
-
`Unexpected raw scale value "${value}".
|
|
41
|
+
rejected: (value, replacement = null, origin = null, note = '') =>
|
|
42
|
+
`Unexpected raw scale value "${value}". ${replacement
|
|
43
|
+
? `Use ${replacement}${origin ? ` (${origin})` : ''}.`
|
|
44
|
+
: 'No known token holds this value; use the nearest token or add one.'}${note ? ` ${note}` : ''}`,
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
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)`;
|
|
48
|
+
return !replacement || parsedLength.number >= 0 ? replacement : negateReplacement(replacement);
|
|
63
49
|
}
|
|
64
50
|
|
|
51
|
+
/** The token for a raw length, and the text the fix writes (negated when the literal is). */
|
|
65
52
|
function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
|
|
53
|
+
const found = (token) => ({ replacement: applyNegativeToken(token, parsedLength), token });
|
|
54
|
+
|
|
66
55
|
if (Object.prototype.hasOwnProperty.call(tokenMap, raw)) {
|
|
67
|
-
return
|
|
56
|
+
return found(tokenMap[raw]);
|
|
68
57
|
}
|
|
69
58
|
|
|
70
59
|
const absoluteRaw = formatLength(Math.abs(parsedLength.number), parsedLength.unit || 'px');
|
|
71
60
|
if (Object.prototype.hasOwnProperty.call(tokenMap, absoluteRaw)) {
|
|
72
|
-
return
|
|
61
|
+
return found(tokenMap[absoluteRaw]);
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
if (options.unitStrategy === 'convert') {
|
|
@@ -77,7 +66,7 @@ function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
|
|
|
77
66
|
if (absPx !== null) {
|
|
78
67
|
const pxKey = `${absPx}px`;
|
|
79
68
|
if (Object.prototype.hasOwnProperty.call(tokenMap, pxKey)) {
|
|
80
|
-
return
|
|
69
|
+
return found(tokenMap[pxKey]);
|
|
81
70
|
}
|
|
82
71
|
}
|
|
83
72
|
}
|
|
@@ -108,8 +97,10 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
108
97
|
withResolvedScale(options, root);
|
|
109
98
|
|
|
110
99
|
const tokenRegex = createTokenRegex(options.tokenPattern, result, ruleName);
|
|
100
|
+
const origins = {};
|
|
111
101
|
const tokenMap = buildEffectiveTokenMap({
|
|
112
102
|
options,
|
|
103
|
+
origins,
|
|
113
104
|
root,
|
|
114
105
|
tokenRegex,
|
|
115
106
|
});
|
|
@@ -130,8 +121,10 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
130
121
|
const { scaleByUnit, scalePx } = getScaleStateForProperty(prop);
|
|
131
122
|
let changed = false;
|
|
132
123
|
|
|
133
|
-
const reportNode = (node,
|
|
134
|
-
|
|
124
|
+
const reportNode = (node, resolved = null) => {
|
|
125
|
+
const replacement = resolved ? resolved.replacement : null;
|
|
126
|
+
const origin = resolved ? origins[resolved.token] || null : null;
|
|
127
|
+
reportValueNode({ decl, message: messages.rejected(node.value, replacement, origin, options.note), node, replacement, result, ruleName });
|
|
135
128
|
};
|
|
136
129
|
|
|
137
130
|
const checkWordNode = (node, context) => {
|
|
@@ -197,9 +190,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
197
190
|
}
|
|
198
191
|
}
|
|
199
192
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
reportNode(node, replacement);
|
|
193
|
+
reportNode(node, resolveTokenReplacement(tokenMap, node.value, parsedLength, options));
|
|
203
194
|
return true;
|
|
204
195
|
};
|
|
205
196
|
|
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
|
};
|
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
|
|
21
21
|
const { reportValueNode } = require('../report');
|
|
22
22
|
const { validatePrimary } = require('../validate');
|
|
23
|
+
const { isNote, normalizeNote, withNote } = require('../../core/options');
|
|
23
24
|
|
|
24
25
|
const ruleName = 'rhythmguard/use-motion-scale';
|
|
25
26
|
const DURATION_PROPERTIES = new Set([
|
|
@@ -39,12 +40,12 @@ const EASING_PROPERTIES = new Set([
|
|
|
39
40
|
const EASING_FUNCTIONS = new Set(['cubic-bezier', 'linear', 'steps']);
|
|
40
41
|
|
|
41
42
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
42
|
-
invalidDuration: (value) =>
|
|
43
|
-
`Unexpected negative motion duration "${value}". Use non-negative duration values.`,
|
|
44
|
-
rejectedDuration: (value, lower, upper) =>
|
|
45
|
-
`Unexpected motion duration "${value}". Use duration scale values (nearest: ${lower} or ${upper}).`,
|
|
46
|
-
rejectedEasing: (value) =>
|
|
47
|
-
`Unexpected raw motion easing "${value}". Use motion tokens for easing decisions.`,
|
|
43
|
+
invalidDuration: (value, note = '') =>
|
|
44
|
+
withNote(`Unexpected negative motion duration "${value}". Use non-negative duration values.`, note),
|
|
45
|
+
rejectedDuration: (value, lower, upper, note = '') =>
|
|
46
|
+
withNote(`Unexpected motion duration "${value}". Use duration scale values (nearest: ${lower} or ${upper}).`, note),
|
|
47
|
+
rejectedEasing: (value, note = '') =>
|
|
48
|
+
withNote(`Unexpected raw motion easing "${value}". Use motion tokens for easing decisions.`, note),
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
function isPlainObject(value) {
|
|
@@ -59,6 +60,7 @@ function buildOptions(rawOptions) {
|
|
|
59
60
|
durationUnits: normalizeDurationUnits(options.durationUnits),
|
|
60
61
|
easingTokenMap: isPlainObject(options.easingTokenMap) ? options.easingTokenMap : {},
|
|
61
62
|
fixToScale: options.fixToScale !== false,
|
|
63
|
+
note: normalizeNote(options.note),
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -78,6 +80,7 @@ function validateSecondaryOptions(result, secondaryOptions) {
|
|
|
78
80
|
typeof entry === 'string' && entry.trim().length > 0,
|
|
79
81
|
)],
|
|
80
82
|
fixToScale: [true, false],
|
|
83
|
+
note: [isNote],
|
|
81
84
|
},
|
|
82
85
|
});
|
|
83
86
|
}
|
|
@@ -124,8 +127,9 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
124
127
|
node.value,
|
|
125
128
|
formatTime(nearest.lower, 'ms'),
|
|
126
129
|
formatTime(nearest.upper, 'ms'),
|
|
130
|
+
options.note,
|
|
127
131
|
)
|
|
128
|
-
: messages.invalidDuration(node.value),
|
|
132
|
+
: messages.invalidDuration(node.value, options.note),
|
|
129
133
|
node,
|
|
130
134
|
replacement: fixedValue,
|
|
131
135
|
result,
|
|
@@ -146,7 +150,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
146
150
|
}
|
|
147
151
|
: null,
|
|
148
152
|
length: source.length,
|
|
149
|
-
message: messages.rejectedEasing(source),
|
|
153
|
+
message: messages.rejectedEasing(source, options.note),
|
|
150
154
|
node,
|
|
151
155
|
result,
|
|
152
156
|
ruleName,
|
|
@@ -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, tokenHoldsNote, 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,19 +181,27 @@ 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
|
|
|
170
198
|
const report = (value, decl, node, nearest, fixedValue = null, nearestUnit = 'px') => {
|
|
171
199
|
const lower = nearest ? formatLength(nearest.lower, nearestUnit) : 'n/a';
|
|
172
200
|
const upper = nearest ? formatLength(nearest.upper, nearestUnit) : 'n/a';
|
|
201
|
+
const tokenNote = nearest ? tokenHoldsNote(fixedValue, formatLength(nearest.nearest, nearestUnit)) : '';
|
|
173
202
|
reportValueNode({
|
|
174
203
|
decl,
|
|
175
|
-
message: messages.rejected(value, lower, upper, fallbackNote),
|
|
204
|
+
message: messages.rejected(value, lower, upper, [fallbackNote, tokenNote, options.note].filter(Boolean).join(' ')),
|
|
176
205
|
node,
|
|
177
206
|
replacement: fixedValue,
|
|
178
207
|
result,
|
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,12 @@ 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;
|
|
24
|
+
/** A sentence of up to 200 characters appended to every finding, after the built-in text. */
|
|
25
|
+
note?: string;
|
|
20
26
|
baseFontSize?: number;
|
|
21
27
|
customScale?: ScaleValue[];
|
|
22
28
|
includeMathFunctions?: boolean;
|