@usefragments/core 1.9.1 → 1.10.1
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-DGHZQTLH.js → chunk-XN3LSDPY.js} +3 -2
- package/dist/chunk-XN3LSDPY.js.map +1 -0
- 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-eEzCyfes.d.ts → governance-CLk_wkP9.d.ts} +20 -4
- package/dist/index.d.ts +13 -226
- package/dist/index.js +106 -497
- package/dist/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +48 -0
- package/dist/preview-runtime.js +91 -0
- package/dist/preview-runtime.js.map +1 -0
- package/dist/react-types.d.ts +3 -2
- package/dist/schemas/index.js +1 -1
- package/dist/storyAdapter.d.ts +187 -0
- package/dist/storyAdapter.js +371 -0
- package/dist/storyAdapter.js.map +1 -0
- package/dist/test-utils.d.ts +1 -1
- package/package.json +9 -1
- package/src/governance-integrity.test.ts +93 -2
- package/src/governance-integrity.ts +46 -2
- package/src/governance.ts +15 -3
- package/src/index.ts +0 -35
- package/src/react-create-element.test.ts +22 -0
- package/src/react-create-element.ts +12 -0
- package/src/react-types.ts +2 -1
- package/src/rules/a11y-required-accessible-name.ts +45 -2
- package/src/rules/index.ts +4 -0
- package/src/rules/rules.test.ts +72 -0
- package/src/storyAdapter.ts +3 -2
- package/dist/chunk-DGHZQTLH.js.map +0 -1
package/src/governance.ts
CHANGED
|
@@ -355,6 +355,7 @@ export const governanceConfigSchema = z
|
|
|
355
355
|
.object({
|
|
356
356
|
failOnWarnings: z.boolean().optional(),
|
|
357
357
|
failOnInert: z.boolean().optional(),
|
|
358
|
+
failOnAdoptionRegression: z.boolean().optional(),
|
|
358
359
|
})
|
|
359
360
|
.passthrough()
|
|
360
361
|
.optional(),
|
|
@@ -443,9 +444,13 @@ export interface GovernanceConfig {
|
|
|
443
444
|
overrides?: ComponentPolicyOverride[];
|
|
444
445
|
|
|
445
446
|
/**
|
|
446
|
-
* Governance CI rendering options
|
|
447
|
-
*
|
|
448
|
-
*
|
|
447
|
+
* Governance CI rendering options. Under `--ci`, `failOnWarnings` defaults to `true`
|
|
448
|
+
* when absent; set it to `false` to keep warning findings reported but advisory.
|
|
449
|
+
* `failOnInert` is opt-in and makes inert-config diagnostics (FUI9004-FUI9008) fail
|
|
450
|
+
* the verdict; `--allow-inert` bypasses the inert gates.
|
|
451
|
+
* `failOnAdoptionRegression` is opt-in and makes `check --ci` compare the
|
|
452
|
+
* current component-identity adoption percentage with the committed
|
|
453
|
+
* `.fragments/adoption-baseline.json` floor.
|
|
449
454
|
*/
|
|
450
455
|
ci?: {
|
|
451
456
|
failOnWarnings?: boolean;
|
|
@@ -456,6 +461,13 @@ export interface GovernanceConfig {
|
|
|
456
461
|
* bypasses it, like the governance-inert gate.
|
|
457
462
|
*/
|
|
458
463
|
failOnInert?: boolean;
|
|
464
|
+
/**
|
|
465
|
+
* Fail `check --ci` when measurable component-identity adoption falls below
|
|
466
|
+
* the committed local floor. Update the floor through `check
|
|
467
|
+
* --update-baseline`; accepting a lower floor additionally requires
|
|
468
|
+
* `--allow-regression`.
|
|
469
|
+
*/
|
|
470
|
+
failOnAdoptionRegression?: boolean;
|
|
459
471
|
[key: string]: unknown;
|
|
460
472
|
};
|
|
461
473
|
[key: string]: unknown;
|
package/src/index.ts
CHANGED
|
@@ -501,28 +501,6 @@ export {
|
|
|
501
501
|
} from "./policy-exclude.js";
|
|
502
502
|
export type { PolicyExclude, PolicyExcludeInput } from "./policy-exclude.js";
|
|
503
503
|
|
|
504
|
-
// Story adapter (runtime conversion of Storybook modules)
|
|
505
|
-
export {
|
|
506
|
-
storyModuleToFragment,
|
|
507
|
-
setPreviewConfig,
|
|
508
|
-
getPreviewConfig,
|
|
509
|
-
// Re-export @storybook/csf utilities
|
|
510
|
-
toId,
|
|
511
|
-
storyNameFromExport,
|
|
512
|
-
isExportStory,
|
|
513
|
-
} from "./storyAdapter.js";
|
|
514
|
-
export type {
|
|
515
|
-
StoryModule,
|
|
516
|
-
StoryMeta,
|
|
517
|
-
Story,
|
|
518
|
-
StoryArgType,
|
|
519
|
-
StoryContext,
|
|
520
|
-
Decorator,
|
|
521
|
-
Loader,
|
|
522
|
-
PreviewConfig,
|
|
523
|
-
CSF2Story,
|
|
524
|
-
} from "./storyAdapter.js";
|
|
525
|
-
|
|
526
504
|
// Storybook adapter filtering
|
|
527
505
|
export {
|
|
528
506
|
checkStoryExclusion,
|
|
@@ -640,19 +618,6 @@ export type {
|
|
|
640
618
|
CompositionGuideline,
|
|
641
619
|
} from "./composition.js";
|
|
642
620
|
|
|
643
|
-
// Shared preview runtime
|
|
644
|
-
export {
|
|
645
|
-
executeVariantLoaders,
|
|
646
|
-
resolvePreviewRuntimeState,
|
|
647
|
-
usePreviewVariantRuntime,
|
|
648
|
-
PreviewVariantRuntime,
|
|
649
|
-
} from "./preview-runtime.js";
|
|
650
|
-
export type {
|
|
651
|
-
PreviewVariantLike,
|
|
652
|
-
PreviewRuntimeState,
|
|
653
|
-
PreviewRuntimeOptions,
|
|
654
|
-
} from "./preview-runtime.js";
|
|
655
|
-
|
|
656
621
|
// Component discovery
|
|
657
622
|
export { discoverComponents, isReactComponent } from "./component-discovery.js";
|
|
658
623
|
export type { DiscoverComponentsOptions } from "./component-discovery.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("getCreateElement", () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.resetModules();
|
|
6
|
+
vi.doUnmock("node:module");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("throws a clear error when react cannot be resolved", async () => {
|
|
10
|
+
vi.doMock("node:module", () => ({
|
|
11
|
+
createRequire: () => () => {
|
|
12
|
+
throw new Error("Cannot find module 'react'");
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const { getCreateElement } = await import("./react-create-element.js");
|
|
17
|
+
|
|
18
|
+
expect(() => getCreateElement()).toThrow(
|
|
19
|
+
'@usefragments/core: story rendering requires the optional peer dependency "react" (>=18). Install react to use storyModuleToFragment default render paths.',
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
export function getCreateElement(): typeof import("react")["createElement"] {
|
|
4
|
+
try {
|
|
5
|
+
return (createRequire(import.meta.url)("react") as typeof import("react")).createElement;
|
|
6
|
+
} catch (error) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
'@usefragments/core: story rendering requires the optional peer dependency "react" (>=18). Install react to use storyModuleToFragment default render paths.',
|
|
9
|
+
{ cause: error instanceof Error ? error : undefined },
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/react-types.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Import from '@usefragments/core/react' when you need React-typed
|
|
5
5
|
* component definitions. The main '@usefragments/core' entrypoint
|
|
6
|
-
* works without React installed
|
|
6
|
+
* works without React installed — story and preview-runtime APIs live on
|
|
7
|
+
* `@usefragments/core/story` and `@usefragments/core/preview-runtime`.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { ComponentType, ReactNode, JSX } from 'react';
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
UsageNodeFact,
|
|
25
25
|
UsagePropResolvedFact,
|
|
26
26
|
} from "../facts/index.js";
|
|
27
|
+
import { z } from "zod";
|
|
27
28
|
|
|
28
29
|
import { isAriaHidden } from "./a11y-utils.js";
|
|
29
30
|
import { makeFinding } from "./finding.js";
|
|
@@ -31,7 +32,16 @@ import type { Finding } from "./types.js";
|
|
|
31
32
|
import { indexPropsByNodeId, indexTextChildrenByNodeId } from "./utils.js";
|
|
32
33
|
|
|
33
34
|
export const RULE_ID = "a11y/required-accessible-name";
|
|
34
|
-
export const RULE_VERSION = "
|
|
35
|
+
export const RULE_VERSION = "3";
|
|
36
|
+
|
|
37
|
+
export const a11yRequiredAccessibleNameOptionsSchema = z.object({
|
|
38
|
+
ignoreParents: z.array(z.string().min(1)).optional(),
|
|
39
|
+
ignoreSpread: z.boolean().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type A11yRequiredAccessibleNameOptions = z.infer<
|
|
43
|
+
typeof a11yRequiredAccessibleNameOptionsSchema
|
|
44
|
+
>;
|
|
35
45
|
|
|
36
46
|
const NAME_SOURCE_PROPS = new Set(["aria-label", "aria-labelledby", "title"]);
|
|
37
47
|
const COMPONENT_NAME_SOURCE_PROPS = new Set([...NAME_SOURCE_PROPS, "label"]);
|
|
@@ -57,7 +67,12 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
57
67
|
ix.byKind("usage_child_content").map((content) => [content.nodeId, content])
|
|
58
68
|
);
|
|
59
69
|
const globalPolicy = ix.policy.ruleConfig(RULE_ID);
|
|
70
|
+
const parsedOptions = a11yRequiredAccessibleNameOptionsSchema.safeParse(globalPolicy?.options);
|
|
71
|
+
const options: A11yRequiredAccessibleNameOptions = parsedOptions.success
|
|
72
|
+
? parsedOptions.data
|
|
73
|
+
: {};
|
|
60
74
|
const usageByNode = new Map(ix.byKind("usage_component").map((usage) => [usage.nodeId, usage]));
|
|
75
|
+
const usageNodes = ix.byKind("usage_node");
|
|
61
76
|
const definitions = new Map(
|
|
62
77
|
ix.byKind("component_definition").map((definition) => [definition.componentKey, definition])
|
|
63
78
|
);
|
|
@@ -73,6 +88,7 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
73
88
|
globalPolicy?.enabled === true &&
|
|
74
89
|
requiresAccessibleName(node, usageComponent, definitions, props);
|
|
75
90
|
if (!componentPolicy && !genericControl) continue;
|
|
91
|
+
if (isInsideIgnoredAttributeParent(node, usageNodes, options.ignoreParents)) continue;
|
|
76
92
|
|
|
77
93
|
if (
|
|
78
94
|
hasAccessibleName(
|
|
@@ -86,11 +102,16 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
86
102
|
continue;
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
const hasSpreadProps = props.some((prop) => prop.resolution === "spread");
|
|
106
|
+
if (hasSpreadProps && options.ignoreSpread === true) continue;
|
|
107
|
+
|
|
89
108
|
findings.push(
|
|
90
109
|
makeFinding({
|
|
91
110
|
ruleId: RULE_ID,
|
|
92
111
|
ruleVersion: RULE_VERSION,
|
|
93
|
-
severity:
|
|
112
|
+
severity: hasSpreadProps
|
|
113
|
+
? "minor"
|
|
114
|
+
: (componentPolicy?.severity ?? globalPolicy?.severity ?? "warn"),
|
|
94
115
|
message: `<${node.element}> has no accessible name. ${
|
|
95
116
|
componentPolicy?.because ??
|
|
96
117
|
"Add visible text, aria-label, aria-labelledby, or a component label."
|
|
@@ -119,6 +140,28 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
119
140
|
return findings;
|
|
120
141
|
}
|
|
121
142
|
|
|
143
|
+
function isInsideIgnoredAttributeParent(
|
|
144
|
+
node: UsageNodeFact,
|
|
145
|
+
usageNodes: readonly UsageNodeFact[],
|
|
146
|
+
ignoreParents: readonly string[] | undefined
|
|
147
|
+
): boolean {
|
|
148
|
+
if (!ignoreParents?.length || !node.nodePath.includes("/attr:")) return false;
|
|
149
|
+
const ignored = new Set(ignoreParents);
|
|
150
|
+
const segments = node.nodePath.split("/");
|
|
151
|
+
const ancestorPaths = new Set<string>();
|
|
152
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
153
|
+
if (segments[index].startsWith("attr:")) {
|
|
154
|
+
ancestorPaths.add(segments.slice(0, index).join("/"));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return usageNodes.some(
|
|
158
|
+
(candidate) =>
|
|
159
|
+
candidate.file === node.file &&
|
|
160
|
+
ancestorPaths.has(candidate.nodePath) &&
|
|
161
|
+
ignored.has(candidate.element)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
function hasAccessibleName(
|
|
123
166
|
props: UsagePropResolvedFact[],
|
|
124
167
|
textChildren: ReadonlyArray<{ text: string }>,
|
package/src/rules/index.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { FactIndex } from "../facts/index.js";
|
|
11
|
+
import type { ZodTypeAny } from "zod";
|
|
11
12
|
|
|
12
13
|
import {
|
|
14
|
+
a11yRequiredAccessibleNameOptionsSchema,
|
|
13
15
|
RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION,
|
|
14
16
|
ruleA11yRequiredAccessibleName,
|
|
15
17
|
} from "./a11y-required-accessible-name.js";
|
|
@@ -44,6 +46,7 @@ export interface Rule {
|
|
|
44
46
|
id: string;
|
|
45
47
|
version: string;
|
|
46
48
|
run: RuleFn;
|
|
49
|
+
optionsSchema?: ZodTypeAny;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export const RULES: readonly Rule[] = [
|
|
@@ -156,6 +159,7 @@ export const RULES: readonly Rule[] = [
|
|
|
156
159
|
id: "a11y/required-accessible-name",
|
|
157
160
|
version: A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION,
|
|
158
161
|
run: ruleA11yRequiredAccessibleName,
|
|
162
|
+
optionsSchema: a11yRequiredAccessibleNameOptionsSchema,
|
|
159
163
|
},
|
|
160
164
|
{
|
|
161
165
|
id: "a11y/standard",
|
package/src/rules/rules.test.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
runRules,
|
|
37
37
|
} from "../index.js";
|
|
38
38
|
import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
|
|
39
|
+
import { makeGovernanceRuleConfigFact } from "../facts/index.js";
|
|
39
40
|
import { RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION } from "./a11y-required-accessible-name.js";
|
|
40
41
|
import { makeFinding } from "./finding.js";
|
|
41
42
|
|
|
@@ -1425,6 +1426,77 @@ describe("a11y/required-accessible-name", () => {
|
|
|
1425
1426
|
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1426
1427
|
});
|
|
1427
1428
|
|
|
1429
|
+
it("ignores controls rendered through an ignored same-file attribute parent", () => {
|
|
1430
|
+
const ix = emptyIndexWithGovernance();
|
|
1431
|
+
ix.add(
|
|
1432
|
+
makeGovernanceRuleConfigFact({
|
|
1433
|
+
ruleId: "a11y/required-accessible-name",
|
|
1434
|
+
enabled: true,
|
|
1435
|
+
options: { ignoreParents: ["FormControlLabel"] },
|
|
1436
|
+
})
|
|
1437
|
+
);
|
|
1438
|
+
ix.add(
|
|
1439
|
+
makeUsageNodeFact({
|
|
1440
|
+
file: "apps/x.tsx",
|
|
1441
|
+
nodePath: "0:0",
|
|
1442
|
+
element: "FormControlLabel",
|
|
1443
|
+
location: { file: "apps/x.tsx", line: 1, column: 0 },
|
|
1444
|
+
})
|
|
1445
|
+
);
|
|
1446
|
+
addButtonUsage(ix, {
|
|
1447
|
+
file: "apps/x.tsx",
|
|
1448
|
+
nodePath: "0:0/attr:control/0:0",
|
|
1449
|
+
line: 1,
|
|
1450
|
+
column: 28,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
it("demotes an unnamed component with spread props to minor", () => {
|
|
1457
|
+
const ix = emptyIndexWithGovernance();
|
|
1458
|
+
addButtonUsage(ix, {
|
|
1459
|
+
file: "apps/x.tsx",
|
|
1460
|
+
nodePath: "0:0",
|
|
1461
|
+
line: 1,
|
|
1462
|
+
column: 0,
|
|
1463
|
+
props: [{ prop: "...props", resolution: "spread" }],
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toMatchObject([{ severity: "minor" }]);
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it("skips an unnamed component with spread props when ignoreSpread is enabled", () => {
|
|
1470
|
+
const ix = emptyIndexWithGovernance();
|
|
1471
|
+
ix.add(
|
|
1472
|
+
makeGovernanceRuleConfigFact({
|
|
1473
|
+
ruleId: "a11y/required-accessible-name",
|
|
1474
|
+
enabled: true,
|
|
1475
|
+
options: { ignoreSpread: true },
|
|
1476
|
+
})
|
|
1477
|
+
);
|
|
1478
|
+
addButtonUsage(ix, {
|
|
1479
|
+
file: "apps/x.tsx",
|
|
1480
|
+
nodePath: "0:0",
|
|
1481
|
+
line: 1,
|
|
1482
|
+
column: 0,
|
|
1483
|
+
props: [{ prop: "...props", resolution: "spread" }],
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it("registers ignoreParents and ignoreSpread as supported rule options", () => {
|
|
1490
|
+
const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
|
|
1491
|
+
|
|
1492
|
+
expect(
|
|
1493
|
+
registered?.optionsSchema?.safeParse({
|
|
1494
|
+
ignoreParents: ["FormControlLabel"],
|
|
1495
|
+
ignoreSpread: true,
|
|
1496
|
+
}).success
|
|
1497
|
+
).toBe(true);
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1428
1500
|
it("keeps the registry version in lockstep with the emitted finding version", () => {
|
|
1429
1501
|
const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
|
|
1430
1502
|
expect(registered?.version).toBe(A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION);
|
package/src/storyAdapter.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* Supports Storybook 8.x with both CSF2 (Template.bind) and CSF3 (object stories).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import type { ComponentType, ReactNode } from "react";
|
|
12
12
|
import { toId, storyNameFromExport, isExportStory } from "./storybook-csf.js";
|
|
13
|
+
import { getCreateElement } from "./react-create-element.js";
|
|
13
14
|
import type {
|
|
14
15
|
FragmentDefinition,
|
|
15
16
|
FragmentMeta,
|
|
@@ -711,7 +712,7 @@ function createRenderFunction(
|
|
|
711
712
|
: meta.render!(args);
|
|
712
713
|
} else {
|
|
713
714
|
// Default: render component with args
|
|
714
|
-
renderFn = () =>
|
|
715
|
+
renderFn = () => getCreateElement()(component, args);
|
|
715
716
|
}
|
|
716
717
|
|
|
717
718
|
// Collect decorators in Storybook order
|