api_binding_ui 2.0.2 → 3.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.
Files changed (39) hide show
  1. package/README.md +68 -48
  2. package/SKILL.md +29 -34
  3. package/dist/app/index-4mgcb4wd.js +367 -0
  4. package/dist/app/index-aaxy714b.css +1 -0
  5. package/dist/app/index.html +7 -0
  6. package/dist/cli.js +1006 -0
  7. package/dist/codegen/generate.d.ts +22 -0
  8. package/dist/codegen/helpers.d.ts +2 -0
  9. package/dist/framework/contract.d.ts +38 -23
  10. package/dist/framework/expr.d.ts +26 -27
  11. package/dist/framework/file.d.ts +28 -0
  12. package/dist/framework/store.d.ts +3 -15
  13. package/dist/{index-3q1vx729.js → index-jj8twdw9.js} +158 -173
  14. package/dist/index-jj8twdw9.js.map +16 -0
  15. package/dist/index.d.ts +6 -8
  16. package/dist/index.js +461 -298
  17. package/dist/index.js.map +6 -8
  18. package/dist/overlay.js +24 -0
  19. package/dist/server/handler.d.ts +18 -0
  20. package/dist/studio/BindingStudio.d.ts +8 -11
  21. package/dist/studio/SettingsPanel.d.ts +3 -7
  22. package/dist/studio/index.css +0 -149
  23. package/dist/studio/index.d.ts +1 -2
  24. package/dist/studio/index.js +231 -438
  25. package/dist/studio/index.js.map +8 -10
  26. package/dist/studio/useBindings.d.ts +7 -5
  27. package/dist/studio/useCatalog.d.ts +2 -0
  28. package/dist/vite/plugin.d.ts +36 -0
  29. package/dist/vite.js +201 -0
  30. package/package.json +68 -52
  31. package/spec/FRAMEWORK.md +107 -201
  32. package/dist/framework/runner.d.ts +0 -36
  33. package/dist/index-3q1vx729.js.map +0 -16
  34. package/dist/runtime/BindingProvider.d.ts +0 -42
  35. package/dist/runtime/BindingScreen.d.ts +0 -23
  36. package/dist/runtime/hooks.d.ts +0 -22
  37. package/dist/runtime/screenStore.d.ts +0 -7
  38. package/dist/studio/BindingWidget.d.ts +0 -9
  39. package/dist/studio/useSettings.d.ts +0 -8
package/dist/index.js CHANGED
@@ -1,27 +1,22 @@
1
1
  import {
2
2
  catalogFromOpenApi2,
3
- localStorageAdapter2,
4
- memoryAdapter2,
5
- toManifest2,
3
+ CatalogLoadError2,
4
+ catalogFromText2,
5
+ loadCatalog2,
6
+ ID2,
7
+ parseBindingsFile2,
8
+ screenOf2,
9
+ fromFile2,
10
+ toFile2,
11
+ memoryStore2,
12
+ httpStore2,
13
+ flat,
6
14
  createEngine2,
7
15
  validateRegistry2,
8
- validateBindings2,
9
- useBindingRuntime,
10
- BindingProvider2
11
- } from "./index-3q1vx729.js";
16
+ validateBindings2
17
+ } from "./index-jj8twdw9.js";
12
18
  // src/framework/expr.ts
13
- var PIPE = /^([A-Za-z_][\w-]*)\((.*)\)$/;
14
19
  var cache = new Map;
15
- function parseArg(raw) {
16
- const t = raw.trim();
17
- if (!t)
18
- return;
19
- try {
20
- return JSON.parse(t);
21
- } catch {
22
- return t;
23
- }
24
- }
25
20
  function segments(path) {
26
21
  return path.replace(/\[(\*|\d+)\]/g, ".$1").split(".").filter(Boolean);
27
22
  }
@@ -29,320 +24,488 @@ function parse(expr) {
29
24
  const hit = cache.get(expr);
30
25
  if (hit)
31
26
  return hit;
32
- const [main, ...rest] = expr.split("|");
33
- const segs = segments(main.trim());
34
- if (!segs.length)
35
- throw new Error(`Empty expression: "${expr}"`);
36
- const width = segs[0] === "$item" || segs[0].startsWith("$") && !["$context", "$steps"].includes(segs[0]) ? 1 : 2;
37
- const pipes = rest.map((p) => {
38
- const m = PIPE.exec(p.trim());
39
- if (!m)
40
- throw new Error(`Bad transform "${p.trim()}" in "${expr}"`);
41
- return { name: m[1], args: m[2] ? m[2].split(",").map(parseArg) : [] };
42
- });
43
- const parsed = { head: segs.slice(0, width), path: segs.slice(width), pipes };
27
+ const e = expr.trim();
28
+ let parsed;
29
+ if (e.startsWith("$")) {
30
+ const segs = segments(e);
31
+ const [h, id] = segs;
32
+ if (h === "$item")
33
+ parsed = { head: { type: "item" }, path: segs.slice(1) };
34
+ else if ((h === "$context" || h === "$steps") && id)
35
+ parsed = { head: { type: h === "$context" ? "context" : "steps", id }, path: segs.slice(2) };
36
+ else
37
+ throw new Error(`Unknown source "${h}" in "${expr}".`);
38
+ } else {
39
+ const colon = e.indexOf(":");
40
+ if (colon < 1)
41
+ throw new Error(`"${expr}" must read "<componentId>:<slot>".`);
42
+ const [slot, ...path] = segments(e.slice(colon + 1));
43
+ if (!slot)
44
+ throw new Error(`"${expr}" names no slot.`);
45
+ parsed = { head: { type: "slot", component: e.slice(0, colon), slot }, path };
46
+ }
44
47
  cache.set(expr, parsed);
45
48
  return parsed;
46
49
  }
47
- function walk(value, path) {
48
- let v = value;
49
- for (let i = 0;i < path.length; i++) {
50
- if (v == null)
51
- return;
52
- if (path[i] === "*") {
53
- if (!Array.isArray(v))
54
- return;
55
- const rest = path.slice(i + 1);
56
- return v.map((x) => walk(x, rest));
57
- }
50
+ // src/codegen/helpers.ts
51
+ var HELPERS = `import { useMutation, useQuery } from "@tanstack/react-query";
52
+
53
+ /** Data for a slot. \`bound\` is false when no API feeds it; keep the UI's own value then. */
54
+ export interface Data<T> { bound: boolean; data: T | undefined; loading: boolean; error: unknown }
55
+ /** An action. \`run\` rejects when a call fails; it resolves at once when no API is connected. */
56
+ export interface Action { bound: boolean; run: (payload?: any) => Promise<void>; running: boolean; error: unknown }
57
+ /** A per-item value, such as a cell in a row. */
58
+ export interface Cell<T> { bound: boolean; read: (item: any) => T | undefined }
59
+
60
+ export const UNBOUND_DATA: Data<never> = { bound: false, data: undefined, loading: false, error: undefined };
61
+ export const UNBOUND_ACTION: Action = { bound: false, run: async () => {}, running: false, error: undefined };
62
+ export const UNBOUND_CELL: Cell<never> = { bound: false, read: () => undefined };
63
+
64
+ export class ApiError extends Error {
65
+ constructor(readonly status: number, readonly data: unknown) { super(\`API call failed (\${status})\`); this.name = "ApiError"; }
66
+ }
67
+
68
+ /** Reads a path; "*" maps the rest of the path over a list. */
69
+ export function at(value: unknown, path: readonly string[]): any {
70
+ let v: any = value;
71
+ for (let i = 0; i < path.length; i++) {
72
+ if (v == null) return undefined;
73
+ if (path[i] === "*") return Array.isArray(v) ? v.map(x => at(x, path.slice(i + 1))) : undefined;
58
74
  v = v[path[i]];
59
75
  }
60
76
  return v;
61
77
  }
62
- function evaluate(expr, scope) {
63
- const { head, path, pipes } = parse(expr);
64
- let root;
65
- if (head[0] === "$item")
66
- root = scope.item;
67
- else if (head[0] === "$context")
68
- root = scope.context?.[head[1]];
69
- else if (head[0] === "$steps")
70
- root = scope.steps?.[head[1]];
71
- else if (head[0].startsWith("$"))
72
- root = scope.providers?.[head[0].slice(1)];
73
- else
74
- root = scope.slot(head[0], head[1]);
75
- let value = walk(root, path);
76
- for (const p of pipes) {
77
- const fn = scope.transforms?.[p.name];
78
- if (!fn)
79
- throw new Error(`Unknown transform "${p.name}" in "${expr}"`);
80
- value = fn(value, ...p.args);
81
- }
82
- return value;
83
- }
84
- // src/framework/runner.ts
85
- class BindingError extends Error {
86
- detail;
87
- constructor(message, detail) {
88
- super(message);
89
- this.detail = detail;
90
- this.name = "BindingError";
78
+
79
+ /** The body of a client response; throws on a non-2xx status. Passes through clients that return the body. */
80
+ export function ok(res: any): any {
81
+ if (res && typeof res === "object" && typeof res.status === "number" && "data" in res && "headers" in res) {
82
+ if (res.status < 200 || res.status >= 300) throw new ApiError(res.status, res.data);
83
+ return res.data;
91
84
  }
85
+ return res;
92
86
  }
93
- function setPointer(target, keys, value) {
94
- let node = target;
95
- keys.slice(0, -1).forEach((k) => {
96
- node = node[k] ??= {};
97
- });
98
- node[keys[keys.length - 1]] = value;
87
+
88
+ /** True when every required input has a value. */
89
+ export const has = (inputs: Record<string, unknown>) => Object.values(inputs).every(v => v !== undefined);
90
+
91
+ /** Throws when a required input has no value. */
92
+ export function need(operation: string, inputs: Record<string, unknown>) {
93
+ const missing = Object.keys(inputs).filter(k => inputs[k] === undefined);
94
+ if (missing.length) throw new Error(\`\${operation}: missing \${missing.join(", ")}\`);
99
95
  }
100
- var decode = (k) => k.replace(/~1/g, "/").replace(/~0/g, "~");
101
- function buildRequest(op, request, scope) {
102
- const out = { method: op.method, path: op.path, query: {}, headers: {} };
103
- let body;
104
- for (const [pointer, expr] of Object.entries(request)) {
105
- const value = evaluate(expr, scope);
106
- if (value === undefined)
107
- continue;
108
- const [, where, ...rest] = pointer.split("/");
109
- const keys = rest.map(decode);
110
- if (where === "path")
111
- out.path = out.path.replace(`{${keys[0]}}`, encodeURIComponent(String(value)));
112
- else if (where === "query")
113
- out.query[keys[0]] = String(value);
114
- else if (where === "header")
115
- out.headers[keys[0]] = String(value);
116
- else if (where === "body") {
117
- if (!keys.length)
118
- out.body = value;
119
- else
120
- setPointer(body ??= {}, keys, value);
121
- }
96
+
97
+ /** Drops undefined values. */
98
+ export const defined = (o: Record<string, unknown>): any => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
99
+
100
+ export const path = (v: unknown): any => (v === undefined ? undefined : encodeURIComponent(String(v)));
101
+
102
+ /** A body from a base value and [keys, value] entries; undefined values are skipped. */
103
+ export function assemble(base: unknown, entries: Array<[string[], unknown]>): any {
104
+ const out: any = base && typeof base === "object" ? { ...base } : {};
105
+ for (const [keys, value] of entries) {
106
+ if (value === undefined) continue;
107
+ let node = out;
108
+ keys.slice(0, -1).forEach(k => { node = node[k] = node[k] && typeof node[k] === "object" ? { ...node[k] } : {}; });
109
+ node[keys[keys.length - 1]] = value;
122
110
  }
123
- if (body)
124
- out.body = { ...out.body, ...body };
125
- const missing = op.parameters.filter((p) => p.required && !((p.pointer in request) && evaluate(request[p.pointer], scope) !== undefined));
126
- if (missing.length)
127
- throw new BindingError(`Missing required input: ${missing.map((p) => p.name).join(", ")}.`);
128
111
  return out;
129
112
  }
130
- var requestKey = (r) => JSON.stringify([r.method, r.path, r.query, r.body ?? null]);
131
- function select(response, selector) {
132
- const path = selector.replace(/^\$\.?/, "").replace(/\[\*\]$/, "").split(".").filter(Boolean);
133
- return path.reduce((v, k) => v == null ? undefined : v[k], response);
134
- }
135
- function mapItems(data, binding, scope) {
136
- if (!binding.map || !Object.keys(binding.map).length)
137
- return data;
138
- const one = (item) => ({
139
- ...item && typeof item === "object" ? item : {},
140
- ...Object.fromEntries(Object.entries(binding.map).map(([k, e]) => [k, evaluate(e, { ...scope, item })]))
141
- });
113
+
114
+ /** Adds mapped fields to each item, keeping the fields the API returned. */
115
+ export function mapItems(data: any, map: (item: any) => Record<string, unknown>): any {
116
+ const one = (item: any) => ({ ...(item && typeof item === "object" ? item : {}), ...map(item) });
142
117
  return Array.isArray(data) ? data.map(one) : data == null ? data : one(data);
143
118
  }
144
- async function runConsume(binding, op, scope, fetcher) {
145
- const response = await fetcher(buildRequest(op, binding.request, scope), op);
146
- return mapItems(select(response, binding.select), binding, scope);
119
+
120
+ export const list = (v: unknown): unknown[] => (Array.isArray(v) ? v : v === undefined ? [] : [v]);
121
+
122
+ export function finish(failed: unknown[]) {
123
+ if (failed.length === 1) throw failed[0];
124
+ if (failed.length) throw new AggregateError(failed, \`\${failed.length} API calls failed\`);
147
125
  }
148
- async function runSteps(steps, operations, scope, fetcher) {
149
- const outcomes = [];
150
- const results = { ...scope.steps };
151
- for (const step of steps) {
152
- const op = operations.get(step.operation);
153
- if (!op)
154
- throw new BindingError(`Operation "${step.operation}" is not in the catalog.`);
155
- const items = step.forEach ? evaluate(step.forEach, scope) : [scope.item];
156
- const list = Array.isArray(items) ? items : [items];
157
- const responses = [];
158
- let failed = false;
159
- for (const item of list) {
160
- try {
161
- responses.push(await fetcher(buildRequest(op, step.request, { ...scope, item, steps: results }), op));
162
- } catch (error) {
163
- failed = true;
164
- outcomes.push({ step: step.id, ok: false, error });
165
- if (step.onError === "stop")
166
- return outcomes;
167
- }
168
- }
169
- const response = step.forEach ? responses : responses[0];
170
- results[step.id] = { response };
171
- if (!failed)
172
- outcomes.push({ step: step.id, ok: true, response });
173
- }
174
- return outcomes;
126
+
127
+ export function useData<T>(key: readonly unknown[], fetch: () => Promise<unknown>, select: (data: any) => T, options: { enabled: boolean; everySeconds?: number }): Data<T> {
128
+ const q = useQuery({ queryKey: key, queryFn: fetch, select, enabled: options.enabled, refetchInterval: options.everySeconds ? options.everySeconds * 1000 : false });
129
+ return { bound: true, data: q.data, loading: q.isLoading, error: q.error ?? undefined };
175
130
  }
176
- // src/runtime/BindingScreen.tsx
177
- import * as React from "react";
178
131
 
179
- // src/runtime/screenStore.ts
180
- function createScreenStore() {
181
- const values = new Map;
182
- const listeners = new Set;
183
- return {
184
- get: (b, s) => values.get(`${b}.${s}`),
185
- set(b, s, value) {
186
- const k = `${b}.${s}`;
187
- if (Object.is(values.get(k), value))
188
- return;
189
- values.set(k, value);
190
- listeners.forEach((l) => l());
191
- },
192
- subscribe(l) {
193
- listeners.add(l);
194
- return () => {
195
- listeners.delete(l);
196
- };
197
- }
198
- };
132
+ export function useAction(run: (payload?: any) => Promise<void>): Action {
133
+ const m = useMutation({ mutationFn: run });
134
+ return { bound: true, run: payload => m.mutateAsync(payload), running: m.isPending, error: m.error ?? undefined };
199
135
  }
136
+ `;
200
137
 
201
- // src/runtime/BindingScreen.tsx
202
- import { jsx } from "react/jsx-runtime";
203
- var ScreenContext = React.createContext(null);
204
- var ShownContext = React.createContext(true);
205
- function useScreen() {
206
- const s = React.useContext(ScreenContext);
207
- if (!s)
208
- throw new Error("Use binding hooks inside <BindingScreen id=…>.");
209
- return s;
138
+ // src/codegen/generate.ts
139
+ var GENERATED_MARK = "@generated";
140
+ var HEADER = `// ${GENERATED_MARK} Do not edit: change the bindings and regenerate.
141
+ `;
142
+ var words = (s) => s.split(/[.\-_]+/).filter(Boolean);
143
+ var pascal = (s) => words(s).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
144
+ var camel = (s) => {
145
+ const p = pascal(s);
146
+ return p[0].toLowerCase() + p.slice(1);
147
+ };
148
+ var str = (v) => JSON.stringify(v);
149
+ var IDENT = /^[A-Za-z_$][\w$]*$/;
150
+ var prop = (k) => IDENT.test(k) ? k : str(k);
151
+ var orvalName = (id) => IDENT.test(id) ? id[0].toLowerCase() + id.slice(1) : camel(id);
152
+ function tsType(schema, many = false, depth = 0) {
153
+ const one = () => {
154
+ if (!schema || typeof schema !== "object" || depth > 3)
155
+ return "any";
156
+ const s = schema;
157
+ if (Array.isArray(s.enum) && s.enum.length && s.enum.every((v) => typeof v === "string"))
158
+ return s.enum.map(str).join(" | ");
159
+ switch (s.type) {
160
+ case "string":
161
+ return "string";
162
+ case "integer":
163
+ case "number":
164
+ return "number";
165
+ case "boolean":
166
+ return "boolean";
167
+ case "array":
168
+ return `Array<${tsType(s.items, false, depth + 1)}>`;
169
+ case "object": {
170
+ const props = Object.entries(s.properties ?? {});
171
+ if (!props.length)
172
+ return "Record<string, any>";
173
+ return `{ ${props.map(([k, v]) => `${prop(k)}?: ${tsType(v, false, depth + 1)}`).join("; ")}; [key: string]: any }`;
174
+ }
175
+ default:
176
+ return "any";
177
+ }
178
+ };
179
+ return many ? `Array<${one()}>` : one();
210
180
  }
211
- function BindingActive({ value, children }) {
212
- const parent = React.useContext(ShownContext);
213
- return /* @__PURE__ */ jsx(ShownContext.Provider, {
214
- value: parent && value,
215
- children
216
- });
181
+ function compileExpr(expr, cx, scope, at) {
182
+ let p;
183
+ try {
184
+ p = parse(expr);
185
+ } catch (e) {
186
+ cx.issue("expr.invalid", e.message, at);
187
+ return "undefined";
188
+ }
189
+ const path = p.path.map(str);
190
+ const h = p.head;
191
+ const read = (root, keys) => {
192
+ cx.use.add("at");
193
+ return `at(${root}, [${keys.join(", ")}])`;
194
+ };
195
+ if (h.type === "item") {
196
+ if (!scope.item) {
197
+ cx.issue("expr.no-item", `"${expr}" reads $item, which this slot does not have.`, at);
198
+ return "undefined";
199
+ }
200
+ return path.length ? read("item", path) : "item";
201
+ }
202
+ if (h.type === "context") {
203
+ if (!cx.screen.context.some((c) => c.id === h.id)) {
204
+ cx.issue("expr.unknown-context", `"${expr}": ${cx.screen.id} declares no context "${h.id}".`, at);
205
+ return "undefined";
206
+ }
207
+ return read("state.context", [str(h.id), ...path]);
208
+ }
209
+ if (h.type === "steps") {
210
+ if (!scope.steps?.has(h.id)) {
211
+ cx.issue("expr.unknown-step", `"${expr}" reads step "${h.id}", which does not run before it.`, at);
212
+ return "undefined";
213
+ }
214
+ return read("steps", [str(h.id), ...path]);
215
+ }
216
+ if (!cx.produces.get(h.component)?.has(h.slot)) {
217
+ cx.issue("expr.unknown-slot", `"${expr}" reads a value that is not produced on ${cx.screen.id}.`, at);
218
+ return "undefined";
219
+ }
220
+ return read("state", [str(cx.keys.get(h.component)), str(h.slot), ...path]);
217
221
  }
218
- var NO_CONTEXT = {};
219
- function BindingScreen({ id, context = NO_CONTEXT, children }) {
220
- const [store] = React.useState(createScreenStore);
221
- const shown = React.useContext(ShownContext);
222
- const { loadSaved, mountScreen } = useBindingRuntime();
223
- React.useEffect(() => {
224
- loadSaved(id);
225
- }, [loadSaved, id]);
226
- React.useEffect(() => mountScreen(id, shown), [mountScreen, id, shown]);
227
- const scope = React.useMemo(() => ({ id, store, context }), [id, store, context]);
228
- return /* @__PURE__ */ jsx(ScreenContext.Provider, {
229
- value: scope,
230
- children
222
+ var decode = (k) => k.replace(/~1/g, "/").replace(/~0/g, "~");
223
+ function compileCall(op, request, cx, scope, at) {
224
+ const e = (x) => compileExpr(x, cx, scope, at);
225
+ const known = new Set(op.parameters.map((p) => p.pointer));
226
+ const body = [];
227
+ let whole;
228
+ const query = [];
229
+ const headers = [];
230
+ for (const [pointer, x] of Object.entries(request)) {
231
+ const [, where, ...rest] = pointer.split("/");
232
+ const keys = rest.map(decode);
233
+ if (where === "body") {
234
+ if (!op.body) {
235
+ cx.issue("request.no-body", `${op.id} takes no body, but "${pointer}" is set.`, at);
236
+ continue;
237
+ }
238
+ if (!keys.length)
239
+ whole = e(x);
240
+ else
241
+ body.push(`[[${keys.map(str).join(", ")}], ${e(x)}]`);
242
+ } else if (!known.has(pointer))
243
+ cx.issue("request.unknown-input", `${op.id} has no input "${pointer}".`, at);
244
+ else if (where === "query")
245
+ query.push(`${prop(keys[0])}: ${e(x)}`);
246
+ else if (where === "header")
247
+ headers.push(`${str(keys[0])}: ${e(x)}`);
248
+ }
249
+ const args = [...op.path.matchAll(/\{([^}]+)\}/g)].map((m) => {
250
+ const x = request[`/path/${m[1]}`];
251
+ if (x === undefined) {
252
+ cx.issue("request.missing-path", `${op.id} needs "${m[1]}" in its path; it is not connected.`, at);
253
+ return "undefined";
254
+ }
255
+ cx.use.add("path");
256
+ return `path(${e(x)})`;
231
257
  });
258
+ if (op.body) {
259
+ const set = new Set(Object.keys(request).filter((k) => k.startsWith("/body/")).map((k) => decode(k.split("/")[2])));
260
+ const gaps = whole ? [] : (flat(op.body.schema).required ?? []).filter((f) => !set.has(f));
261
+ if (gaps.length)
262
+ cx.warn("request.required-body", `${op.id} requires ${gaps.join(", ")} in its body; not connected.`, at);
263
+ cx.use.add("assemble");
264
+ args.push(`assemble(${whole ?? "undefined"}, [${body.join(", ")}])`);
265
+ }
266
+ if (op.parameters.some((p) => p.pointer.startsWith("/query/"))) {
267
+ cx.use.add("defined");
268
+ args.push(`defined({ ${query.join(", ")} })`);
269
+ }
270
+ const key = `[${[str(op.id), ...args].join(", ")}]`;
271
+ if (headers.length) {
272
+ cx.use.add("defined");
273
+ args.push(`{ headers: defined({ ${headers.join(", ")} }) }`);
274
+ }
275
+ const required = op.parameters.filter((p) => p.required).map((p) => `${prop(p.name)}: ${request[p.pointer] === undefined ? "undefined" : e(request[p.pointer])}`);
276
+ return { fn: orvalName(op.id), call: `${orvalName(op.id)}(${args.join(", ")})`, key, required: required.length ? `{ ${required.join(", ")} }` : "" };
232
277
  }
233
- // src/runtime/hooks.ts
234
- import * as React2 from "react";
235
- function scopeOf(runtime, screen, item) {
236
- return { slot: screen.store.get, context: screen.context, providers: runtime.providers(), transforms: runtime.transforms, item };
237
- }
238
- function useProduce(bindId, slot, value) {
239
- const { store } = useScreen();
240
- React2.useEffect(() => {
241
- store.set(bindId, slot, value);
242
- }, [store, bindId, slot, value]);
243
- }
244
- var WAIT = "\x00wait:";
245
- function useSlot(bindId, slot) {
246
- const runtime = useBindingRuntime();
247
- const screen = useScreen();
248
- const binding = runtime.binding(screen.id, bindId, slot);
249
- const [nonce, setNonce] = React2.useState(0);
250
- const [state, setState] = React2.useState({ key: "", status: "loading" });
251
- const snapshot = React2.useCallback(() => {
252
- if (!binding)
253
- return "";
254
- try {
255
- const scope = scopeOf(runtime, screen);
256
- if (binding.type === "expression")
257
- return JSON.stringify(evaluate(binding.value, scope) ?? null);
258
- if (binding.type !== "operation")
259
- return "";
260
- const op = runtime.operations.get(binding.operation);
261
- return op ? requestKey(buildRequest(op, binding.request, scope)) : WAIT + "operation missing";
262
- } catch (e) {
263
- return WAIT + (e instanceof Error ? e.message : String(e));
278
+ var selectPath = (selector) => segments(selector.replace(/^\$\.?/, "").replace(/\[\*\]$/, ""));
279
+ function compileSteps(steps, cx, at, ops, imports) {
280
+ const done = new Set;
281
+ const blocks = [];
282
+ let usesPayload = false;
283
+ cx.use.add("ok");
284
+ cx.use.add("finish");
285
+ for (const st of steps) {
286
+ const op = ops.get(st.operation);
287
+ if (!op) {
288
+ cx.issue("catalog.operation-missing", `${st.operation} is not in the API docs.`, at);
289
+ continue;
264
290
  }
265
- }, [binding, runtime, screen]);
266
- const key = React2.useSyncExternalStore(screen.store.subscribe, snapshot, snapshot);
267
- React2.useEffect(() => {
268
- if (!binding || binding.type !== "operation" || !key || key.startsWith(WAIT))
269
- return;
270
- const op = runtime.operations.get(binding.operation);
271
- const scope = scopeOf(runtime, screen);
272
- const request = buildRequest(op, binding.request, scope);
273
- const k = requestKey(request);
274
- let live = true;
275
- setState((s) => ({ key, status: "loading", data: s.data }));
276
- let p = nonce ? undefined : runtime.inflight.get(k);
277
- if (!p) {
278
- p = runtime.fetcher(request, op).finally(() => runtime.inflight.delete(k));
279
- runtime.inflight.set(k, p);
291
+ const scope = { item: true, steps: done };
292
+ const call = compileCall(op, st.request, cx, scope, at);
293
+ imports.add(call.fn);
294
+ const each = st.forEach ? compileExpr(st.forEach, cx, scope, at) : undefined;
295
+ if (each)
296
+ cx.use.add("list");
297
+ else
298
+ usesPayload = true;
299
+ if (call.required)
300
+ cx.use.add("need");
301
+ const readsItem = /\bitem\b/.test(call.call + call.required);
302
+ blocks.push([
303
+ ` {`,
304
+ ` const responses: unknown[] = [];`,
305
+ ` for (const item of ${each ? `list(${each})` : "[payload]"}) {`,
306
+ readsItem ? "" : ` void item;`,
307
+ ` try {`,
308
+ call.required ? ` need(${str(op.id)}, ${call.required});` : "",
309
+ ` responses.push(ok(await ${call.call}));`,
310
+ ` } catch (error) {`,
311
+ st.onError === "stop" ? ` throw error;` : ` failed.push(error);`,
312
+ ` }`,
313
+ ` }`,
314
+ ` steps[${str(st.id)}] = { response: ${each ? "responses" : "responses[0]"} };`,
315
+ ` }`
316
+ ].filter(Boolean).join(`
317
+ `));
318
+ done.add(st.id);
319
+ }
320
+ return [
321
+ `async (${usesPayload ? "payload" : "_payload"}?: any) => {`,
322
+ ` const steps: Record<string, unknown> = {};`,
323
+ ` const failed: unknown[] = [];`,
324
+ ...blocks,
325
+ ` finish(failed);`,
326
+ ` }`
327
+ ].join(`
328
+ `);
329
+ }
330
+ function generate(file, catalog) {
331
+ const issues = [];
332
+ const { fw, index, issues: fileIssues } = fromFile2(file, catalog);
333
+ const valid = validateRegistry2(fw);
334
+ issues.push(...fileIssues, ...valid.issues);
335
+ const ops = new Map(catalog.operations.map((o) => [o.id, o]));
336
+ const DESC = new Map(fw.descriptors.map((d) => [d.kind, d]));
337
+ const client = file.output?.client;
338
+ if (!client)
339
+ issues.push({ severity: "error", screen: "", code: "output.missing", message: `Set "output": { "dir", "client" } in the bindings file.` });
340
+ const files = [{ path: "screen-helpers.ts", content: HEADER + HELPERS }];
341
+ const exports = [];
342
+ for (const screen of valid.registry.screens) {
343
+ const name = pascal(screen.id);
344
+ const keys = new Map;
345
+ const taken = new Map;
346
+ for (const c of screen.components) {
347
+ const k = camel(c.bindId.slice(screen.id.length + 1));
348
+ if (taken.has(k))
349
+ issues.push({ severity: "error", screen: screen.id, component: c.bindId, code: "codegen.name-clash", message: `"${c.bindId}" and "${taken.get(k)}" both become "${k}".` });
350
+ taken.set(k, c.bindId);
351
+ keys.set(c.bindId, k);
280
352
  }
281
- p.then((res) => {
282
- if (live)
283
- setState({ key, status: "ready", data: mapItems(select(res, binding.select), binding, scope) });
284
- }, (error) => {
285
- if (live)
286
- setState({ key, status: "error", error });
287
- });
288
- return () => {
289
- live = false;
353
+ const cx = {
354
+ screen,
355
+ keys,
356
+ use: new Set,
357
+ produces: new Map(screen.components.map((c) => [c.bindId, new Set(DESC.get(c.kind).slots.filter((s) => s.role === "produce").map((s) => s.id))])),
358
+ issue: (code, message, at) => issues.push({ severity: "error", screen: screen.id, code, message, ...at }),
359
+ warn: (code, message, at) => issues.push({ severity: "warning", screen: screen.id, code, message, ...at })
290
360
  };
291
- }, [binding, key, nonce, runtime, screen]);
292
- const reload = React2.useCallback(() => setNonce((n) => n + 1), []);
293
- if (!binding || binding.type === "steps" || binding.type === "none")
294
- return { status: "unbound", data: undefined, error: undefined, reload };
295
- if (binding.type === "expression")
296
- return { status: "ready", data: key ? JSON.parse(key) : undefined, error: undefined, reload };
297
- if (key.startsWith(WAIT))
298
- return { status: "waiting", data: undefined, error: new BindingError(key.slice(WAIT.length)), reload };
299
- return { status: state.key === key ? state.status : "loading", data: state.data, error: state.error, reload };
300
- }
301
- function useCell(bindId, slot) {
302
- const runtime = useBindingRuntime();
303
- const screen = useScreen();
304
- const binding = runtime.binding(screen.id, bindId, slot);
305
- return React2.useCallback((item) => binding?.type === "expression" ? evaluate(binding.value, scopeOf(runtime, screen, item)) : undefined, [binding, runtime, screen]);
306
- }
307
- function useTrigger(bindId, slot) {
308
- const runtime = useBindingRuntime();
309
- const screen = useScreen();
310
- const binding = runtime.binding(screen.id, bindId, slot);
311
- const [running, setRunning] = React2.useState(false);
312
- const run = React2.useCallback(async (item) => {
313
- if (binding?.type !== "steps")
314
- return [];
315
- setRunning(true);
316
- try {
317
- return await runSteps(binding.steps, runtime.operations, scopeOf(runtime, screen, item), runtime.fetcher);
318
- } finally {
319
- setRunning(false);
361
+ const bindings = new Map;
362
+ for (const b of index[screen.id] ?? []) {
363
+ if (!keys.has(b.component)) {
364
+ cx.issue("binding.unknown-component", `A binding names "${b.component}", which is not registered.`, b);
365
+ continue;
366
+ }
367
+ if (b.state !== "confirmed") {
368
+ issues.push({ severity: "warning", screen: screen.id, component: b.component, slot: b.slot, code: "binding.draft", message: `${b.component} · ${b.slot} is a draft; it is not generated.` });
369
+ continue;
370
+ }
371
+ bindings.set(`${b.component}|${b.slot}`, b);
320
372
  }
321
- }, [binding, runtime, screen]);
322
- return { bound: binding?.type === "steps" || binding?.type === "none", running, run };
373
+ const imports = new Set;
374
+ const types = new Set;
375
+ const body = [];
376
+ const ret = [];
377
+ const apiType = [];
378
+ const stateType = [];
379
+ for (const c of screen.components) {
380
+ const d = DESC.get(c.kind);
381
+ const k = keys.get(c.bindId);
382
+ const produce = d.slots.filter((s) => s.role === "produce");
383
+ if (produce.length)
384
+ stateType.push(` ${prop(k)}?: { ${produce.map((s) => `${prop(s.id)}?: ${tsType(s.schema, s.many)}`).join("; ")} };`);
385
+ const out = [];
386
+ const outT = [];
387
+ const unbound = (slot, v) => {
388
+ cx.use.add(v);
389
+ out.push(`${prop(slot)}: ${v}`);
390
+ };
391
+ for (const s of d.slots.filter((x) => x.role !== "produce")) {
392
+ const b = bindings.get(`${c.bindId}|${s.id}`);
393
+ const at = { component: c.bindId, slot: s.id };
394
+ const v = `${k}${pascal(s.id)}`;
395
+ if (s.role === "trigger") {
396
+ types.add("Action");
397
+ outT.push(`${prop(s.id)}: Action`);
398
+ if (b?.type !== "steps") {
399
+ unbound(s.id, "UNBOUND_ACTION");
400
+ continue;
401
+ }
402
+ cx.use.add("useAction");
403
+ body.push(` const ${v} = useAction(${compileSteps(b.steps, cx, at, ops, imports)});`);
404
+ out.push(`${prop(s.id)}: ${v}`);
405
+ continue;
406
+ }
407
+ const T = tsType(s.schema, s.many);
408
+ if (s.scope) {
409
+ types.add("Cell");
410
+ outT.push(`${prop(s.id)}: Cell<${T}>`);
411
+ if (b?.type !== "expression") {
412
+ unbound(s.id, "UNBOUND_CELL");
413
+ continue;
414
+ }
415
+ out.push(`${prop(s.id)}: { bound: true, read: (item: any) => ${compileExpr(b.value, cx, { item: true }, at)} }`);
416
+ continue;
417
+ }
418
+ types.add("Data");
419
+ outT.push(`${prop(s.id)}: Data<${T}>`);
420
+ if (b?.type === "expression") {
421
+ out.push(`${prop(s.id)}: { bound: true, data: ${compileExpr(b.value, cx, { item: false }, at)}, loading: false, error: undefined }`);
422
+ continue;
423
+ }
424
+ const op = b?.type === "operation" ? ops.get(b.operation) : undefined;
425
+ if (b?.type === "operation" && !op)
426
+ cx.issue("catalog.operation-missing", `${b.operation} is not in the API docs.`, at);
427
+ if (b?.type !== "operation" || !op) {
428
+ unbound(s.id, "UNBOUND_DATA");
429
+ continue;
430
+ }
431
+ const call = compileCall(op, b.request, cx, { item: false }, at);
432
+ imports.add(call.fn);
433
+ ["useData", "ok"].forEach((h) => cx.use.add(h));
434
+ if (call.required)
435
+ cx.use.add("has");
436
+ const sel = selectPath(b.select).map(str);
437
+ if (sel.length)
438
+ cx.use.add("at");
439
+ const picked = sel.length ? `at(data, [${sel.join(", ")}])` : "data";
440
+ const map = Object.entries(b.map ?? {});
441
+ if (map.length)
442
+ cx.use.add("mapItems");
443
+ const select = map.length ? `mapItems(${picked}, (item: any) => ({ ${map.map(([f, x]) => `${prop(f)}: ${compileExpr(x, cx, { item: true }, at)}`).join(", ")} }))` : picked;
444
+ const every = typeof b.load === "object" ? `, everySeconds: ${b.load.everySeconds}` : "";
445
+ body.push(` const ${v} = useData<${T}>(${call.key}, async () => ok(await ${call.call}), (data: any) => ${select}, { enabled: ${call.required ? `has(${call.required})` : "true"}${every} });`);
446
+ out.push(`${prop(s.id)}: ${v}`);
447
+ }
448
+ if (out.length) {
449
+ ret.push(` ${prop(k)}: { ${out.join(", ")} },`);
450
+ apiType.push(` ${prop(k)}: { ${outT.join("; ")} };`);
451
+ }
452
+ }
453
+ if (screen.context.length)
454
+ stateType.push(` context?: { ${screen.context.map((c) => `${prop(c.id)}?: ${"schema" in c.shape ? tsType(c.shape.schema, c.many) : c.many ? "Array<any>" : "any"}`).join("; ")} };`);
455
+ const usesState = [...body, ...ret].some((l) => /\bstate\b/.test(l));
456
+ const lines = [
457
+ HEADER.trimEnd(),
458
+ imports.size ? `import { ${[...imports].sort().join(", ")} } from ${str(client ?? "")};` : null,
459
+ cx.use.size ? `import { ${[...cx.use].sort().join(", ")} } from "./screen-helpers";` : null,
460
+ types.size ? `import type { ${[...types].sort().join(", ")} } from "./screen-helpers";` : null,
461
+ "",
462
+ `/** ${screen.label}: values its components produce, and what it was opened with. */`,
463
+ stateType.length ? `export interface ${name}State {
464
+ ${stateType.join(`
465
+ `)}
466
+ }` : `export type ${name}State = Record<string, never>;`,
467
+ "",
468
+ `export interface ${name}Api {
469
+ ${apiType.join(`
470
+ `)}
471
+ }`,
472
+ "",
473
+ `/** API data and actions for ${screen.label} (${screen.id}). */`,
474
+ `export function use${name}Api(${usesState ? "state" : "_state"}: ${name}State = {}): ${name}Api {`,
475
+ ...body,
476
+ ` return {`,
477
+ ...ret,
478
+ ` };`,
479
+ `}`,
480
+ ""
481
+ ].filter((l) => l !== null);
482
+ files.push({ path: `${screen.id}.ts`, content: lines.join(`
483
+ `) });
484
+ exports.push(`export * from "./${screen.id}";`);
485
+ }
486
+ files.push({ path: "index.ts", content: HEADER + exports.join(`
487
+ `) + `
488
+ ` });
489
+ return { files, issues };
323
490
  }
324
491
  export {
325
- BindingActive,
326
- BindingError,
327
- BindingProvider2 as BindingProvider,
328
- BindingScreen,
329
- buildRequest,
492
+ CatalogLoadError2 as CatalogLoadError,
493
+ ID2 as ID,
330
494
  catalogFromOpenApi2 as catalogFromOpenApi,
495
+ catalogFromText2 as catalogFromText,
331
496
  createEngine2 as createEngine,
332
- evaluate,
333
- localStorageAdapter2 as localStorageAdapter,
334
- memoryAdapter2 as memoryAdapter,
335
- runConsume,
336
- runSteps,
337
- select,
338
- toManifest2 as toManifest,
339
- useCell,
340
- useProduce,
341
- useSlot,
342
- useTrigger,
497
+ fromFile2 as fromFile,
498
+ generate,
499
+ httpStore2 as httpStore,
500
+ loadCatalog2 as loadCatalog,
501
+ memoryStore2 as memoryStore,
502
+ parseBindingsFile2 as parseBindingsFile,
503
+ parse as parseExpression,
504
+ screenOf2 as screenOf,
505
+ toFile2 as toFile,
343
506
  validateBindings2 as validateBindings,
344
507
  validateRegistry2 as validateRegistry
345
508
  };
346
509
 
347
- //# debugId=64D1E40B946F01F764756E2164756E21
510
+ //# debugId=BEA7AA51ED34223764756E2164756E21
348
511
  //# sourceMappingURL=index.js.map