@assistant-ui/react-generative-ui 0.0.24 → 0.1.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/README.md +2 -0
- package/dist/a2ui.d.ts +1 -4
- package/dist/a2ui.d.ts.map +1 -1
- package/dist/a2ui.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.server.d.ts +1 -2
- package/dist/index.server.d.ts.map +1 -1
- package/dist/index.server.js +1 -10
- package/dist/ir.d.ts +1 -111
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +1 -175
- package/dist/slack.d.ts +1 -4
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +1 -4
- package/dist/teams.d.ts +1 -4
- package/dist/teams.d.ts.map +1 -1
- package/dist/teams.js +1 -4
- package/package.json +4 -5
- package/src/a2ui.ts +1 -22
- package/src/index.server.ts +1 -2
- package/src/index.test.ts +75 -0
- package/src/index.ts +1 -2
- package/src/ir.ts +1 -279
- package/src/slack.ts +1 -39
- package/src/teams.ts +1 -35
- package/dist/JSONGenerativeUI.client.d.ts +0 -28
- package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.client.js +0 -101
- package/dist/JSONGenerativeUI.client.js.map +0 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -21
- package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.server.js +0 -32
- package/dist/JSONGenerativeUI.server.js.map +0 -1
- package/dist/JSONGenerativeUI.shared.d.ts +0 -63
- package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.shared.js +0 -31
- package/dist/JSONGenerativeUI.shared.js.map +0 -1
- package/dist/RadioGroupScope.d.ts +0 -6
- package/dist/RadioGroupScope.d.ts.map +0 -1
- package/dist/RadioGroupScope.js +0 -25
- package/dist/RadioGroupScope.js.map +0 -1
- package/dist/a2ui/BindingContext.d.ts +0 -11
- package/dist/a2ui/BindingContext.d.ts.map +0 -1
- package/dist/a2ui/BindingContext.js +0 -12
- package/dist/a2ui/BindingContext.js.map +0 -1
- package/dist/a2ui/PresentRenderer.d.ts +0 -11
- package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
- package/dist/a2ui/PresentRenderer.js +0 -39
- package/dist/a2ui/PresentRenderer.js.map +0 -1
- package/dist/a2ui/convert.d.ts +0 -15
- package/dist/a2ui/convert.d.ts.map +0 -1
- package/dist/a2ui/convert.js +0 -728
- package/dist/a2ui/convert.js.map +0 -1
- package/dist/a2ui/dataModel.d.ts +0 -9
- package/dist/a2ui/dataModel.d.ts.map +0 -1
- package/dist/a2ui/dataModel.js +0 -44
- package/dist/a2ui/dataModel.js.map +0 -1
- package/dist/a2ui/reducer.d.ts +0 -8
- package/dist/a2ui/reducer.d.ts.map +0 -1
- package/dist/a2ui/reducer.js +0 -242
- package/dist/a2ui/reducer.js.map +0 -1
- package/dist/a2ui/snapshot.d.ts +0 -3
- package/dist/a2ui/snapshot.d.ts.map +0 -1
- package/dist/a2ui/snapshot.js +0 -33
- package/dist/a2ui/snapshot.js.map +0 -1
- package/dist/a2ui/types.d.ts +0 -88
- package/dist/a2ui/types.d.ts.map +0 -1
- package/dist/a2ui/types.js +0 -6
- package/dist/a2ui/types.js.map +0 -1
- package/dist/a2ui/valueFunctions.d.ts +0 -16
- package/dist/a2ui/valueFunctions.d.ts.map +0 -1
- package/dist/a2ui/valueFunctions.js +0 -332
- package/dist/a2ui/valueFunctions.js.map +0 -1
- package/dist/actionRegistry.d.ts +0 -29
- package/dist/actionRegistry.d.ts.map +0 -1
- package/dist/actionRegistry.js +0 -41
- package/dist/actionRegistry.js.map +0 -1
- package/dist/answeredValues.d.ts +0 -16
- package/dist/answeredValues.d.ts.map +0 -1
- package/dist/answeredValues.js +0 -37
- package/dist/answeredValues.js.map +0 -1
- package/dist/buildPresentParameters.d.ts +0 -21
- package/dist/buildPresentParameters.d.ts.map +0 -1
- package/dist/buildPresentParameters.js +0 -95
- package/dist/buildPresentParameters.js.map +0 -1
- package/dist/constants.d.ts +0 -15
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -19
- package/dist/constants.js.map +0 -1
- package/dist/convert/boundSpec.d.ts +0 -48
- package/dist/convert/boundSpec.d.ts.map +0 -1
- package/dist/convert/boundSpec.js +0 -108
- package/dist/convert/boundSpec.js.map +0 -1
- package/dist/convert/copyBounded.d.ts +0 -25
- package/dist/convert/copyBounded.d.ts.map +0 -1
- package/dist/convert/copyBounded.js +0 -38
- package/dist/convert/copyBounded.js.map +0 -1
- package/dist/convert/isElement.d.ts +0 -3
- package/dist/convert/isElement.d.ts.map +0 -1
- package/dist/convert/isElement.js +0 -6
- package/dist/convert/isElement.js.map +0 -1
- package/dist/convert/takeRun.d.ts +0 -6
- package/dist/convert/takeRun.d.ts.map +0 -1
- package/dist/convert/takeRun.js +0 -20
- package/dist/convert/takeRun.js.map +0 -1
- package/dist/defineGenerativeComponents.d.ts +0 -29
- package/dist/defineGenerativeComponents.d.ts.map +0 -1
- package/dist/defineGenerativeComponents.js +0 -34
- package/dist/defineGenerativeComponents.js.map +0 -1
- package/dist/fieldReferences.d.ts +0 -9
- package/dist/fieldReferences.d.ts.map +0 -1
- package/dist/fieldReferences.js +0 -53
- package/dist/fieldReferences.js.map +0 -1
- package/dist/generativeUIToJSX.d.ts +0 -18
- package/dist/generativeUIToJSX.d.ts.map +0 -1
- package/dist/generativeUIToJSX.js +0 -76
- package/dist/generativeUIToJSX.js.map +0 -1
- package/dist/index.shared.d.ts +0 -15
- package/dist/index.shared.d.ts.map +0 -1
- package/dist/index.shared.js +0 -9
- package/dist/ir.js.map +0 -1
- package/dist/renderGenerativeUI.d.ts +0 -14
- package/dist/renderGenerativeUI.d.ts.map +0 -1
- package/dist/renderGenerativeUI.js +0 -83
- package/dist/renderGenerativeUI.js.map +0 -1
- package/dist/scopeSchema.d.ts +0 -6
- package/dist/scopeSchema.d.ts.map +0 -1
- package/dist/scopeSchema.js +0 -56
- package/dist/scopeSchema.js.map +0 -1
- package/dist/slack/constants.d.ts +0 -93
- package/dist/slack/constants.d.ts.map +0 -1
- package/dist/slack/constants.js +0 -146
- package/dist/slack/constants.js.map +0 -1
- package/dist/slack/decodeBlockAction.d.ts +0 -10
- package/dist/slack/decodeBlockAction.d.ts.map +0 -1
- package/dist/slack/decodeBlockAction.js +0 -129
- package/dist/slack/decodeBlockAction.js.map +0 -1
- package/dist/slack/fromSlackBlocks.d.ts +0 -27
- package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
- package/dist/slack/fromSlackBlocks.js +0 -483
- package/dist/slack/fromSlackBlocks.js.map +0 -1
- package/dist/slack/toSlackBlocks.d.ts +0 -4
- package/dist/slack/toSlackBlocks.d.ts.map +0 -1
- package/dist/slack/toSlackBlocks.js +0 -901
- package/dist/slack/toSlackBlocks.js.map +0 -1
- package/dist/slack/types.d.ts +0 -204
- package/dist/slack/types.d.ts.map +0 -1
- package/dist/slack/types.js +0 -0
- package/dist/teams/constants.d.ts +0 -34
- package/dist/teams/constants.d.ts.map +0 -1
- package/dist/teams/constants.js +0 -68
- package/dist/teams/constants.js.map +0 -1
- package/dist/teams/decodeSubmitData.d.ts +0 -26
- package/dist/teams/decodeSubmitData.d.ts.map +0 -1
- package/dist/teams/decodeSubmitData.js +0 -70
- package/dist/teams/decodeSubmitData.js.map +0 -1
- package/dist/teams/temporalId.d.ts +0 -5
- package/dist/teams/temporalId.d.ts.map +0 -1
- package/dist/teams/temporalId.js +0 -28
- package/dist/teams/temporalId.js.map +0 -1
- package/dist/teams/toAdaptiveCard.d.ts +0 -45
- package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
- package/dist/teams/toAdaptiveCard.js +0 -642
- package/dist/teams/toAdaptiveCard.js.map +0 -1
- package/dist/teams/toTeamsAttachments.d.ts +0 -10
- package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
- package/dist/teams/toTeamsAttachments.js +0 -99
- package/dist/teams/toTeamsAttachments.js.map +0 -1
- package/dist/teams/types.d.ts +0 -224
- package/dist/teams/types.d.ts.map +0 -1
- package/dist/teams/types.js +0 -0
- package/dist/temporal.d.ts +0 -33
- package/dist/temporal.d.ts.map +0 -1
- package/dist/temporal.js +0 -153
- package/dist/temporal.js.map +0 -1
- package/dist/types.d.ts +0 -99
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -0
- package/dist/vocabulary/alert.d.ts +0 -32
- package/dist/vocabulary/alert.d.ts.map +0 -1
- package/dist/vocabulary/alert.js +0 -160
- package/dist/vocabulary/alert.js.map +0 -1
- package/dist/vocabulary/collectFormValues.d.ts +0 -23
- package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
- package/dist/vocabulary/collectFormValues.js +0 -44
- package/dist/vocabulary/collectFormValues.js.map +0 -1
- package/dist/vocabulary/data.d.ts +0 -66
- package/dist/vocabulary/data.d.ts.map +0 -1
- package/dist/vocabulary/data.js +0 -503
- package/dist/vocabulary/data.js.map +0 -1
- package/dist/vocabulary/dispatch.d.ts +0 -9
- package/dist/vocabulary/dispatch.d.ts.map +0 -1
- package/dist/vocabulary/dispatch.js +0 -50
- package/dist/vocabulary/dispatch.js.map +0 -1
- package/dist/vocabulary/fact.d.ts +0 -19
- package/dist/vocabulary/fact.d.ts.map +0 -1
- package/dist/vocabulary/fact.js +0 -51
- package/dist/vocabulary/fact.js.map +0 -1
- package/dist/vocabulary/form.d.ts +0 -11
- package/dist/vocabulary/form.d.ts.map +0 -1
- package/dist/vocabulary/form.js +0 -23
- package/dist/vocabulary/form.js.map +0 -1
- package/dist/vocabulary/formatValue.d.ts +0 -7
- package/dist/vocabulary/formatValue.d.ts.map +0 -1
- package/dist/vocabulary/formatValue.js +0 -76
- package/dist/vocabulary/formatValue.js.map +0 -1
- package/dist/vocabulary/icon.d.ts +0 -41
- package/dist/vocabulary/icon.d.ts.map +0 -1
- package/dist/vocabulary/icon.js +0 -272
- package/dist/vocabulary/icon.js.map +0 -1
- package/dist/vocabulary/index.d.ts +0 -12
- package/dist/vocabulary/index.d.ts.map +0 -1
- package/dist/vocabulary/index.js +0 -36
- package/dist/vocabulary/index.js.map +0 -1
- package/dist/vocabulary/interactive.d.ts +0 -230
- package/dist/vocabulary/interactive.d.ts.map +0 -1
- package/dist/vocabulary/interactive.js +0 -584
- package/dist/vocabulary/interactive.js.map +0 -1
- package/dist/vocabulary/layout.d.ts +0 -79
- package/dist/vocabulary/layout.d.ts.map +0 -1
- package/dist/vocabulary/layout.js +0 -139
- package/dist/vocabulary/layout.js.map +0 -1
- package/dist/vocabulary/list.d.ts +0 -14
- package/dist/vocabulary/list.d.ts.map +0 -1
- package/dist/vocabulary/list.js +0 -50
- package/dist/vocabulary/list.js.map +0 -1
- package/dist/vocabulary/media.d.ts +0 -25
- package/dist/vocabulary/media.d.ts.map +0 -1
- package/dist/vocabulary/media.js +0 -42
- package/dist/vocabulary/media.js.map +0 -1
- package/dist/vocabulary/text.d.ts +0 -57
- package/dist/vocabulary/text.d.ts.map +0 -1
- package/dist/vocabulary/text.js +0 -49
- package/dist/vocabulary/text.js.map +0 -1
- package/dist/vocabulary/toTextContent.d.ts +0 -2
- package/dist/vocabulary/toTextContent.d.ts.map +0 -1
- package/dist/vocabulary/toTextContent.js +0 -6
- package/dist/vocabulary/toTextContent.js.map +0 -1
- package/src/JSONGenerativeUI.client.tsx +0 -168
- package/src/JSONGenerativeUI.server.tsx +0 -39
- package/src/JSONGenerativeUI.shared.ts +0 -90
- package/src/JSONGenerativeUI.test.tsx +0 -1298
- package/src/RadioGroupScope.server.test.tsx +0 -24
- package/src/RadioGroupScope.tsx +0 -34
- package/src/a2ui/BindingContext.ts +0 -26
- package/src/a2ui/PresentRenderer.tsx +0 -83
- package/src/a2ui/convert.test.ts +0 -2923
- package/src/a2ui/convert.ts +0 -1207
- package/src/a2ui/dataModel.ts +0 -69
- package/src/a2ui/reducer.test.ts +0 -422
- package/src/a2ui/reducer.ts +0 -363
- package/src/a2ui/snapshot.test.ts +0 -52
- package/src/a2ui/snapshot.ts +0 -52
- package/src/a2ui/types.ts +0 -114
- package/src/a2ui/valueFunctions.test.ts +0 -271
- package/src/a2ui/valueFunctions.ts +0 -523
- package/src/actionRegistry.test.ts +0 -90
- package/src/actionRegistry.ts +0 -88
- package/src/answeredValues.test.ts +0 -33
- package/src/answeredValues.tsx +0 -70
- package/src/buildPresentParameters.test.ts +0 -246
- package/src/buildPresentParameters.ts +0 -130
- package/src/constants.ts +0 -17
- package/src/convert/boundSpec.test.ts +0 -192
- package/src/convert/boundSpec.ts +0 -134
- package/src/convert/copyBounded.test.ts +0 -131
- package/src/convert/copyBounded.ts +0 -38
- package/src/convert/isElement.ts +0 -6
- package/src/convert/takeRun.test.ts +0 -64
- package/src/convert/takeRun.ts +0 -19
- package/src/defineGenerativeComponents.ts +0 -38
- package/src/fieldReferences.test.ts +0 -66
- package/src/fieldReferences.ts +0 -83
- package/src/generativeUIToJSX.test.ts +0 -268
- package/src/generativeUIToJSX.ts +0 -152
- package/src/index.server.test.ts +0 -106
- package/src/index.shared.ts +0 -61
- package/src/ir.test.ts +0 -203
- package/src/renderGenerativeUI.test.tsx +0 -373
- package/src/renderGenerativeUI.tsx +0 -165
- package/src/scopeSchema.test.ts +0 -72
- package/src/scopeSchema.ts +0 -71
- package/src/slack/a2uiSlack.test.ts +0 -198
- package/src/slack/constants.ts +0 -194
- package/src/slack/datePicker.test.ts +0 -612
- package/src/slack/decodeBlockAction.test.ts +0 -233
- package/src/slack/decodeBlockAction.ts +0 -232
- package/src/slack/fromSlackBlocks.test.ts +0 -1465
- package/src/slack/fromSlackBlocks.ts +0 -789
- package/src/slack/toSlackBlocks.test.ts +0 -3065
- package/src/slack/toSlackBlocks.ts +0 -1818
- package/src/slack/types.ts +0 -264
- package/src/teams/constants.ts +0 -93
- package/src/teams/decodeSubmitData.test.ts +0 -397
- package/src/teams/decodeSubmitData.ts +0 -110
- package/src/teams/temporal.test.ts +0 -766
- package/src/teams/temporalId.ts +0 -56
- package/src/teams/toAdaptiveCard.test.ts +0 -1897
- package/src/teams/toAdaptiveCard.ts +0 -1057
- package/src/teams/toTeamsAttachments.test.ts +0 -74
- package/src/teams/toTeamsAttachments.ts +0 -133
- package/src/teams/types.ts +0 -266
- package/src/temporal.test.ts +0 -337
- package/src/temporal.ts +0 -263
- package/src/types.ts +0 -119
- package/src/vocabulary/alert.test.tsx +0 -427
- package/src/vocabulary/alert.tsx +0 -217
- package/src/vocabulary/collectFormValues.test.ts +0 -389
- package/src/vocabulary/collectFormValues.ts +0 -116
- package/src/vocabulary/data.test.tsx +0 -858
- package/src/vocabulary/data.tsx +0 -819
- package/src/vocabulary/datepicker.dom.test.tsx +0 -1457
- package/src/vocabulary/dispatch.ts +0 -83
- package/src/vocabulary/fact.test.tsx +0 -94
- package/src/vocabulary/fact.tsx +0 -54
- package/src/vocabulary/form.test.tsx +0 -113
- package/src/vocabulary/form.tsx +0 -39
- package/src/vocabulary/formatValue.test.ts +0 -72
- package/src/vocabulary/formatValue.ts +0 -127
- package/src/vocabulary/icon.test.tsx +0 -64
- package/src/vocabulary/icon.tsx +0 -174
- package/src/vocabulary/index.test.ts +0 -65
- package/src/vocabulary/index.ts +0 -33
- package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
- package/src/vocabulary/interactive.dom.test.tsx +0 -1747
- package/src/vocabulary/interactive.test.tsx +0 -377
- package/src/vocabulary/interactive.tsx +0 -1109
- package/src/vocabulary/layout.test.tsx +0 -323
- package/src/vocabulary/layout.tsx +0 -247
- package/src/vocabulary/list.test.tsx +0 -222
- package/src/vocabulary/list.tsx +0 -50
- package/src/vocabulary/media.test.tsx +0 -85
- package/src/vocabulary/media.tsx +0 -56
- package/src/vocabulary/text.test.tsx +0 -102
- package/src/vocabulary/text.tsx +0 -60
- package/src/vocabulary/toTextContent.ts +0 -2
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { findAnsweredValues } from "./answeredValues";
|
|
3
|
-
|
|
4
|
-
describe("findAnsweredValues", () => {
|
|
5
|
-
it("returns the latest action's object input", () => {
|
|
6
|
-
expect(
|
|
7
|
-
findAnsweredValues({
|
|
8
|
-
entries: [
|
|
9
|
-
{
|
|
10
|
-
type: "action",
|
|
11
|
-
payload: { $input: { choice: "first" } },
|
|
12
|
-
},
|
|
13
|
-
{ type: "human-response", payload: { choice: "ignored" } },
|
|
14
|
-
{
|
|
15
|
-
type: "action",
|
|
16
|
-
payload: { $input: { choice: "last" } },
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
}),
|
|
20
|
-
).toEqual({ choice: "last" });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("ignores actions whose input is not a plain object", () => {
|
|
24
|
-
expect(
|
|
25
|
-
findAnsweredValues({
|
|
26
|
-
entries: [
|
|
27
|
-
{ type: "action", payload: { $input: { choice: "kept" } } },
|
|
28
|
-
{ type: "action", payload: { $input: ["not", "an", "object"] } },
|
|
29
|
-
],
|
|
30
|
-
}),
|
|
31
|
-
).toEqual({ choice: "kept" });
|
|
32
|
-
});
|
|
33
|
-
});
|
package/src/answeredValues.tsx
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
type AnsweredValues = Readonly<Record<string, unknown>>;
|
|
4
|
-
|
|
5
|
-
type ToolInteractionLog = {
|
|
6
|
-
readonly entries: readonly {
|
|
7
|
-
readonly type: string;
|
|
8
|
-
readonly payload: unknown;
|
|
9
|
-
}[];
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
let answeredValuesContext:
|
|
13
|
-
| React.Context<AnsweredValues | undefined>
|
|
14
|
-
| undefined;
|
|
15
|
-
const supportsContext = typeof React.createContext === "function";
|
|
16
|
-
const useValuesContext = supportsContext ? React.useContext : () => undefined;
|
|
17
|
-
|
|
18
|
-
const getAnsweredValuesContext = () =>
|
|
19
|
-
supportsContext
|
|
20
|
-
? (answeredValuesContext ??= React.createContext<
|
|
21
|
-
AnsweredValues | undefined
|
|
22
|
-
>(undefined))
|
|
23
|
-
: undefined;
|
|
24
|
-
|
|
25
|
-
const isPlainObject = (value: unknown): value is AnsweredValues => {
|
|
26
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
const prototype = Object.getPrototypeOf(value);
|
|
30
|
-
return prototype === Object.prototype || prototype === null;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const findAnsweredValues = (
|
|
34
|
-
interactions: ToolInteractionLog | undefined,
|
|
35
|
-
): AnsweredValues | undefined => {
|
|
36
|
-
const entries = interactions?.entries;
|
|
37
|
-
if (!entries) return undefined;
|
|
38
|
-
|
|
39
|
-
for (let index = entries.length - 1; index >= 0; index--) {
|
|
40
|
-
const interaction = entries[index]!;
|
|
41
|
-
if (interaction.type !== "action" || !isPlainObject(interaction.payload)) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const input = interaction.payload["$input"];
|
|
45
|
-
if (isPlainObject(input)) return input;
|
|
46
|
-
}
|
|
47
|
-
return undefined;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const AnsweredValuesProvider = ({
|
|
51
|
-
values,
|
|
52
|
-
children,
|
|
53
|
-
}: {
|
|
54
|
-
values: AnsweredValues | undefined;
|
|
55
|
-
children: React.ReactNode;
|
|
56
|
-
}) => {
|
|
57
|
-
const Context = getAnsweredValuesContext();
|
|
58
|
-
return Context ? (
|
|
59
|
-
<Context.Provider value={values}>{children}</Context.Provider>
|
|
60
|
-
) : (
|
|
61
|
-
children
|
|
62
|
-
);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const useAnsweredValue = (name: string | undefined): unknown => {
|
|
66
|
-
const values = useValuesContext(getAnsweredValuesContext()!);
|
|
67
|
-
return name !== undefined && values && Object.hasOwn(values, name)
|
|
68
|
-
? values[name]
|
|
69
|
-
: undefined;
|
|
70
|
-
};
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { buildPresentParameters } from "./buildPresentParameters";
|
|
5
|
-
|
|
6
|
-
const component = (properties: z.ZodType) => ({
|
|
7
|
-
description: "A component",
|
|
8
|
-
properties,
|
|
9
|
-
render: () => null,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
function resolve(root: JSONSchema7, value: JSONSchema7Definition) {
|
|
13
|
-
let schema = value as JSONSchema7;
|
|
14
|
-
if (schema.$ref === "#") return root;
|
|
15
|
-
if (schema.$ref) {
|
|
16
|
-
expect(schema.$ref).toMatch(/^#\//);
|
|
17
|
-
let target: unknown = root;
|
|
18
|
-
for (const segment of schema.$ref.slice(2).split("/")) {
|
|
19
|
-
const key = decodeURIComponent(segment)
|
|
20
|
-
.replace(/~1/g, "/")
|
|
21
|
-
.replace(/~0/g, "~");
|
|
22
|
-
target = (target as Record<string, unknown> | undefined)?.[key];
|
|
23
|
-
}
|
|
24
|
-
expect(target).toBeDefined();
|
|
25
|
-
schema = target as JSONSchema7;
|
|
26
|
-
}
|
|
27
|
-
return schema;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("component schema references", () => {
|
|
31
|
-
it("retains recursive definitions used by a component prop", () => {
|
|
32
|
-
const Tree = z.object({
|
|
33
|
-
label: z.string(),
|
|
34
|
-
get descendants() {
|
|
35
|
-
return z.array(Tree);
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
const schema = buildPresentParameters({
|
|
39
|
-
Tree: component(z.object({ tree: Tree })),
|
|
40
|
-
});
|
|
41
|
-
const tree = resolve(schema, schema.properties!.tree!);
|
|
42
|
-
expect(tree.properties!.label).toEqual({ type: "string" });
|
|
43
|
-
const descendants = tree.properties!.descendants as JSONSchema7;
|
|
44
|
-
expect(resolve(schema, descendants.items as JSONSchema7Definition)).toBe(
|
|
45
|
-
tree,
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("keeps definitions with the same generated name isolated across components", () => {
|
|
50
|
-
const TextTree = z.object({
|
|
51
|
-
value: z.string(),
|
|
52
|
-
get descendants() {
|
|
53
|
-
return z.array(TextTree);
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
const NumberTree = z.object({
|
|
57
|
-
value: z.number(),
|
|
58
|
-
get descendants() {
|
|
59
|
-
return z.array(NumberTree);
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
const schema = buildPresentParameters({
|
|
63
|
-
node: component(z.object({ textTree: TextTree })),
|
|
64
|
-
children: component(z.object({ numberTree: NumberTree })),
|
|
65
|
-
});
|
|
66
|
-
const textTree = resolve(schema, schema.properties!.textTree!);
|
|
67
|
-
const numberTree = resolve(schema, schema.properties!.numberTree!);
|
|
68
|
-
expect(textTree.properties!.value).toEqual({ type: "string" });
|
|
69
|
-
expect(numberTree.properties!.value).toEqual({ type: "number" });
|
|
70
|
-
expect(
|
|
71
|
-
resolve(
|
|
72
|
-
schema,
|
|
73
|
-
(textTree.properties!.descendants as JSONSchema7)
|
|
74
|
-
.items as JSONSchema7Definition,
|
|
75
|
-
),
|
|
76
|
-
).toBe(textTree);
|
|
77
|
-
expect(
|
|
78
|
-
resolve(
|
|
79
|
-
schema,
|
|
80
|
-
(numberTree.properties!.descendants as JSONSchema7)
|
|
81
|
-
.items as JSONSchema7Definition,
|
|
82
|
-
),
|
|
83
|
-
).toBe(numberTree);
|
|
84
|
-
expect(resolve(schema, schema.properties!.children!).anyOf).toBeDefined();
|
|
85
|
-
expect((schema.$defs!.node as JSONSchema7).properties!.$type).toBeDefined();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("rebases root references to component props instead of the generated UI node", () => {
|
|
89
|
-
const Props = z.object({
|
|
90
|
-
label: z.string(),
|
|
91
|
-
get nested() {
|
|
92
|
-
return z.array(Props);
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
const schema = buildPresentParameters({ Recursive: component(Props) });
|
|
96
|
-
const nested = schema.properties!.nested as JSONSchema7;
|
|
97
|
-
const props = resolve(schema, nested.items as JSONSchema7Definition);
|
|
98
|
-
expect(props.properties!.label).toEqual({ type: "string" });
|
|
99
|
-
expect(props.properties!.$type).toBeUndefined();
|
|
100
|
-
expect(props.required).toEqual(["label", "nested"]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("rebases recursive references inside unions and records", () => {
|
|
104
|
-
const Tree = z.object({
|
|
105
|
-
get branches() {
|
|
106
|
-
return z.record(z.string(), z.union([z.string(), Tree]));
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
const schema = buildPresentParameters({
|
|
110
|
-
Tree: component(z.object({ tree: Tree })),
|
|
111
|
-
});
|
|
112
|
-
const tree = resolve(schema, schema.properties!.tree!);
|
|
113
|
-
const branches = tree.properties!.branches as JSONSchema7;
|
|
114
|
-
const variants = (branches.additionalProperties as JSONSchema7).anyOf!;
|
|
115
|
-
expect(resolve(schema, variants[1]!)).toBe(tree);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("retains recursive definitions referenced from tuple items", () => {
|
|
119
|
-
const Tree = z.object({
|
|
120
|
-
label: z.string(),
|
|
121
|
-
get descendants() {
|
|
122
|
-
return z.array(Tree);
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
const schema = buildPresentParameters({
|
|
126
|
-
Pair: component(z.object({ pair: z.tuple([Tree, z.string()]) })),
|
|
127
|
-
});
|
|
128
|
-
const pair = schema.properties!.pair as JSONSchema7 & {
|
|
129
|
-
prefixItems?: JSONSchema7Definition[];
|
|
130
|
-
};
|
|
131
|
-
const first = (pair.prefixItems ??
|
|
132
|
-
(pair.items as JSONSchema7Definition[]))[0]!;
|
|
133
|
-
const tree = resolve(schema, first);
|
|
134
|
-
expect(tree.properties!.label).toEqual({ type: "string" });
|
|
135
|
-
expect(
|
|
136
|
-
resolve(
|
|
137
|
-
schema,
|
|
138
|
-
(tree.properties!.descendants as JSONSchema7)
|
|
139
|
-
.items as JSONSchema7Definition,
|
|
140
|
-
),
|
|
141
|
-
).toBe(tree);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("rebases prefixItems in draft 2020-12 converter output", () => {
|
|
145
|
-
const raw = {
|
|
146
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
147
|
-
$id: "https://example.com/pair",
|
|
148
|
-
type: "object",
|
|
149
|
-
properties: {
|
|
150
|
-
pair: {
|
|
151
|
-
type: "array",
|
|
152
|
-
prefixItems: [{ $ref: "#/$defs/item" }, { type: "string" }],
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
$defs: {
|
|
156
|
-
item: {
|
|
157
|
-
type: "object",
|
|
158
|
-
properties: {
|
|
159
|
-
descendants: { type: "array", items: { $ref: "#/$defs/item" } },
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
const schema = buildPresentParameters({
|
|
165
|
-
Pair: component(raw as unknown as z.ZodType),
|
|
166
|
-
});
|
|
167
|
-
const pair = schema.properties!.pair as JSONSchema7 & {
|
|
168
|
-
prefixItems: JSONSchema7Definition[];
|
|
169
|
-
};
|
|
170
|
-
const item = resolve(schema, pair.prefixItems[0]!);
|
|
171
|
-
expect(item.properties!.descendants).toBeDefined();
|
|
172
|
-
expect(
|
|
173
|
-
resolve(
|
|
174
|
-
schema,
|
|
175
|
-
(item.properties!.descendants as JSONSchema7)
|
|
176
|
-
.items as JSONSchema7Definition,
|
|
177
|
-
),
|
|
178
|
-
).toBe(item);
|
|
179
|
-
const embedded = schema.$defs!.component0 as JSONSchema7;
|
|
180
|
-
expect(Object.keys(embedded)).not.toContain("$schema");
|
|
181
|
-
expect(Object.keys(embedded)).not.toContain("$id");
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("strips root resource metadata from embedded component schemas", () => {
|
|
185
|
-
const Tree = z.object({
|
|
186
|
-
get descendants() {
|
|
187
|
-
return z.array(Tree);
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
const schema = buildPresentParameters({
|
|
191
|
-
Tree: component(z.object({ tree: Tree })),
|
|
192
|
-
});
|
|
193
|
-
const embedded = schema.$defs!.component0 as JSONSchema7;
|
|
194
|
-
expect(Object.keys(embedded)).not.toContain("$schema");
|
|
195
|
-
expect(embedded.type).toBe("object");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("omits component definitions whose properties all lose the duplicate-name merge", () => {
|
|
199
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
200
|
-
try {
|
|
201
|
-
const StringTree = z.object({
|
|
202
|
-
get tree() {
|
|
203
|
-
return z.array(StringTree);
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
const NumberTree = z.object({
|
|
207
|
-
value: z.number(),
|
|
208
|
-
get tree() {
|
|
209
|
-
return z.array(NumberTree);
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
const schema = buildPresentParameters({
|
|
213
|
-
first: component(z.object({ tree: StringTree })),
|
|
214
|
-
second: component(z.object({ tree: NumberTree })),
|
|
215
|
-
});
|
|
216
|
-
expect(Object.keys(schema.$defs!)).toEqual([
|
|
217
|
-
"node",
|
|
218
|
-
"children",
|
|
219
|
-
"component0",
|
|
220
|
-
]);
|
|
221
|
-
expect(warn).toHaveBeenCalledOnce();
|
|
222
|
-
} finally {
|
|
223
|
-
warn.mockRestore();
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("leaves literal reference-shaped data and non-recursive schemas unchanged", () => {
|
|
228
|
-
const literal = {
|
|
229
|
-
$ref: "#/literal",
|
|
230
|
-
properties: { nested: { $ref: "#" } },
|
|
231
|
-
};
|
|
232
|
-
const schema = buildPresentParameters({
|
|
233
|
-
Card: component(
|
|
234
|
-
z.object({
|
|
235
|
-
label: z.string().meta({ examples: [literal], default: literal }),
|
|
236
|
-
}),
|
|
237
|
-
),
|
|
238
|
-
});
|
|
239
|
-
expect(schema.properties!.label).toEqual({
|
|
240
|
-
type: "string",
|
|
241
|
-
examples: [literal],
|
|
242
|
-
default: literal,
|
|
243
|
-
});
|
|
244
|
-
expect(Object.keys(schema.$defs!)).toEqual(["node", "children"]);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { toJSONSchema } from "assistant-stream";
|
|
2
|
-
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
3
|
-
import { TYPE_KEY } from "./constants";
|
|
4
|
-
import type { GenerativeUILibrary } from "./types";
|
|
5
|
-
import { scopeSchema } from "./scopeSchema";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Builds the JSON schema for the `present` tool from a {@link GenerativeUILibrary}.
|
|
9
|
-
*
|
|
10
|
-
* The model produces a node `{ $type, $key?, ...props }` where `$type` selects
|
|
11
|
-
* a component, the optional `$key` pins a stable identity for list items that
|
|
12
|
-
* may reorder, and the rest are its props. The schema is a flat object: `$type`
|
|
13
|
-
* is an enum of the component names, every component's props are merged into
|
|
14
|
-
* one optional bag, and `children` recurses via `$defs` so the tree can nest.
|
|
15
|
-
*
|
|
16
|
-
* It is intentionally flat rather than a per-`$type` discriminated union. Tool /
|
|
17
|
-
* function-call schemas (OpenAI and others) require the top-level parameters to
|
|
18
|
-
* be a plain object and reject a top-level `oneOf`/`anyOf`/`enum`. So props can't
|
|
19
|
-
* be refined per `$type` at the root; the model is guided instead by `$type`'s
|
|
20
|
-
* description (which lists each component) and each prop's own description. The
|
|
21
|
-
* renderer validates nothing here — an unknown `$type` or stray prop is handled
|
|
22
|
-
* at render time — so a looser schema only costs the model a hint, not safety.
|
|
23
|
-
*/
|
|
24
|
-
export function buildPresentParameters(
|
|
25
|
-
library: GenerativeUILibrary,
|
|
26
|
-
): JSONSchema7 {
|
|
27
|
-
const names = Object.keys(library);
|
|
28
|
-
|
|
29
|
-
// Merge every component's props into one optional bag. `$`-prefixed keys and
|
|
30
|
-
// `children` are framework-reserved (see ir.ts), so drop any author-declared
|
|
31
|
-
// copies. On a name clash the first component's schema wins — props are an
|
|
32
|
-
// advisory hint here, not a strict per-component contract.
|
|
33
|
-
const props = new Map<string, JSONSchema7Definition>();
|
|
34
|
-
const propOwners = new Map<string, string[]>();
|
|
35
|
-
const componentSchemas: Record<string, JSONSchema7> = {};
|
|
36
|
-
for (const [index, name] of names.entries()) {
|
|
37
|
-
const definition = `component${index}`;
|
|
38
|
-
const { schema: propsSchema, referenced } = scopeSchema(
|
|
39
|
-
toJSONSchema(library[name]!.properties),
|
|
40
|
-
`#/$defs/${definition}`,
|
|
41
|
-
);
|
|
42
|
-
if (propsSchema.type !== "object") {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`[@assistant-ui/react-generative-ui] Component "${name}": ` +
|
|
45
|
-
"`properties` must be an object schema (e.g. `z.object({ ... })`).",
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
let merged = false;
|
|
49
|
-
for (const [key, schema] of Object.entries(propsSchema.properties ?? {})) {
|
|
50
|
-
if (key.startsWith("$") || key === "children") continue;
|
|
51
|
-
// secure-json-parse rejects the whole tool-argument payload on this key,
|
|
52
|
-
// so advertising it would cost the model the node rather than one prop.
|
|
53
|
-
if (key === "__proto__") continue;
|
|
54
|
-
if (!props.has(key)) {
|
|
55
|
-
props.set(key, schema);
|
|
56
|
-
merged = true;
|
|
57
|
-
}
|
|
58
|
-
propOwners.set(key, [...(propOwners.get(key) ?? []), name]);
|
|
59
|
-
}
|
|
60
|
-
if (referenced && merged) {
|
|
61
|
-
// `$schema` is only valid at a schema-resource root, and an embedded
|
|
62
|
-
// `$id` would both collide across components and re-base the pointers
|
|
63
|
-
// scopeSchema just rewrote to be document-root relative.
|
|
64
|
-
const { $schema: _, $id: _id, ...embedded } = propsSchema;
|
|
65
|
-
componentSchemas[definition] = embedded;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (process.env["NODE_ENV"] !== "production") {
|
|
70
|
-
for (const [key, owners] of propOwners) {
|
|
71
|
-
if (owners.length < 2) continue;
|
|
72
|
-
// eslint-disable-next-line no-console
|
|
73
|
-
console.warn(
|
|
74
|
-
`[@assistant-ui/react-generative-ui] Prop "${key}" is declared by ` +
|
|
75
|
-
`${formatComponentList(owners)}; keeping "${owners[0]}"'s schema. ` +
|
|
76
|
-
"Rename or align the prop type to avoid an ambiguous schema.",
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Carry each component's description on the `$type` enum, since there are no
|
|
82
|
-
// per-branch schemas to hang them on anymore.
|
|
83
|
-
const typeDescription =
|
|
84
|
-
names.length > 0
|
|
85
|
-
? `The component to render. ${names
|
|
86
|
-
.map((name) => `"${name}": ${library[name]!.description}`)
|
|
87
|
-
.join("; ")}`
|
|
88
|
-
: "The component to render.";
|
|
89
|
-
|
|
90
|
-
const node: JSONSchema7 = {
|
|
91
|
-
type: "object",
|
|
92
|
-
properties: {
|
|
93
|
-
[TYPE_KEY]: { type: "string", enum: names, description: typeDescription },
|
|
94
|
-
$key: {
|
|
95
|
-
description:
|
|
96
|
-
"Stable identity for this UI node. Use it for list items that may reorder.",
|
|
97
|
-
anyOf: [{ type: "string" }, { type: "number" }],
|
|
98
|
-
},
|
|
99
|
-
...Object.fromEntries(props),
|
|
100
|
-
children: { $ref: "#/$defs/children" },
|
|
101
|
-
},
|
|
102
|
-
required: [TYPE_KEY],
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const children: JSONSchema7 = {
|
|
106
|
-
description: "Nested generative UI rendered inside this component.",
|
|
107
|
-
anyOf: [
|
|
108
|
-
{ type: "string" },
|
|
109
|
-
{ $ref: "#/$defs/node" },
|
|
110
|
-
{
|
|
111
|
-
type: "array",
|
|
112
|
-
items: { anyOf: [{ type: "string" }, { $ref: "#/$defs/node" }] },
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
...node,
|
|
119
|
-
$defs: { node, children, ...componentSchemas },
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function formatComponentList(names: string[]) {
|
|
124
|
-
if (names.length <= 2) return names.map((name) => `"${name}"`).join(" and ");
|
|
125
|
-
|
|
126
|
-
return `${names
|
|
127
|
-
.slice(0, -1)
|
|
128
|
-
.map((name) => `"${name}"`)
|
|
129
|
-
.join(", ")}, and "${names[names.length - 1]}"`;
|
|
130
|
-
}
|
package/src/constants.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The wire key naming the component to render.
|
|
3
|
-
*
|
|
4
|
-
* The model emits nodes as flat objects `{ [TYPE_KEY]: name, ...props }`. We
|
|
5
|
-
* use `$type` rather than `type` so a component's own `type` prop (e.g. a
|
|
6
|
-
* button's `type="submit"`) flows through as an ordinary prop without
|
|
7
|
-
* colliding with the discriminator. It mirrors the `$type` discriminator
|
|
8
|
-
* convention used by JSON polymorphism elsewhere.
|
|
9
|
-
*/
|
|
10
|
-
export const TYPE_KEY = "$type";
|
|
11
|
-
|
|
12
|
-
export const GENERATED_NAME_ATTR = "data-aui-generated-name";
|
|
13
|
-
|
|
14
|
-
export const FIELD_NAME_ATTR = "data-aui-field-name";
|
|
15
|
-
export const FIELD_VALUE_ATTR = "data-aui-field-value";
|
|
16
|
-
|
|
17
|
-
export const CHECKBOX_GROUP_ATTR = "data-aui-checkbox-group";
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
CHILDREN_CAP,
|
|
4
|
-
MAX_ELEMENT_DEPTH,
|
|
5
|
-
NODE_BUDGET,
|
|
6
|
-
boundSpec,
|
|
7
|
-
clampReasonDetail,
|
|
8
|
-
type ClampReason,
|
|
9
|
-
} from "./boundSpec";
|
|
10
|
-
|
|
11
|
-
const walk = (spec: unknown) => {
|
|
12
|
-
const reasons: ClampReason[] = [];
|
|
13
|
-
const result = boundSpec(spec, (reason) => reasons.push(reason));
|
|
14
|
-
return { reasons, result };
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const nest = (levels: number): unknown =>
|
|
18
|
-
levels === 0
|
|
19
|
-
? { type: "Text" }
|
|
20
|
-
: { type: "Box", children: [nest(levels - 1)] };
|
|
21
|
-
|
|
22
|
-
describe("boundSpec", () => {
|
|
23
|
-
it("copies an object without children and passes primitives through", () => {
|
|
24
|
-
const leaf = { type: "Text", props: { value: "hi" } };
|
|
25
|
-
const { reasons, result } = walk(leaf);
|
|
26
|
-
|
|
27
|
-
expect({ reasons, result }).toEqual({ reasons: [], result: leaf });
|
|
28
|
-
expect(result).not.toBe(leaf);
|
|
29
|
-
expect(walk("text").result).toBe("text");
|
|
30
|
-
expect(walk(null).result).toBe(null);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("copies a node with children instead of mutating the caller's object", () => {
|
|
34
|
-
const spec = { type: "Box", children: [] };
|
|
35
|
-
const { result } = walk(spec);
|
|
36
|
-
|
|
37
|
-
expect(result).toEqual(spec);
|
|
38
|
-
expect(result).not.toBe(spec);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("clamps any level wider than the children cap", () => {
|
|
42
|
-
const { reasons, result } = walk(Array(CHILDREN_CAP + 1).fill("x"));
|
|
43
|
-
|
|
44
|
-
expect(reasons).toEqual(["children"]);
|
|
45
|
-
expect(result as unknown[]).toHaveLength(CHILDREN_CAP);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("bounds an array whose reported length is fabricated", () => {
|
|
49
|
-
const hostile = new Proxy(["a", "b"], {
|
|
50
|
-
get: (target, prop, receiver) =>
|
|
51
|
-
prop === "length"
|
|
52
|
-
? Number.MAX_SAFE_INTEGER
|
|
53
|
-
: Reflect.get(target, prop, receiver),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const { reasons, result } = walk(hostile);
|
|
57
|
-
|
|
58
|
-
expect(reasons).toEqual(["children"]);
|
|
59
|
-
expect(result as unknown[]).toHaveLength(CHILDREN_CAP);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("bounds an array that hijacks its own slice", () => {
|
|
63
|
-
const hostile = new Proxy(["a", "b"], {
|
|
64
|
-
get: (target, prop, receiver) =>
|
|
65
|
-
prop === "slice"
|
|
66
|
-
? () => Array(CHILDREN_CAP * 2).fill("x")
|
|
67
|
-
: Reflect.get(target, prop, receiver),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(walk(hostile).result).toEqual(["a", "b"]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("bounds an array that redirects Symbol.species to a foreign object", () => {
|
|
74
|
-
function HostileCtor() {
|
|
75
|
-
return { map: () => Array(500).fill({ type: "Text" }), length: 0 };
|
|
76
|
-
}
|
|
77
|
-
const constructor = { [Symbol.species]: HostileCtor };
|
|
78
|
-
const hostile = new Proxy([{ type: "Text" }, { type: "Text" }], {
|
|
79
|
-
get: (target, prop, receiver) =>
|
|
80
|
-
prop === "constructor"
|
|
81
|
-
? constructor
|
|
82
|
-
: Reflect.get(target, prop, receiver),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const { reasons, result } = walk(hostile);
|
|
86
|
-
|
|
87
|
-
expect(reasons).toEqual([]);
|
|
88
|
-
expect(result).toEqual([{ type: "Text" }, { type: "Text" }]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("bounds a record that hides its children behind a has trap", () => {
|
|
92
|
-
const hostileChildren = new Proxy([{ type: "Text" }], {
|
|
93
|
-
get: (target, prop, receiver) =>
|
|
94
|
-
prop === "length"
|
|
95
|
-
? Number.MAX_SAFE_INTEGER
|
|
96
|
-
: Reflect.get(target, prop, receiver),
|
|
97
|
-
});
|
|
98
|
-
const hostile = new Proxy(
|
|
99
|
-
{ type: "Box", children: hostileChildren } as Record<string, unknown>,
|
|
100
|
-
{
|
|
101
|
-
has: (target, prop) => prop !== "children" && Reflect.has(target, prop),
|
|
102
|
-
},
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
const { reasons, result } = walk(hostile);
|
|
106
|
-
|
|
107
|
-
expect(reasons).toEqual(["children"]);
|
|
108
|
-
expect((result as { children: unknown[] }).children).toHaveLength(
|
|
109
|
-
CHILDREN_CAP,
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("bounds a record that answers a later read of children differently", () => {
|
|
114
|
-
let reads = 0;
|
|
115
|
-
const hostile = new Proxy(
|
|
116
|
-
{ type: "Box", children: undefined } as Record<string, unknown>,
|
|
117
|
-
{
|
|
118
|
-
get: (target, prop, receiver) => {
|
|
119
|
-
if (prop !== "children") return Reflect.get(target, prop, receiver);
|
|
120
|
-
reads += 1;
|
|
121
|
-
return reads === 1
|
|
122
|
-
? undefined
|
|
123
|
-
: Array(CHILDREN_CAP * 2).fill({ type: "Text" });
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const { reasons, result } = walk(hostile);
|
|
129
|
-
|
|
130
|
-
expect(reasons).toEqual([]);
|
|
131
|
-
expect((result as { children: unknown }).children).toBeUndefined();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("accepts nesting exactly at the element-depth ceiling", () => {
|
|
135
|
-
expect(walk(nest(MAX_ELEMENT_DEPTH)).reasons).toEqual([]);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("drops nodes one level past the element-depth ceiling", () => {
|
|
139
|
-
const { reasons, result } = walk(nest(MAX_ELEMENT_DEPTH + 1));
|
|
140
|
-
|
|
141
|
-
expect(reasons).toEqual(["depth"]);
|
|
142
|
-
expect(JSON.stringify(result)).toContain('"children":null');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("spends a single budget across shared references and reports it once", () => {
|
|
146
|
-
let spec: unknown = { type: "Text" };
|
|
147
|
-
for (let level = 0; level < 20; level += 1) {
|
|
148
|
-
spec = { type: "Box", children: [spec, spec] };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
expect(walk(spec).reasons).toEqual(["budget"]);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("cuts a node that is its own ancestor", () => {
|
|
155
|
-
const cyclic: Record<string, unknown> = { type: "Box" };
|
|
156
|
-
cyclic["children"] = [cyclic];
|
|
157
|
-
|
|
158
|
-
const { reasons, result } = walk(cyclic);
|
|
159
|
-
|
|
160
|
-
expect(reasons).toEqual(["cycle"]);
|
|
161
|
-
expect(result).toEqual({ type: "Box", children: [null] });
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("keeps a repeated sibling that is not an ancestor", () => {
|
|
165
|
-
const shared = { type: "Box", children: [] };
|
|
166
|
-
|
|
167
|
-
const { reasons, result } = walk({
|
|
168
|
-
type: "Row",
|
|
169
|
-
children: [shared, shared],
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
expect(reasons).toEqual([]);
|
|
173
|
-
expect(result).toEqual({ type: "Row", children: [shared, shared] });
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe("clampReasonDetail", () => {
|
|
178
|
-
it("renders one detail per reason", () => {
|
|
179
|
-
expect(clampReasonDetail("budget")).toBe(
|
|
180
|
-
`the tree was truncated after ${NODE_BUDGET} nodes.`,
|
|
181
|
-
);
|
|
182
|
-
expect(clampReasonDetail("cycle")).toBe(
|
|
183
|
-
"a self-referencing node was dropped.",
|
|
184
|
-
);
|
|
185
|
-
expect(clampReasonDetail("depth")).toBe(
|
|
186
|
-
`nodes deeper than ${MAX_ELEMENT_DEPTH} levels were dropped.`,
|
|
187
|
-
);
|
|
188
|
-
expect(clampReasonDetail("children")).toBe(
|
|
189
|
-
`children were clamped to ${CHILDREN_CAP} entries.`,
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
});
|