api_binding_ui 3.1.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/README.md +118 -94
- package/SKILL.md +45 -45
- package/dist/app/{index-4mgcb4wd.js → index-xww2x0kd.js} +72 -72
- package/dist/app/index.html +1 -1
- package/dist/cli/scan.d.ts +2 -0
- package/dist/cli.js +218 -129
- package/dist/codegen/write.d.ts +18 -0
- package/dist/framework/contract.d.ts +25 -1
- package/dist/framework/file.d.ts +6 -0
- package/dist/{index-jj8twdw9.js → index-gfw08965.js} +5 -2
- package/dist/{index-jj8twdw9.js.map → index-gfw08965.js.map} +3 -3
- package/dist/index.js +11 -4
- package/dist/index.js.map +3 -3
- package/dist/overlay.js +3 -3
- package/dist/server/handler.d.ts +8 -1
- package/dist/studio/index.js +1 -1
- package/dist/vite/plugin.d.ts +14 -1
- package/dist/vite.js +921 -9
- package/package.json +2 -2
- package/spec/FRAMEWORK.md +178 -177
package/dist/app/index.html
CHANGED
|
@@ -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-
|
|
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>
|
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
|
|
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
|
-
|
|
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
|
|
901
|
+
send(res, 200, JSON.stringify(await readBindings(file)), "application/json");
|
|
823
902
|
else if (req.method === "PUT") {
|
|
824
|
-
|
|
903
|
+
let merged;
|
|
825
904
|
try {
|
|
826
|
-
parseBindingsFile(
|
|
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
|
|
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
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
const
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
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?: {
|
package/dist/framework/file.d.ts
CHANGED
|
@@ -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=
|
|
744
|
-
//# sourceMappingURL=index-
|
|
746
|
+
//# debugId=548F2EE21986ECDC64756E2164756E21
|
|
747
|
+
//# sourceMappingURL=index-gfw08965.js.map
|