@visual-platform/contracts 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-sources.d.ts +206 -0
- package/dist/app-sources.d.ts.map +1 -0
- package/dist/app-sources.js +287 -0
- package/dist/app-sources.js.map +1 -0
- package/dist/component.d.ts +95 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +20 -1
- package/dist/component.js.map +1 -1
- package/dist/data.d.ts +55 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +17 -2
- package/dist/data.js.map +1 -1
- package/dist/design.d.ts +144 -0
- package/dist/design.d.ts.map +1 -1
- package/dist/design.js +9 -1
- package/dist/design.js.map +1 -1
- package/dist/events.d.ts +40 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +17 -0
- package/dist/events.js.map +1 -1
- package/dist/flow-sim.d.ts +36 -0
- package/dist/flow-sim.d.ts.map +1 -0
- package/dist/flow-sim.js +128 -0
- package/dist/flow-sim.js.map +1 -0
- package/dist/flows.d.ts +267 -0
- package/dist/flows.d.ts.map +1 -0
- package/dist/flows.js +387 -0
- package/dist/flows.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/motion.d.ts +161 -0
- package/dist/motion.d.ts.map +1 -0
- package/dist/motion.js +228 -0
- package/dist/motion.js.map +1 -0
- package/dist/page-document.d.ts +128 -0
- package/dist/page-document.d.ts.map +1 -1
- package/dist/page-document.js +12 -1
- package/dist/page-document.js.map +1 -1
- package/dist/published.d.ts +365 -0
- package/dist/published.d.ts.map +1 -1
- package/dist/published.js +5 -0
- package/dist/published.js.map +1 -1
- package/dist/sources.d.ts +170 -0
- package/dist/sources.d.ts.map +1 -0
- package/dist/sources.js +236 -0
- package/dist/sources.js.map +1 -0
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +2 -1
- package/dist/tables.js.map +1 -1
- package/package.json +1 -1
package/dist/flows.d.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ComponentNode } from "./page-document.js";
|
|
3
|
+
/**
|
|
4
|
+
* Flows (ADR-049, ADR-050): a named sequence of steps an event runs — call an app action, open or close
|
|
5
|
+
* a modal, go to a link (same or new tab), remember a value, branch, wait, scroll/focus a block, copy
|
|
6
|
+
* text, send an analytics event, refresh data, or run another flow with arguments. Steps are a closed
|
|
7
|
+
* set: never code.
|
|
8
|
+
*
|
|
9
|
+
* Where flows live (name lookup in this order): the page (root.props.flows) → components on it (flows)
|
|
10
|
+
* → the project's flow library (artifact type "flow", versioned, pinned by each release).
|
|
11
|
+
* A block's event binds a flow by name, like a handler: node.events.click = "onCheckout".
|
|
12
|
+
*/
|
|
13
|
+
export declare const FLOW_NAME: RegExp;
|
|
14
|
+
/** Roots the browser knows while a flow runs; other paths are read on the server at render. */
|
|
15
|
+
export declare const FLOW_CLIENT_ROOTS: readonly ["event", "state", "steps", "args"];
|
|
16
|
+
export declare const MAX_FLOW_DEPTH = 5;
|
|
17
|
+
export declare const MAX_WAIT_MS = 30000;
|
|
18
|
+
export declare const ValueRefSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
19
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
|
|
20
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
21
|
+
path: z.ZodString;
|
|
22
|
+
}, z.core.$strict>]>;
|
|
23
|
+
export type ValueRef = z.infer<typeof ValueRefSchema>;
|
|
24
|
+
export type FlowStep = {
|
|
25
|
+
kind: "action";
|
|
26
|
+
action: string;
|
|
27
|
+
input?: Record<string, ValueRef> | undefined;
|
|
28
|
+
as?: string | undefined;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "flow";
|
|
31
|
+
flow: string;
|
|
32
|
+
args?: Record<string, ValueRef> | undefined;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "open";
|
|
35
|
+
modal: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "close";
|
|
38
|
+
modal?: string | undefined;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "navigate";
|
|
41
|
+
href?: string | undefined;
|
|
42
|
+
hrefFrom?: ValueRef | undefined;
|
|
43
|
+
newTab?: boolean | undefined;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "toast";
|
|
46
|
+
text: string;
|
|
47
|
+
tone?: "info" | "success" | "error" | undefined;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "set";
|
|
50
|
+
key: string;
|
|
51
|
+
value?: ValueRef | undefined;
|
|
52
|
+
op?: "set" | "increment" | "decrement" | "toggle" | undefined;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "if";
|
|
55
|
+
when: string;
|
|
56
|
+
then: FlowStep[];
|
|
57
|
+
else?: FlowStep[] | undefined;
|
|
58
|
+
} | {
|
|
59
|
+
kind: "wait";
|
|
60
|
+
ms: number;
|
|
61
|
+
} | {
|
|
62
|
+
kind: "scroll";
|
|
63
|
+
target: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: "focus";
|
|
66
|
+
target: string;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "copy";
|
|
69
|
+
value: ValueRef;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "track";
|
|
72
|
+
event: string;
|
|
73
|
+
props?: Record<string, ValueRef> | undefined;
|
|
74
|
+
} | {
|
|
75
|
+
kind: "animate";
|
|
76
|
+
target: string;
|
|
77
|
+
preset: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "refresh";
|
|
80
|
+
} | {
|
|
81
|
+
kind: "stop";
|
|
82
|
+
};
|
|
83
|
+
export type FlowStepKind = FlowStep["kind"];
|
|
84
|
+
export declare const FlowStepSchema: z.ZodType<FlowStep>;
|
|
85
|
+
export declare const FLOW_PARAM_TYPES: readonly ["text", "number", "boolean"];
|
|
86
|
+
export declare const FlowParamSchema: z.ZodObject<{
|
|
87
|
+
name: z.ZodString;
|
|
88
|
+
label: z.ZodOptional<z.ZodString>;
|
|
89
|
+
type: z.ZodEnum<{
|
|
90
|
+
boolean: "boolean";
|
|
91
|
+
number: "number";
|
|
92
|
+
text: "text";
|
|
93
|
+
}>;
|
|
94
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
95
|
+
}, z.core.$strict>;
|
|
96
|
+
export type FlowParam = z.infer<typeof FlowParamSchema>;
|
|
97
|
+
export type Flow = {
|
|
98
|
+
name: string;
|
|
99
|
+
label?: string | undefined;
|
|
100
|
+
description?: string | undefined;
|
|
101
|
+
params?: FlowParam[] | undefined;
|
|
102
|
+
steps: FlowStep[];
|
|
103
|
+
onError?: FlowStep[] | undefined;
|
|
104
|
+
};
|
|
105
|
+
export declare const FlowSchema: z.ZodObject<{
|
|
106
|
+
name: z.ZodString;
|
|
107
|
+
label: z.ZodOptional<z.ZodString>;
|
|
108
|
+
description: z.ZodOptional<z.ZodString>;
|
|
109
|
+
params: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
110
|
+
name: z.ZodString;
|
|
111
|
+
label: z.ZodOptional<z.ZodString>;
|
|
112
|
+
type: z.ZodEnum<{
|
|
113
|
+
boolean: "boolean";
|
|
114
|
+
number: "number";
|
|
115
|
+
text: "text";
|
|
116
|
+
}>;
|
|
117
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
118
|
+
}, z.core.$strict>>>;
|
|
119
|
+
steps: z.ZodArray<z.ZodType<FlowStep, unknown, z.core.$ZodTypeInternals<FlowStep, unknown>>>;
|
|
120
|
+
onError: z.ZodOptional<z.ZodArray<z.ZodType<FlowStep, unknown, z.core.$ZodTypeInternals<FlowStep, unknown>>>>;
|
|
121
|
+
}, z.core.$strict>;
|
|
122
|
+
/** Every step, nested ones included. */
|
|
123
|
+
export declare function walkSteps(steps: readonly FlowStep[] | undefined, fn: (s: FlowStep, depth: number) => void, depth?: number): void;
|
|
124
|
+
/** Names of the flows a flow calls (sub-flows). */
|
|
125
|
+
export declare function flowCalls(f: Flow): string[];
|
|
126
|
+
/** Sub-flow cycle (a → b → a) as a path, or null. */
|
|
127
|
+
export declare function findFlowCycle(flows: readonly Flow[]): string[] | null;
|
|
128
|
+
/** Sub-flows called but not defined in this set (they must come from the project's flow library). */
|
|
129
|
+
export declare function externalFlowCalls(flows: readonly Flow[]): string[];
|
|
130
|
+
/** Flows of a page/component. Sub-flows not defined here are looked up in the flow library at publish. */
|
|
131
|
+
export declare const FlowsSchema: z.ZodArray<z.ZodObject<{
|
|
132
|
+
name: z.ZodString;
|
|
133
|
+
label: z.ZodOptional<z.ZodString>;
|
|
134
|
+
description: z.ZodOptional<z.ZodString>;
|
|
135
|
+
params: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
136
|
+
name: z.ZodString;
|
|
137
|
+
label: z.ZodOptional<z.ZodString>;
|
|
138
|
+
type: z.ZodEnum<{
|
|
139
|
+
boolean: "boolean";
|
|
140
|
+
number: "number";
|
|
141
|
+
text: "text";
|
|
142
|
+
}>;
|
|
143
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
144
|
+
}, z.core.$strict>>>;
|
|
145
|
+
steps: z.ZodArray<z.ZodType<FlowStep, unknown, z.core.$ZodTypeInternals<FlowStep, unknown>>>;
|
|
146
|
+
onError: z.ZodOptional<z.ZodArray<z.ZodType<FlowStep, unknown, z.core.$ZodTypeInternals<FlowStep, unknown>>>>;
|
|
147
|
+
}, z.core.$strict>>;
|
|
148
|
+
/**
|
|
149
|
+
* Page triggers (ADR-050/051): run a flow (or handler) when the page loads (optionally later), when the
|
|
150
|
+
* visitor is about to leave (mouse exits at the top), scrolls past a percentage, stays idle, every N
|
|
151
|
+
* seconds, when the tab is hidden/shown again, or on leaving the page (send analytics only there).
|
|
152
|
+
*/
|
|
153
|
+
export declare const PAGE_TRIGGERS: readonly ["load", "exitIntent", "scroll", "idle", "interval", "tabHidden", "tabVisible", "leave"];
|
|
154
|
+
export type PageTriggerKind = (typeof PAGE_TRIGGERS)[number];
|
|
155
|
+
export declare const FlowTriggerSchema: z.ZodObject<{
|
|
156
|
+
on: z.ZodEnum<{
|
|
157
|
+
exitIntent: "exitIntent";
|
|
158
|
+
idle: "idle";
|
|
159
|
+
interval: "interval";
|
|
160
|
+
leave: "leave";
|
|
161
|
+
load: "load";
|
|
162
|
+
scroll: "scroll";
|
|
163
|
+
tabHidden: "tabHidden";
|
|
164
|
+
tabVisible: "tabVisible";
|
|
165
|
+
}>;
|
|
166
|
+
flow: z.ZodString;
|
|
167
|
+
delayMs: z.ZodOptional<z.ZodNumber>;
|
|
168
|
+
percent: z.ZodOptional<z.ZodNumber>;
|
|
169
|
+
ms: z.ZodOptional<z.ZodNumber>;
|
|
170
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
171
|
+
once: z.ZodOptional<z.ZodBoolean>;
|
|
172
|
+
}, z.core.$strict>;
|
|
173
|
+
export type FlowTrigger = z.infer<typeof FlowTriggerSchema>;
|
|
174
|
+
export declare const FlowTriggersSchema: z.ZodArray<z.ZodObject<{
|
|
175
|
+
on: z.ZodEnum<{
|
|
176
|
+
exitIntent: "exitIntent";
|
|
177
|
+
idle: "idle";
|
|
178
|
+
interval: "interval";
|
|
179
|
+
leave: "leave";
|
|
180
|
+
load: "load";
|
|
181
|
+
scroll: "scroll";
|
|
182
|
+
tabHidden: "tabHidden";
|
|
183
|
+
tabVisible: "tabVisible";
|
|
184
|
+
}>;
|
|
185
|
+
flow: z.ZodString;
|
|
186
|
+
delayMs: z.ZodOptional<z.ZodNumber>;
|
|
187
|
+
percent: z.ZodOptional<z.ZodNumber>;
|
|
188
|
+
ms: z.ZodOptional<z.ZodNumber>;
|
|
189
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
190
|
+
once: z.ZodOptional<z.ZodBoolean>;
|
|
191
|
+
}, z.core.$strict>>;
|
|
192
|
+
/**
|
|
193
|
+
* Block lifecycle (ADR-051): every block can run a flow (or handler) when it appears in the browser
|
|
194
|
+
* (mount), disappears (unmount — a state gate hides it, the page changes), scrolls into view (visible,
|
|
195
|
+
* once per view unless visibleOnce = false), scrolls out (leave), or every N seconds while shown (every).
|
|
196
|
+
*/
|
|
197
|
+
export declare const LIFECYCLE_KINDS: readonly ["mount", "unmount", "visible", "leave"];
|
|
198
|
+
export type LifecycleKind = (typeof LIFECYCLE_KINDS)[number] | "every";
|
|
199
|
+
export declare const NodeLifecycleSchema: z.ZodObject<{
|
|
200
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
201
|
+
unmount: z.ZodOptional<z.ZodString>;
|
|
202
|
+
visible: z.ZodOptional<z.ZodString>;
|
|
203
|
+
leave: z.ZodOptional<z.ZodString>;
|
|
204
|
+
visibleOnce: z.ZodOptional<z.ZodBoolean>;
|
|
205
|
+
every: z.ZodOptional<z.ZodObject<{
|
|
206
|
+
run: z.ZodString;
|
|
207
|
+
ms: z.ZodNumber;
|
|
208
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
209
|
+
}, z.core.$strict>>;
|
|
210
|
+
}, z.core.$strict>;
|
|
211
|
+
export type NodeLifecycle = z.infer<typeof NodeLifecycleSchema>;
|
|
212
|
+
/** Names a block's lifecycle runs. */
|
|
213
|
+
export declare const lifecycleNames: (l: NodeLifecycle | undefined) => string[];
|
|
214
|
+
/** Starting values of the page state (state.*): wizard step, quantity… */
|
|
215
|
+
export declare const FlowStateSchema: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>;
|
|
216
|
+
/** A flow in the project library (artifact type "flow"): one flow + schema version. */
|
|
217
|
+
export declare const FlowDocumentSchema: z.ZodObject<{
|
|
218
|
+
name: z.ZodString;
|
|
219
|
+
label: z.ZodOptional<z.ZodString>;
|
|
220
|
+
description: z.ZodOptional<z.ZodString>;
|
|
221
|
+
params: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
222
|
+
name: z.ZodString;
|
|
223
|
+
label: z.ZodOptional<z.ZodString>;
|
|
224
|
+
type: z.ZodEnum<{
|
|
225
|
+
boolean: "boolean";
|
|
226
|
+
number: "number";
|
|
227
|
+
text: "text";
|
|
228
|
+
}>;
|
|
229
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
230
|
+
}, z.core.$strict>>>;
|
|
231
|
+
steps: z.ZodArray<z.ZodType<FlowStep, unknown, z.core.$ZodTypeInternals<FlowStep, unknown>>>;
|
|
232
|
+
onError: z.ZodOptional<z.ZodArray<z.ZodType<FlowStep, unknown, z.core.$ZodTypeInternals<FlowStep, unknown>>>>;
|
|
233
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
234
|
+
}, z.core.$strict>;
|
|
235
|
+
export type FlowDocument = z.infer<typeof FlowDocumentSchema>;
|
|
236
|
+
export declare const flowFromDocument: ({ schemaVersion: _v, ...f }: FlowDocument) => Flow;
|
|
237
|
+
export declare function filterFields(expr: string): string[];
|
|
238
|
+
/**
|
|
239
|
+
* Paths a flow (and its sub-flows) reads that the browser does not know — item.*, page data… The server
|
|
240
|
+
* reads them from the block's data at render and hands them to the event boundary.
|
|
241
|
+
*/
|
|
242
|
+
export declare function flowServerPaths(name: string, flows: readonly Flow[]): string[];
|
|
243
|
+
/** Does a showIf read the page state? Such blocks are shown/hidden in the browser (ADR-050). */
|
|
244
|
+
export declare const showIfUsesState: (expr: string | undefined) => boolean;
|
|
245
|
+
/** Non-state paths of a state showIf (read on the server, passed to the browser gate). */
|
|
246
|
+
export declare const showIfServerPaths: (expr: string) => string[];
|
|
247
|
+
/** Modal names opened/closed by flows (editor checks that a Modal with that name exists). */
|
|
248
|
+
export declare function flowModals(flows: readonly Flow[]): string[];
|
|
249
|
+
/** App action names flows call (publish/editor can check them against the app registry). */
|
|
250
|
+
export declare function flowActions(flows: readonly Flow[]): string[];
|
|
251
|
+
/**
|
|
252
|
+
* Flow names a document needs: event names of its blocks, triggers, and sub-flows they call, followed
|
|
253
|
+
* through the given flows (local + library). Used by delivery to ship only the flows a page uses.
|
|
254
|
+
*/
|
|
255
|
+
export declare function neededFlowNames(nodes: readonly ComponentNode[], available: readonly Flow[], extra?: readonly string[]): string[];
|
|
256
|
+
/** A readable sentence for one step (not translated; the editor shows it as the collapsed summary). */
|
|
257
|
+
export declare function describeStep(s: FlowStep): string;
|
|
258
|
+
/** The whole flow as indented lines (then/else nested), used to compare versions. */
|
|
259
|
+
export declare function describeFlow(f: Pick<Flow, "steps" | "onError" | "params" | "label">): string[];
|
|
260
|
+
/** Line diff (LCS) between two describeFlow outputs: unchanged / added / removed lines in order. */
|
|
261
|
+
export declare function diffLines(a: readonly string[], b: readonly string[]): {
|
|
262
|
+
kind: "same" | "add" | "del";
|
|
263
|
+
text: string;
|
|
264
|
+
}[];
|
|
265
|
+
/** Animation presets used by "animate" steps (the runtime needs their keyframes in the browser). */
|
|
266
|
+
export declare function flowPresets(flows: readonly Flow[]): string[];
|
|
267
|
+
//# sourceMappingURL=flows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../src/flows.ts"],"names":[],"mappings":"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"}
|
package/dist/flows.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { parseFilter } from "./tables.js";
|
|
3
|
+
import { walkNodes } from "./page-document.js";
|
|
4
|
+
/**
|
|
5
|
+
* Flows (ADR-049, ADR-050): a named sequence of steps an event runs — call an app action, open or close
|
|
6
|
+
* a modal, go to a link (same or new tab), remember a value, branch, wait, scroll/focus a block, copy
|
|
7
|
+
* text, send an analytics event, refresh data, or run another flow with arguments. Steps are a closed
|
|
8
|
+
* set: never code.
|
|
9
|
+
*
|
|
10
|
+
* Where flows live (name lookup in this order): the page (root.props.flows) → components on it (flows)
|
|
11
|
+
* → the project's flow library (artifact type "flow", versioned, pinned by each release).
|
|
12
|
+
* A block's event binds a flow by name, like a handler: node.events.click = "onCheckout".
|
|
13
|
+
*/
|
|
14
|
+
export const FLOW_NAME = /^on[A-Z][A-Za-z0-9]{0,47}$/;
|
|
15
|
+
const KEY = /^[A-Za-z_][A-Za-z0-9_]{0,39}$/;
|
|
16
|
+
/** event.value · item.id · steps.order.data.id · state.coupon · args.productId · products.0.name */
|
|
17
|
+
const PATH = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+){0,9}$/;
|
|
18
|
+
const SAFE_HREF = /^(\/(?!\/)|https?:\/\/|mailto:|tel:)[^\s"'<>]*$/;
|
|
19
|
+
const NODE_ID = /^n_[A-Za-z0-9_-]{3,80}$/;
|
|
20
|
+
/** Roots the browser knows while a flow runs; other paths are read on the server at render. */
|
|
21
|
+
export const FLOW_CLIENT_ROOTS = ["event", "state", "steps", "args"];
|
|
22
|
+
export const MAX_FLOW_DEPTH = 5;
|
|
23
|
+
export const MAX_WAIT_MS = 30_000;
|
|
24
|
+
const Literal = z.union([z.string().max(500), z.number(), z.boolean(), z.null()]);
|
|
25
|
+
export const ValueRefSchema = z.union([
|
|
26
|
+
z.object({ value: Literal }).strict(),
|
|
27
|
+
z.object({ path: z.string().max(200).regex(PATH, "paths look like item.id or event.value") }).strict(),
|
|
28
|
+
]);
|
|
29
|
+
const checkFilter = (s) => { try {
|
|
30
|
+
parseFilter(s);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
} };
|
|
36
|
+
const refs = z.record(z.string().regex(KEY), ValueRefSchema).refine((r) => Object.keys(r).length <= 20, "at most 20 values");
|
|
37
|
+
export const FlowStepSchema = z.lazy(() => z.discriminatedUnion("kind", [
|
|
38
|
+
z.object({ kind: z.literal("action"), action: z.string().regex(FLOW_NAME, "action names look like onAddToCart"), input: refs.optional(), as: z.string().regex(KEY).optional() }).strict(),
|
|
39
|
+
z.object({ kind: z.literal("flow"), flow: z.string().regex(FLOW_NAME, "flow names look like onCheckout"), args: refs.optional() }).strict(),
|
|
40
|
+
z.object({ kind: z.literal("open"), modal: z.string().regex(KEY, "modal names are letters/digits, e.g. checkout") }).strict(),
|
|
41
|
+
z.object({ kind: z.literal("close"), modal: z.string().regex(KEY).optional() }).strict(),
|
|
42
|
+
z.object({ kind: z.literal("navigate"), href: z.string().max(500).regex(SAFE_HREF, "links start with /, https://, mailto: or tel:").optional(), hrefFrom: ValueRefSchema.optional(), newTab: z.boolean().optional() }).strict()
|
|
43
|
+
.refine((s) => !!s.href !== !!s.hrefFrom, "give a link or where to read it from"),
|
|
44
|
+
z.object({ kind: z.literal("toast"), text: z.string().trim().min(1).max(200), tone: z.enum(["info", "success", "error"]).optional() }).strict(),
|
|
45
|
+
z.object({ kind: z.literal("set"), key: z.string().regex(KEY), value: ValueRefSchema.optional(), op: z.enum(["set", "increment", "decrement", "toggle"]).optional() }).strict()
|
|
46
|
+
.refine((s) => (s.op === "toggle" || s.op === "increment" || s.op === "decrement") || s.value !== undefined, "give a value"),
|
|
47
|
+
z.object({ kind: z.literal("if"), when: z.string().trim().min(1).max(500).refine(checkFilter, "condition syntax: like showIf, e.g. state.qty > 0"), then: z.array(FlowStepSchema).max(20), else: z.array(FlowStepSchema).max(20).optional() }).strict(),
|
|
48
|
+
z.object({ kind: z.literal("wait"), ms: z.number().int().min(50).max(MAX_WAIT_MS) }).strict(),
|
|
49
|
+
z.object({ kind: z.literal("scroll"), target: z.string().regex(NODE_ID, "pick a block") }).strict(),
|
|
50
|
+
z.object({ kind: z.literal("focus"), target: z.string().regex(NODE_ID, "pick a block") }).strict(),
|
|
51
|
+
z.object({ kind: z.literal("copy"), value: ValueRefSchema }).strict(),
|
|
52
|
+
z.object({ kind: z.literal("track"), event: z.string().regex(/^[A-Za-z][A-Za-z0-9_.:-]{0,59}$/, "analytics event names: letters, digits, _ . : -"), props: refs.optional() }).strict(),
|
|
53
|
+
z.object({ kind: z.literal("animate"), target: z.string().regex(NODE_ID, "pick a block"), preset: z.string().regex(/^[a-z][A-Za-z0-9]{0,39}$/, "pick an animation") }).strict(),
|
|
54
|
+
z.object({ kind: z.literal("refresh") }).strict(),
|
|
55
|
+
z.object({ kind: z.literal("stop") }).strict(),
|
|
56
|
+
]));
|
|
57
|
+
export const FLOW_PARAM_TYPES = ["text", "number", "boolean"];
|
|
58
|
+
export const FlowParamSchema = z.object({
|
|
59
|
+
name: z.string().regex(KEY),
|
|
60
|
+
label: z.string().max(60).optional(),
|
|
61
|
+
type: z.enum(FLOW_PARAM_TYPES),
|
|
62
|
+
required: z.boolean().optional(),
|
|
63
|
+
}).strict();
|
|
64
|
+
const flowShape = {
|
|
65
|
+
name: z.string().regex(FLOW_NAME, "flow names look like onCheckout"),
|
|
66
|
+
label: z.string().max(60).optional(),
|
|
67
|
+
description: z.string().max(300).optional(),
|
|
68
|
+
/** Arguments a caller passes (`flow` step args); read inside as args.<name>. */
|
|
69
|
+
params: z.array(FlowParamSchema).max(10).optional(),
|
|
70
|
+
steps: z.array(FlowStepSchema).min(1).max(30),
|
|
71
|
+
/** Runs when an action step fails (the flow then stops). */
|
|
72
|
+
onError: z.array(FlowStepSchema).max(10).optional(),
|
|
73
|
+
};
|
|
74
|
+
export const FlowSchema = z.object(flowShape).strict();
|
|
75
|
+
/** Every step, nested ones included. */
|
|
76
|
+
export function walkSteps(steps, fn, depth = 0) {
|
|
77
|
+
for (const s of steps ?? []) {
|
|
78
|
+
fn(s, depth);
|
|
79
|
+
if (s.kind === "if") {
|
|
80
|
+
walkSteps(s.then, fn, depth + 1);
|
|
81
|
+
walkSteps(s.else, fn, depth + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const allSteps = (f) => [...f.steps, ...(f.onError ?? [])];
|
|
86
|
+
/** Names of the flows a flow calls (sub-flows). */
|
|
87
|
+
export function flowCalls(f) {
|
|
88
|
+
const out = new Set();
|
|
89
|
+
walkSteps(allSteps(f), (s) => { if (s.kind === "flow")
|
|
90
|
+
out.add(s.flow); });
|
|
91
|
+
return [...out];
|
|
92
|
+
}
|
|
93
|
+
/** Sub-flow cycle (a → b → a) as a path, or null. */
|
|
94
|
+
export function findFlowCycle(flows) {
|
|
95
|
+
const calls = new Map(flows.map((f) => [f.name, flowCalls(f)]));
|
|
96
|
+
const state = new Map();
|
|
97
|
+
const stack = [];
|
|
98
|
+
const visit = (n) => {
|
|
99
|
+
if (state.get(n) === 2)
|
|
100
|
+
return null;
|
|
101
|
+
if (state.get(n) === 1)
|
|
102
|
+
return [...stack.slice(stack.indexOf(n)), n];
|
|
103
|
+
state.set(n, 1);
|
|
104
|
+
stack.push(n);
|
|
105
|
+
for (const c of calls.get(n) ?? [])
|
|
106
|
+
if (calls.has(c)) {
|
|
107
|
+
const r = visit(c);
|
|
108
|
+
if (r)
|
|
109
|
+
return r;
|
|
110
|
+
}
|
|
111
|
+
stack.pop();
|
|
112
|
+
state.set(n, 2);
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
for (const f of flows) {
|
|
116
|
+
const r = visit(f.name);
|
|
117
|
+
if (r)
|
|
118
|
+
return r;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/** Sub-flows called but not defined in this set (they must come from the project's flow library). */
|
|
123
|
+
export function externalFlowCalls(flows) {
|
|
124
|
+
const own = new Set(flows.map((f) => f.name));
|
|
125
|
+
return [...new Set(flows.flatMap(flowCalls))].filter((n) => !own.has(n)).sort();
|
|
126
|
+
}
|
|
127
|
+
function checkFlowSet(flows, ctx) {
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
for (const f of flows) {
|
|
130
|
+
if (seen.has(f.name))
|
|
131
|
+
ctx.addIssue({ code: "custom", message: `duplicate flow "${f.name}"` });
|
|
132
|
+
seen.add(f.name);
|
|
133
|
+
}
|
|
134
|
+
for (const f of flows) {
|
|
135
|
+
let total = 0;
|
|
136
|
+
let deep = 0;
|
|
137
|
+
walkSteps(allSteps(f), (_s, d) => { total++; deep = Math.max(deep, d); });
|
|
138
|
+
if (total > 80)
|
|
139
|
+
ctx.addIssue({ code: "custom", message: `flow "${f.name}" has more than 80 steps` });
|
|
140
|
+
if (deep > 3)
|
|
141
|
+
ctx.addIssue({ code: "custom", message: `flow "${f.name}": conditions nest at most 4 levels` });
|
|
142
|
+
}
|
|
143
|
+
const cycle = findFlowCycle(flows);
|
|
144
|
+
if (cycle)
|
|
145
|
+
ctx.addIssue({ code: "custom", message: `FLOW_CYCLE: flows call each other (${cycle.join(" → ")})` });
|
|
146
|
+
}
|
|
147
|
+
/** Flows of a page/component. Sub-flows not defined here are looked up in the flow library at publish. */
|
|
148
|
+
export const FlowsSchema = z.array(FlowSchema).max(30).superRefine((flows, ctx) => checkFlowSet(flows, ctx));
|
|
149
|
+
/**
|
|
150
|
+
* Page triggers (ADR-050/051): run a flow (or handler) when the page loads (optionally later), when the
|
|
151
|
+
* visitor is about to leave (mouse exits at the top), scrolls past a percentage, stays idle, every N
|
|
152
|
+
* seconds, when the tab is hidden/shown again, or on leaving the page (send analytics only there).
|
|
153
|
+
*/
|
|
154
|
+
export const PAGE_TRIGGERS = ["load", "exitIntent", "scroll", "idle", "interval", "tabHidden", "tabVisible", "leave"];
|
|
155
|
+
export const FlowTriggerSchema = z.object({
|
|
156
|
+
on: z.enum(PAGE_TRIGGERS),
|
|
157
|
+
flow: z.string().regex(FLOW_NAME),
|
|
158
|
+
/** load: wait before running. */
|
|
159
|
+
delayMs: z.number().int().min(0).max(120_000).optional(),
|
|
160
|
+
/** scroll: page depth reached (1–100 %). */
|
|
161
|
+
percent: z.number().int().min(1).max(100).optional(),
|
|
162
|
+
/** idle: no input for this long · interval: every this long. */
|
|
163
|
+
ms: z.number().int().min(1000).max(3_600_000).optional(),
|
|
164
|
+
/** interval: stop after this many runs. */
|
|
165
|
+
max: z.number().int().min(1).max(1000).optional(),
|
|
166
|
+
/** exitIntent / scroll / idle / tabHidden / tabVisible: only the first time per page view (default true). */
|
|
167
|
+
once: z.boolean().optional(),
|
|
168
|
+
}).strict().superRefine((t, ctx) => {
|
|
169
|
+
if ((t.on === "idle" || t.on === "interval") && !t.ms)
|
|
170
|
+
ctx.addIssue({ code: "custom", path: ["ms"], message: `${t.on} needs a duration` });
|
|
171
|
+
if (t.on === "scroll" && !t.percent)
|
|
172
|
+
ctx.addIssue({ code: "custom", path: ["percent"], message: "scroll needs a percentage" });
|
|
173
|
+
});
|
|
174
|
+
export const FlowTriggersSchema = z.array(FlowTriggerSchema).max(10);
|
|
175
|
+
/**
|
|
176
|
+
* Block lifecycle (ADR-051): every block can run a flow (or handler) when it appears in the browser
|
|
177
|
+
* (mount), disappears (unmount — a state gate hides it, the page changes), scrolls into view (visible,
|
|
178
|
+
* once per view unless visibleOnce = false), scrolls out (leave), or every N seconds while shown (every).
|
|
179
|
+
*/
|
|
180
|
+
export const LIFECYCLE_KINDS = ["mount", "unmount", "visible", "leave"];
|
|
181
|
+
export const NodeLifecycleSchema = z.object({
|
|
182
|
+
mount: z.string().regex(FLOW_NAME).optional(),
|
|
183
|
+
unmount: z.string().regex(FLOW_NAME).optional(),
|
|
184
|
+
visible: z.string().regex(FLOW_NAME).optional(),
|
|
185
|
+
leave: z.string().regex(FLOW_NAME).optional(),
|
|
186
|
+
visibleOnce: z.boolean().optional(),
|
|
187
|
+
every: z.object({ run: z.string().regex(FLOW_NAME), ms: z.number().int().min(1000).max(3_600_000), max: z.number().int().min(1).max(10_000).optional() }).strict().optional(),
|
|
188
|
+
}).strict().refine((l) => Object.keys(l).some((k) => k !== "visibleOnce"), "choose at least one moment");
|
|
189
|
+
/** Names a block's lifecycle runs. */
|
|
190
|
+
export const lifecycleNames = (l) => (l ? [l.mount, l.unmount, l.visible, l.leave, l.every?.run].filter((x) => !!x) : []);
|
|
191
|
+
/** Starting values of the page state (state.*): wizard step, quantity… */
|
|
192
|
+
export const FlowStateSchema = z.record(z.string().regex(KEY), Literal).refine((r) => Object.keys(r).length <= 30, "at most 30 state values");
|
|
193
|
+
/** A flow in the project library (artifact type "flow"): one flow + schema version. */
|
|
194
|
+
export const FlowDocumentSchema = z.object({ schemaVersion: z.literal(1), ...flowShape }).strict();
|
|
195
|
+
export const flowFromDocument = ({ schemaVersion: _v, ...f }) => f;
|
|
196
|
+
// ---------- analysis ----------
|
|
197
|
+
const refPaths = (r, out) => { if (r && "path" in r)
|
|
198
|
+
out.add(r.path); };
|
|
199
|
+
export function filterFields(expr) {
|
|
200
|
+
let f;
|
|
201
|
+
try {
|
|
202
|
+
f = parseFilter(expr);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const out = [];
|
|
208
|
+
const walk = (x) => { if (!x)
|
|
209
|
+
return; if ("and" in x)
|
|
210
|
+
x.and.forEach(walk);
|
|
211
|
+
else if ("or" in x)
|
|
212
|
+
x.or.forEach(walk);
|
|
213
|
+
else if ("not" in x)
|
|
214
|
+
walk(x.not);
|
|
215
|
+
else
|
|
216
|
+
out.push(x.field); };
|
|
217
|
+
walk(f);
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
const isClientPath = (p) => FLOW_CLIENT_ROOTS.includes(p.split(".")[0]);
|
|
221
|
+
/**
|
|
222
|
+
* Paths a flow (and its sub-flows) reads that the browser does not know — item.*, page data… The server
|
|
223
|
+
* reads them from the block's data at render and hands them to the event boundary.
|
|
224
|
+
*/
|
|
225
|
+
export function flowServerPaths(name, flows) {
|
|
226
|
+
const byName = new Map(flows.map((f) => [f.name, f]));
|
|
227
|
+
const out = new Set();
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
const visit = (n) => {
|
|
230
|
+
const f = byName.get(n);
|
|
231
|
+
if (!f || seen.has(n))
|
|
232
|
+
return;
|
|
233
|
+
seen.add(n);
|
|
234
|
+
walkSteps(allSteps(f), (s) => {
|
|
235
|
+
if (s.kind === "action")
|
|
236
|
+
for (const r of Object.values(s.input ?? {}))
|
|
237
|
+
refPaths(r, out);
|
|
238
|
+
else if (s.kind === "flow") {
|
|
239
|
+
for (const r of Object.values(s.args ?? {}))
|
|
240
|
+
refPaths(r, out);
|
|
241
|
+
visit(s.flow);
|
|
242
|
+
}
|
|
243
|
+
else if (s.kind === "set")
|
|
244
|
+
refPaths(s.value, out);
|
|
245
|
+
else if (s.kind === "navigate")
|
|
246
|
+
refPaths(s.hrefFrom, out);
|
|
247
|
+
else if (s.kind === "copy")
|
|
248
|
+
refPaths(s.value, out);
|
|
249
|
+
else if (s.kind === "track")
|
|
250
|
+
for (const r of Object.values(s.props ?? {}))
|
|
251
|
+
refPaths(r, out);
|
|
252
|
+
else if (s.kind === "if")
|
|
253
|
+
for (const p of filterFields(s.when))
|
|
254
|
+
out.add(p);
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
visit(name);
|
|
258
|
+
return [...out].filter((p) => !isClientPath(p)).sort();
|
|
259
|
+
}
|
|
260
|
+
/** Does a showIf read the page state? Such blocks are shown/hidden in the browser (ADR-050). */
|
|
261
|
+
export const showIfUsesState = (expr) => !!expr && filterFields(expr).some((p) => p.split(".")[0] === "state");
|
|
262
|
+
/** Non-state paths of a state showIf (read on the server, passed to the browser gate). */
|
|
263
|
+
export const showIfServerPaths = (expr) => filterFields(expr).filter((p) => !isClientPath(p));
|
|
264
|
+
/** Modal names opened/closed by flows (editor checks that a Modal with that name exists). */
|
|
265
|
+
export function flowModals(flows) {
|
|
266
|
+
const out = new Set();
|
|
267
|
+
for (const f of flows)
|
|
268
|
+
walkSteps(allSteps(f), (s) => { if ((s.kind === "open" || s.kind === "close") && s.modal)
|
|
269
|
+
out.add(s.modal); });
|
|
270
|
+
return [...out].sort();
|
|
271
|
+
}
|
|
272
|
+
/** App action names flows call (publish/editor can check them against the app registry). */
|
|
273
|
+
export function flowActions(flows) {
|
|
274
|
+
const out = new Set();
|
|
275
|
+
for (const f of flows)
|
|
276
|
+
walkSteps(allSteps(f), (s) => { if (s.kind === "action")
|
|
277
|
+
out.add(s.action); });
|
|
278
|
+
return [...out].sort();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Flow names a document needs: event names of its blocks, triggers, and sub-flows they call, followed
|
|
282
|
+
* through the given flows (local + library). Used by delivery to ship only the flows a page uses.
|
|
283
|
+
*/
|
|
284
|
+
export function neededFlowNames(nodes, available, extra = []) {
|
|
285
|
+
const byName = new Map(available.map((f) => [f.name, f]));
|
|
286
|
+
const out = new Set();
|
|
287
|
+
const visit = (n) => { if (out.has(n) || !byName.has(n))
|
|
288
|
+
return; out.add(n); for (const c of flowCalls(byName.get(n)))
|
|
289
|
+
visit(c); };
|
|
290
|
+
for (const n of walkNodes(nodes)) {
|
|
291
|
+
for (const e of Object.values(n.events ?? {}))
|
|
292
|
+
if (e)
|
|
293
|
+
visit(e);
|
|
294
|
+
for (const e of lifecycleNames(n.lifecycle))
|
|
295
|
+
visit(e);
|
|
296
|
+
}
|
|
297
|
+
for (const e of extra)
|
|
298
|
+
visit(e);
|
|
299
|
+
return [...out].sort();
|
|
300
|
+
}
|
|
301
|
+
// ---------- reading flows: one sentence per step (editor summaries, history compare, AI) ----------
|
|
302
|
+
const refText = (r) => (!r ? "—" : "path" in r ? r.path : JSON.stringify(r.value));
|
|
303
|
+
const mapText = (m) => Object.entries(m ?? {}).map(([k, v]) => `${k} = ${refText(v)}`).join(", ");
|
|
304
|
+
/** A readable sentence for one step (not translated; the editor shows it as the collapsed summary). */
|
|
305
|
+
export function describeStep(s) {
|
|
306
|
+
switch (s.kind) {
|
|
307
|
+
case "action": return `Run action ${s.action || "?"}${s.input && Object.keys(s.input).length ? ` with ${mapText(s.input)}` : ""}${s.as ? ` → steps.${s.as}` : ""}`;
|
|
308
|
+
case "flow": return `Run flow ${s.flow || "?"}${s.args && Object.keys(s.args).length ? ` with ${mapText(s.args)}` : ""}`;
|
|
309
|
+
case "open": return `Open modal ${s.modal || "?"}`;
|
|
310
|
+
case "close": return s.modal ? `Close modal ${s.modal}` : "Close all modals";
|
|
311
|
+
case "navigate": return `Go to ${s.href ?? refText(s.hrefFrom)}${s.newTab ? " (new tab)" : ""}`;
|
|
312
|
+
case "toast": return `Show ${s.tone ?? "info"} message “${s.text}”`;
|
|
313
|
+
case "set": {
|
|
314
|
+
const op = s.op ?? "set";
|
|
315
|
+
return op === "toggle" ? `Toggle state.${s.key}` : op === "set" ? `Set state.${s.key} = ${refText(s.value)}` : `${op === "increment" ? "Increase" : "Decrease"} state.${s.key} by ${s.value ? refText(s.value) : "1"}`;
|
|
316
|
+
}
|
|
317
|
+
case "if": return `If ${s.when}`;
|
|
318
|
+
case "wait": return `Wait ${s.ms >= 1000 ? `${s.ms / 1000} s` : `${s.ms} ms`}`;
|
|
319
|
+
case "scroll": return `Scroll to block ${s.target || "?"}`;
|
|
320
|
+
case "focus": return `Focus block ${s.target || "?"}`;
|
|
321
|
+
case "copy": return `Copy ${refText(s.value)}`;
|
|
322
|
+
case "track": return `Track “${s.event}”${s.props && Object.keys(s.props).length ? ` with ${mapText(s.props)}` : ""}`;
|
|
323
|
+
case "animate": return `Animate block ${s.target || "?"} with ${s.preset || "?"}`;
|
|
324
|
+
case "refresh": return "Refresh data";
|
|
325
|
+
case "stop": return "Stop";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** The whole flow as indented lines (then/else nested), used to compare versions. */
|
|
329
|
+
export function describeFlow(f) {
|
|
330
|
+
const out = [];
|
|
331
|
+
if (f.label)
|
|
332
|
+
out.push(`Label: ${f.label}`);
|
|
333
|
+
if (f.params?.length)
|
|
334
|
+
out.push(`Parameters: ${f.params.map((p) => `${p.name}${p.required ? "*" : ""}: ${p.type}`).join(", ")}`);
|
|
335
|
+
const walk = (steps, indent, prefix) => steps.forEach((s, i) => {
|
|
336
|
+
out.push(`${indent}${prefix}${i + 1}. ${describeStep(s)}`);
|
|
337
|
+
if (s.kind === "if") {
|
|
338
|
+
out.push(`${indent} then:`);
|
|
339
|
+
walk(s.then, `${indent} `, "");
|
|
340
|
+
if (s.else?.length) {
|
|
341
|
+
out.push(`${indent} else:`);
|
|
342
|
+
walk(s.else, `${indent} `, "");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
walk(f.steps, "", "");
|
|
347
|
+
if (f.onError?.length) {
|
|
348
|
+
out.push("If an action fails:");
|
|
349
|
+
walk(f.onError, " ", "");
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
/** Line diff (LCS) between two describeFlow outputs: unchanged / added / removed lines in order. */
|
|
354
|
+
export function diffLines(a, b) {
|
|
355
|
+
const m = a.length, n = b.length;
|
|
356
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
357
|
+
for (let i = m - 1; i >= 0; i--)
|
|
358
|
+
for (let j = n - 1; j >= 0; j--)
|
|
359
|
+
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
360
|
+
const out = [];
|
|
361
|
+
let i = 0, j = 0;
|
|
362
|
+
while (i < m && j < n) {
|
|
363
|
+
if (a[i] === b[j]) {
|
|
364
|
+
out.push({ kind: "same", text: a[i] });
|
|
365
|
+
i++;
|
|
366
|
+
j++;
|
|
367
|
+
}
|
|
368
|
+
else if (dp[i + 1][j] >= dp[i][j + 1])
|
|
369
|
+
out.push({ kind: "del", text: a[i++] });
|
|
370
|
+
else
|
|
371
|
+
out.push({ kind: "add", text: b[j++] });
|
|
372
|
+
}
|
|
373
|
+
while (i < m)
|
|
374
|
+
out.push({ kind: "del", text: a[i++] });
|
|
375
|
+
while (j < n)
|
|
376
|
+
out.push({ kind: "add", text: b[j++] });
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
/** Animation presets used by "animate" steps (the runtime needs their keyframes in the browser). */
|
|
380
|
+
export function flowPresets(flows) {
|
|
381
|
+
const out = new Set();
|
|
382
|
+
for (const f of flows)
|
|
383
|
+
walkSteps([...f.steps, ...(f.onError ?? [])], (s) => { if (s.kind === "animate")
|
|
384
|
+
out.add(s.preset); });
|
|
385
|
+
return [...out].sort();
|
|
386
|
+
}
|
|
387
|
+
//# sourceMappingURL=flows.js.map
|