@usefragments/core 1.10.2 → 2.0.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-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
- package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
- package/dist/chunk-7ULGH74M.js +66 -0
- package/dist/chunk-7ULGH74M.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
- package/dist/chunk-BMPYIUZE.js.map +1 -0
- package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/chunk-RYFULE43.js +578 -0
- package/dist/chunk-RYFULE43.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +3 -2
- package/dist/compiled-types/index.d.ts +327 -2
- package/dist/compiled-types/index.js +1 -1
- package/dist/generate/index.d.ts +3 -2
- package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
- package/dist/governance-telemetry.d.ts +6 -0
- package/dist/governance-telemetry.js +1 -1
- package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
- package/dist/index.d.ts +3280 -1305
- package/dist/index.js +2414 -350
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +228 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/preview/index.js +45 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -2
- package/dist/preview-runtime.js +150 -14
- package/dist/preview-runtime.js.map +1 -1
- package/dist/react-types.d.ts +1 -2
- package/dist/registry.d.ts +1412 -203
- package/dist/registry.js +30 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +3 -2
- package/dist/storyAdapter.d.ts +1 -2
- package/dist/storyAdapter.js +11 -49
- package/dist/storyAdapter.js.map +1 -1
- package/dist/test-utils.d.ts +3 -2
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +8 -2
- package/src/__tests__/contract-parser.test.ts +318 -277
- package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
- package/src/__tests__/preview-runtime.test.tsx +30 -8
- package/src/__tests__/schema.test.ts +191 -14
- package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
- package/src/analysis-plan/coverage.ts +181 -0
- package/src/analysis-plan/digest.ts +141 -0
- package/src/analysis-plan/index.ts +34 -0
- package/src/analysis-plan/types.ts +207 -0
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/codes/__tests__/codes.test.ts +13 -0
- package/src/codes/codes.ts +40 -0
- package/src/compiled-types/index.ts +640 -39
- package/src/compiled-types/parse.test.ts +145 -4
- package/src/component-contract.ts +95 -53
- package/src/composition.ts +7 -13
- package/src/constants.ts +3 -6
- package/src/contract/hash.test.ts +20 -0
- package/src/contract/hash.ts +66 -9
- package/src/contract/index.ts +24 -1
- package/src/contract/manifest.test.ts +94 -0
- package/src/contract/manifest.ts +68 -0
- package/src/contract/preimage.test.ts +219 -1
- package/src/contract/preimage.ts +326 -6
- package/src/contract/stamp.test.ts +3 -0
- package/src/contract/stamp.ts +1 -1
- package/src/contract-parser.ts +54 -30
- package/src/defineFragment.test.ts +476 -91
- package/src/defineFragment.ts +204 -114
- package/src/domain-ids.test.ts +35 -0
- package/src/domain-ids.ts +61 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +690 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
- package/src/evaluation/index.ts +58 -0
- package/src/evaluation/receipt.ts +753 -0
- package/src/evaluation/types.ts +406 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/compile.ts +29 -6
- package/src/facts/fact-index.ts +13 -3
- package/src/facts/fact-integrity-v1.test.ts +172 -0
- package/src/facts/facts.test.ts +15 -0
- package/src/facts/ids.ts +46 -3
- package/src/facts/index.ts +14 -1
- package/src/facts/integrity.ts +134 -0
- package/src/facts/types.ts +36 -0
- package/src/governance-integrity.test.ts +1 -0
- package/src/governance-integrity.ts +5 -3
- package/src/governance-telemetry.ts +8 -0
- package/src/governance.ts +70 -8
- package/src/index.ts +230 -37
- package/src/preview/validation.test.ts +62 -0
- package/src/preview/validation.ts +48 -2
- package/src/preview-runtime.tsx +227 -20
- package/src/registry-install-plan.ts +200 -109
- package/src/registry-shards.test.ts +263 -0
- package/src/registry.ts +237 -0
- package/src/repository-binding.test.ts +50 -0
- package/src/repository-binding.ts +96 -0
- package/src/rules/families.test.ts +36 -0
- package/src/rules/finding.ts +7 -2
- package/src/rules/index.ts +17 -1
- package/src/rules/rule-config.test.ts +66 -0
- package/src/rules/rule-config.ts +73 -0
- package/src/rules/rules.test.ts +26 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
- package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
- package/src/schema.ts +293 -113
- package/src/schemas/index.ts +1 -1
- package/src/storyAdapter.test.ts +68 -12
- package/src/storyAdapter.ts +44 -75
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +258 -40
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RANPUC6C.js +0 -72
- package/dist/chunk-RANPUC6C.js.map +0 -1
- package/dist/chunk-XN3LSDPY.js.map +0 -1
- package/dist/chunk-YF65VYRY.js.map +0 -1
- package/src/fragment-types.ts +0 -214
- package/src/react-create-element.test.ts +0 -22
- package/src/react-create-element.ts +0 -12
- package/src/storyFilters.test.ts +0 -350
- package/src/storyFilters.ts +0 -253
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { usePreviewVariantRuntime } from "../preview-runtime.js";
|
|
3
|
+
|
|
4
|
+
const hookHarness = vi.hoisted(() => ({
|
|
5
|
+
cursor: 0,
|
|
6
|
+
renderRequested: false,
|
|
7
|
+
pendingEffects: [] as Array<() => void>,
|
|
8
|
+
slots: [] as Array<
|
|
9
|
+
| { kind: "state"; value: unknown }
|
|
10
|
+
| { kind: "ref"; value: { current: unknown } }
|
|
11
|
+
| {
|
|
12
|
+
kind: "effect";
|
|
13
|
+
cleanup?: () => void;
|
|
14
|
+
deps?: readonly unknown[];
|
|
15
|
+
}
|
|
16
|
+
>,
|
|
17
|
+
updates: [] as Array<{
|
|
18
|
+
state?: { content: unknown; isLoading: boolean; error: unknown };
|
|
19
|
+
}>,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("react", async () => {
|
|
23
|
+
const actual = await vi.importActual<typeof import("react")>("react");
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
useEffect: (effect: () => void | (() => void), deps?: readonly unknown[]) => {
|
|
27
|
+
const slotIndex = hookHarness.cursor++;
|
|
28
|
+
const prior = hookHarness.slots[slotIndex];
|
|
29
|
+
const dependenciesChanged =
|
|
30
|
+
prior?.kind !== "effect" ||
|
|
31
|
+
deps === undefined ||
|
|
32
|
+
prior.deps === undefined ||
|
|
33
|
+
deps.length !== prior.deps.length ||
|
|
34
|
+
deps.some((dependency, index) => !Object.is(dependency, prior.deps?.[index]));
|
|
35
|
+
|
|
36
|
+
if (!dependenciesChanged) return;
|
|
37
|
+
|
|
38
|
+
hookHarness.pendingEffects.push(() => {
|
|
39
|
+
if (prior?.kind === "effect") prior.cleanup?.();
|
|
40
|
+
const cleanup = effect();
|
|
41
|
+
hookHarness.slots[slotIndex] = {
|
|
42
|
+
kind: "effect",
|
|
43
|
+
...(typeof cleanup === "function" ? { cleanup } : {}),
|
|
44
|
+
deps,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
useRef: <T,>(initial: T) => {
|
|
49
|
+
const slotIndex = hookHarness.cursor++;
|
|
50
|
+
const prior = hookHarness.slots[slotIndex];
|
|
51
|
+
if (prior?.kind === "ref") return prior.value;
|
|
52
|
+
|
|
53
|
+
const value = { current: initial };
|
|
54
|
+
hookHarness.slots[slotIndex] = { kind: "ref", value };
|
|
55
|
+
return value;
|
|
56
|
+
},
|
|
57
|
+
useState: <T,>(initial: T) => {
|
|
58
|
+
const slotIndex = hookHarness.cursor++;
|
|
59
|
+
const prior = hookHarness.slots[slotIndex];
|
|
60
|
+
if (prior?.kind !== "state") {
|
|
61
|
+
hookHarness.slots[slotIndex] = { kind: "state", value: initial };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
(hookHarness.slots[slotIndex] as { kind: "state"; value: T }).value,
|
|
66
|
+
(next: T | ((current: T) => T)) => {
|
|
67
|
+
const current = (hookHarness.slots[slotIndex] as { kind: "state"; value: T }).value;
|
|
68
|
+
const value = typeof next === "function" ? (next as (current: T) => T)(current) : next;
|
|
69
|
+
(hookHarness.slots[slotIndex] as { kind: "state"; value: T }).value = value;
|
|
70
|
+
hookHarness.renderRequested = true;
|
|
71
|
+
hookHarness.updates.push(
|
|
72
|
+
value as {
|
|
73
|
+
state?: { content: unknown; isLoading: boolean; error: unknown };
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function renderHook<T>(render: () => T): T {
|
|
83
|
+
hookHarness.cursor = 0;
|
|
84
|
+
hookHarness.renderRequested = false;
|
|
85
|
+
const result = render();
|
|
86
|
+
const effects = hookHarness.pendingEffects.splice(0);
|
|
87
|
+
for (const effect of effects) effect();
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function flushResolution(): Promise<void> {
|
|
92
|
+
await Promise.resolve();
|
|
93
|
+
await Promise.resolve();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function settleHook<T>(render: () => T, maxRenders = 8): Promise<T> {
|
|
97
|
+
let result = renderHook(render);
|
|
98
|
+
for (
|
|
99
|
+
let renderCount = 0;
|
|
100
|
+
renderCount < maxRenders && hookHarness.renderRequested;
|
|
101
|
+
renderCount++
|
|
102
|
+
) {
|
|
103
|
+
await flushResolution();
|
|
104
|
+
result = renderHook(render);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function makeWideLoadedData(version: string): Record<string, unknown> {
|
|
110
|
+
return {
|
|
111
|
+
version,
|
|
112
|
+
items: Array.from({ length: 1_005 }, (_, index) => ({ index })),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function makeDeepLoadedData(depth: number, label = "leaf"): Record<string, unknown> {
|
|
117
|
+
let node: Record<string, unknown> = { label };
|
|
118
|
+
for (let index = 0; index < depth; index++) {
|
|
119
|
+
node = { next: node };
|
|
120
|
+
}
|
|
121
|
+
return { node };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readDeepLabel(loadedData: Record<string, unknown> | undefined): unknown {
|
|
125
|
+
let node = loadedData?.node as Record<string, unknown> | undefined;
|
|
126
|
+
while (node && "next" in node) {
|
|
127
|
+
node = node.next as Record<string, unknown> | undefined;
|
|
128
|
+
}
|
|
129
|
+
return node?.label;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function expectLoadedDataComparisonError(state: ReturnType<typeof usePreviewVariantRuntime>): void {
|
|
133
|
+
expect(state.content).toBeNull();
|
|
134
|
+
expect(state.isLoading).toBe(false);
|
|
135
|
+
expect(state.loadedData).toBeUndefined();
|
|
136
|
+
expect(state.error).toBeInstanceOf(Error);
|
|
137
|
+
expect(state.error?.message).toContain("stable object reference");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe("usePreviewVariantRuntime", () => {
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
hookHarness.cursor = 0;
|
|
143
|
+
hookHarness.renderRequested = false;
|
|
144
|
+
hookHarness.pendingEffects.length = 0;
|
|
145
|
+
hookHarness.slots.length = 0;
|
|
146
|
+
hookHarness.updates.length = 0;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("enters loading state while resolving a variant without loaders", async () => {
|
|
150
|
+
const variant = { render: () => null };
|
|
151
|
+
const initialState = renderHook(() => usePreviewVariantRuntime({ variant }));
|
|
152
|
+
|
|
153
|
+
expect(initialState).toMatchObject({
|
|
154
|
+
content: null,
|
|
155
|
+
isLoading: true,
|
|
156
|
+
error: null,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(hookHarness.updates[0]).toMatchObject({
|
|
160
|
+
state: {
|
|
161
|
+
content: null,
|
|
162
|
+
isLoading: true,
|
|
163
|
+
error: null,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await flushResolution();
|
|
168
|
+
const settledState = renderHook(() => usePreviewVariantRuntime({ variant }));
|
|
169
|
+
|
|
170
|
+
expect(settledState).toMatchObject({
|
|
171
|
+
content: null,
|
|
172
|
+
isLoading: false,
|
|
173
|
+
error: null,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("settles when callers recreate equivalent inline loadedData", async () => {
|
|
178
|
+
const variant = {
|
|
179
|
+
render: (options?: { loadedData?: Record<string, unknown> }) =>
|
|
180
|
+
(options?.loadedData?.user as { profile?: { label?: string } } | undefined)?.profile?.label,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const state = await settleHook(() =>
|
|
184
|
+
usePreviewVariantRuntime({
|
|
185
|
+
variant,
|
|
186
|
+
loadedData: { user: { profile: { label: "from-host" } } },
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(state).toEqual({
|
|
191
|
+
content: "from-host",
|
|
192
|
+
isLoading: false,
|
|
193
|
+
error: null,
|
|
194
|
+
loadedData: { user: { profile: { label: "from-host" } } },
|
|
195
|
+
});
|
|
196
|
+
expect(hookHarness.renderRequested).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("re-resolves when a nested inline loadedData value changes", async () => {
|
|
200
|
+
const variant = {
|
|
201
|
+
render: (options?: { loadedData?: Record<string, unknown> }) =>
|
|
202
|
+
(options?.loadedData?.user as { label?: string } | undefined)?.label,
|
|
203
|
+
};
|
|
204
|
+
let label = "before";
|
|
205
|
+
const render = () => usePreviewVariantRuntime({ variant, loadedData: { user: { label } } });
|
|
206
|
+
|
|
207
|
+
expect(await settleHook(render)).toMatchObject({ content: "before", isLoading: false });
|
|
208
|
+
|
|
209
|
+
label = "after";
|
|
210
|
+
expect(renderHook(render)).toMatchObject({ content: null, isLoading: true });
|
|
211
|
+
await flushResolution();
|
|
212
|
+
expect(await settleHook(render)).toMatchObject({ content: "after", isLoading: false });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("compares recreated cyclic loadedData without recursing forever", async () => {
|
|
216
|
+
const variant = { render: () => "settled" };
|
|
217
|
+
const render = () => {
|
|
218
|
+
const node: Record<string, unknown> = { label: "same" };
|
|
219
|
+
node.self = node;
|
|
220
|
+
return usePreviewVariantRuntime({ variant, loadedData: { node } });
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
expect(await settleHook(render)).toMatchObject({ content: "settled", isLoading: false });
|
|
224
|
+
expect(hookHarness.renderRequested).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("fails closed for recreated loadedData above the comparison-count bound", async () => {
|
|
228
|
+
const stableLoadedData = makeWideLoadedData("same");
|
|
229
|
+
let recreateLoadedData = false;
|
|
230
|
+
const variant = {
|
|
231
|
+
render: (options?: { loadedData?: Record<string, unknown> }) => options?.loadedData?.version,
|
|
232
|
+
};
|
|
233
|
+
const render = () =>
|
|
234
|
+
usePreviewVariantRuntime({
|
|
235
|
+
variant,
|
|
236
|
+
loadedData: recreateLoadedData ? makeWideLoadedData("same") : stableLoadedData,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(await settleHook(render)).toMatchObject({ content: "same", isLoading: false });
|
|
240
|
+
|
|
241
|
+
recreateLoadedData = true;
|
|
242
|
+
expectLoadedDataComparisonError(await settleHook(render));
|
|
243
|
+
expect(hookHarness.renderRequested).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("fails closed for recreated loadedData above the comparison-depth bound", async () => {
|
|
247
|
+
const stableLoadedData = makeDeepLoadedData(40);
|
|
248
|
+
let recreateLoadedData = false;
|
|
249
|
+
const variant = { render: () => "deep-settled" };
|
|
250
|
+
const render = () =>
|
|
251
|
+
usePreviewVariantRuntime({
|
|
252
|
+
variant,
|
|
253
|
+
loadedData: recreateLoadedData ? makeDeepLoadedData(40) : stableLoadedData,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(await settleHook(render)).toMatchObject({ content: "deep-settled", isLoading: false });
|
|
257
|
+
|
|
258
|
+
recreateLoadedData = true;
|
|
259
|
+
expectLoadedDataComparisonError(await settleHook(render));
|
|
260
|
+
expect(hookHarness.renderRequested).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("fails closed before accepting a stable Date identity change", async () => {
|
|
264
|
+
const variant = {
|
|
265
|
+
render: (options?: { loadedData?: Record<string, unknown> }) =>
|
|
266
|
+
(options?.loadedData?.createdAt as Date | undefined)?.getTime(),
|
|
267
|
+
};
|
|
268
|
+
let loadedData = { createdAt: new Date(0) };
|
|
269
|
+
const render = () => usePreviewVariantRuntime({ variant, loadedData });
|
|
270
|
+
|
|
271
|
+
expect(await settleHook(render)).toMatchObject({ content: 0, isLoading: false, error: null });
|
|
272
|
+
|
|
273
|
+
loadedData = { createdAt: new Date(1) };
|
|
274
|
+
expectLoadedDataComparisonError(renderHook(render));
|
|
275
|
+
expect(await settleHook(render)).toMatchObject({ content: 1, isLoading: false, error: null });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("fails closed before accepting a stable function identity change", async () => {
|
|
279
|
+
const variant = {
|
|
280
|
+
render: (options?: { loadedData?: Record<string, unknown> }) =>
|
|
281
|
+
(options?.loadedData?.getValue as (() => number) | undefined)?.(),
|
|
282
|
+
};
|
|
283
|
+
let loadedData = { getValue: () => 1 };
|
|
284
|
+
const render = () => usePreviewVariantRuntime({ variant, loadedData });
|
|
285
|
+
|
|
286
|
+
expect(await settleHook(render)).toMatchObject({ content: 1, isLoading: false, error: null });
|
|
287
|
+
|
|
288
|
+
loadedData = { getValue: () => 2 };
|
|
289
|
+
expectLoadedDataComparisonError(renderHook(render));
|
|
290
|
+
expect(await settleHook(render)).toMatchObject({ content: 2, isLoading: false, error: null });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("fails closed before accepting a stable deep-leaf change beyond the depth bound", async () => {
|
|
294
|
+
const variant = {
|
|
295
|
+
render: (options?: { loadedData?: Record<string, unknown> }) =>
|
|
296
|
+
readDeepLabel(options?.loadedData),
|
|
297
|
+
};
|
|
298
|
+
let loadedData = makeDeepLoadedData(40, "before");
|
|
299
|
+
const render = () => usePreviewVariantRuntime({ variant, loadedData });
|
|
300
|
+
|
|
301
|
+
expect(await settleHook(render)).toMatchObject({
|
|
302
|
+
content: "before",
|
|
303
|
+
isLoading: false,
|
|
304
|
+
error: null,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
loadedData = makeDeepLoadedData(40, "after");
|
|
308
|
+
expectLoadedDataComparisonError(renderHook(render));
|
|
309
|
+
expect(await settleHook(render)).toMatchObject({
|
|
310
|
+
content: "after",
|
|
311
|
+
isLoading: false,
|
|
312
|
+
error: null,
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -14,11 +14,8 @@ describe("executeVariantLoaders", () => {
|
|
|
14
14
|
|
|
15
15
|
it("merges loader results and applies host loadedData last", async () => {
|
|
16
16
|
const result = await executeVariantLoaders(
|
|
17
|
-
[
|
|
18
|
-
|
|
19
|
-
async () => ({ count: 2, fromSecond: true }),
|
|
20
|
-
],
|
|
21
|
-
{ count: 3, fromHost: true },
|
|
17
|
+
[async () => ({ count: 1, fromFirst: true }), async () => ({ count: 2, fromSecond: true })],
|
|
18
|
+
{ count: 3, fromHost: true }
|
|
22
19
|
);
|
|
23
20
|
|
|
24
21
|
expect(result).toEqual({
|
|
@@ -43,6 +40,21 @@ describe("resolvePreviewRuntimeState", () => {
|
|
|
43
40
|
expect(state.content).toBe("Preview content");
|
|
44
41
|
});
|
|
45
42
|
|
|
43
|
+
it("settles a null render without remaining in a loading state", async () => {
|
|
44
|
+
const variant: PreviewVariantLike = {
|
|
45
|
+
render: () => null,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const state = await resolvePreviewRuntimeState({ variant });
|
|
49
|
+
|
|
50
|
+
expect(state).toEqual({
|
|
51
|
+
content: null,
|
|
52
|
+
isLoading: false,
|
|
53
|
+
error: null,
|
|
54
|
+
loadedData: undefined,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
46
58
|
it("renders with successful loaders", async () => {
|
|
47
59
|
const variant: PreviewVariantLike = {
|
|
48
60
|
loaders: [async () => ({ label: "from-loader" })],
|
|
@@ -58,7 +70,11 @@ describe("resolvePreviewRuntimeState", () => {
|
|
|
58
70
|
|
|
59
71
|
it("returns an error when a loader rejects", async () => {
|
|
60
72
|
const variant: PreviewVariantLike = {
|
|
61
|
-
loaders: [
|
|
73
|
+
loaders: [
|
|
74
|
+
async () => {
|
|
75
|
+
throw new Error("loader failed");
|
|
76
|
+
},
|
|
77
|
+
],
|
|
62
78
|
render: () => "should not render",
|
|
63
79
|
};
|
|
64
80
|
|
|
@@ -83,7 +99,11 @@ describe("resolvePreviewRuntimeState", () => {
|
|
|
83
99
|
|
|
84
100
|
it("normalizes non-Error throwables", async () => {
|
|
85
101
|
const variant: PreviewVariantLike = {
|
|
86
|
-
loaders: [
|
|
102
|
+
loaders: [
|
|
103
|
+
async () => {
|
|
104
|
+
throw "string-failure";
|
|
105
|
+
},
|
|
106
|
+
],
|
|
87
107
|
render: () => "never",
|
|
88
108
|
};
|
|
89
109
|
|
|
@@ -93,7 +113,9 @@ describe("resolvePreviewRuntimeState", () => {
|
|
|
93
113
|
});
|
|
94
114
|
|
|
95
115
|
it("uses host data as override input in render options", async () => {
|
|
96
|
-
const renderSpy = vi.fn(
|
|
116
|
+
const renderSpy = vi.fn(
|
|
117
|
+
(options?: { loadedData?: Record<string, unknown> }) => options?.loadedData
|
|
118
|
+
);
|
|
97
119
|
const variant: PreviewVariantLike = {
|
|
98
120
|
loaders: [async () => ({ count: 1 })],
|
|
99
121
|
render: renderSpy,
|
|
@@ -1,31 +1,208 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
5
|
+
import addFormats from "ajv-formats";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
2
7
|
|
|
3
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
fragmentDefinitionV2Schema,
|
|
10
|
+
fragmentDefinitionV3BodySchema,
|
|
11
|
+
fragmentDefinitionV3Schema,
|
|
12
|
+
fragmentGeneratedSchema,
|
|
13
|
+
fragmentProvenanceSchema,
|
|
14
|
+
fragmentsConfigSchema,
|
|
15
|
+
observedUsagePropSchema,
|
|
16
|
+
propAnnotationSchema,
|
|
17
|
+
} from "../schema.js";
|
|
18
|
+
import type { FragmentGenerated, FragmentProvenance } from "../types.js";
|
|
19
|
+
import type { FragmentGenerated as CompiledFragmentGenerated } from "../compiled-types/index.js";
|
|
4
20
|
|
|
5
|
-
|
|
6
|
-
|
|
21
|
+
const testsDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const repoRoot = join(testsDir, "../../../..");
|
|
23
|
+
|
|
24
|
+
const publicV3Schema = JSON.parse(
|
|
25
|
+
readFileSync(join(repoRoot, "apps/docs/public/schemas/fragment.v3.json"), "utf8")
|
|
26
|
+
) as Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The v3 parity corpus. Owned by Core (A06-04 is the parity owner) and read
|
|
30
|
+
* verbatim by the compiler's loader test, so "the loader accepts what the
|
|
31
|
+
* published schema accepts" is one fixture set, not two that can drift. It
|
|
32
|
+
* lives under `test-fixtures/` because `src/` ships inside the package.
|
|
33
|
+
*/
|
|
34
|
+
const parityFixtures = JSON.parse(
|
|
35
|
+
readFileSync(join(testsDir, "../../test-fixtures/fragment-v3-parity.json"), "utf8")
|
|
36
|
+
) as {
|
|
37
|
+
validBody: Record<string, unknown>;
|
|
38
|
+
validTimestamps: string[];
|
|
39
|
+
invalidTimestamps: string[];
|
|
40
|
+
invalidBodies: Array<{ name: string; body: Record<string, unknown> }>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const publicSchemaValidator = new Ajv2020({ allErrors: true, strict: false });
|
|
44
|
+
addFormats(publicSchemaValidator);
|
|
45
|
+
const validatePublicV3 = publicSchemaValidator.compile(publicV3Schema);
|
|
46
|
+
|
|
47
|
+
function publicV3Accepts(value: unknown): boolean {
|
|
48
|
+
return validatePublicV3(value) as boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const scannedAuthoringFragment = { source: "scan" } satisfies FragmentGenerated;
|
|
52
|
+
const scannedCompiledFragment = { source: "scan" } satisfies CompiledFragmentGenerated;
|
|
53
|
+
const verifiedScannedFragment = {
|
|
54
|
+
source: "scan",
|
|
55
|
+
verified: true,
|
|
56
|
+
} satisfies FragmentGenerated & CompiledFragmentGenerated;
|
|
57
|
+
const provenanceWithoutVerification = { source: "scan" } satisfies FragmentProvenance;
|
|
58
|
+
|
|
59
|
+
describe("fragmentGeneratedSchema", () => {
|
|
60
|
+
it("keeps scan provenance aligned across runtime and public types", () => {
|
|
61
|
+
expect(fragmentGeneratedSchema.parse(scannedAuthoringFragment).source).toBe("scan");
|
|
62
|
+
expect(fragmentGeneratedSchema.parse(scannedCompiledFragment).source).toBe("scan");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("preserves verification accepted by both public generated-fragment types", () => {
|
|
66
|
+
expect(fragmentGeneratedSchema.parse(verifiedScannedFragment)).toEqual({
|
|
67
|
+
source: "scan",
|
|
68
|
+
verified: true,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("fragmentProvenanceSchema", () => {
|
|
74
|
+
it("keeps verification optional across runtime and public types", () => {
|
|
75
|
+
expect(fragmentProvenanceSchema.parse(provenanceWithoutVerification)).toEqual({
|
|
76
|
+
source: "scan",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("fragmentsConfigSchema", () => {
|
|
82
|
+
it("does not expose the retired storybook filter option", () => {
|
|
83
|
+
expect(Object.hasOwn(fragmentsConfigSchema.shape, "storybook")).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("observedUsagePropSchema", () => {
|
|
88
|
+
it("accepts structural JSX children observations", () => {
|
|
7
89
|
expect(
|
|
8
90
|
observedUsagePropSchema.safeParse({
|
|
9
|
-
name:
|
|
10
|
-
kind:
|
|
11
|
-
}).success
|
|
91
|
+
name: "children",
|
|
92
|
+
kind: "jsx",
|
|
93
|
+
}).success
|
|
12
94
|
).toBe(true);
|
|
13
95
|
});
|
|
14
96
|
|
|
15
|
-
it(
|
|
97
|
+
it("accepts boolean and null prop observations", () => {
|
|
16
98
|
expect(
|
|
17
99
|
observedUsagePropSchema.safeParse({
|
|
18
|
-
name:
|
|
19
|
-
kind:
|
|
100
|
+
name: "disabled",
|
|
101
|
+
kind: "boolean",
|
|
20
102
|
value: true,
|
|
21
|
-
}).success
|
|
103
|
+
}).success
|
|
22
104
|
).toBe(true);
|
|
23
105
|
expect(
|
|
24
106
|
observedUsagePropSchema.safeParse({
|
|
25
|
-
name:
|
|
26
|
-
kind:
|
|
107
|
+
name: "tone",
|
|
108
|
+
kind: "null",
|
|
27
109
|
value: null,
|
|
28
|
-
}).success
|
|
110
|
+
}).success
|
|
29
111
|
).toBe(true);
|
|
30
112
|
});
|
|
31
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("fragmentDefinitionV2Schema", () => {
|
|
116
|
+
it("accepts migrated provenance with verification fields", () => {
|
|
117
|
+
const result = fragmentDefinitionV2Schema.safeParse({
|
|
118
|
+
component: () => null,
|
|
119
|
+
meta: { name: "Button", description: "A button", category: "actions" },
|
|
120
|
+
guidance: { when: [], whenNot: [] },
|
|
121
|
+
props: {},
|
|
122
|
+
examples: [],
|
|
123
|
+
_provenance: {
|
|
124
|
+
source: "migrated",
|
|
125
|
+
verified: true,
|
|
126
|
+
frameworkSupport: "native",
|
|
127
|
+
sourceHash: "abc123",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
if (result.success) {
|
|
133
|
+
expect(result.data._provenance).toEqual({
|
|
134
|
+
source: "migrated",
|
|
135
|
+
verified: true,
|
|
136
|
+
frameworkSupport: "native",
|
|
137
|
+
sourceHash: "abc123",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("fragmentDefinitionV3Schema parity with public schema (A06-04)", () => {
|
|
144
|
+
const { validBody, validTimestamps, invalidTimestamps, invalidBodies } = parityFixtures;
|
|
145
|
+
|
|
146
|
+
it("accepts the same valid fixture in Zod and the published schema", () => {
|
|
147
|
+
expect(fragmentDefinitionV3BodySchema.safeParse(validBody).success).toBe(true);
|
|
148
|
+
expect(
|
|
149
|
+
fragmentDefinitionV3Schema.safeParse({
|
|
150
|
+
...validBody,
|
|
151
|
+
component: () => null,
|
|
152
|
+
governance: [],
|
|
153
|
+
}).success
|
|
154
|
+
).toBe(true);
|
|
155
|
+
expect(publicV3Accepts(validBody)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it.each(validTimestamps)(
|
|
159
|
+
"accepts RFC 3339 provenance timestamp %s in Zod and the published schema",
|
|
160
|
+
(timestamp) => {
|
|
161
|
+
const withTimestamp = {
|
|
162
|
+
...validBody,
|
|
163
|
+
_provenance: { ...(validBody._provenance as object), timestamp },
|
|
164
|
+
};
|
|
165
|
+
expect(fragmentDefinitionV3BodySchema.safeParse(withTimestamp).success).toBe(true);
|
|
166
|
+
expect(publicV3Accepts(withTimestamp)).toBe(true);
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
it.each(invalidTimestamps)(
|
|
171
|
+
"rejects invalid provenance timestamp %s in Zod and the published schema",
|
|
172
|
+
(timestamp) => {
|
|
173
|
+
const withTimestamp = {
|
|
174
|
+
...validBody,
|
|
175
|
+
_provenance: { ...(validBody._provenance as object), timestamp },
|
|
176
|
+
};
|
|
177
|
+
expect(fragmentDefinitionV3BodySchema.safeParse(withTimestamp).success).toBe(false);
|
|
178
|
+
expect(publicV3Accepts(withTimestamp)).toBe(false);
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
it.each(invalidBodies.map(({ name, body }) => [name, body] as const))(
|
|
183
|
+
"rejects %s in both Zod and the published schema",
|
|
184
|
+
(_name, invalidBody) => {
|
|
185
|
+
expect(fragmentDefinitionV3BodySchema.safeParse(invalidBody).success).toBe(false);
|
|
186
|
+
expect(publicV3Accepts(invalidBody)).toBe(false);
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
it("rejects prop annotations that redefine type/default at the leaf schema", () => {
|
|
191
|
+
expect(
|
|
192
|
+
propAnnotationSchema.safeParse({ description: "x", type: "enum", default: "primary" }).success
|
|
193
|
+
).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("keeps callable govern authoring out of the JSON interchange projection", () => {
|
|
197
|
+
const withGovern = { ...validBody, govern: () => [] };
|
|
198
|
+
expect(fragmentDefinitionV3BodySchema.safeParse(withGovern).success).toBe(true);
|
|
199
|
+
expect(publicV3Accepts(withGovern)).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("publishes the versioned schema document with the usefragments $id", () => {
|
|
203
|
+
expect(publicV3Schema).toMatchObject({
|
|
204
|
+
$id: "https://usefragments.com/schemas/fragment.v3.json",
|
|
205
|
+
});
|
|
206
|
+
expect(publicV3Schema).toHaveProperty("$defs.state.not.required", ["code"]);
|
|
207
|
+
});
|
|
208
|
+
});
|