@usefragments/core 1.2.0 → 1.3.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/dist/{chunk-BQQTMGLA.js → chunk-57QDBEHQ.js} +79 -6
- package/dist/chunk-57QDBEHQ.js.map +1 -0
- package/dist/{chunk-DBVTSUMT.js → chunk-MLRDNSSA.js} +20 -6
- package/dist/chunk-MLRDNSSA.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +2 -2
- package/dist/index-h_yWj15D.d.ts +6981 -0
- package/dist/index.d.ts +106 -92
- package/dist/index.js +235 -98
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +36 -36
- package/dist/schemas/index.d.ts +3 -3377
- package/dist/schemas/index.js +1 -1
- package/package.json +1 -1
- package/src/agent-format.test.ts +75 -3
- package/src/agent-format.ts +28 -6
- package/src/evidence.ts +27 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/fact-index.ts +22 -2
- package/src/facts/facts.test.ts +1 -1
- package/src/facts/types.ts +2 -0
- package/src/index.ts +4 -0
- package/src/rules/__tests__/fix-emission-invariant.test.ts +174 -0
- package/src/rules/__tests__/tokens-require-dual-fallback.test.ts +90 -0
- package/src/rules/emit-gate.test.ts +16 -0
- package/src/rules/emit-gate.ts +6 -3
- package/src/rules/finding.ts +3 -0
- package/src/rules/rules.test.ts +2 -1
- package/src/rules/spacing-resolution.ts +5 -8
- package/src/rules/styles-no-raw-color.ts +32 -23
- package/src/rules/styles-no-raw-dimensions.ts +31 -12
- package/src/rules/styles-no-raw-spacing.ts +33 -11
- package/src/rules/styles-no-raw-typography.ts +37 -15
- package/src/rules/taxonomy.test.ts +13 -0
- package/src/rules/tokens-require-dual-fallback.ts +54 -17
- package/src/rules/utils.ts +51 -0
- package/src/schemas/index.ts +81 -3
- package/src/token-types.ts +6 -0
- package/src/tokens/__tests__/source-names.test.ts +42 -0
- package/src/tokens/design-token-parser.test.ts +24 -5
- package/src/tokens/design-token-parser.ts +40 -8
- package/dist/chunk-BQQTMGLA.js.map +0 -1
- package/dist/chunk-DBVTSUMT.js.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { TokenDefinitionFact } from "../facts/index.js";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
isApplicableTokenReference,
|
|
5
4
|
matchesScale,
|
|
6
5
|
nearestSignedScaleValue,
|
|
7
6
|
normalizeLengthForScale,
|
|
@@ -75,7 +74,7 @@ export function resolveSpacingValue(input: {
|
|
|
75
74
|
let reliableSuggestion = true;
|
|
76
75
|
// Deterministic only when every violating part is a value-preserving,
|
|
77
76
|
// verbatim-resolvable token swap.
|
|
78
|
-
let
|
|
77
|
+
let allValuePreserving = true;
|
|
79
78
|
|
|
80
79
|
for (const part of parts) {
|
|
81
80
|
const normalized = normalizeLengthForScale(part.parsed, input.scale);
|
|
@@ -93,9 +92,7 @@ export function resolveSpacingValue(input: {
|
|
|
93
92
|
// On-grid literal that equals a token: value-preserving swap.
|
|
94
93
|
anyViolation = true;
|
|
95
94
|
const reference = tokenReference(exactToken.name);
|
|
96
|
-
|
|
97
|
-
isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
|
|
98
|
-
if (!applicable) allApplicableValuePreserving = false;
|
|
95
|
+
if (!normalized.deterministicFix) allValuePreserving = false;
|
|
99
96
|
suggestedParts.push(reference);
|
|
100
97
|
if (!firstViolation) {
|
|
101
98
|
firstViolation = {
|
|
@@ -103,7 +100,7 @@ export function resolveSpacingValue(input: {
|
|
|
103
100
|
suggestedValue: reference,
|
|
104
101
|
suggestedToken: exactToken.name,
|
|
105
102
|
fixEmittable: true,
|
|
106
|
-
deterministicFix:
|
|
103
|
+
deterministicFix: normalized.deterministicFix,
|
|
107
104
|
reason: "token-equivalent",
|
|
108
105
|
matchedToken: exactToken.name,
|
|
109
106
|
assumedRootFontSizePx: normalized.assumedRootFontSizePx,
|
|
@@ -117,7 +114,7 @@ export function resolveSpacingValue(input: {
|
|
|
117
114
|
// one exists. Snapping changes rendered value, so this is not deterministic.
|
|
118
115
|
anyViolation = true;
|
|
119
116
|
anyOffScale = true;
|
|
120
|
-
|
|
117
|
+
allValuePreserving = false;
|
|
121
118
|
|
|
122
119
|
const nearest = nearestSignedScaleValue(normalized.value, input.allowed);
|
|
123
120
|
const snapsToZero = nearest === 0 && normalized.value !== 0;
|
|
@@ -158,7 +155,7 @@ export function resolveSpacingValue(input: {
|
|
|
158
155
|
const joined = suggestedParts.join(" ");
|
|
159
156
|
const changes = joined !== input.raw.trim().replace(/\s+/g, " ");
|
|
160
157
|
const fixEmittable = canFixAll && reliableSuggestion && changes;
|
|
161
|
-
const deterministic = fixEmittable && !anyOffScale &&
|
|
158
|
+
const deterministic = fixEmittable && !anyOffScale && allValuePreserving;
|
|
162
159
|
|
|
163
160
|
return {
|
|
164
161
|
...firstViolation,
|
|
@@ -18,11 +18,12 @@ import { makeFinding } from "./finding.js";
|
|
|
18
18
|
import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
|
|
19
19
|
import {
|
|
20
20
|
detectRawColor,
|
|
21
|
+
emitFix,
|
|
21
22
|
indexComponentByNodeId,
|
|
22
|
-
isApplicableTokenReference,
|
|
23
23
|
isExemptColor,
|
|
24
24
|
readUsageNode,
|
|
25
25
|
tokenReference,
|
|
26
|
+
type FixEmission,
|
|
26
27
|
} from "./utils.js";
|
|
27
28
|
|
|
28
29
|
export const RULE_ID = "styles/no-raw-color";
|
|
@@ -43,6 +44,10 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
43
44
|
if (isExemptColor(color, policy.except)) continue;
|
|
44
45
|
|
|
45
46
|
const resolution = resolveColorToken(decl.property, color, colorTokens);
|
|
47
|
+
const fixEmission =
|
|
48
|
+
resolution?.kind === "exact"
|
|
49
|
+
? buildTokenFix(decl.property, decl.value, color, resolution, ix)
|
|
50
|
+
: undefined;
|
|
46
51
|
const evidenceIds = resolution
|
|
47
52
|
? [decl.id, policy.id, resolution.token.id]
|
|
48
53
|
: [decl.id, policy.id];
|
|
@@ -63,10 +68,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
63
68
|
property: decl.property,
|
|
64
69
|
value: color,
|
|
65
70
|
},
|
|
66
|
-
fix:
|
|
67
|
-
resolution?.kind === "exact"
|
|
68
|
-
? buildTokenFix(decl.property, decl.value, color, resolution)
|
|
69
|
-
: undefined,
|
|
71
|
+
fix: fixEmission?.fix,
|
|
70
72
|
attributes: {
|
|
71
73
|
property: decl.property,
|
|
72
74
|
rawValue: decl.value,
|
|
@@ -74,6 +76,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
74
76
|
source: "css",
|
|
75
77
|
suggestedToken: resolution?.token.name,
|
|
76
78
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
79
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
77
80
|
},
|
|
78
81
|
})
|
|
79
82
|
);
|
|
@@ -91,6 +94,10 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
91
94
|
|
|
92
95
|
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
93
96
|
const resolution = resolveColorToken(inline.property, color, colorTokens);
|
|
97
|
+
const fixEmission =
|
|
98
|
+
resolution?.kind === "exact"
|
|
99
|
+
? buildTokenFix(inline.property, inline.value, color, resolution, ix)
|
|
100
|
+
: undefined;
|
|
94
101
|
const baseEvidence = componentEvidenceId
|
|
95
102
|
? [node.id, componentEvidenceId, inline.id, policy.id]
|
|
96
103
|
: [node.id, inline.id, policy.id];
|
|
@@ -112,10 +119,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
112
119
|
property: inline.property,
|
|
113
120
|
value: color,
|
|
114
121
|
},
|
|
115
|
-
fix:
|
|
116
|
-
resolution?.kind === "exact"
|
|
117
|
-
? buildTokenFix(inline.property, inline.value, color, resolution)
|
|
118
|
-
: undefined,
|
|
122
|
+
fix: fixEmission?.fix,
|
|
119
123
|
attributes: {
|
|
120
124
|
property: inline.property,
|
|
121
125
|
rawValue: inline.value,
|
|
@@ -123,6 +127,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
123
127
|
source: "jsx",
|
|
124
128
|
suggestedToken: resolution?.token.name,
|
|
125
129
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
130
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
126
131
|
},
|
|
127
132
|
})
|
|
128
133
|
);
|
|
@@ -298,22 +303,26 @@ function buildTokenFix(
|
|
|
298
303
|
property: string,
|
|
299
304
|
rawValue: string,
|
|
300
305
|
rawColor: string,
|
|
301
|
-
match: TokenMatch
|
|
302
|
-
|
|
306
|
+
match: TokenMatch,
|
|
307
|
+
ix: FactIndex
|
|
308
|
+
): FixEmission<FindingReplaceStyleValueFix> {
|
|
303
309
|
const reference = tokenReference(match.token.name);
|
|
304
310
|
const value = replaceRawColor(rawValue, rawColor, reference);
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
311
|
+
return emitFix(
|
|
312
|
+
{
|
|
313
|
+
kind: "replaceStyleValue",
|
|
314
|
+
title: `Replace ${property} with ${value}`,
|
|
315
|
+
property,
|
|
316
|
+
value,
|
|
317
|
+
deterministic: true,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
symbols: [match.token.name],
|
|
321
|
+
editKind: "replaceStyleValue",
|
|
322
|
+
valuePreserving: !match.ambiguous,
|
|
323
|
+
},
|
|
324
|
+
ix
|
|
325
|
+
);
|
|
317
326
|
}
|
|
318
327
|
|
|
319
328
|
function replaceRawColor(rawValue: string, rawColor: string, replacement: string): string {
|
|
@@ -31,7 +31,13 @@ import type { FactId, FactIndex, TokenDefinitionFact } from "../facts/index.js";
|
|
|
31
31
|
|
|
32
32
|
import { makeFinding } from "./finding.js";
|
|
33
33
|
import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
emitFix,
|
|
36
|
+
indexComponentByNodeId,
|
|
37
|
+
parseLengthValue,
|
|
38
|
+
readUsageNode,
|
|
39
|
+
type FixEmission,
|
|
40
|
+
} from "./utils.js";
|
|
35
41
|
|
|
36
42
|
export const RULE_ID = "styles/no-raw-dimensions";
|
|
37
43
|
export const RULE_VERSION = "1";
|
|
@@ -75,6 +81,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
75
81
|
if (!resolution) continue;
|
|
76
82
|
const tokenIds = resolutionTokenIds(resolution);
|
|
77
83
|
const evidenceIds = tokenIds.length ? [decl.id, policy.id, ...tokenIds] : [decl.id, policy.id];
|
|
84
|
+
const fixEmission = dimensionFix(decl.property, resolution, ix);
|
|
78
85
|
|
|
79
86
|
findings.push(
|
|
80
87
|
makeFinding({
|
|
@@ -92,12 +99,13 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
92
99
|
property: decl.property,
|
|
93
100
|
value: decl.value,
|
|
94
101
|
},
|
|
95
|
-
fix:
|
|
102
|
+
fix: fixEmission?.fix,
|
|
96
103
|
attributes: {
|
|
97
104
|
property: decl.property,
|
|
98
105
|
rawValue: decl.value,
|
|
99
106
|
source: "css",
|
|
100
107
|
suggestedToken: resolutionSuggestedToken(resolution),
|
|
108
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
101
109
|
},
|
|
102
110
|
})
|
|
103
111
|
);
|
|
@@ -121,6 +129,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
121
129
|
: [node.id, inline.id, policy.id];
|
|
122
130
|
const tokenIds = resolutionTokenIds(resolution);
|
|
123
131
|
const evidenceIds = tokenIds.length ? [...baseEvidence, ...tokenIds] : baseEvidence;
|
|
132
|
+
const fixEmission = dimensionFix(inline.property, resolution, ix);
|
|
124
133
|
|
|
125
134
|
findings.push(
|
|
126
135
|
makeFinding({
|
|
@@ -138,12 +147,13 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
138
147
|
property: inline.property,
|
|
139
148
|
value: inline.value,
|
|
140
149
|
},
|
|
141
|
-
fix:
|
|
150
|
+
fix: fixEmission?.fix,
|
|
142
151
|
attributes: {
|
|
143
152
|
property: inline.property,
|
|
144
153
|
rawValue: inline.value,
|
|
145
154
|
source: "jsx",
|
|
146
155
|
suggestedToken: resolutionSuggestedToken(resolution),
|
|
156
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
147
157
|
},
|
|
148
158
|
})
|
|
149
159
|
);
|
|
@@ -219,16 +229,25 @@ function dimensionMessage(
|
|
|
219
229
|
|
|
220
230
|
function dimensionFix(
|
|
221
231
|
property: string,
|
|
222
|
-
resolution: DimensionResolution
|
|
223
|
-
|
|
232
|
+
resolution: DimensionResolution,
|
|
233
|
+
ix: FactIndex
|
|
234
|
+
): FixEmission<FindingReplaceStyleValueFix> | undefined {
|
|
224
235
|
if (resolution.kind === "exact") {
|
|
225
|
-
return
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
return emitFix(
|
|
237
|
+
{
|
|
238
|
+
kind: "replaceStyleValue",
|
|
239
|
+
title: `Replace ${property} with ${resolution.fixValue}`,
|
|
240
|
+
property,
|
|
241
|
+
value: resolution.fixValue,
|
|
242
|
+
deterministic: true,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
symbols: resolution.tokens.map((token) => token.name),
|
|
246
|
+
editKind: "replaceStyleValue",
|
|
247
|
+
valuePreserving: true,
|
|
248
|
+
},
|
|
249
|
+
ix
|
|
250
|
+
);
|
|
232
251
|
}
|
|
233
252
|
// `nearest` deliberately emits NO fix — a value-changing snap must stay a
|
|
234
253
|
// suggestion (see DimensionResolution); only the exact, value-preserving swap
|
|
@@ -42,7 +42,13 @@ import {
|
|
|
42
42
|
type SpacingTokenLookup,
|
|
43
43
|
} from "./spacing-resolution.js";
|
|
44
44
|
import type { Finding } from "./types.js";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
emitFix,
|
|
47
|
+
indexComponentByNodeId,
|
|
48
|
+
readUsageNode,
|
|
49
|
+
tokenSymbolsInText,
|
|
50
|
+
type FixEmission,
|
|
51
|
+
} from "./utils.js";
|
|
46
52
|
|
|
47
53
|
export const RULE_ID = "styles/no-raw-spacing";
|
|
48
54
|
export const RULE_VERSION = "1";
|
|
@@ -104,6 +110,7 @@ function checkDeclaration(
|
|
|
104
110
|
tokens: lookupFor(scale),
|
|
105
111
|
});
|
|
106
112
|
if (!checked) return null;
|
|
113
|
+
const fixEmission = buildSpacingFix(decl.property, checked, ix);
|
|
107
114
|
|
|
108
115
|
return makeFinding({
|
|
109
116
|
ruleId: RULE_ID,
|
|
@@ -120,7 +127,7 @@ function checkDeclaration(
|
|
|
120
127
|
property: decl.property,
|
|
121
128
|
value: decl.value,
|
|
122
129
|
},
|
|
123
|
-
fix:
|
|
130
|
+
fix: fixEmission?.fix,
|
|
124
131
|
attributes: {
|
|
125
132
|
property: decl.property,
|
|
126
133
|
rawValue: decl.value,
|
|
@@ -135,6 +142,7 @@ function checkDeclaration(
|
|
|
135
142
|
assumedRootFontSizePx: checked.assumedRootFontSizePx,
|
|
136
143
|
assumedEmBasePx: checked.assumedEmBasePx,
|
|
137
144
|
source: "css",
|
|
145
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
138
146
|
},
|
|
139
147
|
});
|
|
140
148
|
}
|
|
@@ -162,6 +170,7 @@ function checkInlineStyle(
|
|
|
162
170
|
bareNumberFix: inline.valueKind === "number",
|
|
163
171
|
});
|
|
164
172
|
if (!checked) return null;
|
|
173
|
+
const fixEmission = buildSpacingFix(inline.property, checked, ix);
|
|
165
174
|
|
|
166
175
|
const node = readUsageNode(ix, inline.nodeId);
|
|
167
176
|
if (!node) return null;
|
|
@@ -186,7 +195,7 @@ function checkInlineStyle(
|
|
|
186
195
|
property: inline.property,
|
|
187
196
|
value: inline.value,
|
|
188
197
|
},
|
|
189
|
-
fix:
|
|
198
|
+
fix: fixEmission?.fix,
|
|
190
199
|
attributes: {
|
|
191
200
|
property: inline.property,
|
|
192
201
|
rawValue: inline.value,
|
|
@@ -201,19 +210,32 @@ function checkInlineStyle(
|
|
|
201
210
|
assumedRootFontSizePx: checked.assumedRootFontSizePx,
|
|
202
211
|
assumedEmBasePx: checked.assumedEmBasePx,
|
|
203
212
|
source: "jsx",
|
|
213
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
204
214
|
},
|
|
205
215
|
});
|
|
206
216
|
}
|
|
207
217
|
|
|
208
|
-
function buildSpacingFix(
|
|
218
|
+
function buildSpacingFix(
|
|
219
|
+
property: string,
|
|
220
|
+
checked: CheckedSpacingValue,
|
|
221
|
+
ix: FactIndex
|
|
222
|
+
): FixEmission | undefined {
|
|
209
223
|
if (!checked.fixEmittable || checked.suggestedValue === undefined) return undefined;
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
return emitFix(
|
|
225
|
+
{
|
|
226
|
+
kind: "replaceStyleValue" as const,
|
|
227
|
+
title: `Replace ${property} with ${checked.suggestedValue}`,
|
|
228
|
+
property,
|
|
229
|
+
value: checked.suggestedValue,
|
|
230
|
+
deterministic: true,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
symbols: tokenSymbolsInText(checked.suggestedValue),
|
|
234
|
+
editKind: "replaceStyleValue",
|
|
235
|
+
valuePreserving: checked.deterministicFix,
|
|
236
|
+
},
|
|
237
|
+
ix
|
|
238
|
+
);
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
function spacingMessage(
|
|
@@ -29,13 +29,15 @@ import { makeFinding } from "./finding.js";
|
|
|
29
29
|
import type { Finding } from "./types.js";
|
|
30
30
|
import {
|
|
31
31
|
indexComponentByNodeId,
|
|
32
|
-
|
|
32
|
+
emitFix,
|
|
33
33
|
matchesScale,
|
|
34
34
|
nearestSignedScaleValue,
|
|
35
35
|
normalizeLengthForScale,
|
|
36
36
|
parseLengthValue,
|
|
37
37
|
readUsageNode,
|
|
38
38
|
tokenReference,
|
|
39
|
+
tokenSymbolsInText,
|
|
40
|
+
type FixEmission,
|
|
39
41
|
} from "./utils.js";
|
|
40
42
|
|
|
41
43
|
export const RULE_ID = "styles/no-raw-typography";
|
|
@@ -58,6 +60,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
58
60
|
if (decl.property.toLowerCase() !== "font-size") continue;
|
|
59
61
|
const checked = checkFontSize(decl.value, allowed, scale, tokens);
|
|
60
62
|
if (!checked) continue;
|
|
63
|
+
const fixEmission = buildFix(decl.property, checked, ix);
|
|
61
64
|
findings.push(
|
|
62
65
|
makeFinding({
|
|
63
66
|
ruleId: RULE_ID,
|
|
@@ -74,7 +77,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
74
77
|
property: decl.property,
|
|
75
78
|
value: decl.value,
|
|
76
79
|
},
|
|
77
|
-
fix:
|
|
80
|
+
fix: fixEmission?.fix,
|
|
78
81
|
attributes: {
|
|
79
82
|
property: decl.property,
|
|
80
83
|
rawValue: decl.value,
|
|
@@ -83,6 +86,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
83
86
|
suggestedValue: checked.suggestedValue,
|
|
84
87
|
matchedToken: checked.matchedToken,
|
|
85
88
|
source: "css",
|
|
89
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
86
90
|
},
|
|
87
91
|
})
|
|
88
92
|
);
|
|
@@ -92,8 +96,15 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
92
96
|
for (const inline of ix.byKind("usage_inline_style")) {
|
|
93
97
|
if (inline.valueKind === "css-variable") continue;
|
|
94
98
|
if (!FONT_SIZE_PROPERTIES.has(inline.property.toLowerCase())) continue;
|
|
95
|
-
const checked = checkFontSize(
|
|
99
|
+
const checked = checkFontSize(
|
|
100
|
+
inline.value,
|
|
101
|
+
allowed,
|
|
102
|
+
scale,
|
|
103
|
+
tokens,
|
|
104
|
+
inline.valueKind === "number"
|
|
105
|
+
);
|
|
96
106
|
if (!checked) continue;
|
|
107
|
+
const fixEmission = buildFix(inline.property, checked, ix);
|
|
97
108
|
const node = readUsageNode(ix, inline.nodeId);
|
|
98
109
|
if (!node) continue;
|
|
99
110
|
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
@@ -116,7 +127,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
116
127
|
property: inline.property,
|
|
117
128
|
value: inline.value,
|
|
118
129
|
},
|
|
119
|
-
fix:
|
|
130
|
+
fix: fixEmission?.fix,
|
|
120
131
|
attributes: {
|
|
121
132
|
property: inline.property,
|
|
122
133
|
rawValue: inline.value,
|
|
@@ -125,6 +136,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
125
136
|
suggestedValue: checked.suggestedValue,
|
|
126
137
|
matchedToken: checked.matchedToken,
|
|
127
138
|
source: "jsx",
|
|
139
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
128
140
|
},
|
|
129
141
|
})
|
|
130
142
|
);
|
|
@@ -175,12 +187,10 @@ function checkFontSize(
|
|
|
175
187
|
|
|
176
188
|
if (matchesScale(normalized.value, allowed)) {
|
|
177
189
|
if (!exactToken) return null;
|
|
178
|
-
const applicable =
|
|
179
|
-
isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
|
|
180
190
|
return {
|
|
181
191
|
suggestedValue: tokenReference(exactToken.name),
|
|
182
192
|
fixEmittable: true,
|
|
183
|
-
deterministicFix:
|
|
193
|
+
deterministicFix: normalized.deterministicFix,
|
|
184
194
|
reason: "token-equivalent",
|
|
185
195
|
matchedToken: exactToken.name,
|
|
186
196
|
};
|
|
@@ -206,15 +216,27 @@ function checkFontSize(
|
|
|
206
216
|
};
|
|
207
217
|
}
|
|
208
218
|
|
|
209
|
-
function buildFix(
|
|
219
|
+
function buildFix(
|
|
220
|
+
property: string,
|
|
221
|
+
checked: CheckedFontSize,
|
|
222
|
+
ix: FactIndex
|
|
223
|
+
): FixEmission | undefined {
|
|
210
224
|
if (!checked.fixEmittable || checked.suggestedValue === undefined) return undefined;
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
225
|
+
return emitFix(
|
|
226
|
+
{
|
|
227
|
+
kind: "replaceStyleValue" as const,
|
|
228
|
+
title: `Replace ${property} with ${checked.suggestedValue}`,
|
|
229
|
+
property,
|
|
230
|
+
value: checked.suggestedValue,
|
|
231
|
+
deterministic: true,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
symbols: tokenSymbolsInText(checked.suggestedValue),
|
|
235
|
+
editKind: "replaceStyleValue",
|
|
236
|
+
valuePreserving: checked.deterministicFix,
|
|
237
|
+
},
|
|
238
|
+
ix
|
|
239
|
+
);
|
|
218
240
|
}
|
|
219
241
|
|
|
220
242
|
function typographyMessage(
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
makeContractTokenFact,
|
|
13
13
|
makeStyleCssVarsMustBeDefinedFact,
|
|
14
14
|
makeStyleDeclarationFact,
|
|
15
|
+
makeTokenDefinitionFact,
|
|
15
16
|
makeUsageNodeFact,
|
|
16
17
|
makeUsageTextChildFact,
|
|
17
18
|
RULE_TIER,
|
|
@@ -36,6 +37,7 @@ import type { Fact } from "../index.js";
|
|
|
36
37
|
function rawValueFixture(): Fact[] {
|
|
37
38
|
const file = "src/Widget.module.scss";
|
|
38
39
|
return [
|
|
40
|
+
dualFallbackToken(),
|
|
39
41
|
makeStyleDeclarationFact({
|
|
40
42
|
file,
|
|
41
43
|
selector: ".a",
|
|
@@ -64,6 +66,15 @@ function rawValueFixture(): Fact[] {
|
|
|
64
66
|
];
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function dualFallbackToken(): Fact {
|
|
70
|
+
return makeTokenDefinitionFact({
|
|
71
|
+
name: "$fui-surface-secondary",
|
|
72
|
+
value: "#f1f5f9",
|
|
73
|
+
category: "color",
|
|
74
|
+
referenceFormat: "scss-var",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
function index(facts: Fact[]): FactIndex {
|
|
68
79
|
const ix = new FactIndex();
|
|
69
80
|
ix.addMany(facts);
|
|
@@ -138,12 +149,14 @@ describe("§2 tokens/require-dual-fallback is Fragments-internal", () => {
|
|
|
138
149
|
it("customer preset = 0, fragments preset = 1", () => {
|
|
139
150
|
const customer = index([
|
|
140
151
|
...compileGlobalGovernanceFacts({ rules: customerDefaultRuleStates() }),
|
|
152
|
+
dualFallbackToken(),
|
|
141
153
|
decl,
|
|
142
154
|
]);
|
|
143
155
|
expect(runRules(customer)).toHaveLength(0);
|
|
144
156
|
|
|
145
157
|
const fragments = index([
|
|
146
158
|
...compileGlobalGovernanceFacts({ rules: fragmentsPresetRuleStates() }),
|
|
159
|
+
dualFallbackToken(),
|
|
147
160
|
decl,
|
|
148
161
|
]);
|
|
149
162
|
const fragmentsFindings = runRules(fragments);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { FactIndex } from "../facts/index.js";
|
|
1
|
+
import type { FactIndex, TokenDefinitionFact } from "../facts/index.js";
|
|
2
2
|
|
|
3
3
|
import { makeFinding } from "./finding.js";
|
|
4
4
|
import type { Finding } from "./types.js";
|
|
5
|
-
import {
|
|
5
|
+
import { emitFix } from "./utils.js";
|
|
6
6
|
|
|
7
7
|
export const RULE_ID = "tokens/require-dual-fallback";
|
|
8
8
|
export const RULE_VERSION = "1";
|
|
@@ -22,7 +22,7 @@ export function ruleTokensRequireDualFallback(ix: FactIndex): Finding[] {
|
|
|
22
22
|
const policy = ix.policy.ruleConfig(RULE_ID);
|
|
23
23
|
if (!policy?.enabled) return [];
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const tokensByCssVariable = indexTokensByCssVariable(ix.tokens.list());
|
|
26
26
|
const findings: Finding[] = [];
|
|
27
27
|
|
|
28
28
|
for (const decl of ix.byKind("style_declaration")) {
|
|
@@ -33,21 +33,43 @@ export function ruleTokensRequireDualFallback(ix: FactIndex): Finding[] {
|
|
|
33
33
|
const tokenName = match[1];
|
|
34
34
|
const hasFallback = match[2] !== undefined;
|
|
35
35
|
if (hasFallback) continue;
|
|
36
|
-
|
|
36
|
+
const token = tokensByCssVariable.get(tokenName);
|
|
37
|
+
if (!token) continue;
|
|
37
38
|
|
|
38
39
|
const rawValue = match[0];
|
|
39
40
|
const scssVar = tokenName.replace(/^--/, "$");
|
|
40
|
-
const
|
|
41
|
-
const nextValue =
|
|
41
|
+
const hasScssVariable = token.referenceFormat === "scss-var";
|
|
42
|
+
const nextValue = hasScssVariable
|
|
43
|
+
? decl.value.replace(rawValue, `var(${tokenName}, ${scssVar})`)
|
|
44
|
+
: undefined;
|
|
45
|
+
const fixEmission = nextValue
|
|
46
|
+
? emitFix(
|
|
47
|
+
{
|
|
48
|
+
kind: "replaceStyleValue",
|
|
49
|
+
title: `Add fallback for ${tokenName}`,
|
|
50
|
+
property: decl.property,
|
|
51
|
+
value: nextValue,
|
|
52
|
+
deterministic: true,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
symbols: [tokenName, scssVar],
|
|
56
|
+
editKind: "replaceStyleValue",
|
|
57
|
+
valuePreserving: true,
|
|
58
|
+
},
|
|
59
|
+
ix
|
|
60
|
+
)
|
|
61
|
+
: undefined;
|
|
42
62
|
|
|
43
63
|
findings.push(
|
|
44
64
|
makeFinding({
|
|
45
65
|
ruleId: RULE_ID,
|
|
46
66
|
ruleVersion: RULE_VERSION,
|
|
47
67
|
severity: policy.severity ?? "warn",
|
|
48
|
-
message:
|
|
68
|
+
message: hasScssVariable
|
|
69
|
+
? `var(${tokenName}) is missing an SCSS fallback. Use var(${tokenName}, ${scssVar}).`
|
|
70
|
+
: `var(${tokenName}) is missing a fallback. No matching SCSS variable was found in the token source; add a fallback manually or define ${scssVar}.`,
|
|
49
71
|
location: decl.location,
|
|
50
|
-
evidence: ix.evidence([decl.id, policy.id]),
|
|
72
|
+
evidence: ix.evidence([decl.id, token.id, policy.id]),
|
|
51
73
|
fingerprintIdentity: {
|
|
52
74
|
file: decl.file,
|
|
53
75
|
selector: decl.selector,
|
|
@@ -55,24 +77,39 @@ export function ruleTokensRequireDualFallback(ix: FactIndex): Finding[] {
|
|
|
55
77
|
property: decl.property,
|
|
56
78
|
tokenName,
|
|
57
79
|
},
|
|
58
|
-
fix:
|
|
59
|
-
kind: "replaceStyleValue",
|
|
60
|
-
title: `Add fallback for ${tokenName}`,
|
|
61
|
-
property: decl.property,
|
|
62
|
-
value: nextValue,
|
|
63
|
-
deterministic: true,
|
|
64
|
-
},
|
|
80
|
+
fix: fixEmission?.fix,
|
|
65
81
|
attributes: {
|
|
66
82
|
property: decl.property,
|
|
67
83
|
rawValue: decl.value,
|
|
68
84
|
tokenName,
|
|
69
|
-
suggestedValue: nextValue,
|
|
85
|
+
...(nextValue ? { suggestedValue: nextValue } : {}),
|
|
86
|
+
...(fixEmission?.downgradeReason
|
|
87
|
+
? { downgradeReason: fixEmission.downgradeReason }
|
|
88
|
+
: {}),
|
|
70
89
|
source: "css",
|
|
71
90
|
},
|
|
72
|
-
})
|
|
91
|
+
})
|
|
73
92
|
);
|
|
74
93
|
}
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
return findings;
|
|
78
97
|
}
|
|
98
|
+
|
|
99
|
+
function indexTokensByCssVariable(
|
|
100
|
+
tokens: readonly TokenDefinitionFact[]
|
|
101
|
+
): Map<string, TokenDefinitionFact> {
|
|
102
|
+
const indexed = new Map<string, TokenDefinitionFact>();
|
|
103
|
+
for (const token of tokens) {
|
|
104
|
+
const cssVariable = token.name.startsWith("--")
|
|
105
|
+
? token.name
|
|
106
|
+
: token.name.startsWith("$")
|
|
107
|
+
? `--${token.name.slice(1)}`
|
|
108
|
+
: `--${token.name}`;
|
|
109
|
+
const existing = indexed.get(cssVariable);
|
|
110
|
+
if (!existing || token.referenceFormat === "scss-var") {
|
|
111
|
+
indexed.set(cssVariable, token);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return indexed;
|
|
115
|
+
}
|