api_binding_ui 3.1.0 → 3.3.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.
@@ -2,6 +2,6 @@
2
2
  <html lang="en">
3
3
  <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Bind API</title>
4
4
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500;600&display=swap" />
5
- <style>html, body, #root { height: 100%; margin: 0; overflow: hidden; background: #f8fafc; }</style><link rel="stylesheet" crossorigin href="./index-aaxy714b.css"><script type="module" crossorigin src="./index-4mgcb4wd.js"></script></head>
5
+ <style>html, body, #root { height: 100%; margin: 0; overflow: hidden; background: #f8fafc; }</style><link rel="stylesheet" crossorigin href="./index-aaxy714b.css"><script type="module" crossorigin src="./index-xww2x0kd.js"></script></head>
6
6
  <body><div id="root"></div></body>
7
7
  </html>
@@ -0,0 +1,2 @@
1
+ import type { BindingsFile, ValidationIssue } from "../framework/contract";
2
+ export declare function scanIds(dir: string, file: BindingsFile): Promise<ValidationIssue[]>;
package/dist/cli.js CHANGED
@@ -2,98 +2,12 @@
2
2
 
3
3
  // src/cli/cli.ts
4
4
  import { createServer } from "node:http";
5
+ import path4 from "node:path";
6
+
7
+ // src/codegen/write.ts
5
8
  import { promises as fs3 } from "node:fs";
6
9
  import path3 from "node:path";
7
10
 
8
- // src/framework/names.ts
9
- function humanize(id) {
10
- const s = id.replace(/^\$\.?/, "").replace(/\[\*\]/g, "").split(/[./]/).filter(Boolean).pop() ?? id;
11
- const w = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
12
- return w ? w[0].toUpperCase() + w.slice(1) : id;
13
- }
14
-
15
- // src/framework/file.ts
16
- var ID = /^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/;
17
- var BINDING_TYPES = new Set(["operation", "expression", "steps", "none"]);
18
- function parseBindingsFile(text) {
19
- let data;
20
- try {
21
- data = JSON.parse(text);
22
- } catch {
23
- throw new Error("The bindings file is not valid JSON.");
24
- }
25
- const f = data;
26
- if (!f || typeof f !== "object")
27
- throw new Error("The bindings file must be a JSON object.");
28
- if (f.version !== 3)
29
- throw new Error(`The bindings file must have "version": 3.`);
30
- if (typeof f.openapi !== "string" || !f.openapi)
31
- throw new Error(`The bindings file needs "openapi": the docs URL or a path.`);
32
- for (const k of ["kinds", "screens", "components", "bindings"])
33
- if (!Array.isArray(f[k]))
34
- throw new Error(`"${k}" must be a list.`);
35
- f.bindings.forEach((b, i) => {
36
- const x = b;
37
- if (!x || typeof x.component !== "string" || typeof x.slot !== "string" || !BINDING_TYPES.has(x.type))
38
- throw new Error(`Binding ${i + 1} needs "component", "slot" and a known "type".`);
39
- });
40
- if (f.output && (typeof f.output.dir !== "string" || typeof f.output.client !== "string"))
41
- throw new Error(`"output" needs "dir" and "client".`);
42
- return f;
43
- }
44
- function screenOf(id, screens) {
45
- let best;
46
- for (const s of screens)
47
- if (id.startsWith(s + ".") && (!best || s.length > best.length))
48
- best = s;
49
- return best;
50
- }
51
- function productLabel(seg, catalog) {
52
- const names = new Set(catalog.operations.map((o) => o.product).filter((p) => !!p));
53
- return [...names].find((n) => n.toLowerCase().replace(/[^a-z0-9]+/g, "-") === seg) ?? humanize(seg);
54
- }
55
- function fromFile(file, catalog) {
56
- const issues = [];
57
- const ids = file.screens.map((s) => s.id);
58
- const screens = file.screens.map((s) => ({
59
- id: s.id,
60
- label: s.label,
61
- description: s.description,
62
- context: s.context ?? [],
63
- components: [],
64
- path: [productLabel(s.id.split(".")[0], catalog)],
65
- parent: screenOf(s.id, ids)
66
- }));
67
- const byId = new Map(screens.map((s) => [s.id, s]));
68
- const screenIds = new Set(ids);
69
- for (const c of file.components) {
70
- if (screenIds.has(c.id)) {
71
- 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.` });
72
- continue;
73
- }
74
- const s = screenOf(c.id, ids);
75
- if (!s) {
76
- issues.push({ severity: "error", screen: "", component: c.id, code: "registry.no-screen", message: `"${c.id}" starts with no registered screen id.` });
77
- continue;
78
- }
79
- byId.get(s).components.push({ bindId: c.id, kind: c.kind, label: c.label, parent: c.parent, info: c.info });
80
- }
81
- const index = {};
82
- for (const b of file.bindings) {
83
- const s = screenOf(b.component, ids);
84
- if (!s) {
85
- issues.push({ severity: "error", screen: "", component: b.component, slot: b.slot, code: "binding.no-screen", message: `Binding for "${b.component}" fits no screen.` });
86
- continue;
87
- }
88
- (index[s] ??= []).push(b);
89
- }
90
- return {
91
- fw: { descriptors: file.kinds, registry: { registryVersion: 1, screens }, catalog, glossaries: file.glossaries },
92
- index,
93
- issues
94
- };
95
- }
96
-
97
11
  // src/framework/loadCatalog.ts
98
12
  import { parse } from "yaml";
99
13
 
@@ -269,6 +183,140 @@ function parse2(expr) {
269
183
  return parsed;
270
184
  }
271
185
 
186
+ // src/framework/names.ts
187
+ function humanize(id) {
188
+ const s = id.replace(/^\$\.?/, "").replace(/\[\*\]/g, "").split(/[./]/).filter(Boolean).pop() ?? id;
189
+ const w = s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
190
+ return w ? w[0].toUpperCase() + w.slice(1) : id;
191
+ }
192
+
193
+ // src/framework/file.ts
194
+ var ID = /^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/;
195
+ var BINDING_TYPES = new Set(["operation", "expression", "steps", "none"]);
196
+ function parseBindingsFile(text) {
197
+ let data;
198
+ try {
199
+ data = JSON.parse(text);
200
+ } catch {
201
+ throw new Error("The bindings file is not valid JSON.");
202
+ }
203
+ return checkBindingsFile(data);
204
+ }
205
+ function checkBindingsFile(data) {
206
+ const f = data;
207
+ if (!f || typeof f !== "object")
208
+ throw new Error("The bindings file must be a JSON object.");
209
+ if (f.version !== 3)
210
+ throw new Error(`The bindings file must have "version": 3.`);
211
+ if (typeof f.openapi !== "string" || !f.openapi)
212
+ throw new Error(`The bindings file needs "openapi": the docs URL or a path.`);
213
+ for (const k of ["kinds", "screens", "components", "bindings"])
214
+ if (!Array.isArray(f[k]))
215
+ throw new Error(`"${k}" must be a list.`);
216
+ f.bindings.forEach((b, i) => {
217
+ const x = b;
218
+ if (!x || typeof x.component !== "string" || typeof x.slot !== "string" || !BINDING_TYPES.has(x.type))
219
+ throw new Error(`Binding ${i + 1} needs "component", "slot" and a known "type".`);
220
+ });
221
+ if (f.output && (typeof f.output.dir !== "string" || typeof f.output.client !== "string"))
222
+ throw new Error(`"output" needs "dir" and "client".`);
223
+ return f;
224
+ }
225
+ var REGISTRY_KEYS = ["kinds", "screens", "components"];
226
+ function parseRegistry(text) {
227
+ let r;
228
+ try {
229
+ r = JSON.parse(text);
230
+ } catch {
231
+ throw new Error("The registry file is not valid JSON.");
232
+ }
233
+ if (!r || r.version !== 1)
234
+ throw new Error(`The registry file must have "version": 1.`);
235
+ if (!Array.isArray(r.kinds) || !Array.isArray(r.screens))
236
+ throw new Error(`The registry file needs "kinds" and "screens" lists.`);
237
+ const screens = [];
238
+ const components = [];
239
+ const under = (id, owner, what) => {
240
+ if (typeof id !== "string" || !id)
241
+ throw new Error(`A ${what} in the registry has no "id".`);
242
+ if (owner && !id.startsWith(owner + "."))
243
+ throw new Error(`"${id}" must start with "${owner}.", the id it is nested under.`);
244
+ };
245
+ const node = (n, owner, parent) => {
246
+ under(n.id, owner, "component");
247
+ if (typeof n.kind !== "string" || typeof n.label !== "string")
248
+ throw new Error(`"${n.id}" needs "kind" and "label".`);
249
+ const { children, ...c } = n;
250
+ components.push(parent ? { ...c, parent } : c);
251
+ for (const ch of children ?? [])
252
+ node(ch, n.id, n.id);
253
+ };
254
+ const screen = (s, owner) => {
255
+ under(s.id, owner, "screen");
256
+ const { components: cs, screens: subs, ...rest } = s;
257
+ screens.push(rest);
258
+ for (const c of cs ?? [])
259
+ node(c, s.id);
260
+ for (const sub of subs ?? [])
261
+ screen(sub, s.id);
262
+ };
263
+ for (const s of r.screens)
264
+ screen(s);
265
+ return { kinds: r.kinds, screens, components };
266
+ }
267
+ function screenOf(id, screens) {
268
+ let best;
269
+ for (const s of screens)
270
+ if (id.startsWith(s + ".") && (!best || s.length > best.length))
271
+ best = s;
272
+ return best;
273
+ }
274
+ function productLabel(seg, catalog) {
275
+ const names = new Set(catalog.operations.map((o) => o.product).filter((p) => !!p));
276
+ return [...names].find((n) => n.toLowerCase().replace(/[^a-z0-9]+/g, "-") === seg) ?? humanize(seg);
277
+ }
278
+ function fromFile(file, catalog) {
279
+ const issues = [];
280
+ const ids = file.screens.map((s) => s.id);
281
+ const screens = file.screens.map((s) => ({
282
+ id: s.id,
283
+ label: s.label,
284
+ description: s.description,
285
+ context: s.context ?? [],
286
+ components: [],
287
+ path: [productLabel(s.id.split(".")[0], catalog)],
288
+ parent: screenOf(s.id, ids)
289
+ }));
290
+ const byId = new Map(screens.map((s) => [s.id, s]));
291
+ const screenIds = new Set(ids);
292
+ for (const c of file.components) {
293
+ if (screenIds.has(c.id)) {
294
+ 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.` });
295
+ continue;
296
+ }
297
+ const s = screenOf(c.id, ids);
298
+ if (!s) {
299
+ issues.push({ severity: "error", screen: "", component: c.id, code: "registry.no-screen", message: `"${c.id}" starts with no registered screen id.` });
300
+ continue;
301
+ }
302
+ byId.get(s).components.push({ bindId: c.id, kind: c.kind, label: c.label, parent: c.parent, info: c.info });
303
+ }
304
+ const index = {};
305
+ for (const b of file.bindings) {
306
+ const s = screenOf(b.component, ids);
307
+ if (!s) {
308
+ issues.push({ severity: "error", screen: "", component: b.component, slot: b.slot, code: "binding.no-screen", message: `Binding for "${b.component}" fits no screen.` });
309
+ continue;
310
+ }
311
+ (index[s] ??= []).push(b);
312
+ }
313
+ return {
314
+ fw: { descriptors: file.kinds, registry: { registryVersion: 1, screens }, catalog, glossaries: file.glossaries },
315
+ index,
316
+ issues
317
+ };
318
+ }
319
+
272
320
  // src/framework/schema.ts
273
321
  var obj = (s) => s && typeof s === "object" ? s : {};
274
322
  function flat(s) {
@@ -530,11 +578,18 @@ function compileCall(op, request, cx, scope, at) {
530
578
  });
531
579
  if (op.body) {
532
580
  const set = new Set(Object.keys(request).filter((k) => k.startsWith("/body/")).map((k) => decode(k.split("/")[2])));
533
- const gaps = whole ? [] : (flat(op.body.schema).required ?? []).filter((f) => !set.has(f));
581
+ const schema = flat(op.body.schema);
582
+ const unset = whole ? [] : (schema.required ?? []).filter((f) => !set.has(f));
583
+ const empty = unset.filter((f) => {
584
+ const p = flat(schema.properties?.[f]);
585
+ return p.type === "object" && !p.required?.length;
586
+ });
587
+ const gaps = unset.filter((f) => !empty.includes(f));
534
588
  if (gaps.length)
535
589
  cx.warn("request.required-body", `${op.id} requires ${gaps.join(", ")} in its body; not connected.`, at);
536
590
  cx.use.add("assemble");
537
- args.push(`assemble(${whole ?? "undefined"}, [${body.join(", ")}])`);
591
+ const base = whole ?? (empty.length ? `{ ${empty.map((f) => `${prop(f)}: {}`).join(", ")} }` : "undefined");
592
+ args.push(`assemble(${base}, [${body.join(", ")}])`);
538
593
  }
539
594
  if (op.parameters.some((p) => p.pointer.startsWith("/query/"))) {
540
595
  cx.use.add("defined");
@@ -782,6 +837,30 @@ async function readDocs(src, base) {
782
837
  throw new Error(`"${src}" is not a .yaml, .yml or .json file.`);
783
838
  return fs.readFile(path.resolve(base, src), "utf8");
784
839
  }
840
+ async function readBindings(file) {
841
+ const text = await fs.readFile(file, "utf8");
842
+ let raw;
843
+ try {
844
+ raw = JSON.parse(text);
845
+ } catch {
846
+ throw new Error("The bindings file is not valid JSON.");
847
+ }
848
+ if (typeof raw?.registry !== "string")
849
+ return parseBindingsFile(text);
850
+ const registry = parseRegistry(await fs.readFile(path.resolve(path.dirname(file), raw.registry), "utf8"));
851
+ return checkBindingsFile({ ...raw, ...registry });
852
+ }
853
+ async function saveBindings(file, merged) {
854
+ let out = merged;
855
+ if (merged.registry) {
856
+ const rest = { ...merged };
857
+ for (const k of REGISTRY_KEYS)
858
+ delete rest[k];
859
+ out = rest;
860
+ }
861
+ await writeFileAtomic(file, JSON.stringify(out, null, 2) + `
862
+ `);
863
+ }
785
864
  async function writeFileAtomic(file, text) {
786
865
  const tmp = `${file}.${process.pid}.tmp`;
787
866
  await fs.writeFile(tmp, text, "utf8");
@@ -801,7 +880,7 @@ async function readBody(req, limit = 20 * 1024 * 1024) {
801
880
  function send(res, status, body, type = "text/plain; charset=utf-8") {
802
881
  res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).end(body);
803
882
  }
804
- function createStudioHandler({ file, prefix, allowed }) {
883
+ function createStudioHandler({ file, prefix, allowed, onSave }) {
805
884
  const base = path.dirname(file);
806
885
  return async (req, res) => {
807
886
  const url = new URL(req.url ?? "/", "http://local");
@@ -819,17 +898,18 @@ function createStudioHandler({ file, prefix, allowed }) {
819
898
  }
820
899
  if (rel === "/api/bindings") {
821
900
  if (req.method === "GET")
822
- send(res, 200, await fs.readFile(file, "utf8"), "application/json");
901
+ send(res, 200, JSON.stringify(await readBindings(file)), "application/json");
823
902
  else if (req.method === "PUT") {
824
- const text = await readBody(req);
903
+ let merged;
825
904
  try {
826
- parseBindingsFile(text);
905
+ merged = parseBindingsFile(await readBody(req));
827
906
  } catch (e) {
828
907
  send(res, 400, e.message);
829
908
  return true;
830
909
  }
831
- await writeFileAtomic(file, text);
910
+ await saveBindings(file, merged);
832
911
  send(res, 204, "");
912
+ onSave?.();
833
913
  } else
834
914
  send(res, 405, "Method not allowed.");
835
915
  return true;
@@ -902,6 +982,38 @@ async function scanIds(dir, file) {
902
982
  return issues;
903
983
  }
904
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
+
905
1017
  // src/cli/cli.ts
906
1018
  var HELP = `Usage:
907
1019
  api-binding studio [bindings.json] [--port 5180]
@@ -926,54 +1038,31 @@ function args(argv) {
926
1038
  }
927
1039
  return { rest, flags };
928
1040
  }
929
- function report(issues) {
930
- for (const i of issues)
931
- console.log(`${i.severity === "error" ? "error " : "warning"} ${[i.component ?? i.screen, i.slot].filter(Boolean).join(" · ")}: ${i.message}`);
932
- }
933
1041
  async function runGenerate(filePath, flags) {
934
- const base = path3.dirname(filePath);
935
- const file = parseBindingsFile(await fs3.readFile(filePath, "utf8"));
936
- const catalog = catalogFromText(await readDocs(file.openapi, base), file.openapi);
937
- const { files, issues } = generate(file, catalog);
938
- if (typeof flags.src === "string")
939
- issues.push(...await scanIds(path3.resolve(flags.src), file));
940
- report(issues);
941
- const errors = issues.filter((i) => i.severity === "error").length;
1042
+ const r = await writeScreens(filePath, { check: !!flags.check, src: typeof flags.src === "string" ? flags.src : undefined });
1043
+ for (const i of r.issues)
1044
+ console.log(describeIssue(i));
1045
+ const errors = r.issues.filter((i) => i.severity === "error").length;
942
1046
  if (errors) {
943
1047
  console.log(`
944
1048
  ${errors} error(s). Nothing was written.`);
945
1049
  return 1;
946
1050
  }
947
- const out = path3.resolve(base, file.output.dir);
948
- await fs3.mkdir(out, { recursive: true });
949
- const wanted = new Map(files.map((f) => [path3.join(out, f.path), f.content]));
950
- const existing = (await fs3.readdir(out)).filter((n) => n.endsWith(".ts")).map((n) => path3.join(out, n));
951
- const changed = [];
952
- const stale = [];
953
- for (const [p, content] of wanted)
954
- if (await fs3.readFile(p, "utf8").catch(() => null) !== content)
955
- changed.push(p);
956
- for (const p of existing)
957
- if (!wanted.has(p) && (await fs3.readFile(p, "utf8")).startsWith(`// ${GENERATED_MARK}`))
958
- stale.push(p);
1051
+ const rel = (p) => path4.relative(process.cwd(), p) || ".";
959
1052
  if (flags.check) {
960
- if (changed.length || stale.length) {
961
- console.log(`Out of date: ${[...changed, ...stale].map((p) => path3.relative(process.cwd(), p)).join(", ")}`);
1053
+ if (r.changed.length || r.stale.length) {
1054
+ console.log(`Out of date: ${[...r.changed, ...r.stale].map(rel).join(", ")}`);
962
1055
  return 1;
963
1056
  }
964
1057
  console.log("Generated files are up to date.");
965
1058
  return 0;
966
1059
  }
967
- for (const p of changed)
968
- await writeFileAtomic(p, wanted.get(p));
969
- for (const p of stale)
970
- await fs3.unlink(p);
971
- console.log(`${files.length - 2} screen hook(s) in ${path3.relative(process.cwd(), out) || "."}: ${changed.length} written, ${stale.length} removed.`);
1060
+ console.log(`${r.screens} screen hook(s) in ${rel(r.out)}: ${r.changed.length} written, ${r.stale.length} removed.`);
972
1061
  return 0;
973
1062
  }
974
1063
  async function runStudio(filePath, flags) {
975
1064
  const port = Number(flags.port ?? 5180);
976
- parseBindingsFile(await fs3.readFile(filePath, "utf8"));
1065
+ await readBindings(filePath);
977
1066
  const origins = new Set([`http://localhost:${port}`, `http://127.0.0.1:${port}`]);
978
1067
  const allowed = (req) => origins.has(`http://${req.headers.host}`) && (!req.headers.origin || origins.has(String(req.headers.origin)));
979
1068
  const handle = createStudioHandler({ file: filePath, prefix: "", allowed });
@@ -990,7 +1079,7 @@ Editing ${filePath}. Ctrl+C to stop.`));
990
1079
  async function main() {
991
1080
  const [cmd, ...argv] = process.argv.slice(2);
992
1081
  const { rest, flags } = args(argv);
993
- const filePath = path3.resolve(rest[0] ?? "bindings.json");
1082
+ const filePath = path4.resolve(rest[0] ?? "bindings.json");
994
1083
  if (cmd === "studio")
995
1084
  return runStudio(filePath, flags);
996
1085
  if (cmd === "generate")
@@ -0,0 +1,18 @@
1
+ import type { ValidationIssue } from "../framework/contract";
2
+ export interface WriteResult {
3
+ issues: ValidationIssue[];
4
+ /** Directory the hooks go to. */
5
+ out: string;
6
+ /** Number of screen hooks generated. */
7
+ screens: number;
8
+ /** Files that differ from what is on disk (written unless `check`). */
9
+ changed: string[];
10
+ /** Generated files no screen produces any more (removed unless `check`). */
11
+ stale: string[];
12
+ }
13
+ /** On any error nothing is written. With `check`, nothing is written either; `changed` and `stale` say what is out of date. */
14
+ export declare function writeScreens(filePath: string, opts?: {
15
+ check?: boolean;
16
+ src?: string;
17
+ }): Promise<WriteResult>;
18
+ export declare const describeIssue: (i: ValidationIssue) => string;
@@ -206,10 +206,34 @@ export interface FileScreen extends Labelled {
206
206
  id: ScreenId;
207
207
  context?: ContextDeclaration[];
208
208
  }
209
- /** The one file that holds everything about API binding: kinds, screens, components, bindings and output. */
209
+ /** A component in the registry tree; `children` are the parts it renders (a grid's columns). */
210
+ export interface RegistryNode {
211
+ id: BindId;
212
+ kind: KindId;
213
+ label: string;
214
+ info?: Record<string, string>;
215
+ children?: RegistryNode[];
216
+ }
217
+ /** A screen in the registry tree; `screens` are the sub-screens that open over it (dialogs, drawers). */
218
+ export interface RegistryScreen extends FileScreen {
219
+ components?: RegistryNode[];
220
+ screens?: RegistryScreen[];
221
+ }
222
+ /** registry.json: what exists in the app. Every id extends its parent's id. */
223
+ export interface RegistryFile {
224
+ version: 1;
225
+ kinds: ComponentDescriptor[];
226
+ screens: RegistryScreen[];
227
+ }
228
+ /**
229
+ * The bindings, as the studio and generator see them. On disk, `registry` names registry.json and
230
+ * the file holds only the rest; kinds, screens and components are read from the registry.
231
+ */
210
232
  export interface BindingsFile {
211
233
  $schema?: string;
212
234
  version: 3;
235
+ /** registry.json, relative to this file. */
236
+ registry?: string;
213
237
  /** OpenAPI document: a URL, or a path relative to this file. */
214
238
  openapi: string;
215
239
  output?: {
@@ -5,6 +5,12 @@ import type { BindingIndex, FrameworkData } from "./engine";
5
5
  export declare const ID: RegExp;
6
6
  /** Parses and shape-checks a bindings file. Throws a message a person can act on. */
7
7
  export declare function parseBindingsFile(text: string): BindingsFile;
8
+ /** Shape-checks a bindings file with its registry already merged in. */
9
+ export declare function checkBindingsFile(data: unknown): BindingsFile;
10
+ /** What the registry contributes to the bindings file; on disk these live only in registry.json. */
11
+ export declare const REGISTRY_KEYS: readonly ["kinds", "screens", "components"];
12
+ /** Parses registry.json and flattens its tree into kinds, screens and components (with `parent`). */
13
+ export declare function parseRegistry(text: string): Pick<BindingsFile, (typeof REGISTRY_KEYS)[number]>;
8
14
  /** The screen an id belongs to: the longest screen id that prefixes it. */
9
15
  export declare function screenOf(id: string, screens: readonly ScreenId[]): ScreenId | undefined;
10
16
  /** Builds the studio's model. Components and bindings that fit no screen are dropped with an issue. */
@@ -180,6 +180,9 @@ function parseBindingsFile2(text) {
180
180
  } catch {
181
181
  throw new Error("The bindings file is not valid JSON.");
182
182
  }
183
+ return checkBindingsFile(data);
184
+ }
185
+ function checkBindingsFile(data) {
183
186
  const f = data;
184
187
  if (!f || typeof f !== "object")
185
188
  throw new Error("The bindings file must be a JSON object.");
@@ -740,5 +743,5 @@ function validateBindings2(engine) {
740
743
 
741
744
  export { catalogFromOpenApi2, CatalogLoadError2, catalogFromText2, loadCatalog2, humanize, ID2, parseBindingsFile2, screenOf2, fromFile2, toFile2, memoryStore2, httpStore2, flat, createEngine2, validateRegistry2, validateBindings2 };
742
745
 
743
- //# debugId=CBA6673BB224224F64756E2164756E21
744
- //# sourceMappingURL=index-jj8twdw9.js.map
746
+ //# debugId=548F2EE21986ECDC64756E2164756E21
747
+ //# sourceMappingURL=index-gfw08965.js.map