api_binding_ui 2.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +68 -48
  2. package/SKILL.md +29 -34
  3. package/dist/app/index-4mgcb4wd.js +367 -0
  4. package/dist/app/index-aaxy714b.css +1 -0
  5. package/dist/app/index.html +7 -0
  6. package/dist/cli.js +1006 -0
  7. package/dist/codegen/generate.d.ts +22 -0
  8. package/dist/codegen/helpers.d.ts +2 -0
  9. package/dist/framework/contract.d.ts +38 -23
  10. package/dist/framework/expr.d.ts +26 -27
  11. package/dist/framework/file.d.ts +28 -0
  12. package/dist/framework/store.d.ts +3 -15
  13. package/dist/{index-3q1vx729.js → index-jj8twdw9.js} +158 -173
  14. package/dist/index-jj8twdw9.js.map +16 -0
  15. package/dist/index.d.ts +6 -8
  16. package/dist/index.js +461 -298
  17. package/dist/index.js.map +6 -8
  18. package/dist/overlay.js +24 -0
  19. package/dist/server/handler.d.ts +18 -0
  20. package/dist/studio/BindingStudio.d.ts +8 -11
  21. package/dist/studio/SettingsPanel.d.ts +3 -7
  22. package/dist/studio/index.css +0 -149
  23. package/dist/studio/index.d.ts +1 -2
  24. package/dist/studio/index.js +231 -438
  25. package/dist/studio/index.js.map +8 -10
  26. package/dist/studio/useBindings.d.ts +7 -5
  27. package/dist/studio/useCatalog.d.ts +2 -0
  28. package/dist/vite/plugin.d.ts +36 -0
  29. package/dist/vite.js +201 -0
  30. package/package.json +68 -52
  31. package/spec/FRAMEWORK.md +107 -201
  32. package/dist/framework/runner.d.ts +0 -36
  33. package/dist/index-3q1vx729.js.map +0 -16
  34. package/dist/runtime/BindingProvider.d.ts +0 -42
  35. package/dist/runtime/BindingScreen.d.ts +0 -23
  36. package/dist/runtime/hooks.d.ts +0 -22
  37. package/dist/runtime/screenStore.d.ts +0 -7
  38. package/dist/studio/BindingWidget.d.ts +0 -9
  39. package/dist/studio/useSettings.d.ts +0 -8
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Code generator: bindings file + catalog → one plain React Query hook per screen, calling the Orval client.
3
+ *
4
+ * A screen hook's signature comes from the registry only (its components and slots), so call sites stay put
5
+ * when bindings change; bindings change only the hook bodies. Unbound slots return an unbound value.
6
+ */
7
+ import type { BindingsFile, Catalog, JsonSchema, ValidationIssue } from "../framework/contract";
8
+ export interface GeneratedFile {
9
+ path: string;
10
+ content: string;
11
+ }
12
+ export interface GenerateResult {
13
+ files: GeneratedFile[];
14
+ issues: ValidationIssue[];
15
+ }
16
+ /** First-line marker; the CLI only ever deletes files that carry it. */
17
+ export declare const GENERATED_MARK = "@generated";
18
+ /** Orval names functions after the operationId, first letter lowercased. */
19
+ export declare const orvalName: (id: string) => string;
20
+ /** JSON Schema → a TypeScript type. Loose on purpose: the API checks API data, the UI only reads it. */
21
+ export declare function tsType(schema: JsonSchema | undefined, many?: boolean, depth?: number): string;
22
+ export declare function generate(file: BindingsFile, catalog: Catalog): GenerateResult;
@@ -0,0 +1,2 @@
1
+ /** Source of the generated `screen-helpers.ts`: the only shared code the generated hooks use. */
2
+ export declare const HELPERS = "import { useMutation, useQuery } from \"@tanstack/react-query\";\n\n/** Data for a slot. `bound` is false when no API feeds it; keep the UI's own value then. */\nexport interface Data<T> { bound: boolean; data: T | undefined; loading: boolean; error: unknown }\n/** An action. `run` rejects when a call fails; it resolves at once when no API is connected. */\nexport interface Action { bound: boolean; run: (payload?: any) => Promise<void>; running: boolean; error: unknown }\n/** A per-item value, such as a cell in a row. */\nexport interface Cell<T> { bound: boolean; read: (item: any) => T | undefined }\n\nexport const UNBOUND_DATA: Data<never> = { bound: false, data: undefined, loading: false, error: undefined };\nexport const UNBOUND_ACTION: Action = { bound: false, run: async () => {}, running: false, error: undefined };\nexport const UNBOUND_CELL: Cell<never> = { bound: false, read: () => undefined };\n\nexport class ApiError extends Error {\n constructor(readonly status: number, readonly data: unknown) { super(`API call failed (${status})`); this.name = \"ApiError\"; }\n}\n\n/** Reads a path; \"*\" maps the rest of the path over a list. */\nexport function at(value: unknown, path: readonly string[]): any {\n let v: any = value;\n for (let i = 0; i < path.length; i++) {\n if (v == null) return undefined;\n if (path[i] === \"*\") return Array.isArray(v) ? v.map(x => at(x, path.slice(i + 1))) : undefined;\n v = v[path[i]];\n }\n return v;\n}\n\n/** The body of a client response; throws on a non-2xx status. Passes through clients that return the body. */\nexport function ok(res: any): any {\n if (res && typeof res === \"object\" && typeof res.status === \"number\" && \"data\" in res && \"headers\" in res) {\n if (res.status < 200 || res.status >= 300) throw new ApiError(res.status, res.data);\n return res.data;\n }\n return res;\n}\n\n/** True when every required input has a value. */\nexport const has = (inputs: Record<string, unknown>) => Object.values(inputs).every(v => v !== undefined);\n\n/** Throws when a required input has no value. */\nexport function need(operation: string, inputs: Record<string, unknown>) {\n const missing = Object.keys(inputs).filter(k => inputs[k] === undefined);\n if (missing.length) throw new Error(`${operation}: missing ${missing.join(\", \")}`);\n}\n\n/** Drops undefined values. */\nexport const defined = (o: Record<string, unknown>): any => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));\n\nexport const path = (v: unknown): any => (v === undefined ? undefined : encodeURIComponent(String(v)));\n\n/** A body from a base value and [keys, value] entries; undefined values are skipped. */\nexport function assemble(base: unknown, entries: Array<[string[], unknown]>): any {\n const out: any = base && typeof base === \"object\" ? { ...base } : {};\n for (const [keys, value] of entries) {\n if (value === undefined) continue;\n let node = out;\n keys.slice(0, -1).forEach(k => { node = node[k] = node[k] && typeof node[k] === \"object\" ? { ...node[k] } : {}; });\n node[keys[keys.length - 1]] = value;\n }\n return out;\n}\n\n/** Adds mapped fields to each item, keeping the fields the API returned. */\nexport function mapItems(data: any, map: (item: any) => Record<string, unknown>): any {\n const one = (item: any) => ({ ...(item && typeof item === \"object\" ? item : {}), ...map(item) });\n return Array.isArray(data) ? data.map(one) : data == null ? data : one(data);\n}\n\nexport const list = (v: unknown): unknown[] => (Array.isArray(v) ? v : v === undefined ? [] : [v]);\n\nexport function finish(failed: unknown[]) {\n if (failed.length === 1) throw failed[0];\n if (failed.length) throw new AggregateError(failed, `${failed.length} API calls failed`);\n}\n\nexport function useData<T>(key: readonly unknown[], fetch: () => Promise<unknown>, select: (data: any) => T, options: { enabled: boolean; everySeconds?: number }): Data<T> {\n const q = useQuery({ queryKey: key, queryFn: fetch, select, enabled: options.enabled, refetchInterval: options.everySeconds ? options.everySeconds * 1000 : false });\n return { bound: true, data: q.data, loading: q.isLoading, error: q.error ?? undefined };\n}\n\nexport function useAction(run: (payload?: any) => Promise<void>): Action {\n const m = useMutation({ mutationFn: run });\n return { bound: true, run: payload => m.mutateAsync(payload), running: m.isPending, error: m.error ?? undefined };\n}\n";
@@ -1,13 +1,16 @@
1
- /** Binding framework contract v1. Semantics: spec/FRAMEWORK.md. */
1
+ /** Binding framework contract v3. Semantics: spec/FRAMEWORK.md. */
2
2
  /** JSON Schema (2020-12). */
3
3
  export type JsonSchema = boolean | {
4
4
  [keyword: string]: unknown;
5
5
  };
6
6
  /** Namespaced component kind, e.g. "acme-ds/grid". */
7
7
  export type KindId = string;
8
- /** Component instance id, unique within its screen. Letters, digits, - and _; no dots. */
8
+ /**
9
+ * Component id, unique in the app: "<screen id>.<component>[.<part>]", e.g. "collections.worklist.grid.row.add-notes".
10
+ * Lowercase letters, digits and -, in dot-separated segments.
11
+ */
9
12
  export type BindId = string;
10
- /** Screen id, e.g. "<product>.<area>.<screen>". */
13
+ /** Screen id, e.g. "<product>.<screen>[.<sub-screen>]". Same segment rules as component ids. */
11
14
  export type ScreenId = string;
12
15
  /** Expression string; grammar in FRAMEWORK.md §7. */
13
16
  export type Expr = string;
@@ -66,7 +69,6 @@ export interface ComponentDescriptor extends Labelled {
66
69
  export interface ComponentInstance {
67
70
  bindId: BindId;
68
71
  kind: KindId;
69
- kindVersion: number;
70
72
  label: string;
71
73
  /** Parent component's bindId. */
72
74
  parent?: BindId;
@@ -74,7 +76,7 @@ export interface ComponentInstance {
74
76
  info?: Record<string, string>;
75
77
  }
76
78
  export interface ContextDeclaration extends Labelled {
77
- /** Name used in expressions: `$context.<id>`. */
79
+ /** Name used in expressions: `$context.<id>`. A word: letters and digits. */
78
80
  id: string;
79
81
  /** A schema, or another screen's produce slot whose bound shape it follows. */
80
82
  shape: {
@@ -191,16 +193,36 @@ export interface NoApiBinding extends BindingBase {
191
193
  type: "none";
192
194
  }
193
195
  export type SlotBinding = OperationConsumeBinding | ExpressionConsumeBinding | TriggerBinding | NoApiBinding;
194
- export interface BindingManifest {
196
+ /** A registered component in the bindings file. Its screen is the longest screen id that prefixes its id. */
197
+ export interface FileComponent {
198
+ id: BindId;
199
+ kind: KindId;
200
+ label: string;
201
+ /** Parent component id (a column's grid). */
202
+ parent?: BindId;
203
+ info?: Record<string, string>;
204
+ }
205
+ export interface FileScreen extends Labelled {
206
+ id: ScreenId;
207
+ context?: ContextDeclaration[];
208
+ }
209
+ /** The one file that holds everything about API binding: kinds, screens, components, bindings and output. */
210
+ export interface BindingsFile {
195
211
  $schema?: string;
196
- manifestVersion: 1;
197
- screen: ScreenId;
198
- catalog: {
199
- hash: string;
212
+ version: 3;
213
+ /** OpenAPI document: a URL, or a path relative to this file. */
214
+ openapi: string;
215
+ output?: {
216
+ /** Folder the screen hooks are written to, relative to this file. */
217
+ dir: string;
218
+ /** Import specifier of the Orval output, as the app imports it (e.g. "@/api/generated"). */
219
+ client: string;
200
220
  };
201
- /** Descriptor versions the bindings were made against. */
202
- kinds: Record<KindId, number>;
221
+ kinds: ComponentDescriptor[];
222
+ screens: FileScreen[];
223
+ components: FileComponent[];
203
224
  bindings: SlotBinding[];
225
+ glossaries?: Glossary[];
204
226
  }
205
227
  /** A named, typed value transform usable as `expr | name(args)`. */
206
228
  export interface TransformDefinition extends Labelled {
@@ -243,17 +265,10 @@ export interface Glossary {
243
265
  /** Groups of words that mean the same thing in this product. */
244
266
  synonyms: string[][];
245
267
  }
246
- export interface PersistenceAdapter {
247
- load: (screen: ScreenId) => Promise<BindingManifest | null>;
248
- save: (manifest: BindingManifest) => Promise<void>;
249
- }
250
- export interface FrameworkRegistry {
251
- registerDescriptor: (descriptor: ComponentDescriptor) => void;
252
- registerTransform: (transform: TransformDefinition) => void;
253
- registerContext: (provider: ContextProvider) => void;
254
- registerMatcher: (matcher: Matcher) => void;
255
- registerGlossary: (glossary: Glossary) => void;
256
- setPersistence: (adapter: PersistenceAdapter) => void;
268
+ /** Where the studio reads and writes the bindings file. */
269
+ export interface BindingsStore {
270
+ load: () => Promise<BindingsFile>;
271
+ save: (file: BindingsFile) => Promise<void>;
257
272
  }
258
273
  export type IssueSeverity = "error" | "warning";
259
274
  export interface ValidationIssue {
@@ -1,34 +1,33 @@
1
1
  /**
2
- * Expression evaluator (FRAMEWORK.md §7). One grammar for every source:
2
+ * Expression grammar (FRAMEWORK.md §7). One grammar for every source:
3
3
  *
4
- * <bindId>.<slot>[.path] a produce slot on this screen
5
- * $item[.path] the current item (scoped slot or forEach)
6
- * $context.<id>[.path] a value the screen received
7
- * $steps.<stepId>[.path] an earlier call step's result ({ response })
8
- * $<provider>[.path] a registered context provider
9
- * <expr>[*].<path> every item of a list
10
- * <expr> | name(args) a registered transform
4
+ * <componentId>:<slot>[.path] a produce slot on the screen
5
+ * $item[.path] the current item (scoped slot, forEach, or the trigger's payload)
6
+ * $context.<id>[.path] a value the screen received
7
+ * $steps.<stepId>[.path] an earlier call step's result ({ response })
8
+ * <expr>[*].<path> every item of a list
11
9
  *
12
- * Nothing outside this grammar is evaluated; unknown heads resolve to undefined.
10
+ * Component ids contain dots, so ":" ends the id. Nothing outside this grammar is valid.
13
11
  */
14
- export type Transforms = Record<string, (value: unknown, ...args: unknown[]) => unknown>;
15
- export interface Scope {
16
- /** Current value of a produce slot on the screen. */
17
- slot: (bindId: string, slotId: string) => unknown;
18
- item?: unknown;
19
- context?: Record<string, unknown>;
20
- providers?: Record<string, unknown>;
21
- steps?: Record<string, unknown>;
22
- transforms?: Transforms;
23
- }
24
- interface Parsed {
25
- head: string[];
12
+ export type Head = {
13
+ type: "slot";
14
+ component: string;
15
+ slot: string;
16
+ } | {
17
+ type: "item";
18
+ } | {
19
+ type: "context";
20
+ id: string;
21
+ } | {
22
+ type: "steps";
23
+ id: string;
24
+ };
25
+ export interface Parsed {
26
+ head: Head;
26
27
  path: string[];
27
- pipes: Array<{
28
- name: string;
29
- args: unknown[];
30
- }>;
31
28
  }
29
+ /** "a.b[*].c" → ["a", "b", "*", "c"] */
30
+ export declare function segments(path: string): string[];
32
31
  export declare function parse(expr: string): Parsed;
33
- export declare function evaluate(expr: string, scope: Scope): unknown;
34
- export {};
32
+ /** Reads a path; "*" maps the rest of the path over a list. */
33
+ export declare function walk(value: unknown, path: readonly string[]): unknown;
@@ -0,0 +1,28 @@
1
+ /** The bindings file: parsing, and conversion to and from the studio's working model. */
2
+ import type { BindingsFile, Catalog, ScreenId, ValidationIssue } from "./contract";
3
+ import type { BindingIndex, FrameworkData } from "./engine";
4
+ /** Screen and component ids: lowercase kebab-case segments joined by dots, at least two. */
5
+ export declare const ID: RegExp;
6
+ /** Parses and shape-checks a bindings file. Throws a message a person can act on. */
7
+ export declare function parseBindingsFile(text: string): BindingsFile;
8
+ /** The screen an id belongs to: the longest screen id that prefixes it. */
9
+ export declare function screenOf(id: string, screens: readonly ScreenId[]): ScreenId | undefined;
10
+ /** Builds the studio's model. Components and bindings that fit no screen are dropped with an issue. */
11
+ export declare function fromFile(file: BindingsFile, catalog: Catalog): {
12
+ fw: FrameworkData;
13
+ index: BindingIndex;
14
+ issues: ValidationIssue[];
15
+ };
16
+ /** The file with its bindings replaced by the index, in registry order so diffs stay small. */
17
+ export declare function toFile(file: BindingsFile, index: BindingIndex): BindingsFile;
18
+ /** In-memory store, for tests and embedding. */
19
+ export declare function memoryStore(seed: BindingsFile): {
20
+ load: () => Promise<BindingsFile>;
21
+ save: (f: BindingsFile) => Promise<void>;
22
+ readonly current: BindingsFile;
23
+ };
24
+ /** Store backed by the CLI's studio server. */
25
+ export declare function httpStore(url: string): {
26
+ load(): Promise<BindingsFile>;
27
+ save(file: BindingsFile): Promise<void>;
28
+ };
@@ -1,17 +1,5 @@
1
- /** Manifest persistence and edit helpers. */
2
- import type { BindingManifest, PersistenceAdapter, ScreenId, SlotBinding } from "./contract";
3
- import type { BindingIndex, FrameworkData } from "./engine";
4
- export declare function localStorageAdapter(prefix?: string): PersistenceAdapter;
5
- export declare function memoryAdapter(seed?: BindingManifest[]): PersistenceAdapter;
6
- export declare function toManifest(fw: FrameworkData, screen: ScreenId, bindings: SlotBinding[]): BindingManifest;
7
- /** Unsaved screens with an edit counter; a screen is cleared only by a save that covers its latest edit. */
8
- export type Pending = Map<ScreenId, number>;
9
- export declare function markDirty(pending: Pending, screen: ScreenId): void;
10
- /** Saves every pending screen from `idx`. Returns the screens whose save failed (they stay pending). */
11
- export declare function savePending(adapter: PersistenceAdapter, fw: FrameworkData, idx: BindingIndex, pending: Pending): Promise<ScreenId[]>;
12
- export declare function loadAll(adapter: PersistenceAdapter, screens: ScreenId[]): Promise<BindingIndex>;
13
- /** Pure edit helpers: return a new index. */
1
+ /** Pure edit helpers for the binding index: each returns a new index. */
2
+ import type { ScreenId, SlotBinding } from "./contract";
3
+ import type { BindingIndex } from "./engine";
14
4
  export declare function putBinding(idx: BindingIndex, screen: ScreenId, b: SlotBinding): BindingIndex;
15
5
  export declare function dropBinding(idx: BindingIndex, screen: ScreenId, component: string, slot: string): BindingIndex;
16
- /** Parses imported manifests (one or a list). Throws on the first invalid entry. */
17
- export declare function parseManifests(text: string): BindingManifest[];
@@ -111,100 +111,42 @@ function catalogFromOpenApi2(input, source = {}) {
111
111
  };
112
112
  }
113
113
 
114
- // src/framework/store.ts
115
- function localStorageAdapter2(prefix = "abs.manifest.") {
116
- return {
117
- async load(screen) {
118
- try {
119
- const raw = globalThis.localStorage?.getItem(prefix + screen);
120
- return raw ? JSON.parse(raw) : null;
121
- } catch {
122
- return null;
123
- }
124
- },
125
- async save(m) {
126
- if (!globalThis.localStorage)
127
- throw new Error("Browser storage is not available.");
128
- globalThis.localStorage.setItem(prefix + m.screen, JSON.stringify(m));
129
- }
130
- };
131
- }
132
- function memoryAdapter2(seed = []) {
133
- const m = new Map(seed.map((x) => [x.screen, x]));
134
- return { async load(s) {
135
- return m.get(s) ?? null;
136
- }, async save(x) {
137
- m.set(x.screen, x);
138
- } };
139
- }
140
- function toManifest2(fw, screen, bindings) {
141
- const S = fw.registry.screens.find((s) => s.id === screen);
142
- const kinds = {};
143
- S?.components.forEach((c) => {
144
- const d = fw.descriptors.find((x) => x.kind === c.kind);
145
- if (d)
146
- kinds[c.kind] = d.version;
147
- });
148
- return { manifestVersion: 1, screen, catalog: { hash: fw.catalog.hash }, kinds, bindings };
149
- }
150
- function markDirty(pending, screen) {
151
- pending.set(screen, (pending.get(screen) ?? 0) + 1);
152
- }
153
- async function savePending(adapter, fw, idx, pending) {
154
- const batch = [...pending];
155
- const results = await Promise.allSettled(batch.map(([s]) => adapter.save(toManifest2(fw, s, idx[s] ?? []))));
156
- const failed = [];
157
- batch.forEach(([s, version], i) => {
158
- if (results[i].status === "rejected")
159
- failed.push(s);
160
- else if (pending.get(s) === version)
161
- pending.delete(s);
162
- });
163
- return failed;
164
- }
165
- async function loadAll(adapter, screens) {
166
- const out = {};
167
- await Promise.all(screens.map(async (s) => {
168
- const m = await adapter.load(s);
169
- if (m)
170
- out[s] = m.bindings;
171
- }));
172
- return out;
173
- }
174
- function putBinding(idx, screen, b) {
175
- const list = (idx[screen] ?? []).filter((x) => !(x.component === b.component && x.slot === b.slot));
176
- return { ...idx, [screen]: [...list, b] };
114
+ // src/framework/loadCatalog.ts
115
+ import { parse } from "yaml";
116
+ class CatalogLoadError2 extends Error {
117
+ source;
118
+ constructor(message, source) {
119
+ super(message);
120
+ this.source = source;
121
+ this.name = "CatalogLoadError";
122
+ }
177
123
  }
178
- function dropBinding(idx, screen, component, slot) {
179
- return { ...idx, [screen]: (idx[screen] ?? []).filter((x) => !(x.component === component && x.slot === slot)) };
124
+ function catalogFromText2(text, source) {
125
+ const body = text.trim();
126
+ if (!body)
127
+ throw new CatalogLoadError2("Nothing to read: the text is empty.", source);
128
+ let doc;
129
+ try {
130
+ doc = body.startsWith("{") ? JSON.parse(body) : parse(body);
131
+ } catch {
132
+ throw new CatalogLoadError2("The text is not valid JSON or YAML.", source);
133
+ }
134
+ try {
135
+ return catalogFromOpenApi2(doc, source.includes("://") ? { url: source } : {});
136
+ } catch (e) {
137
+ throw new CatalogLoadError2(e instanceof Error ? e.message : "The text is not an OpenAPI document.", source);
138
+ }
180
139
  }
181
- var BINDING_TYPES = new Set(["operation", "expression", "steps", "none"]);
182
- function parseManifests(text) {
183
- let data;
140
+ async function loadCatalog2(url, init) {
141
+ let res;
184
142
  try {
185
- data = JSON.parse(text);
143
+ res = await fetch(url, { ...init, headers: { Accept: "application/yaml, application/json, text/yaml, */*", ...init?.headers } });
186
144
  } catch {
187
- throw new Error("The file is not valid JSON.");
188
- }
189
- const list = Array.isArray(data) ? data : [data];
190
- list.forEach((m, i) => {
191
- const at = `Manifest ${i + 1}`;
192
- if (!m || typeof m !== "object")
193
- throw new Error(`${at} is not an object.`);
194
- const r = m;
195
- if (r.manifestVersion !== 1)
196
- throw new Error(`${at}: manifestVersion must be 1.`);
197
- if (typeof r.screen !== "string" || !r.screen)
198
- throw new Error(`${at}: "screen" is missing.`);
199
- if (!Array.isArray(r.bindings))
200
- throw new Error(`${at} (${r.screen}): "bindings" must be a list.`);
201
- r.bindings.forEach((b, j) => {
202
- const x = b;
203
- if (!x || typeof x.component !== "string" || typeof x.slot !== "string" || !BINDING_TYPES.has(x.type))
204
- throw new Error(`${at} (${r.screen}), binding ${j + 1}: needs "component", "slot" and a known "type".`);
205
- });
206
- });
207
- return list;
145
+ throw new CatalogLoadError2("The docs could not be reached. Check the URL, your network, and that the docs server allows this site (CORS).", url);
146
+ }
147
+ if (!res.ok)
148
+ throw new CatalogLoadError2(`The docs server answered ${res.status} ${res.statusText}. Check the URL.`, url);
149
+ return catalogFromText2(await res.text(), url);
208
150
  }
209
151
 
210
152
  // src/framework/names.ts
@@ -228,6 +170,117 @@ function humanize(id) {
228
170
  return w ? w[0].toUpperCase() + w.slice(1) : id;
229
171
  }
230
172
 
173
+ // src/framework/file.ts
174
+ var ID2 = /^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/;
175
+ var BINDING_TYPES = new Set(["operation", "expression", "steps", "none"]);
176
+ function parseBindingsFile2(text) {
177
+ let data;
178
+ try {
179
+ data = JSON.parse(text);
180
+ } catch {
181
+ throw new Error("The bindings file is not valid JSON.");
182
+ }
183
+ const f = data;
184
+ if (!f || typeof f !== "object")
185
+ throw new Error("The bindings file must be a JSON object.");
186
+ if (f.version !== 3)
187
+ throw new Error(`The bindings file must have "version": 3.`);
188
+ if (typeof f.openapi !== "string" || !f.openapi)
189
+ throw new Error(`The bindings file needs "openapi": the docs URL or a path.`);
190
+ for (const k of ["kinds", "screens", "components", "bindings"])
191
+ if (!Array.isArray(f[k]))
192
+ throw new Error(`"${k}" must be a list.`);
193
+ f.bindings.forEach((b, i) => {
194
+ const x = b;
195
+ if (!x || typeof x.component !== "string" || typeof x.slot !== "string" || !BINDING_TYPES.has(x.type))
196
+ throw new Error(`Binding ${i + 1} needs "component", "slot" and a known "type".`);
197
+ });
198
+ if (f.output && (typeof f.output.dir !== "string" || typeof f.output.client !== "string"))
199
+ throw new Error(`"output" needs "dir" and "client".`);
200
+ return f;
201
+ }
202
+ function screenOf2(id, screens) {
203
+ let best;
204
+ for (const s of screens)
205
+ if (id.startsWith(s + ".") && (!best || s.length > best.length))
206
+ best = s;
207
+ return best;
208
+ }
209
+ function productLabel(seg, catalog) {
210
+ const names = new Set(catalog.operations.map((o) => o.product).filter((p) => !!p));
211
+ return [...names].find((n) => n.toLowerCase().replace(/[^a-z0-9]+/g, "-") === seg) ?? humanize(seg);
212
+ }
213
+ function fromFile2(file, catalog) {
214
+ const issues = [];
215
+ const ids = file.screens.map((s) => s.id);
216
+ const screens = file.screens.map((s) => ({
217
+ id: s.id,
218
+ label: s.label,
219
+ description: s.description,
220
+ context: s.context ?? [],
221
+ components: [],
222
+ path: [productLabel(s.id.split(".")[0], catalog)],
223
+ parent: screenOf2(s.id, ids)
224
+ }));
225
+ const byId = new Map(screens.map((s) => [s.id, s]));
226
+ const screenIds = new Set(ids);
227
+ for (const c of file.components) {
228
+ if (screenIds.has(c.id)) {
229
+ 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.` });
230
+ continue;
231
+ }
232
+ const s = screenOf2(c.id, ids);
233
+ if (!s) {
234
+ issues.push({ severity: "error", screen: "", component: c.id, code: "registry.no-screen", message: `"${c.id}" starts with no registered screen id.` });
235
+ continue;
236
+ }
237
+ byId.get(s).components.push({ bindId: c.id, kind: c.kind, label: c.label, parent: c.parent, info: c.info });
238
+ }
239
+ const index = {};
240
+ for (const b of file.bindings) {
241
+ const s = screenOf2(b.component, ids);
242
+ if (!s) {
243
+ issues.push({ severity: "error", screen: "", component: b.component, slot: b.slot, code: "binding.no-screen", message: `Binding for "${b.component}" fits no screen.` });
244
+ continue;
245
+ }
246
+ (index[s] ??= []).push(b);
247
+ }
248
+ return {
249
+ fw: { descriptors: file.kinds, registry: { registryVersion: 1, screens }, catalog, glossaries: file.glossaries },
250
+ index,
251
+ issues
252
+ };
253
+ }
254
+ function toFile2(file, index) {
255
+ const order = new Map(file.components.map((c, i) => [c.id, i]));
256
+ const bindings = Object.values(index).flat().sort((a, b) => (order.get(a.component) ?? 1e9) - (order.get(b.component) ?? 1e9) || a.slot.localeCompare(b.slot));
257
+ return { ...file, bindings };
258
+ }
259
+ function memoryStore2(seed) {
260
+ let file = seed;
261
+ return { load: async () => file, save: async (f) => {
262
+ file = f;
263
+ }, get current() {
264
+ return file;
265
+ } };
266
+ }
267
+ function httpStore2(url) {
268
+ return {
269
+ async load() {
270
+ const res = await fetch(url, { cache: "no-store" });
271
+ if (!res.ok)
272
+ throw new Error(`Could not read the bindings file (HTTP ${res.status}).`);
273
+ return parseBindingsFile2(await res.text());
274
+ },
275
+ async save(file) {
276
+ const res = await fetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(file, null, 2) + `
277
+ ` });
278
+ if (!res.ok)
279
+ throw new Error(await res.text() || `Could not save (HTTP ${res.status}).`);
280
+ }
281
+ };
282
+ }
283
+
231
284
  // src/framework/schema.ts
232
285
  var obj = (s) => s && typeof s === "object" ? s : {};
233
286
  function flat(s) {
@@ -412,13 +465,13 @@ function createEngine2(fw, manifests) {
412
465
  const sh = shapeOf(s, c, sl);
413
466
  if (sh.fields?.length) {
414
467
  if (sh.many)
415
- out.push({ expr: `${c.bindId}.${sl.id}`, label: `${c.label} · ${sl.label}`, type: "list", group: "screen", fields: sh.fields });
416
- sh.fields.forEach(([f, t]) => out.push({ expr: `${c.bindId}.${sl.id}${sh.many ? "[*]" : ""}.${f}`, label: `${c.label} · ${humanize(f)}`, name: f, type: sh.many ? t + "[]" : t, group: "screen" }));
468
+ out.push({ expr: `${c.bindId}:${sl.id}`, label: `${c.label} · ${sl.label}`, type: "list", group: "screen", fields: sh.fields });
469
+ sh.fields.forEach(([f, t]) => out.push({ expr: `${c.bindId}:${sl.id}${sh.many ? "[*]" : ""}.${f}`, label: `${c.label} · ${humanize(f)}`, name: f, type: sh.many ? t + "[]" : t, group: "screen" }));
417
470
  } else if (sh.via) {
418
471
  const via = d.slots.find((x) => x.id === sh.via);
419
- out.push({ expr: `${c.bindId}.${sl.id}`, label: `${c.label} · ${sl.label}`, type: "pending", group: "screen", waitsFor: `${c.label} · ${via?.label ?? sh.via}` });
472
+ out.push({ expr: `${c.bindId}:${sl.id}`, label: `${c.label} · ${sl.label}`, type: "pending", group: "screen", waitsFor: `${c.label} · ${via?.label ?? sh.via}` });
420
473
  } else
421
- out.push({ expr: `${c.bindId}.${sl.id}`, label: c.label + (sl.label && bindable(d).length + 1 < d.slots.length ? ` · ${sl.label}` : ""), name: c.label, type: sh.many ? "list" : sh.type ?? "any", group: "screen" });
474
+ out.push({ expr: `${c.bindId}:${sl.id}`, label: c.label + (sl.label && bindable(d).length + 1 < d.slots.length ? ` · ${sl.label}` : ""), name: c.label, type: sh.many ? "list" : sh.type ?? "any", group: "screen" });
422
475
  });
423
476
  });
424
477
  S.context.forEach((ctx) => {
@@ -641,16 +694,19 @@ function createEngine2(fw, manifests) {
641
694
  }
642
695
 
643
696
  // src/framework/validate.ts
644
- var BIND_ID = /^[A-Za-z_][\w-]*$/;
645
697
  function validateRegistry2(fw) {
646
698
  const kinds = new Set(fw.descriptors.map((d) => d.kind));
647
699
  const issues = [];
648
700
  const ids = new Set(fw.registry.screens.map((s) => s.id));
701
+ const seen = new Set;
649
702
  const screens = fw.registry.screens.map((S) => {
650
- const seen = new Set;
703
+ if (!ID2.test(S.id)) {
704
+ 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.` });
705
+ return { ...S, components: [] };
706
+ }
651
707
  const components = S.components.filter((c) => {
652
- if (!BIND_ID.test(c.bindId)) {
653
- issues.push({ severity: "error", screen: S.id, component: c.bindId, code: "registry.invalid-bind-id", message: `bindId "${c.bindId}" must use letters, digits, - or _ only (no dots).` });
708
+ if (!ID2.test(c.bindId)) {
709
+ 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.` });
654
710
  return false;
655
711
  }
656
712
  if (!kinds.has(c.kind)) {
@@ -658,7 +714,7 @@ function validateRegistry2(fw) {
658
714
  return false;
659
715
  }
660
716
  if (seen.has(c.bindId)) {
661
- issues.push({ severity: "error", screen: S.id, component: c.bindId, code: "registry.duplicate-bind-id", message: `bindId "${c.bindId}" is used twice.` });
717
+ issues.push({ severity: "error", screen: S.id, component: c.bindId, code: "registry.duplicate-id", message: `Component id "${c.bindId}" is used twice.` });
662
718
  return false;
663
719
  }
664
720
  seen.add(c.bindId);
@@ -682,78 +738,7 @@ function validateBindings2(engine) {
682
738
  return issues;
683
739
  }
684
740
 
685
- // src/runtime/BindingProvider.tsx
686
- import * as React from "react";
687
- import { jsx } from "react/jsx-runtime";
688
- var RuntimeContext = React.createContext(null);
689
- function latestShown(screens) {
690
- let best;
691
- for (const m of screens)
692
- if (m.shown && (!best || m.at > best.at))
693
- best = m;
694
- return best?.screen;
695
- }
696
- function useBindingRuntime() {
697
- const r = React.useContext(RuntimeContext);
698
- if (!r)
699
- throw new Error("Wrap the app in <BindingProvider> before using binding hooks.");
700
- return r;
701
- }
702
- function BindingProvider2({ catalog, manifests, fetcher, transforms, providers, persistence, children }) {
703
- const [overrides, setOverrides] = React.useState({});
704
- const requested = React.useRef(new Set);
705
- const mounted = React.useRef(new Map);
706
- const clock = React.useRef(0);
707
- const loadSaved = React.useCallback((screen) => {
708
- if (!persistence || requested.current.has(screen))
709
- return;
710
- requested.current.add(screen);
711
- persistence.load(screen).then((m) => {
712
- if (m)
713
- setOverrides((o) => ({ ...o, [screen]: m }));
714
- }, () => requested.current.delete(screen));
715
- }, [persistence]);
716
- const reloadSaved = React.useCallback(() => {
717
- requested.current.clear();
718
- new Set([...mounted.current.values()].map((m) => m.screen)).forEach(loadSaved);
719
- }, [loadSaved]);
720
- const mountScreen = React.useCallback((screen, shown) => {
721
- const key = Symbol(screen);
722
- mounted.current.set(key, { screen, shown, at: ++clock.current });
723
- return () => {
724
- mounted.current.delete(key);
725
- };
726
- }, []);
727
- const activeScreen = React.useCallback(() => latestShown(mounted.current.values()), []);
728
- const runtime = React.useMemo(() => {
729
- const index = new Map;
730
- const byScreen = new Map(manifests.map((m) => [m.screen, m]));
731
- Object.values(overrides).forEach((m) => byScreen.set(m.screen, m));
732
- byScreen.forEach((m) => m.bindings.forEach((b) => {
733
- if (b.state === "confirmed")
734
- index.set(`${m.screen}|${b.component}|${b.slot}`, b);
735
- }));
736
- return {
737
- operations: new Map(catalog.operations.map((o) => [o.id, o])),
738
- binding: (s, c, sl) => index.get(`${s}|${c}|${sl}`),
739
- fetcher,
740
- transforms: Object.fromEntries((transforms ?? []).map((t) => [t.name, t.apply])),
741
- providers: () => Object.fromEntries((providers ?? []).map((p) => [p.id, p.read()])),
742
- inflight: new Map,
743
- persistence,
744
- loadSaved,
745
- reloadSaved,
746
- mountScreen,
747
- activeScreen
748
- };
749
- }, [catalog, manifests, overrides, fetcher, transforms, providers, persistence, loadSaved, reloadSaved, mountScreen, activeScreen]);
750
- return /* @__PURE__ */ jsx(RuntimeContext.Provider, {
751
- value: runtime,
752
- children
753
- });
754
- }
755
-
756
- export { catalogFromOpenApi2, localStorageAdapter2, memoryAdapter2, toManifest2, markDirty, savePending, loadAll, putBinding, dropBinding, parseManifests, humanize, createEngine2, validateRegistry2, validateBindings2, useBindingRuntime, BindingProvider2 };
741
+ export { catalogFromOpenApi2, CatalogLoadError2, catalogFromText2, loadCatalog2, humanize, ID2, parseBindingsFile2, screenOf2, fromFile2, toFile2, memoryStore2, httpStore2, flat, createEngine2, validateRegistry2, validateBindings2 };
757
742
 
758
- //# debugId=4412F540958D6E7164756E2164756E21
759
- //# sourceMappingURL=index-3q1vx729.js.map
743
+ //# debugId=CBA6673BB224224F64756E2164756E21
744
+ //# sourceMappingURL=index-jj8twdw9.js.map