@usefragments/core 1.6.0 → 1.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/dist/chunk-RANPUC6C.js +72 -0
- package/dist/chunk-RANPUC6C.js.map +1 -0
- package/dist/{chunk-WVFNDPM4.js → chunk-WNMWKUYG.js} +26 -14
- package/dist/chunk-WNMWKUYG.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/compiled-types/index.js +8 -0
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-DxFipN5V.d.ts → governance-D9KtH-vg.d.ts} +9 -3
- package/dist/index.d.ts +165 -139
- package/dist/index.js +700 -201
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +36 -36
- package/dist/schemas/index.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +2 -1
- package/src/agent-format.test.ts +13 -0
- package/src/agent-format.ts +9 -3
- package/src/codes/__tests__/codes.test.ts +0 -1
- package/src/codes/codes.ts +1 -2
- package/src/compiled-types/index.ts +81 -0
- package/src/compiled-types/parse.test.ts +47 -0
- package/src/facts/builders.ts +13 -0
- package/src/facts/compile.ts +13 -13
- package/src/facts/facts.test.ts +38 -1
- package/src/facts/index.ts +2 -0
- package/src/facts/types.ts +15 -0
- package/src/governance-integrity.test.ts +98 -1
- package/src/governance-integrity.ts +40 -20
- package/src/index.ts +8 -0
- package/src/rules/a11y-required-accessible-name.ts +175 -28
- package/src/rules/a11y-standard.ts +102 -0
- package/src/rules/a11y-utils.ts +7 -0
- package/src/rules/components-prefer-library.test.ts +75 -28
- package/src/rules/components-prefer-library.ts +35 -15
- package/src/rules/components-shadow-component.test.ts +21 -9
- package/src/rules/emit-gate.test.ts +74 -4
- package/src/rules/emit-gate.ts +24 -9
- package/src/rules/families.ts +1 -1
- package/src/rules/fix-availability.ts +1 -0
- package/src/rules/index.ts +12 -2
- package/src/rules/rules.test.ts +63 -7
- package/src/rules/tiers.ts +1 -0
- package/src/tokens/design-token-parser.test.ts +131 -0
- package/src/tokens/design-token-parser.ts +362 -49
- package/src/types.ts +2 -2
- package/dist/chunk-WVFNDPM4.js.map +0 -1
- package/dist/{index-DbkPE46t.d.ts → index-hZAlYCli.d.ts} +8 -8
package/src/rules/rules.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
makeClassNameDynamicFact,
|
|
12
12
|
makeStyleDeclarationFact,
|
|
13
13
|
makeTokenDefinitionFact,
|
|
14
|
+
makeUsageChildContentFact,
|
|
14
15
|
makeUsageComponentFact,
|
|
15
16
|
makeUsageImportFact,
|
|
16
17
|
makeUsageInlineStyleFact,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
makeUsagePropResolvedFact,
|
|
19
20
|
makeUsageTextChildFact,
|
|
20
21
|
projectSupersededImportPathPreferences,
|
|
22
|
+
RULES,
|
|
21
23
|
ruleA11yRequiredAccessibleName,
|
|
22
24
|
ruleComponentsForbiddenPropValue,
|
|
23
25
|
ruleComponentsPreferLibrary,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
runRules,
|
|
35
37
|
} from "../index.js";
|
|
36
38
|
import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
|
|
39
|
+
import { RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION } from "./a11y-required-accessible-name.js";
|
|
37
40
|
|
|
38
41
|
type ButtonProps = {
|
|
39
42
|
variant?: "primary" | "secondary" | "ghost" | "link";
|
|
@@ -138,6 +141,7 @@ interface ButtonUsageInput {
|
|
|
138
141
|
value: string;
|
|
139
142
|
}>;
|
|
140
143
|
text?: string;
|
|
144
|
+
childContent?: "static-text" | "runtime" | "structural" | "empty";
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
function addButtonUsage(ix: FactIndex, input: ButtonUsageInput): { nodeId: FactId } {
|
|
@@ -176,6 +180,14 @@ function addButtonUsage(ix: FactIndex, input: ButtonUsageInput): { nodeId: FactI
|
|
|
176
180
|
if (input.text !== undefined) {
|
|
177
181
|
ix.add(makeUsageTextChildFact({ nodeId: node.id, text: input.text, index: 0 }));
|
|
178
182
|
}
|
|
183
|
+
if (input.childContent !== undefined) {
|
|
184
|
+
ix.add(
|
|
185
|
+
makeUsageChildContentFact({
|
|
186
|
+
nodeId: node.id,
|
|
187
|
+
content: input.childContent,
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
}
|
|
179
191
|
return { nodeId: node.id };
|
|
180
192
|
}
|
|
181
193
|
|
|
@@ -1272,6 +1284,47 @@ describe("a11y/required-accessible-name", () => {
|
|
|
1272
1284
|
}
|
|
1273
1285
|
});
|
|
1274
1286
|
|
|
1287
|
+
it("does not fire for conservatively classified runtime child content", () => {
|
|
1288
|
+
const ix = emptyIndexWithGovernance();
|
|
1289
|
+
addButtonUsage(ix, {
|
|
1290
|
+
file: "apps/x.tsx",
|
|
1291
|
+
nodePath: "0",
|
|
1292
|
+
line: 1,
|
|
1293
|
+
column: 0,
|
|
1294
|
+
props: [{ prop: "children", resolution: "jsx" }],
|
|
1295
|
+
childContent: "runtime",
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
it.each(["structural", "empty"] as const)("still fires for %s child content", (childContent) => {
|
|
1302
|
+
const ix = emptyIndexWithGovernance();
|
|
1303
|
+
addButtonUsage(ix, {
|
|
1304
|
+
file: "apps/x.tsx",
|
|
1305
|
+
nodePath: "0",
|
|
1306
|
+
line: 1,
|
|
1307
|
+
column: 0,
|
|
1308
|
+
props: [{ prop: "children", resolution: "jsx" }],
|
|
1309
|
+
childContent,
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(1);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
it("does not infer a name from the public children prop-resolution fact", () => {
|
|
1316
|
+
const ix = emptyIndexWithGovernance();
|
|
1317
|
+
addButtonUsage(ix, {
|
|
1318
|
+
file: "apps/x.tsx",
|
|
1319
|
+
nodePath: "0",
|
|
1320
|
+
line: 1,
|
|
1321
|
+
column: 0,
|
|
1322
|
+
props: [{ prop: "children", resolution: "dynamic" }],
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(1);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1275
1328
|
it("fires for an empty Button with no name source", () => {
|
|
1276
1329
|
const ix = emptyIndexWithGovernance();
|
|
1277
1330
|
addButtonUsage(ix, {
|
|
@@ -1298,6 +1351,11 @@ describe("a11y/required-accessible-name", () => {
|
|
|
1298
1351
|
|
|
1299
1352
|
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1300
1353
|
});
|
|
1354
|
+
|
|
1355
|
+
it("keeps the registry version in lockstep with the emitted finding version", () => {
|
|
1356
|
+
const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
|
|
1357
|
+
expect(registered?.version).toBe(A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION);
|
|
1358
|
+
});
|
|
1301
1359
|
});
|
|
1302
1360
|
|
|
1303
1361
|
// ---------------------------------------------------------------------------
|
|
@@ -3115,7 +3173,7 @@ describe("components/prefer-library", () => {
|
|
|
3115
3173
|
expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
|
|
3116
3174
|
});
|
|
3117
3175
|
|
|
3118
|
-
it("
|
|
3176
|
+
it("keeps raw <button> → Button advisory without treating the directory as import-ready", () => {
|
|
3119
3177
|
const ix = new FactIndex();
|
|
3120
3178
|
ix.addMany(
|
|
3121
3179
|
compileGlobalGovernanceFacts({
|
|
@@ -3151,14 +3209,12 @@ describe("components/prefer-library", () => {
|
|
|
3151
3209
|
rawValue: "button",
|
|
3152
3210
|
suggestedComponent: "Button",
|
|
3153
3211
|
suggestedImportSourceKind: "directory",
|
|
3212
|
+
canonicalDirectory: "src/fragments/ui/components",
|
|
3213
|
+
importReady: false,
|
|
3214
|
+
advisory: true,
|
|
3154
3215
|
},
|
|
3155
3216
|
});
|
|
3156
|
-
expect(findings[0].fix).
|
|
3157
|
-
kind: "replaceComponent",
|
|
3158
|
-
from: "button",
|
|
3159
|
-
to: "Button",
|
|
3160
|
-
deterministic: false,
|
|
3161
|
-
});
|
|
3217
|
+
expect(findings[0].fix).toBeUndefined();
|
|
3162
3218
|
});
|
|
3163
3219
|
|
|
3164
3220
|
it("flags a raw element by canonical vocab id when display name diverges (Modal → Dialog)", () => {
|
package/src/rules/tiers.ts
CHANGED
|
@@ -51,6 +51,7 @@ export const RULE_TIER: Readonly<Record<string, RuleTier>> = {
|
|
|
51
51
|
"tokens/require-dual-fallback": "hygiene",
|
|
52
52
|
"theme/no-theme-coupled-literal": "hygiene",
|
|
53
53
|
"a11y/required-accessible-name": "hygiene",
|
|
54
|
+
"a11y/standard": "hygiene",
|
|
54
55
|
// Composition constraints are authored contracts, but not part of the
|
|
55
56
|
// two-choice headline contract; they self-activate when patterns are declared
|
|
56
57
|
// (the compiler injects their configs), so off-by-default here is harmless.
|
|
@@ -175,4 +175,135 @@ describe("parseDesignTokenContent — CSS terminator robustness (#34/#35)", () =
|
|
|
175
175
|
expect(res.tokens.find((token) => token.name === "--sp-1")?.category).toBe("spacing");
|
|
176
176
|
expect(res.tokens.find((token) => token.name === "--fui-sp-2")?.category).toBe("spacing");
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
it("preserves the same custom property across configured theme selectors", () => {
|
|
180
|
+
const content = [
|
|
181
|
+
":root { --surface: #ffffff; --text: #111111; }",
|
|
182
|
+
'[data-theme="dark"] { --surface: #111111; --text: var(--surface); }',
|
|
183
|
+
".high-contrast { --surface: #000000; }",
|
|
184
|
+
].join("\n");
|
|
185
|
+
const res = parseDesignTokenContent(content, {
|
|
186
|
+
filePath: "tokens.css",
|
|
187
|
+
themeSelectors: {
|
|
188
|
+
":root": "default",
|
|
189
|
+
'[data-theme="dark"]': "dark",
|
|
190
|
+
".high-contrast": "high-contrast",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
res.tokens
|
|
196
|
+
.filter((token) => token.name === "--surface")
|
|
197
|
+
.map((token) => [token.theme, token.resolvedValue, token.selector])
|
|
198
|
+
).toEqual([
|
|
199
|
+
["default", "#ffffff", ":root"],
|
|
200
|
+
["dark", "#111111", '[data-theme="dark"]'],
|
|
201
|
+
["high-contrast", "#000000", ".high-contrast"],
|
|
202
|
+
]);
|
|
203
|
+
expect(
|
|
204
|
+
res.tokens.find((token) => token.name === "--text" && token.theme === "dark")?.resolvedValue
|
|
205
|
+
).toBe("#111111");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("preserves selector-specific values in compact single-line theme CSS", () => {
|
|
209
|
+
const content =
|
|
210
|
+
':root{--surface:#ffffff;--text:#111111}[data-theme="dark"]{--surface:#111111;--text:var(--surface)}.high-contrast{--surface:#000000}';
|
|
211
|
+
const res = parseDesignTokenContent(content, {
|
|
212
|
+
filePath: "tokens.min.css",
|
|
213
|
+
themeSelectors: {
|
|
214
|
+
":root": "default",
|
|
215
|
+
'[data-theme="dark"]': "dark",
|
|
216
|
+
".high-contrast": "high-contrast",
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(
|
|
221
|
+
res.tokens
|
|
222
|
+
.filter((token) => token.name === "--surface")
|
|
223
|
+
.map((token) => [token.theme, token.resolvedValue, token.selector])
|
|
224
|
+
).toEqual([
|
|
225
|
+
["default", "#ffffff", ":root"],
|
|
226
|
+
["dark", "#111111", '[data-theme="dark"]'],
|
|
227
|
+
["high-contrast", "#000000", ".high-contrast"],
|
|
228
|
+
]);
|
|
229
|
+
expect(
|
|
230
|
+
res.tokens.find((token) => token.name === "--text" && token.theme === "dark")?.resolvedValue
|
|
231
|
+
).toBe("#111111");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("uses the nearest selector through compact at-rules and ignores structural text in comments", () => {
|
|
235
|
+
const content =
|
|
236
|
+
':root{/* fake } [data-theme="dark"] { */--surface:#fff}@media(prefers-color-scheme:dark){[data-theme="dark"]{--surface:#111}}';
|
|
237
|
+
const res = parseDesignTokenContent(content, {
|
|
238
|
+
filePath: "tokens.min.css",
|
|
239
|
+
themeSelectors: {
|
|
240
|
+
":root": "default",
|
|
241
|
+
'[data-theme="dark"]': "dark",
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(
|
|
246
|
+
res.tokens
|
|
247
|
+
.filter((token) => token.name === "--surface")
|
|
248
|
+
.map((token) => [token.theme, token.resolvedValue, token.selector])
|
|
249
|
+
).toEqual([
|
|
250
|
+
["default", "#fff", ":root"],
|
|
251
|
+
["dark", "#111", '[data-theme="dark"]'],
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("ignores declaration-shaped text in comments and strings while preserving quoted delimiters", () => {
|
|
256
|
+
const content =
|
|
257
|
+
':root{/* --comment-token:red; } */--label:"literal; { value }";--image:url("data:image/svg+xml;utf8,<svg>{}</svg>");--raw-image:url(data:image/svg+xml;utf8,<svg>{}</svg>);--real:#fff;content:"--string-token:blue;"}';
|
|
258
|
+
const res = parseDesignTokenContent(content, { filePath: "tokens.min.css" });
|
|
259
|
+
|
|
260
|
+
expect(res.tokens.map((token) => [token.name, token.rawValue])).toEqual([
|
|
261
|
+
["--label", '"literal; { value }"'],
|
|
262
|
+
["--image", 'url("data:image/svg+xml;utf8,<svg>{}</svg>")'],
|
|
263
|
+
["--raw-image", "url(data:image/svg+xml;utf8,<svg>{}</svg>)"],
|
|
264
|
+
["--real", "#fff"],
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("keeps the nearest theme selector through multiple nested compact at-rules", () => {
|
|
269
|
+
const content =
|
|
270
|
+
'@layer theme{@supports(--feature-probe:#fff){:root{--surface:#fff}@media(prefers-color-scheme:dark){[data-theme="dark"]{--surface:#111;--text:var(--surface)}}}}';
|
|
271
|
+
const res = parseDesignTokenContent(content, {
|
|
272
|
+
filePath: "tokens.min.css",
|
|
273
|
+
themeSelectors: {
|
|
274
|
+
":root": "default",
|
|
275
|
+
'[data-theme="dark"]': "dark",
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(
|
|
280
|
+
res.tokens.map((token) => [token.name, token.theme, token.selector, token.resolvedValue])
|
|
281
|
+
).toEqual([
|
|
282
|
+
["--surface", "default", ":root", "#fff"],
|
|
283
|
+
["--surface", "dark", '[data-theme="dark"]', "#111"],
|
|
284
|
+
["--text", "dark", '[data-theme="dark"]', "#111"],
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("handles thousands of compact declarations without losing selector context", () => {
|
|
289
|
+
const declarationCount = 4_000;
|
|
290
|
+
const declarations = Array.from(
|
|
291
|
+
{ length: declarationCount },
|
|
292
|
+
(_, index) => `--token-${index}:${index}px`
|
|
293
|
+
).join(";");
|
|
294
|
+
const res = parseDesignTokenContent(
|
|
295
|
+
`@layer theme{@media(min-width:0){[data-theme="dense"]{${declarations}}}}`,
|
|
296
|
+
{
|
|
297
|
+
filePath: "tokens.min.css",
|
|
298
|
+
themeSelectors: { '[data-theme="dense"]': "dense" },
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(res.tokens).toHaveLength(declarationCount);
|
|
303
|
+
expect(res.tokens.at(-1)).toMatchObject({
|
|
304
|
+
name: `--token-${declarationCount - 1}`,
|
|
305
|
+
theme: "dense",
|
|
306
|
+
selector: '[data-theme="dense"]',
|
|
307
|
+
});
|
|
308
|
+
});
|
|
178
309
|
});
|