@usefragments/core 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-MLRDNSSA.js → chunk-CBSNXFOD.js} +1 -1
- package/dist/chunk-CBSNXFOD.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-DYSirwJu.d.ts → governance-DxdJV6lx.d.ts} +10 -0
- package/dist/index.d.ts +97 -75
- package/dist/index.js +163 -30
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +18 -18
- package/dist/schemas/index.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/constants.ts +3 -0
- package/src/facts/builders.ts +1 -1
- package/src/facts/types.ts +14 -1
- package/src/rules/components-prefer-library.test.ts +148 -0
- package/src/rules/components-prefer-library.ts +52 -1
- package/src/rules/styles-no-raw-color.test.ts +213 -0
- package/src/rules/styles-no-raw-color.ts +125 -0
- package/src/schema.ts +6 -2
- package/src/token-types.ts +12 -0
- package/src/tokens/categories.ts +54 -24
- package/src/tokens/scss.test.ts +94 -0
- package/src/tokens/scss.ts +11 -4
- package/src/types.ts +11 -0
- package/dist/chunk-MLRDNSSA.js.map +0 -1
- package/dist/{index-h_yWj15D.d.ts → index-0lmh0Lbo.d.ts} +8 -8
|
@@ -37,6 +37,12 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
37
37
|
const preferLabel = policy.prefer === "token" ? "design token" : "CSS variable";
|
|
38
38
|
const findings: Finding[] = [];
|
|
39
39
|
|
|
40
|
+
// Newly-broadened detections (SVG paint attributes, runtime-constructed
|
|
41
|
+
// colors) ship advisory this release so CI gates do not harden on findings
|
|
42
|
+
// teams have not yet had a chance to triage. Existing passes keep the
|
|
43
|
+
// configured severity.
|
|
44
|
+
const advisorySeverity = policy.severity === "error" ? "warn" : policy.severity;
|
|
45
|
+
|
|
40
46
|
for (const decl of ix.byKind("style_declaration")) {
|
|
41
47
|
if (decl.property.startsWith("--") && decl.declaredTokenSource === true) continue;
|
|
42
48
|
const color = detectRawColor(decl.value);
|
|
@@ -84,6 +90,43 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
84
90
|
|
|
85
91
|
const componentByNode = indexComponentByNodeId(ix);
|
|
86
92
|
for (const inline of ix.byKind("usage_inline_style")) {
|
|
93
|
+
// #11b — a runtime-constructed color (`` `#${red}` ``, `"rgb(" + r + …`).
|
|
94
|
+
// The extractor already proved the static fragments begin a color literal,
|
|
95
|
+
// so surface it detect-only: the value is partly runtime, so it never
|
|
96
|
+
// carries a deterministic fix and cannot resolve to a token.
|
|
97
|
+
if (inline.valueKind === "dynamic-raw") {
|
|
98
|
+
const node = readUsageNode(ix, inline.nodeId);
|
|
99
|
+
if (!node) continue;
|
|
100
|
+
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
101
|
+
const evidenceIds = componentEvidenceId
|
|
102
|
+
? [node.id, componentEvidenceId, inline.id, policy.id]
|
|
103
|
+
: [node.id, inline.id, policy.id];
|
|
104
|
+
findings.push(
|
|
105
|
+
makeFinding({
|
|
106
|
+
ruleId: RULE_ID,
|
|
107
|
+
ruleVersion: RULE_VERSION,
|
|
108
|
+
severity: advisorySeverity,
|
|
109
|
+
message: `Runtime-constructed raw color \`${inline.value}\` on inline \`${inline.property}\`. Use a ${preferLabel} instead.`,
|
|
110
|
+
location: node.location,
|
|
111
|
+
evidence: ix.evidence(evidenceIds),
|
|
112
|
+
fingerprintIdentity: {
|
|
113
|
+
source: "usage_inline_style_dynamic",
|
|
114
|
+
file: node.file,
|
|
115
|
+
nodePath: node.nodePath,
|
|
116
|
+
element: node.element,
|
|
117
|
+
property: inline.property,
|
|
118
|
+
value: inline.value,
|
|
119
|
+
},
|
|
120
|
+
attributes: {
|
|
121
|
+
property: inline.property,
|
|
122
|
+
rawValue: inline.value,
|
|
123
|
+
source: "jsx",
|
|
124
|
+
dynamic: true,
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
87
130
|
if (inline.valueKind === "css-variable") continue;
|
|
88
131
|
const color = detectRawColor(inline.value);
|
|
89
132
|
if (!color) continue;
|
|
@@ -133,9 +176,91 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
133
176
|
);
|
|
134
177
|
}
|
|
135
178
|
|
|
179
|
+
// #9d — SVG presentation attributes carry color in JSX markup, not CSS, so
|
|
180
|
+
// they arrive as `usage_prop_resolved` facts the two passes above never read.
|
|
181
|
+
// Scan a fixed paint-attribute allowlist for static raw colors. Data-URI
|
|
182
|
+
// colors (hex inside an encoded `url("data:…")`) are intentionally left out.
|
|
183
|
+
for (const prop of ix.byKind("usage_prop_resolved")) {
|
|
184
|
+
if (prop.resolution !== "static") continue;
|
|
185
|
+
if (typeof prop.value !== "string") continue;
|
|
186
|
+
if (!SVG_PAINT_ATTRS.has(prop.prop)) continue;
|
|
187
|
+
const color = detectRawColor(prop.value);
|
|
188
|
+
if (!color) continue;
|
|
189
|
+
if (isExemptColor(color, policy.except)) continue;
|
|
190
|
+
|
|
191
|
+
const node = readUsageNode(ix, prop.nodeId);
|
|
192
|
+
if (!node) continue;
|
|
193
|
+
|
|
194
|
+
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
195
|
+
const cssProperty = SVG_PAINT_CSS_PROPERTY[prop.prop] ?? prop.prop;
|
|
196
|
+
const resolution = resolveColorToken(cssProperty, color, colorTokens);
|
|
197
|
+
const fixEmission =
|
|
198
|
+
resolution?.kind === "exact"
|
|
199
|
+
? buildTokenFix(cssProperty, prop.value, color, resolution, ix)
|
|
200
|
+
: undefined;
|
|
201
|
+
const baseEvidence = componentEvidenceId
|
|
202
|
+
? [node.id, componentEvidenceId, prop.id, policy.id]
|
|
203
|
+
: [node.id, prop.id, policy.id];
|
|
204
|
+
const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
|
|
205
|
+
|
|
206
|
+
findings.push(
|
|
207
|
+
makeFinding({
|
|
208
|
+
ruleId: RULE_ID,
|
|
209
|
+
ruleVersion: RULE_VERSION,
|
|
210
|
+
severity: advisorySeverity,
|
|
211
|
+
message: colorMessage(color, `\`${prop.prop}\``, resolution, preferLabel),
|
|
212
|
+
location: prop.location ?? node.location,
|
|
213
|
+
evidence: ix.evidence(evidenceIds),
|
|
214
|
+
fingerprintIdentity: {
|
|
215
|
+
source: "usage_prop_resolved",
|
|
216
|
+
file: node.file,
|
|
217
|
+
nodePath: node.nodePath,
|
|
218
|
+
element: node.element,
|
|
219
|
+
property: prop.prop,
|
|
220
|
+
value: color,
|
|
221
|
+
},
|
|
222
|
+
fix: fixEmission?.fix,
|
|
223
|
+
attributes: {
|
|
224
|
+
property: prop.prop,
|
|
225
|
+
rawValue: prop.value,
|
|
226
|
+
color,
|
|
227
|
+
source: "jsx",
|
|
228
|
+
suggestedToken: resolution?.token.name,
|
|
229
|
+
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
230
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
136
236
|
return findings;
|
|
137
237
|
}
|
|
138
238
|
|
|
239
|
+
/**
|
|
240
|
+
* SVG presentation attributes that carry a paint (color) value in JSX markup.
|
|
241
|
+
* Both the raw-SVG kebab spelling and React's camelCase normalization are
|
|
242
|
+
* listed so `stop-color` and `stopColor` both resolve.
|
|
243
|
+
*/
|
|
244
|
+
const SVG_PAINT_ATTRS = new Set([
|
|
245
|
+
"fill",
|
|
246
|
+
"stroke",
|
|
247
|
+
"color",
|
|
248
|
+
"stop-color",
|
|
249
|
+
"stopColor",
|
|
250
|
+
"flood-color",
|
|
251
|
+
"floodColor",
|
|
252
|
+
"lighting-color",
|
|
253
|
+
"lightingColor",
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
/** Map camelCase paint attributes to their CSS property name so the token
|
|
257
|
+
* role-resolver (`colorRoleForProperty`) reasons over a canonical spelling. */
|
|
258
|
+
const SVG_PAINT_CSS_PROPERTY: Record<string, string> = {
|
|
259
|
+
stopColor: "stop-color",
|
|
260
|
+
floodColor: "flood-color",
|
|
261
|
+
lightingColor: "lighting-color",
|
|
262
|
+
};
|
|
263
|
+
|
|
139
264
|
interface TokenMatch {
|
|
140
265
|
token: TokenDefinitionFact;
|
|
141
266
|
/** True when several tokens share the value and none matches the property's
|
package/src/schema.ts
CHANGED
|
@@ -244,6 +244,10 @@ const tokenConfigSchema = z
|
|
|
244
244
|
.object({
|
|
245
245
|
include: repoRelativePathsSchema.optional(),
|
|
246
246
|
sources: z.array(tokenSourceSchema).min(1).optional(),
|
|
247
|
+
// Declared design-system packages whose shipped fragments.json token
|
|
248
|
+
// vocabulary seeds the known-token set (#7c). A packages-only tokens block
|
|
249
|
+
// is valid — see the superRefine below.
|
|
250
|
+
packages: z.array(z.string().min(1)).optional(),
|
|
247
251
|
exclude: z.array(repoRelativePathSchema).optional(),
|
|
248
252
|
themeSelectors: z.record(z.string()).optional(),
|
|
249
253
|
enabled: z.boolean().optional(),
|
|
@@ -252,10 +256,10 @@ const tokenConfigSchema = z
|
|
|
252
256
|
})
|
|
253
257
|
.passthrough()
|
|
254
258
|
.superRefine((tokens, ctx) => {
|
|
255
|
-
if (!tokens.include?.length && !tokens.sources?.length) {
|
|
259
|
+
if (!tokens.include?.length && !tokens.sources?.length && !tokens.packages?.length) {
|
|
256
260
|
ctx.addIssue({
|
|
257
261
|
code: z.ZodIssueCode.custom,
|
|
258
|
-
message: "Add tokens.include or tokens.
|
|
262
|
+
message: "Add tokens.include, tokens.sources, or tokens.packages",
|
|
259
263
|
path: ["sources"],
|
|
260
264
|
});
|
|
261
265
|
}
|
package/src/token-types.ts
CHANGED
|
@@ -193,6 +193,18 @@ export interface TokenConfig {
|
|
|
193
193
|
*/
|
|
194
194
|
exclude?: string[];
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* npm package names whose SHIPPED `fragments.json` token vocabulary should seed
|
|
198
|
+
* the known-token set (e.g. `["@usefragments/ui"]`). This is a DECLARED-manifest
|
|
199
|
+
* ingest — the parser reads each package's published `fragments.json` (via its
|
|
200
|
+
* `<pkg>/fragments.json` export or its package.json `fragments` field) and
|
|
201
|
+
* merges the token NAMES into the vocabulary consumed by
|
|
202
|
+
* `tokens/css-vars-must-be-defined`, so a consumer using those `--fui-*` vars is
|
|
203
|
+
* not flagged as off-contract drift. node_modules SCSS is NEVER globbed — only
|
|
204
|
+
* the declared manifest is trusted, keeping the no-inference mandate intact.
|
|
205
|
+
*/
|
|
206
|
+
packages?: string[];
|
|
207
|
+
|
|
196
208
|
/**
|
|
197
209
|
* Map CSS selectors to theme names
|
|
198
210
|
* @example { ":root": "default", "[data-theme='dark']": "dark" }
|
package/src/tokens/categories.ts
CHANGED
|
@@ -96,7 +96,48 @@ export function inferTokenGroup(name: string): string {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Curated section-comment → category map. ONLY these headings (and short, clean
|
|
101
|
+
* uncurated headings) become token categories. A prose comment latched as a
|
|
102
|
+
* category is the #7d smell (`the-@supports-block-below-…` etc.), so anything
|
|
103
|
+
* that is not curated AND does not read like a heading returns `null`, letting
|
|
104
|
+
* the caller fall back to name-based `inferTokenGroup`.
|
|
105
|
+
*/
|
|
106
|
+
const TOKEN_GROUP_COMMENT_MAPPINGS: Record<string, string> = {
|
|
107
|
+
"base configuration": "base",
|
|
108
|
+
typography: "typography",
|
|
109
|
+
"spacing (micro)": "spacing",
|
|
110
|
+
spacing: "spacing",
|
|
111
|
+
"density padding": "spacing",
|
|
112
|
+
"border radius": "radius",
|
|
113
|
+
transitions: "transitions",
|
|
114
|
+
colors: "colors",
|
|
115
|
+
surfaces: "surfaces",
|
|
116
|
+
text: "text",
|
|
117
|
+
borders: "borders",
|
|
118
|
+
shadows: "shadows",
|
|
119
|
+
focus: "focus",
|
|
120
|
+
scrollbar: "scrollbar",
|
|
121
|
+
"component heights": "component-sizing",
|
|
122
|
+
"appshell layout": "layout",
|
|
123
|
+
codeblock: "code",
|
|
124
|
+
tooltip: "tooltip",
|
|
125
|
+
"hero/marketing gradient": "marketing",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A "clean heading" is alphanumerics + spaces + hyphens only (no punctuation,
|
|
130
|
+
* symbols, em/en dashes, parens, `@`, `/`, etc.) and at most three words. Prose
|
|
131
|
+
* comments — which carry punctuation and/or run long — fail this and are NOT
|
|
132
|
+
* treated as categories.
|
|
133
|
+
*/
|
|
134
|
+
function looksLikeCleanHeading(text: string): boolean {
|
|
135
|
+
if (!/^[a-z0-9][a-z0-9 -]*$/.test(text)) return false;
|
|
136
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
137
|
+
return wordCount > 0 && wordCount <= 3;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function normalizeTokenGroupComment(comment: string): string | null {
|
|
100
141
|
const text = comment
|
|
101
142
|
.trim()
|
|
102
143
|
.replace(/^\/\/\s*/, "")
|
|
@@ -105,27 +146,16 @@ export function normalizeTokenGroupComment(comment: string): string {
|
|
|
105
146
|
.trim()
|
|
106
147
|
.toLowerCase();
|
|
107
148
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
shadows: "shadows",
|
|
121
|
-
focus: "focus",
|
|
122
|
-
scrollbar: "scrollbar",
|
|
123
|
-
"component heights": "component-sizing",
|
|
124
|
-
"appshell layout": "layout",
|
|
125
|
-
codeblock: "code",
|
|
126
|
-
tooltip: "tooltip",
|
|
127
|
-
"hero/marketing gradient": "marketing",
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return mappings[text] ?? text.replace(/\s+/g, "-");
|
|
149
|
+
if (text in TOKEN_GROUP_COMMENT_MAPPINGS) {
|
|
150
|
+
return TOKEN_GROUP_COMMENT_MAPPINGS[text];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Uncurated: only accept short, clean headings as categories. Prose comments
|
|
154
|
+
// (punctuation/symbols or >3 words) return null so the token falls through to
|
|
155
|
+
// name-based inference instead of minting a garbage category slug (#7d).
|
|
156
|
+
if (!looksLikeCleanHeading(text)) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return text.replace(/\s+/g, "-");
|
|
131
161
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { normalizeTokenGroupComment } from "./categories.js";
|
|
4
|
+
import { parseScssTokens } from "./scss.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* FRICTION #7d — stop prose SCSS comments from being slugified into token
|
|
8
|
+
* categories (the `the-@supports-block-below-…` / `colors-—-accent-(…)` smell in
|
|
9
|
+
* ui/fragments.json). Prose comments must fall through to name-based inference;
|
|
10
|
+
* curated section headings still map.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
describe("normalizeTokenGroupComment (#7d)", () => {
|
|
14
|
+
it("maps curated section headings", () => {
|
|
15
|
+
expect(normalizeTokenGroupComment("// Typography")).toBe("typography");
|
|
16
|
+
expect(normalizeTokenGroupComment("// Spacing")).toBe("spacing");
|
|
17
|
+
expect(normalizeTokenGroupComment("// Spacing (micro)")).toBe("spacing");
|
|
18
|
+
expect(normalizeTokenGroupComment("// Component heights")).toBe("component-sizing");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns null for prose comments (punctuation / symbols / long)", () => {
|
|
22
|
+
expect(
|
|
23
|
+
normalizeTokenGroupComment("// The @supports block below derives accent tokens")
|
|
24
|
+
).toBeNull();
|
|
25
|
+
expect(
|
|
26
|
+
normalizeTokenGroupComment("// Colors — accent (light/dark for automatic dark mode)")
|
|
27
|
+
).toBeNull();
|
|
28
|
+
expect(normalizeTokenGroupComment("// Shadows (not <color> type)")).toBeNull();
|
|
29
|
+
expect(
|
|
30
|
+
normalizeTokenGroupComment("// Touch targets (density-responsive, control floor: 32px)")
|
|
31
|
+
).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("slugifies a short, clean uncurated heading", () => {
|
|
35
|
+
expect(normalizeTokenGroupComment("// Custom Section")).toBe("custom-section");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("parseScssTokens category derivation (#7d)", () => {
|
|
40
|
+
it("does not create a category from a prose comment; tokens fall to name inference", () => {
|
|
41
|
+
const scss = `
|
|
42
|
+
:root {
|
|
43
|
+
// The @supports block below derives accent/semantic tokens from these seeds
|
|
44
|
+
--fui-color-accent: #39594d;
|
|
45
|
+
--fui-space-3: 12px;
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
const out = parseScssTokens(scss);
|
|
49
|
+
const categoryKeys = Object.keys(out.categories);
|
|
50
|
+
|
|
51
|
+
// No garbage slug leaked in.
|
|
52
|
+
for (const key of categoryKeys) {
|
|
53
|
+
expect(key).not.toMatch(/[@(<>—/]/);
|
|
54
|
+
}
|
|
55
|
+
expect(categoryKeys).not.toContain(
|
|
56
|
+
"the-@supports-block-below-derives-accent/semantic-tokens-from-these-seeds"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Tokens are categorized by NAME inference instead.
|
|
60
|
+
const accent = out.categories.colors?.find((t) => t.name === "--fui-color-accent");
|
|
61
|
+
expect(accent).toBeDefined();
|
|
62
|
+
const space = out.categories.spacing?.find((t) => t.name === "--fui-space-3");
|
|
63
|
+
expect(space).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("still honors curated section headings", () => {
|
|
67
|
+
const scss = `
|
|
68
|
+
:root {
|
|
69
|
+
// Typography
|
|
70
|
+
--fui-font-size-md: 1rem;
|
|
71
|
+
// Spacing
|
|
72
|
+
--fui-space-1: 4px;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
const out = parseScssTokens(scss);
|
|
76
|
+
expect(out.categories.typography?.some((t) => t.name === "--fui-font-size-md")).toBe(true);
|
|
77
|
+
expect(out.categories.spacing?.some((t) => t.name === "--fui-space-1")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("resets to name inference after a prose comment interrupts a curated section", () => {
|
|
81
|
+
const scss = `
|
|
82
|
+
:root {
|
|
83
|
+
// Typography
|
|
84
|
+
--fui-font-size-md: 1rem;
|
|
85
|
+
// This is an explanatory note about the following block, not a heading!
|
|
86
|
+
--fui-color-accent: #39594d;
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
const out = parseScssTokens(scss);
|
|
90
|
+
// The prose comment must NOT latch "typography" onto the color token.
|
|
91
|
+
expect(out.categories.typography?.some((t) => t.name === "--fui-color-accent")).not.toBe(true);
|
|
92
|
+
expect(out.categories.colors?.some((t) => t.name === "--fui-color-accent")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/tokens/scss.ts
CHANGED
|
@@ -87,7 +87,11 @@ export function parseScssTokens(content: string, filePath = "tokens.scss"): Toke
|
|
|
87
87
|
const tokens: ParsedToken[] = [];
|
|
88
88
|
const seenNames = new Set<string>();
|
|
89
89
|
let currentCategory = "other";
|
|
90
|
-
|
|
90
|
+
// Tracks whether the CURRENT section comment yielded a curated category. A
|
|
91
|
+
// prose comment (normalizeTokenGroupComment → null) resets this so subsequent
|
|
92
|
+
// tokens fall through to name-based inference instead of inheriting a stale or
|
|
93
|
+
// garbage comment-derived category (#7d).
|
|
94
|
+
let currentCategoryIsCurated = false;
|
|
91
95
|
|
|
92
96
|
const scssVars = parseScssVariables(content);
|
|
93
97
|
const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/;
|
|
@@ -99,7 +103,10 @@ export function parseScssTokens(content: string, filePath = "tokens.scss"): Toke
|
|
|
99
103
|
const normalized = normalizeTokenGroupComment(commentMatch[0]);
|
|
100
104
|
if (normalized) {
|
|
101
105
|
currentCategory = normalized;
|
|
102
|
-
|
|
106
|
+
currentCategoryIsCurated = true;
|
|
107
|
+
} else {
|
|
108
|
+
// Prose comment: not a category. Drop back to per-token name inference.
|
|
109
|
+
currentCategoryIsCurated = false;
|
|
103
110
|
}
|
|
104
111
|
continue;
|
|
105
112
|
}
|
|
@@ -119,7 +126,7 @@ export function parseScssTokens(content: string, filePath = "tokens.scss"): Toke
|
|
|
119
126
|
tokens.push({
|
|
120
127
|
name,
|
|
121
128
|
value: cleanValue || undefined,
|
|
122
|
-
category:
|
|
129
|
+
category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name),
|
|
123
130
|
description,
|
|
124
131
|
});
|
|
125
132
|
}
|
|
@@ -131,7 +138,7 @@ export function parseScssTokens(content: string, filePath = "tokens.scss"): Toke
|
|
|
131
138
|
tokens.push({
|
|
132
139
|
name,
|
|
133
140
|
value,
|
|
134
|
-
category:
|
|
141
|
+
category: currentCategoryIsCurated ? currentCategory : inferTokenGroup(name),
|
|
135
142
|
});
|
|
136
143
|
}
|
|
137
144
|
|
package/src/types.ts
CHANGED
|
@@ -581,6 +581,17 @@ export interface TokenConfig {
|
|
|
581
581
|
/** Repo-root-relative token source files/globs for monorepos and Cloud setup. */
|
|
582
582
|
sources?: TokenSourceConfig[];
|
|
583
583
|
|
|
584
|
+
/**
|
|
585
|
+
* npm package names whose shipped `fragments.json` token vocabulary seeds the
|
|
586
|
+
* known-token set (e.g. `["@usefragments/ui"]`). DECLARED-manifest ingest —
|
|
587
|
+
* the parser reads each package's published `fragments.json` and merges its
|
|
588
|
+
* token names into the vocabulary `tokens/css-vars-must-be-defined` consumes,
|
|
589
|
+
* so a consumer using those `--fui-*` vars is not flagged as drift. Never
|
|
590
|
+
* globs node_modules SCSS — only the declared manifest is trusted, keeping the
|
|
591
|
+
* no-inference mandate intact.
|
|
592
|
+
*/
|
|
593
|
+
packages?: string[];
|
|
594
|
+
|
|
584
595
|
/**
|
|
585
596
|
* Glob patterns to exclude
|
|
586
597
|
* @example ["node_modules"]
|