api_binding_ui 3.0.0 → 3.2.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/vite.js ADDED
@@ -0,0 +1,1113 @@
1
+ // src/vite/plugin.ts
2
+ import { readFileSync } from "node:fs";
3
+ import path4 from "node:path";
4
+
5
+ // src/server/handler.ts
6
+ import { promises as fs } from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ // src/framework/names.ts
11
+ function humanize(id) {
12
+ const s = id.replace(/^\$\.?/, "").replace(/\[\*\]/g, "").split(/[./]/).filter(Boolean).pop() ?? id;
13
+ const w = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
14
+ return w ? w[0].toUpperCase() + w.slice(1) : id;
15
+ }
16
+
17
+ // src/framework/file.ts
18
+ var ID = /^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/;
19
+ var BINDING_TYPES = new Set(["operation", "expression", "steps", "none"]);
20
+ function parseBindingsFile(text) {
21
+ let data;
22
+ try {
23
+ data = JSON.parse(text);
24
+ } catch {
25
+ throw new Error("The bindings file is not valid JSON.");
26
+ }
27
+ return checkBindingsFile(data);
28
+ }
29
+ function checkBindingsFile(data) {
30
+ const f = data;
31
+ if (!f || typeof f !== "object")
32
+ throw new Error("The bindings file must be a JSON object.");
33
+ if (f.version !== 3)
34
+ throw new Error(`The bindings file must have "version": 3.`);
35
+ if (typeof f.openapi !== "string" || !f.openapi)
36
+ throw new Error(`The bindings file needs "openapi": the docs URL or a path.`);
37
+ for (const k of ["kinds", "screens", "components", "bindings"])
38
+ if (!Array.isArray(f[k]))
39
+ throw new Error(`"${k}" must be a list.`);
40
+ f.bindings.forEach((b, i) => {
41
+ const x = b;
42
+ if (!x || typeof x.component !== "string" || typeof x.slot !== "string" || !BINDING_TYPES.has(x.type))
43
+ throw new Error(`Binding ${i + 1} needs "component", "slot" and a known "type".`);
44
+ });
45
+ if (f.output && (typeof f.output.dir !== "string" || typeof f.output.client !== "string"))
46
+ throw new Error(`"output" needs "dir" and "client".`);
47
+ return f;
48
+ }
49
+ var REGISTRY_KEYS = ["kinds", "screens", "components"];
50
+ function parseRegistry(text) {
51
+ let r;
52
+ try {
53
+ r = JSON.parse(text);
54
+ } catch {
55
+ throw new Error("The registry file is not valid JSON.");
56
+ }
57
+ if (!r || r.version !== 1)
58
+ throw new Error(`The registry file must have "version": 1.`);
59
+ if (!Array.isArray(r.kinds) || !Array.isArray(r.screens))
60
+ throw new Error(`The registry file needs "kinds" and "screens" lists.`);
61
+ const screens = [];
62
+ const components = [];
63
+ const under = (id, owner, what) => {
64
+ if (typeof id !== "string" || !id)
65
+ throw new Error(`A ${what} in the registry has no "id".`);
66
+ if (owner && !id.startsWith(owner + "."))
67
+ throw new Error(`"${id}" must start with "${owner}.", the id it is nested under.`);
68
+ };
69
+ const node = (n, owner, parent) => {
70
+ under(n.id, owner, "component");
71
+ if (typeof n.kind !== "string" || typeof n.label !== "string")
72
+ throw new Error(`"${n.id}" needs "kind" and "label".`);
73
+ const { children, ...c } = n;
74
+ components.push(parent ? { ...c, parent } : c);
75
+ for (const ch of children ?? [])
76
+ node(ch, n.id, n.id);
77
+ };
78
+ const screen = (s, owner) => {
79
+ under(s.id, owner, "screen");
80
+ const { components: cs, screens: subs, ...rest } = s;
81
+ screens.push(rest);
82
+ for (const c of cs ?? [])
83
+ node(c, s.id);
84
+ for (const sub of subs ?? [])
85
+ screen(sub, s.id);
86
+ };
87
+ for (const s of r.screens)
88
+ screen(s);
89
+ return { kinds: r.kinds, screens, components };
90
+ }
91
+ function screenOf(id, screens) {
92
+ let best;
93
+ for (const s of screens)
94
+ if (id.startsWith(s + ".") && (!best || s.length > best.length))
95
+ best = s;
96
+ return best;
97
+ }
98
+ function productLabel(seg, catalog) {
99
+ const names = new Set(catalog.operations.map((o) => o.product).filter((p) => !!p));
100
+ return [...names].find((n) => n.toLowerCase().replace(/[^a-z0-9]+/g, "-") === seg) ?? humanize(seg);
101
+ }
102
+ function fromFile(file, catalog) {
103
+ const issues = [];
104
+ const ids = file.screens.map((s) => s.id);
105
+ const screens = file.screens.map((s) => ({
106
+ id: s.id,
107
+ label: s.label,
108
+ description: s.description,
109
+ context: s.context ?? [],
110
+ components: [],
111
+ path: [productLabel(s.id.split(".")[0], catalog)],
112
+ parent: screenOf(s.id, ids)
113
+ }));
114
+ const byId = new Map(screens.map((s) => [s.id, s]));
115
+ const screenIds = new Set(ids);
116
+ for (const c of file.components) {
117
+ if (screenIds.has(c.id)) {
118
+ issues.push({ severity: "error", screen: c.id, component: c.id, code: "registry.id-clash", message: `"${c.id}" is both a screen and a component id.` });
119
+ continue;
120
+ }
121
+ const s = screenOf(c.id, ids);
122
+ if (!s) {
123
+ issues.push({ severity: "error", screen: "", component: c.id, code: "registry.no-screen", message: `"${c.id}" starts with no registered screen id.` });
124
+ continue;
125
+ }
126
+ byId.get(s).components.push({ bindId: c.id, kind: c.kind, label: c.label, parent: c.parent, info: c.info });
127
+ }
128
+ const index = {};
129
+ for (const b of file.bindings) {
130
+ const s = screenOf(b.component, ids);
131
+ if (!s) {
132
+ issues.push({ severity: "error", screen: "", component: b.component, slot: b.slot, code: "binding.no-screen", message: `Binding for "${b.component}" fits no screen.` });
133
+ continue;
134
+ }
135
+ (index[s] ??= []).push(b);
136
+ }
137
+ return {
138
+ fw: { descriptors: file.kinds, registry: { registryVersion: 1, screens }, catalog, glossaries: file.glossaries },
139
+ index,
140
+ issues
141
+ };
142
+ }
143
+
144
+ // src/server/handler.ts
145
+ var TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".map": "application/json", ".woff2": "font/woff2" };
146
+ var DIST = fileURLToPath(new URL(".", import.meta.url));
147
+ var APP_DIR = path.join(DIST, "app");
148
+ var OVERLAY_FILE = path.join(DIST, "overlay.js");
149
+ var isUrl = (s) => /^https?:\/\//i.test(s);
150
+ async function readDocs(src, base) {
151
+ if (isUrl(src)) {
152
+ const res = await fetch(src);
153
+ if (!res.ok)
154
+ throw new Error(`${src} returned HTTP ${res.status}.`);
155
+ return res.text();
156
+ }
157
+ if (!/\.(ya?ml|json)$/i.test(src))
158
+ throw new Error(`"${src}" is not a .yaml, .yml or .json file.`);
159
+ return fs.readFile(path.resolve(base, src), "utf8");
160
+ }
161
+ async function readBindings(file) {
162
+ const text = await fs.readFile(file, "utf8");
163
+ let raw;
164
+ try {
165
+ raw = JSON.parse(text);
166
+ } catch {
167
+ throw new Error("The bindings file is not valid JSON.");
168
+ }
169
+ if (typeof raw?.registry !== "string")
170
+ return parseBindingsFile(text);
171
+ const registry = parseRegistry(await fs.readFile(path.resolve(path.dirname(file), raw.registry), "utf8"));
172
+ return checkBindingsFile({ ...raw, ...registry });
173
+ }
174
+ async function saveBindings(file, merged) {
175
+ let out = merged;
176
+ if (merged.registry) {
177
+ const rest = { ...merged };
178
+ for (const k of REGISTRY_KEYS)
179
+ delete rest[k];
180
+ out = rest;
181
+ }
182
+ await writeFileAtomic(file, JSON.stringify(out, null, 2) + `
183
+ `);
184
+ }
185
+ async function writeFileAtomic(file, text) {
186
+ const tmp = `${file}.${process.pid}.tmp`;
187
+ await fs.writeFile(tmp, text, "utf8");
188
+ await fs.rename(tmp, file);
189
+ }
190
+ async function readBody(req, limit = 20 * 1024 * 1024) {
191
+ const chunks = [];
192
+ let size = 0;
193
+ for await (const c of req) {
194
+ size += c.length;
195
+ if (size > limit)
196
+ throw new Error("Body too large.");
197
+ chunks.push(c);
198
+ }
199
+ return Buffer.concat(chunks).toString("utf8");
200
+ }
201
+ function send(res, status, body, type = "text/plain; charset=utf-8") {
202
+ res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).end(body);
203
+ }
204
+ function createStudioHandler({ file, prefix, allowed, onSave }) {
205
+ const base = path.dirname(file);
206
+ return async (req, res) => {
207
+ const url = new URL(req.url ?? "/", "http://local");
208
+ if (prefix && url.pathname !== prefix && !url.pathname.startsWith(prefix + "/"))
209
+ return false;
210
+ const rel = url.pathname.slice(prefix.length) || "/";
211
+ try {
212
+ if (prefix && url.pathname === prefix) {
213
+ res.writeHead(302, { Location: prefix + "/" + url.search }).end();
214
+ return true;
215
+ }
216
+ if (rel.startsWith("/api/") && !allowed(req)) {
217
+ send(res, 403, "Forbidden.");
218
+ return true;
219
+ }
220
+ if (rel === "/api/bindings") {
221
+ if (req.method === "GET")
222
+ send(res, 200, JSON.stringify(await readBindings(file)), "application/json");
223
+ else if (req.method === "PUT") {
224
+ let merged;
225
+ try {
226
+ merged = parseBindingsFile(await readBody(req));
227
+ } catch (e) {
228
+ send(res, 400, e.message);
229
+ return true;
230
+ }
231
+ await saveBindings(file, merged);
232
+ send(res, 204, "");
233
+ onSave?.();
234
+ } else
235
+ send(res, 405, "Method not allowed.");
236
+ return true;
237
+ }
238
+ if (rel === "/api/openapi" && req.method === "GET") {
239
+ const src = url.searchParams.get("src") ?? "";
240
+ try {
241
+ send(res, 200, await readDocs(src, base), src.endsWith(".json") ? "application/json" : "text/yaml; charset=utf-8");
242
+ } catch (e) {
243
+ send(res, 502, e.message);
244
+ }
245
+ return true;
246
+ }
247
+ if (req.method !== "GET" && req.method !== "HEAD") {
248
+ send(res, 405, "Method not allowed.");
249
+ return true;
250
+ }
251
+ if (rel === "/overlay.js") {
252
+ send(res, 200, await fs.readFile(OVERLAY_FILE), TYPES[".js"]);
253
+ return true;
254
+ }
255
+ const target = path.resolve(APP_DIR, rel === "/" ? "index.html" : decodeURIComponent(rel.slice(1)));
256
+ if (!target.startsWith(APP_DIR + path.sep)) {
257
+ send(res, 404, "Not found.");
258
+ return true;
259
+ }
260
+ const data = await fs.readFile(target).catch(() => null);
261
+ if (!data) {
262
+ send(res, 404, "Not found.");
263
+ return true;
264
+ }
265
+ send(res, 200, data, TYPES[path.extname(target)] ?? "application/octet-stream");
266
+ } catch (e) {
267
+ send(res, 500, e.message);
268
+ }
269
+ return true;
270
+ };
271
+ }
272
+
273
+ // src/codegen/write.ts
274
+ import { promises as fs3 } from "node:fs";
275
+ import path3 from "node:path";
276
+
277
+ // src/framework/loadCatalog.ts
278
+ import { parse } from "yaml";
279
+
280
+ // src/framework/openapi.ts
281
+ var METHODS = ["get", "put", "post", "delete", "patch", "head", "options", "trace", "query"];
282
+ function hashString(s) {
283
+ let h = 2166136261;
284
+ for (let i = 0;i < s.length; i++) {
285
+ h ^= s.charCodeAt(i);
286
+ h = Math.imul(h, 16777619);
287
+ }
288
+ return (h >>> 0).toString(16).padStart(8, "0");
289
+ }
290
+ function pointerGet(doc, ref) {
291
+ if (!ref.startsWith("#/"))
292
+ return;
293
+ return ref.slice(2).split("/").map((k) => k.replace(/~1/g, "/").replace(/~0/g, "~")).reduce((o, k) => o == null ? undefined : o[k], doc);
294
+ }
295
+ function deref(doc, node, seen = []) {
296
+ if (Array.isArray(node))
297
+ return node.map((n) => deref(doc, n, seen));
298
+ if (!node || typeof node !== "object")
299
+ return node;
300
+ const n = node;
301
+ if (typeof n.$ref === "string") {
302
+ if (seen.includes(n.$ref) || seen.length > 24)
303
+ return { $ref: n.$ref };
304
+ const { $ref, ...rest } = n;
305
+ const target = deref(doc, pointerGet(doc, $ref), [...seen, $ref]);
306
+ return Object.keys(rest).length ? { ...target, ...deref(doc, rest, seen) } : target;
307
+ }
308
+ const out = {};
309
+ for (const [k, v] of Object.entries(n))
310
+ out[k] = deref(doc, v, seen);
311
+ return out;
312
+ }
313
+ function jsonContent(content) {
314
+ if (!content)
315
+ return;
316
+ const key = Object.keys(content).find((k) => /json/i.test(k)) ?? Object.keys(content)[0];
317
+ return key ? content[key]?.schema : undefined;
318
+ }
319
+ function intentOf(method, op) {
320
+ if (typeof op["x-binding-intent"] === "string")
321
+ return op["x-binding-intent"];
322
+ if (method === "get" || method === "head" || method === "query")
323
+ return "read";
324
+ if (["put", "patch", "delete"].includes(method))
325
+ return "write";
326
+ return;
327
+ }
328
+ function catalogFromOpenApi(input, source = {}) {
329
+ const doc = input;
330
+ if (!doc || typeof doc !== "object" || !doc.paths)
331
+ throw new Error("This file is not an OpenAPI document: it has no paths.");
332
+ const operations = [];
333
+ const productOf = new Map;
334
+ for (const g of doc["x-tagGroups"] ?? [])
335
+ g.tags?.forEach((t) => {
336
+ if (g.name)
337
+ productOf.set(t, g.name);
338
+ });
339
+ for (const [path, rawItem] of Object.entries(doc.paths)) {
340
+ const item = deref(doc, rawItem);
341
+ for (const method of METHODS) {
342
+ const op = item?.[method];
343
+ if (!op || !op.operationId)
344
+ continue;
345
+ const params = [];
346
+ for (const p of [...item.parameters ?? [], ...op.parameters ?? []]) {
347
+ if (!p?.name || !p?.in)
348
+ continue;
349
+ const pointer = `/${p.in}/${p.name}`;
350
+ const at = params.findIndex((x) => x.pointer === pointer);
351
+ const entry = { pointer, name: p.name, required: !!p.required || p.in === "path", schema: p.schema ?? {}, description: p.description };
352
+ if (at >= 0)
353
+ params[at] = entry;
354
+ else
355
+ params.push(entry);
356
+ }
357
+ for (const m of path.matchAll(/\{([^}]+)\}/g)) {
358
+ if (!params.some((x) => x.pointer === `/path/${m[1]}`))
359
+ params.unshift({ pointer: `/path/${m[1]}`, name: m[1], required: true, schema: { type: "string" } });
360
+ }
361
+ const bodySchema = jsonContent(op.requestBody?.content);
362
+ const responses = Object.entries(op.responses ?? {}).map(([status, r]) => ({ status, schema: jsonContent(r?.content) }));
363
+ const extensions = {};
364
+ for (const [k, v] of Object.entries(op))
365
+ if (k.startsWith("x-"))
366
+ extensions[k] = v;
367
+ operations.push({
368
+ id: op.operationId,
369
+ method: method.toUpperCase(),
370
+ path,
371
+ summary: op.summary,
372
+ description: op.description,
373
+ tags: op.tags ?? [],
374
+ product: op.tags?.map((t) => productOf.get(t)).find(Boolean),
375
+ intent: intentOf(method, op),
376
+ parameters: params,
377
+ body: bodySchema ? { required: !!op.requestBody?.required, schema: bodySchema } : undefined,
378
+ responses,
379
+ extensions
380
+ });
381
+ }
382
+ }
383
+ const body = JSON.stringify(operations);
384
+ return {
385
+ catalogVersion: 1,
386
+ source: { title: doc.info?.title ?? "API", version: doc.info?.version ?? "", url: source.url },
387
+ hash: hashString(body),
388
+ operations,
389
+ schemas: {}
390
+ };
391
+ }
392
+
393
+ // src/framework/loadCatalog.ts
394
+ class CatalogLoadError extends Error {
395
+ source;
396
+ constructor(message, source) {
397
+ super(message);
398
+ this.source = source;
399
+ this.name = "CatalogLoadError";
400
+ }
401
+ }
402
+ function catalogFromText(text, source) {
403
+ const body = text.trim();
404
+ if (!body)
405
+ throw new CatalogLoadError("Nothing to read: the text is empty.", source);
406
+ let doc;
407
+ try {
408
+ doc = body.startsWith("{") ? JSON.parse(body) : parse(body);
409
+ } catch {
410
+ throw new CatalogLoadError("The text is not valid JSON or YAML.", source);
411
+ }
412
+ try {
413
+ return catalogFromOpenApi(doc, source.includes("://") ? { url: source } : {});
414
+ } catch (e) {
415
+ throw new CatalogLoadError(e instanceof Error ? e.message : "The text is not an OpenAPI document.", source);
416
+ }
417
+ }
418
+
419
+ // src/framework/expr.ts
420
+ var cache = new Map;
421
+ function segments(path) {
422
+ return path.replace(/\[(\*|\d+)\]/g, ".$1").split(".").filter(Boolean);
423
+ }
424
+ function parse2(expr) {
425
+ const hit = cache.get(expr);
426
+ if (hit)
427
+ return hit;
428
+ const e = expr.trim();
429
+ let parsed;
430
+ if (e.startsWith("$")) {
431
+ const segs = segments(e);
432
+ const [h, id] = segs;
433
+ if (h === "$item")
434
+ parsed = { head: { type: "item" }, path: segs.slice(1) };
435
+ else if ((h === "$context" || h === "$steps") && id)
436
+ parsed = { head: { type: h === "$context" ? "context" : "steps", id }, path: segs.slice(2) };
437
+ else
438
+ throw new Error(`Unknown source "${h}" in "${expr}".`);
439
+ } else {
440
+ const colon = e.indexOf(":");
441
+ if (colon < 1)
442
+ throw new Error(`"${expr}" must read "<componentId>:<slot>".`);
443
+ const [slot, ...path] = segments(e.slice(colon + 1));
444
+ if (!slot)
445
+ throw new Error(`"${expr}" names no slot.`);
446
+ parsed = { head: { type: "slot", component: e.slice(0, colon), slot }, path };
447
+ }
448
+ cache.set(expr, parsed);
449
+ return parsed;
450
+ }
451
+
452
+ // src/framework/schema.ts
453
+ var obj = (s) => s && typeof s === "object" ? s : {};
454
+ function flat(s) {
455
+ const o = obj(s);
456
+ const parts = [o, ...(o.allOf ?? []).map(flat)];
457
+ if (parts.length === 1 && !o.oneOf && !o.anyOf)
458
+ return o;
459
+ const props = {};
460
+ const required = [];
461
+ parts.forEach((p) => {
462
+ Object.assign(props, p.properties ?? {});
463
+ required.push(...p.required ?? []);
464
+ });
465
+ const alt = (o.oneOf ?? o.anyOf)?.map(flat).find((x) => x.type !== "null");
466
+ return { ...alt ?? {}, ...o, properties: Object.keys(props).length ? props : alt?.properties, required };
467
+ }
468
+ var views = new WeakMap;
469
+
470
+ // src/framework/validate.ts
471
+ function validateRegistry(fw) {
472
+ const kinds = new Set(fw.descriptors.map((d) => d.kind));
473
+ const issues = [];
474
+ const ids = new Set(fw.registry.screens.map((s) => s.id));
475
+ const seen = new Set;
476
+ const screens = fw.registry.screens.map((S) => {
477
+ if (!ID.test(S.id)) {
478
+ issues.push({ severity: "error", screen: S.id, code: "registry.invalid-id", message: `Screen id "${S.id}" must be lowercase kebab-case segments joined by dots.` });
479
+ return { ...S, components: [] };
480
+ }
481
+ const components = S.components.filter((c) => {
482
+ if (!ID.test(c.bindId)) {
483
+ issues.push({ severity: "error", screen: S.id, component: c.bindId, code: "registry.invalid-id", message: `Component id "${c.bindId}" must be lowercase kebab-case segments joined by dots.` });
484
+ return false;
485
+ }
486
+ if (!kinds.has(c.kind)) {
487
+ issues.push({ severity: "error", screen: S.id, component: c.bindId, code: "registry.unknown-kind", message: `"${c.label}" uses ${c.kind}, which has no descriptor.` });
488
+ return false;
489
+ }
490
+ if (seen.has(c.bindId)) {
491
+ issues.push({ severity: "error", screen: S.id, component: c.bindId, code: "registry.duplicate-id", message: `Component id "${c.bindId}" is used twice.` });
492
+ return false;
493
+ }
494
+ seen.add(c.bindId);
495
+ return true;
496
+ }).filter((c) => !c.parent || seen.has(c.parent) || (issues.push({ severity: "error", screen: S.id, component: c.bindId, code: "registry.missing-parent", message: `"${c.label}" names a parent that is not on the screen.` }), false));
497
+ const parent = S.parent && ids.has(S.parent) ? S.parent : undefined;
498
+ return { ...S, components, parent };
499
+ });
500
+ return { registry: { ...fw.registry, screens }, issues };
501
+ }
502
+
503
+ // src/codegen/helpers.ts
504
+ var HELPERS = `import { useMutation, useQuery } from "@tanstack/react-query";
505
+
506
+ /** Data for a slot. \`bound\` is false when no API feeds it; keep the UI's own value then. */
507
+ export interface Data<T> { bound: boolean; data: T | undefined; loading: boolean; error: unknown }
508
+ /** An action. \`run\` rejects when a call fails; it resolves at once when no API is connected. */
509
+ export interface Action { bound: boolean; run: (payload?: any) => Promise<void>; running: boolean; error: unknown }
510
+ /** A per-item value, such as a cell in a row. */
511
+ export interface Cell<T> { bound: boolean; read: (item: any) => T | undefined }
512
+
513
+ export const UNBOUND_DATA: Data<never> = { bound: false, data: undefined, loading: false, error: undefined };
514
+ export const UNBOUND_ACTION: Action = { bound: false, run: async () => {}, running: false, error: undefined };
515
+ export const UNBOUND_CELL: Cell<never> = { bound: false, read: () => undefined };
516
+
517
+ export class ApiError extends Error {
518
+ constructor(readonly status: number, readonly data: unknown) { super(\`API call failed (\${status})\`); this.name = "ApiError"; }
519
+ }
520
+
521
+ /** Reads a path; "*" maps the rest of the path over a list. */
522
+ export function at(value: unknown, path: readonly string[]): any {
523
+ let v: any = value;
524
+ for (let i = 0; i < path.length; i++) {
525
+ if (v == null) return undefined;
526
+ if (path[i] === "*") return Array.isArray(v) ? v.map(x => at(x, path.slice(i + 1))) : undefined;
527
+ v = v[path[i]];
528
+ }
529
+ return v;
530
+ }
531
+
532
+ /** The body of a client response; throws on a non-2xx status. Passes through clients that return the body. */
533
+ export function ok(res: any): any {
534
+ if (res && typeof res === "object" && typeof res.status === "number" && "data" in res && "headers" in res) {
535
+ if (res.status < 200 || res.status >= 300) throw new ApiError(res.status, res.data);
536
+ return res.data;
537
+ }
538
+ return res;
539
+ }
540
+
541
+ /** True when every required input has a value. */
542
+ export const has = (inputs: Record<string, unknown>) => Object.values(inputs).every(v => v !== undefined);
543
+
544
+ /** Throws when a required input has no value. */
545
+ export function need(operation: string, inputs: Record<string, unknown>) {
546
+ const missing = Object.keys(inputs).filter(k => inputs[k] === undefined);
547
+ if (missing.length) throw new Error(\`\${operation}: missing \${missing.join(", ")}\`);
548
+ }
549
+
550
+ /** Drops undefined values. */
551
+ export const defined = (o: Record<string, unknown>): any => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
552
+
553
+ export const path = (v: unknown): any => (v === undefined ? undefined : encodeURIComponent(String(v)));
554
+
555
+ /** A body from a base value and [keys, value] entries; undefined values are skipped. */
556
+ export function assemble(base: unknown, entries: Array<[string[], unknown]>): any {
557
+ const out: any = base && typeof base === "object" ? { ...base } : {};
558
+ for (const [keys, value] of entries) {
559
+ if (value === undefined) continue;
560
+ let node = out;
561
+ keys.slice(0, -1).forEach(k => { node = node[k] = node[k] && typeof node[k] === "object" ? { ...node[k] } : {}; });
562
+ node[keys[keys.length - 1]] = value;
563
+ }
564
+ return out;
565
+ }
566
+
567
+ /** Adds mapped fields to each item, keeping the fields the API returned. */
568
+ export function mapItems(data: any, map: (item: any) => Record<string, unknown>): any {
569
+ const one = (item: any) => ({ ...(item && typeof item === "object" ? item : {}), ...map(item) });
570
+ return Array.isArray(data) ? data.map(one) : data == null ? data : one(data);
571
+ }
572
+
573
+ export const list = (v: unknown): unknown[] => (Array.isArray(v) ? v : v === undefined ? [] : [v]);
574
+
575
+ export function finish(failed: unknown[]) {
576
+ if (failed.length === 1) throw failed[0];
577
+ if (failed.length) throw new AggregateError(failed, \`\${failed.length} API calls failed\`);
578
+ }
579
+
580
+ export function useData<T>(key: readonly unknown[], fetch: () => Promise<unknown>, select: (data: any) => T, options: { enabled: boolean; everySeconds?: number }): Data<T> {
581
+ const q = useQuery({ queryKey: key, queryFn: fetch, select, enabled: options.enabled, refetchInterval: options.everySeconds ? options.everySeconds * 1000 : false });
582
+ return { bound: true, data: q.data, loading: q.isLoading, error: q.error ?? undefined };
583
+ }
584
+
585
+ export function useAction(run: (payload?: any) => Promise<void>): Action {
586
+ const m = useMutation({ mutationFn: run });
587
+ return { bound: true, run: payload => m.mutateAsync(payload), running: m.isPending, error: m.error ?? undefined };
588
+ }
589
+ `;
590
+
591
+ // src/codegen/generate.ts
592
+ var GENERATED_MARK = "@generated";
593
+ var HEADER = `// ${GENERATED_MARK} Do not edit: change the bindings and regenerate.
594
+ `;
595
+ var words = (s) => s.split(/[.\-_]+/).filter(Boolean);
596
+ var pascal = (s) => words(s).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
597
+ var camel = (s) => {
598
+ const p = pascal(s);
599
+ return p[0].toLowerCase() + p.slice(1);
600
+ };
601
+ var str = (v) => JSON.stringify(v);
602
+ var IDENT = /^[A-Za-z_$][\w$]*$/;
603
+ var prop = (k) => IDENT.test(k) ? k : str(k);
604
+ var orvalName = (id) => IDENT.test(id) ? id[0].toLowerCase() + id.slice(1) : camel(id);
605
+ function tsType(schema, many = false, depth = 0) {
606
+ const one = () => {
607
+ if (!schema || typeof schema !== "object" || depth > 3)
608
+ return "any";
609
+ const s = schema;
610
+ if (Array.isArray(s.enum) && s.enum.length && s.enum.every((v) => typeof v === "string"))
611
+ return s.enum.map(str).join(" | ");
612
+ switch (s.type) {
613
+ case "string":
614
+ return "string";
615
+ case "integer":
616
+ case "number":
617
+ return "number";
618
+ case "boolean":
619
+ return "boolean";
620
+ case "array":
621
+ return `Array<${tsType(s.items, false, depth + 1)}>`;
622
+ case "object": {
623
+ const props = Object.entries(s.properties ?? {});
624
+ if (!props.length)
625
+ return "Record<string, any>";
626
+ return `{ ${props.map(([k, v]) => `${prop(k)}?: ${tsType(v, false, depth + 1)}`).join("; ")}; [key: string]: any }`;
627
+ }
628
+ default:
629
+ return "any";
630
+ }
631
+ };
632
+ return many ? `Array<${one()}>` : one();
633
+ }
634
+ function compileExpr(expr, cx, scope, at) {
635
+ let p;
636
+ try {
637
+ p = parse2(expr);
638
+ } catch (e) {
639
+ cx.issue("expr.invalid", e.message, at);
640
+ return "undefined";
641
+ }
642
+ const path = p.path.map(str);
643
+ const h = p.head;
644
+ const read = (root, keys) => {
645
+ cx.use.add("at");
646
+ return `at(${root}, [${keys.join(", ")}])`;
647
+ };
648
+ if (h.type === "item") {
649
+ if (!scope.item) {
650
+ cx.issue("expr.no-item", `"${expr}" reads $item, which this slot does not have.`, at);
651
+ return "undefined";
652
+ }
653
+ return path.length ? read("item", path) : "item";
654
+ }
655
+ if (h.type === "context") {
656
+ if (!cx.screen.context.some((c) => c.id === h.id)) {
657
+ cx.issue("expr.unknown-context", `"${expr}": ${cx.screen.id} declares no context "${h.id}".`, at);
658
+ return "undefined";
659
+ }
660
+ return read("state.context", [str(h.id), ...path]);
661
+ }
662
+ if (h.type === "steps") {
663
+ if (!scope.steps?.has(h.id)) {
664
+ cx.issue("expr.unknown-step", `"${expr}" reads step "${h.id}", which does not run before it.`, at);
665
+ return "undefined";
666
+ }
667
+ return read("steps", [str(h.id), ...path]);
668
+ }
669
+ if (!cx.produces.get(h.component)?.has(h.slot)) {
670
+ cx.issue("expr.unknown-slot", `"${expr}" reads a value that is not produced on ${cx.screen.id}.`, at);
671
+ return "undefined";
672
+ }
673
+ return read("state", [str(cx.keys.get(h.component)), str(h.slot), ...path]);
674
+ }
675
+ var decode = (k) => k.replace(/~1/g, "/").replace(/~0/g, "~");
676
+ function compileCall(op, request, cx, scope, at) {
677
+ const e = (x) => compileExpr(x, cx, scope, at);
678
+ const known = new Set(op.parameters.map((p) => p.pointer));
679
+ const body = [];
680
+ let whole;
681
+ const query = [];
682
+ const headers = [];
683
+ for (const [pointer, x] of Object.entries(request)) {
684
+ const [, where, ...rest] = pointer.split("/");
685
+ const keys = rest.map(decode);
686
+ if (where === "body") {
687
+ if (!op.body) {
688
+ cx.issue("request.no-body", `${op.id} takes no body, but "${pointer}" is set.`, at);
689
+ continue;
690
+ }
691
+ if (!keys.length)
692
+ whole = e(x);
693
+ else
694
+ body.push(`[[${keys.map(str).join(", ")}], ${e(x)}]`);
695
+ } else if (!known.has(pointer))
696
+ cx.issue("request.unknown-input", `${op.id} has no input "${pointer}".`, at);
697
+ else if (where === "query")
698
+ query.push(`${prop(keys[0])}: ${e(x)}`);
699
+ else if (where === "header")
700
+ headers.push(`${str(keys[0])}: ${e(x)}`);
701
+ }
702
+ const args = [...op.path.matchAll(/\{([^}]+)\}/g)].map((m) => {
703
+ const x = request[`/path/${m[1]}`];
704
+ if (x === undefined) {
705
+ cx.issue("request.missing-path", `${op.id} needs "${m[1]}" in its path; it is not connected.`, at);
706
+ return "undefined";
707
+ }
708
+ cx.use.add("path");
709
+ return `path(${e(x)})`;
710
+ });
711
+ if (op.body) {
712
+ const set = new Set(Object.keys(request).filter((k) => k.startsWith("/body/")).map((k) => decode(k.split("/")[2])));
713
+ const schema = flat(op.body.schema);
714
+ const unset = whole ? [] : (schema.required ?? []).filter((f) => !set.has(f));
715
+ const empty = unset.filter((f) => {
716
+ const p = flat(schema.properties?.[f]);
717
+ return p.type === "object" && !p.required?.length;
718
+ });
719
+ const gaps = unset.filter((f) => !empty.includes(f));
720
+ if (gaps.length)
721
+ cx.warn("request.required-body", `${op.id} requires ${gaps.join(", ")} in its body; not connected.`, at);
722
+ cx.use.add("assemble");
723
+ const base = whole ?? (empty.length ? `{ ${empty.map((f) => `${prop(f)}: {}`).join(", ")} }` : "undefined");
724
+ args.push(`assemble(${base}, [${body.join(", ")}])`);
725
+ }
726
+ if (op.parameters.some((p) => p.pointer.startsWith("/query/"))) {
727
+ cx.use.add("defined");
728
+ args.push(`defined({ ${query.join(", ")} })`);
729
+ }
730
+ const key = `[${[str(op.id), ...args].join(", ")}]`;
731
+ if (headers.length) {
732
+ cx.use.add("defined");
733
+ args.push(`{ headers: defined({ ${headers.join(", ")} }) }`);
734
+ }
735
+ const required = op.parameters.filter((p) => p.required).map((p) => `${prop(p.name)}: ${request[p.pointer] === undefined ? "undefined" : e(request[p.pointer])}`);
736
+ return { fn: orvalName(op.id), call: `${orvalName(op.id)}(${args.join(", ")})`, key, required: required.length ? `{ ${required.join(", ")} }` : "" };
737
+ }
738
+ var selectPath = (selector) => segments(selector.replace(/^\$\.?/, "").replace(/\[\*\]$/, ""));
739
+ function compileSteps(steps, cx, at, ops, imports) {
740
+ const done = new Set;
741
+ const blocks = [];
742
+ let usesPayload = false;
743
+ cx.use.add("ok");
744
+ cx.use.add("finish");
745
+ for (const st of steps) {
746
+ const op = ops.get(st.operation);
747
+ if (!op) {
748
+ cx.issue("catalog.operation-missing", `${st.operation} is not in the API docs.`, at);
749
+ continue;
750
+ }
751
+ const scope = { item: true, steps: done };
752
+ const call = compileCall(op, st.request, cx, scope, at);
753
+ imports.add(call.fn);
754
+ const each = st.forEach ? compileExpr(st.forEach, cx, scope, at) : undefined;
755
+ if (each)
756
+ cx.use.add("list");
757
+ else
758
+ usesPayload = true;
759
+ if (call.required)
760
+ cx.use.add("need");
761
+ const readsItem = /\bitem\b/.test(call.call + call.required);
762
+ blocks.push([
763
+ ` {`,
764
+ ` const responses: unknown[] = [];`,
765
+ ` for (const item of ${each ? `list(${each})` : "[payload]"}) {`,
766
+ readsItem ? "" : ` void item;`,
767
+ ` try {`,
768
+ call.required ? ` need(${str(op.id)}, ${call.required});` : "",
769
+ ` responses.push(ok(await ${call.call}));`,
770
+ ` } catch (error) {`,
771
+ st.onError === "stop" ? ` throw error;` : ` failed.push(error);`,
772
+ ` }`,
773
+ ` }`,
774
+ ` steps[${str(st.id)}] = { response: ${each ? "responses" : "responses[0]"} };`,
775
+ ` }`
776
+ ].filter(Boolean).join(`
777
+ `));
778
+ done.add(st.id);
779
+ }
780
+ return [
781
+ `async (${usesPayload ? "payload" : "_payload"}?: any) => {`,
782
+ ` const steps: Record<string, unknown> = {};`,
783
+ ` const failed: unknown[] = [];`,
784
+ ...blocks,
785
+ ` finish(failed);`,
786
+ ` }`
787
+ ].join(`
788
+ `);
789
+ }
790
+ function generate(file, catalog) {
791
+ const issues = [];
792
+ const { fw, index, issues: fileIssues } = fromFile(file, catalog);
793
+ const valid = validateRegistry(fw);
794
+ issues.push(...fileIssues, ...valid.issues);
795
+ const ops = new Map(catalog.operations.map((o) => [o.id, o]));
796
+ const DESC = new Map(fw.descriptors.map((d) => [d.kind, d]));
797
+ const client = file.output?.client;
798
+ if (!client)
799
+ issues.push({ severity: "error", screen: "", code: "output.missing", message: `Set "output": { "dir", "client" } in the bindings file.` });
800
+ const files = [{ path: "screen-helpers.ts", content: HEADER + HELPERS }];
801
+ const exports = [];
802
+ for (const screen of valid.registry.screens) {
803
+ const name = pascal(screen.id);
804
+ const keys = new Map;
805
+ const taken = new Map;
806
+ for (const c of screen.components) {
807
+ const k = camel(c.bindId.slice(screen.id.length + 1));
808
+ if (taken.has(k))
809
+ issues.push({ severity: "error", screen: screen.id, component: c.bindId, code: "codegen.name-clash", message: `"${c.bindId}" and "${taken.get(k)}" both become "${k}".` });
810
+ taken.set(k, c.bindId);
811
+ keys.set(c.bindId, k);
812
+ }
813
+ const cx = {
814
+ screen,
815
+ keys,
816
+ use: new Set,
817
+ 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))])),
818
+ issue: (code, message, at) => issues.push({ severity: "error", screen: screen.id, code, message, ...at }),
819
+ warn: (code, message, at) => issues.push({ severity: "warning", screen: screen.id, code, message, ...at })
820
+ };
821
+ const bindings = new Map;
822
+ for (const b of index[screen.id] ?? []) {
823
+ if (!keys.has(b.component)) {
824
+ cx.issue("binding.unknown-component", `A binding names "${b.component}", which is not registered.`, b);
825
+ continue;
826
+ }
827
+ if (b.state !== "confirmed") {
828
+ 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.` });
829
+ continue;
830
+ }
831
+ bindings.set(`${b.component}|${b.slot}`, b);
832
+ }
833
+ const imports = new Set;
834
+ const types = new Set;
835
+ const body = [];
836
+ const ret = [];
837
+ const apiType = [];
838
+ const stateType = [];
839
+ for (const c of screen.components) {
840
+ const d = DESC.get(c.kind);
841
+ const k = keys.get(c.bindId);
842
+ const produce = d.slots.filter((s) => s.role === "produce");
843
+ if (produce.length)
844
+ stateType.push(` ${prop(k)}?: { ${produce.map((s) => `${prop(s.id)}?: ${tsType(s.schema, s.many)}`).join("; ")} };`);
845
+ const out = [];
846
+ const outT = [];
847
+ const unbound = (slot, v) => {
848
+ cx.use.add(v);
849
+ out.push(`${prop(slot)}: ${v}`);
850
+ };
851
+ for (const s of d.slots.filter((x) => x.role !== "produce")) {
852
+ const b = bindings.get(`${c.bindId}|${s.id}`);
853
+ const at = { component: c.bindId, slot: s.id };
854
+ const v = `${k}${pascal(s.id)}`;
855
+ if (s.role === "trigger") {
856
+ types.add("Action");
857
+ outT.push(`${prop(s.id)}: Action`);
858
+ if (b?.type !== "steps") {
859
+ unbound(s.id, "UNBOUND_ACTION");
860
+ continue;
861
+ }
862
+ cx.use.add("useAction");
863
+ body.push(` const ${v} = useAction(${compileSteps(b.steps, cx, at, ops, imports)});`);
864
+ out.push(`${prop(s.id)}: ${v}`);
865
+ continue;
866
+ }
867
+ const T = tsType(s.schema, s.many);
868
+ if (s.scope) {
869
+ types.add("Cell");
870
+ outT.push(`${prop(s.id)}: Cell<${T}>`);
871
+ if (b?.type !== "expression") {
872
+ unbound(s.id, "UNBOUND_CELL");
873
+ continue;
874
+ }
875
+ out.push(`${prop(s.id)}: { bound: true, read: (item: any) => ${compileExpr(b.value, cx, { item: true }, at)} }`);
876
+ continue;
877
+ }
878
+ types.add("Data");
879
+ outT.push(`${prop(s.id)}: Data<${T}>`);
880
+ if (b?.type === "expression") {
881
+ out.push(`${prop(s.id)}: { bound: true, data: ${compileExpr(b.value, cx, { item: false }, at)}, loading: false, error: undefined }`);
882
+ continue;
883
+ }
884
+ const op = b?.type === "operation" ? ops.get(b.operation) : undefined;
885
+ if (b?.type === "operation" && !op)
886
+ cx.issue("catalog.operation-missing", `${b.operation} is not in the API docs.`, at);
887
+ if (b?.type !== "operation" || !op) {
888
+ unbound(s.id, "UNBOUND_DATA");
889
+ continue;
890
+ }
891
+ const call = compileCall(op, b.request, cx, { item: false }, at);
892
+ imports.add(call.fn);
893
+ ["useData", "ok"].forEach((h) => cx.use.add(h));
894
+ if (call.required)
895
+ cx.use.add("has");
896
+ const sel = selectPath(b.select).map(str);
897
+ if (sel.length)
898
+ cx.use.add("at");
899
+ const picked = sel.length ? `at(data, [${sel.join(", ")}])` : "data";
900
+ const map = Object.entries(b.map ?? {});
901
+ if (map.length)
902
+ cx.use.add("mapItems");
903
+ const select = map.length ? `mapItems(${picked}, (item: any) => ({ ${map.map(([f, x]) => `${prop(f)}: ${compileExpr(x, cx, { item: true }, at)}`).join(", ")} }))` : picked;
904
+ const every = typeof b.load === "object" ? `, everySeconds: ${b.load.everySeconds}` : "";
905
+ body.push(` const ${v} = useData<${T}>(${call.key}, async () => ok(await ${call.call}), (data: any) => ${select}, { enabled: ${call.required ? `has(${call.required})` : "true"}${every} });`);
906
+ out.push(`${prop(s.id)}: ${v}`);
907
+ }
908
+ if (out.length) {
909
+ ret.push(` ${prop(k)}: { ${out.join(", ")} },`);
910
+ apiType.push(` ${prop(k)}: { ${outT.join("; ")} };`);
911
+ }
912
+ }
913
+ if (screen.context.length)
914
+ 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("; ")} };`);
915
+ const usesState = [...body, ...ret].some((l) => /\bstate\b/.test(l));
916
+ const lines = [
917
+ HEADER.trimEnd(),
918
+ imports.size ? `import { ${[...imports].sort().join(", ")} } from ${str(client ?? "")};` : null,
919
+ cx.use.size ? `import { ${[...cx.use].sort().join(", ")} } from "./screen-helpers";` : null,
920
+ types.size ? `import type { ${[...types].sort().join(", ")} } from "./screen-helpers";` : null,
921
+ "",
922
+ `/** ${screen.label}: values its components produce, and what it was opened with. */`,
923
+ stateType.length ? `export interface ${name}State {
924
+ ${stateType.join(`
925
+ `)}
926
+ }` : `export type ${name}State = Record<string, never>;`,
927
+ "",
928
+ `export interface ${name}Api {
929
+ ${apiType.join(`
930
+ `)}
931
+ }`,
932
+ "",
933
+ `/** API data and actions for ${screen.label} (${screen.id}). */`,
934
+ `export function use${name}Api(${usesState ? "state" : "_state"}: ${name}State = {}): ${name}Api {`,
935
+ ...body,
936
+ ` return {`,
937
+ ...ret,
938
+ ` };`,
939
+ `}`,
940
+ ""
941
+ ].filter((l) => l !== null);
942
+ files.push({ path: `${screen.id}.ts`, content: lines.join(`
943
+ `) });
944
+ exports.push(`export * from "./${screen.id}";`);
945
+ }
946
+ files.push({ path: "index.ts", content: HEADER + exports.join(`
947
+ `) + `
948
+ ` });
949
+ return { files, issues };
950
+ }
951
+
952
+ // src/cli/scan.ts
953
+ import { promises as fs2 } from "node:fs";
954
+ import path2 from "node:path";
955
+ var LITERAL = /componentId\s*(?:=\s*\{?\s*|:\s*)(["'`])([a-z0-9.-]+)\1/g;
956
+ var SKIP = new Set(["node_modules", "dist", "build", ".git", ".next", ".output"]);
957
+ async function* sources(dir) {
958
+ for (const e of await fs2.readdir(dir, { withFileTypes: true })) {
959
+ if (SKIP.has(e.name))
960
+ continue;
961
+ const p = path2.join(dir, e.name);
962
+ if (e.isDirectory())
963
+ yield* sources(p);
964
+ else if (/\.(tsx?|jsx?)$/.test(e.name))
965
+ yield p;
966
+ }
967
+ }
968
+ async function scanIds(dir, file) {
969
+ const found = new Map;
970
+ for await (const p of sources(dir))
971
+ for (const m of (await fs2.readFile(p, "utf8")).matchAll(LITERAL))
972
+ if (!found.has(m[2]))
973
+ found.set(m[2], path2.relative(dir, p));
974
+ const registered = new Set(file.components.map((c) => c.id));
975
+ const issues = [];
976
+ for (const [id, where] of found)
977
+ if (!registered.has(id))
978
+ issues.push({ severity: "warning", screen: "", component: id, code: "scan.unregistered", message: `In ${where}, but not registered.` });
979
+ for (const id of registered)
980
+ if (!found.has(id))
981
+ issues.push({ severity: "warning", screen: "", component: id, code: "scan.not-found", message: "Registered, but no componentId literal in the source (fine if the id is built from config)." });
982
+ return issues;
983
+ }
984
+
985
+ // src/codegen/write.ts
986
+ async function writeScreens(filePath, opts = {}) {
987
+ const base = path3.dirname(filePath);
988
+ const file = await readBindings(filePath);
989
+ const catalog = catalogFromText(await readDocs(file.openapi, base), file.openapi);
990
+ const { files, issues } = generate(file, catalog);
991
+ if (opts.src)
992
+ issues.push(...await scanIds(path3.resolve(opts.src), file));
993
+ const out = path3.resolve(base, file.output?.dir ?? ".");
994
+ const result = { issues, out, screens: files.length - 2, changed: [], stale: [] };
995
+ if (issues.some((i) => i.severity === "error"))
996
+ return result;
997
+ await fs3.mkdir(out, { recursive: true });
998
+ const wanted = new Map(files.map((f) => [path3.join(out, f.path), f.content]));
999
+ for (const [p, content] of wanted)
1000
+ if (await fs3.readFile(p, "utf8").catch(() => null) !== content)
1001
+ result.changed.push(p);
1002
+ for (const n of await fs3.readdir(out)) {
1003
+ const p = path3.join(out, n);
1004
+ if (n.endsWith(".ts") && !wanted.has(p) && (await fs3.readFile(p, "utf8")).startsWith(`// ${GENERATED_MARK}`))
1005
+ result.stale.push(p);
1006
+ }
1007
+ if (opts.check)
1008
+ return result;
1009
+ for (const p of result.changed)
1010
+ await writeFileAtomic(p, wanted.get(p));
1011
+ for (const p of result.stale)
1012
+ await fs3.unlink(p);
1013
+ return result;
1014
+ }
1015
+ var describeIssue = (i) => `${i.severity === "error" ? "error " : "warning"} ${[i.component ?? i.screen, i.slot].filter(Boolean).join(" · ")}: ${i.message}`;
1016
+
1017
+ // src/vite/plugin.ts
1018
+ function injectOverlay(res, tag) {
1019
+ let done = false;
1020
+ const isHtml = () => String(res.getHeader("content-type") ?? "").includes("text/html");
1021
+ const patch = (chunk) => {
1022
+ if (done || !isHtml())
1023
+ return chunk;
1024
+ const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : null;
1025
+ if (text === null)
1026
+ return chunk;
1027
+ if (text.includes(tag)) {
1028
+ done = true;
1029
+ return chunk;
1030
+ }
1031
+ const i = text.lastIndexOf("</body>");
1032
+ if (i < 0)
1033
+ return chunk;
1034
+ done = true;
1035
+ if (!res.headersSent)
1036
+ res.removeHeader("content-length");
1037
+ return text.slice(0, i) + tag + text.slice(i);
1038
+ };
1039
+ const write = res.write.bind(res);
1040
+ const end = res.end.bind(res);
1041
+ res.write = (chunk, ...rest) => write(patch(chunk), ...rest);
1042
+ res.end = (chunk, ...rest) => chunk == null || typeof chunk === "function" ? end(chunk, ...rest) : end(patch(chunk), ...rest);
1043
+ }
1044
+ async function regenerate(file, log = console.log) {
1045
+ try {
1046
+ const r = await writeScreens(file);
1047
+ for (const i of r.issues)
1048
+ if (i.severity === "error")
1049
+ log(`[api-binding] ${describeIssue(i)}`);
1050
+ if (r.issues.some((i) => i.severity === "error"))
1051
+ log("[api-binding] Screen hooks not regenerated: fix the errors above.");
1052
+ else if (r.changed.length || r.stale.length)
1053
+ log(`[api-binding] Screen hooks regenerated: ${r.changed.length} written, ${r.stale.length} removed.`);
1054
+ } catch (e) {
1055
+ log(`[api-binding] Screen hooks not regenerated: ${e.message}`);
1056
+ }
1057
+ }
1058
+ function bindingStudio(options = {}) {
1059
+ const base = (options.base ?? "/__binding").replace(/\/+$/, "");
1060
+ const tag = `<script type="module" src="${base}/overlay.js"></script>`;
1061
+ let file = "";
1062
+ const norm = (s) => process.platform === "win32" ? path4.resolve(s).toLowerCase() : path4.resolve(s);
1063
+ const isBindingsFile = (p) => {
1064
+ const abs = norm(p), f = norm(file);
1065
+ return !!file && (abs === f || abs.startsWith(f + ".") && abs.endsWith(".tmp"));
1066
+ };
1067
+ const isRegistryFile = (p) => {
1068
+ try {
1069
+ const registry = JSON.parse(readFileSync(file, "utf8")).registry;
1070
+ return typeof registry === "string" && norm(p) === norm(path4.resolve(path4.dirname(file), registry));
1071
+ } catch {
1072
+ return false;
1073
+ }
1074
+ };
1075
+ return {
1076
+ name: "api-binding-studio",
1077
+ apply: "serve",
1078
+ configResolved(config) {
1079
+ file = path4.resolve(config.root, options.file ?? "bindings.json");
1080
+ },
1081
+ transformIndexHtml: (html) => html.includes(tag) ? html : html.replace(/<\/body>/i, `${tag}</body>`),
1082
+ config: () => ({ server: { watch: { ignored: [(p) => isBindingsFile(p)] } } }),
1083
+ configureServer(server) {
1084
+ const allowed = (req) => {
1085
+ const origin = req.headers.origin;
1086
+ return req.method === "GET" || !origin || new URL(origin).host === req.headers.host;
1087
+ };
1088
+ let queue = Promise.resolve();
1089
+ const onSave = () => {
1090
+ queue = queue.then(() => regenerate(file));
1091
+ };
1092
+ const handle = createStudioHandler({ file, prefix: base, allowed, onSave });
1093
+ server.watcher?.on("change", (p) => {
1094
+ if (p.endsWith(".json") && isRegistryFile(p))
1095
+ onSave();
1096
+ });
1097
+ server.middlewares.use((req, res, next) => {
1098
+ handle(req, res).then((handled) => {
1099
+ if (handled)
1100
+ return;
1101
+ if ((req.headers.accept ?? "").includes("text/html"))
1102
+ injectOverlay(res, tag);
1103
+ next();
1104
+ }, next);
1105
+ });
1106
+ }
1107
+ };
1108
+ }
1109
+ export {
1110
+ bindingStudio,
1111
+ injectOverlay,
1112
+ regenerate
1113
+ };