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,36 @@
1
+ /**
2
+ * Vite plugin: the binding studio inside the app's dev server, never in a build.
3
+ *
4
+ * // vite.config.ts
5
+ * import { bindingStudio } from "api_binding_ui/vite";
6
+ * plugins: [bindingStudio()]
7
+ *
8
+ * Serves the studio under /__binding/ and injects a floating button into the app's HTML pages
9
+ * (SPA and server-rendered alike). `vite build` never runs it (apply: "serve").
10
+ */
11
+ import type { IncomingMessage, ServerResponse } from "node:http";
12
+ export interface BindingStudioOptions {
13
+ /** bindings.json, relative to the Vite root. Default "bindings.json". */
14
+ file?: string;
15
+ /** Path the studio is served under. Default "/__binding". */
16
+ base?: string;
17
+ }
18
+ type Next = (err?: unknown) => void;
19
+ /** The parts of Vite's plugin API this uses; typed here so the package does not depend on Vite. */
20
+ export interface VitePluginLike {
21
+ name: string;
22
+ apply: "serve";
23
+ configResolved: (config: {
24
+ root: string;
25
+ }) => void;
26
+ transformIndexHtml: (html: string) => string;
27
+ configureServer: (server: {
28
+ middlewares: {
29
+ use: (fn: (req: IncomingMessage, res: ServerResponse, next: Next) => void) => void;
30
+ };
31
+ }) => void;
32
+ }
33
+ /** Adds the overlay script before the last </body> of HTML chunks; leaves everything else as it is. */
34
+ export declare function injectOverlay(res: ServerResponse, tag: string): void;
35
+ export declare function bindingStudio(options?: BindingStudioOptions): VitePluginLike;
36
+ export {};
package/dist/vite.js ADDED
@@ -0,0 +1,201 @@
1
+ // src/vite/plugin.ts
2
+ import path2 from "node:path";
3
+
4
+ // src/server/handler.ts
5
+ import { promises as fs } from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ // src/framework/file.ts
10
+ var BINDING_TYPES = new Set(["operation", "expression", "steps", "none"]);
11
+ function parseBindingsFile(text) {
12
+ let data;
13
+ try {
14
+ data = JSON.parse(text);
15
+ } catch {
16
+ throw new Error("The bindings file is not valid JSON.");
17
+ }
18
+ const f = data;
19
+ if (!f || typeof f !== "object")
20
+ throw new Error("The bindings file must be a JSON object.");
21
+ if (f.version !== 3)
22
+ throw new Error(`The bindings file must have "version": 3.`);
23
+ if (typeof f.openapi !== "string" || !f.openapi)
24
+ throw new Error(`The bindings file needs "openapi": the docs URL or a path.`);
25
+ for (const k of ["kinds", "screens", "components", "bindings"])
26
+ if (!Array.isArray(f[k]))
27
+ throw new Error(`"${k}" must be a list.`);
28
+ f.bindings.forEach((b, i) => {
29
+ const x = b;
30
+ if (!x || typeof x.component !== "string" || typeof x.slot !== "string" || !BINDING_TYPES.has(x.type))
31
+ throw new Error(`Binding ${i + 1} needs "component", "slot" and a known "type".`);
32
+ });
33
+ if (f.output && (typeof f.output.dir !== "string" || typeof f.output.client !== "string"))
34
+ throw new Error(`"output" needs "dir" and "client".`);
35
+ return f;
36
+ }
37
+
38
+ // src/server/handler.ts
39
+ var TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".map": "application/json", ".woff2": "font/woff2" };
40
+ var DIST = fileURLToPath(new URL(".", import.meta.url));
41
+ var APP_DIR = path.join(DIST, "app");
42
+ var OVERLAY_FILE = path.join(DIST, "overlay.js");
43
+ var isUrl = (s) => /^https?:\/\//i.test(s);
44
+ async function readDocs(src, base) {
45
+ if (isUrl(src)) {
46
+ const res = await fetch(src);
47
+ if (!res.ok)
48
+ throw new Error(`${src} returned HTTP ${res.status}.`);
49
+ return res.text();
50
+ }
51
+ if (!/\.(ya?ml|json)$/i.test(src))
52
+ throw new Error(`"${src}" is not a .yaml, .yml or .json file.`);
53
+ return fs.readFile(path.resolve(base, src), "utf8");
54
+ }
55
+ async function writeFileAtomic(file, text) {
56
+ const tmp = `${file}.${process.pid}.tmp`;
57
+ await fs.writeFile(tmp, text, "utf8");
58
+ await fs.rename(tmp, file);
59
+ }
60
+ async function readBody(req, limit = 20 * 1024 * 1024) {
61
+ const chunks = [];
62
+ let size = 0;
63
+ for await (const c of req) {
64
+ size += c.length;
65
+ if (size > limit)
66
+ throw new Error("Body too large.");
67
+ chunks.push(c);
68
+ }
69
+ return Buffer.concat(chunks).toString("utf8");
70
+ }
71
+ function send(res, status, body, type = "text/plain; charset=utf-8") {
72
+ res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).end(body);
73
+ }
74
+ function createStudioHandler({ file, prefix, allowed }) {
75
+ const base = path.dirname(file);
76
+ return async (req, res) => {
77
+ const url = new URL(req.url ?? "/", "http://local");
78
+ if (prefix && url.pathname !== prefix && !url.pathname.startsWith(prefix + "/"))
79
+ return false;
80
+ const rel = url.pathname.slice(prefix.length) || "/";
81
+ try {
82
+ if (prefix && url.pathname === prefix) {
83
+ res.writeHead(302, { Location: prefix + "/" + url.search }).end();
84
+ return true;
85
+ }
86
+ if (rel.startsWith("/api/") && !allowed(req)) {
87
+ send(res, 403, "Forbidden.");
88
+ return true;
89
+ }
90
+ if (rel === "/api/bindings") {
91
+ if (req.method === "GET")
92
+ send(res, 200, await fs.readFile(file, "utf8"), "application/json");
93
+ else if (req.method === "PUT") {
94
+ const text = await readBody(req);
95
+ try {
96
+ parseBindingsFile(text);
97
+ } catch (e) {
98
+ send(res, 400, e.message);
99
+ return true;
100
+ }
101
+ await writeFileAtomic(file, text);
102
+ send(res, 204, "");
103
+ } else
104
+ send(res, 405, "Method not allowed.");
105
+ return true;
106
+ }
107
+ if (rel === "/api/openapi" && req.method === "GET") {
108
+ const src = url.searchParams.get("src") ?? "";
109
+ try {
110
+ send(res, 200, await readDocs(src, base), src.endsWith(".json") ? "application/json" : "text/yaml; charset=utf-8");
111
+ } catch (e) {
112
+ send(res, 502, e.message);
113
+ }
114
+ return true;
115
+ }
116
+ if (req.method !== "GET" && req.method !== "HEAD") {
117
+ send(res, 405, "Method not allowed.");
118
+ return true;
119
+ }
120
+ if (rel === "/overlay.js") {
121
+ send(res, 200, await fs.readFile(OVERLAY_FILE), TYPES[".js"]);
122
+ return true;
123
+ }
124
+ const target = path.resolve(APP_DIR, rel === "/" ? "index.html" : decodeURIComponent(rel.slice(1)));
125
+ if (!target.startsWith(APP_DIR + path.sep)) {
126
+ send(res, 404, "Not found.");
127
+ return true;
128
+ }
129
+ const data = await fs.readFile(target).catch(() => null);
130
+ if (!data) {
131
+ send(res, 404, "Not found.");
132
+ return true;
133
+ }
134
+ send(res, 200, data, TYPES[path.extname(target)] ?? "application/octet-stream");
135
+ } catch (e) {
136
+ send(res, 500, e.message);
137
+ }
138
+ return true;
139
+ };
140
+ }
141
+
142
+ // src/vite/plugin.ts
143
+ function injectOverlay(res, tag) {
144
+ let done = false;
145
+ const isHtml = () => String(res.getHeader("content-type") ?? "").includes("text/html");
146
+ const patch = (chunk) => {
147
+ if (done || !isHtml())
148
+ return chunk;
149
+ const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : null;
150
+ if (text === null)
151
+ return chunk;
152
+ if (text.includes(tag)) {
153
+ done = true;
154
+ return chunk;
155
+ }
156
+ const i = text.lastIndexOf("</body>");
157
+ if (i < 0)
158
+ return chunk;
159
+ done = true;
160
+ if (!res.headersSent)
161
+ res.removeHeader("content-length");
162
+ return text.slice(0, i) + tag + text.slice(i);
163
+ };
164
+ const write = res.write.bind(res);
165
+ const end = res.end.bind(res);
166
+ res.write = (chunk, ...rest) => write(patch(chunk), ...rest);
167
+ res.end = (chunk, ...rest) => chunk == null || typeof chunk === "function" ? end(chunk, ...rest) : end(patch(chunk), ...rest);
168
+ }
169
+ function bindingStudio(options = {}) {
170
+ const base = (options.base ?? "/__binding").replace(/\/+$/, "");
171
+ const tag = `<script type="module" src="${base}/overlay.js"></script>`;
172
+ let file = "";
173
+ return {
174
+ name: "api-binding-studio",
175
+ apply: "serve",
176
+ configResolved(config) {
177
+ file = path2.resolve(config.root, options.file ?? "bindings.json");
178
+ },
179
+ transformIndexHtml: (html) => html.includes(tag) ? html : html.replace(/<\/body>/i, `${tag}</body>`),
180
+ configureServer(server) {
181
+ const allowed = (req) => {
182
+ const origin = req.headers.origin;
183
+ return req.method === "GET" || !origin || new URL(origin).host === req.headers.host;
184
+ };
185
+ const handle = createStudioHandler({ file, prefix: base, allowed });
186
+ server.middlewares.use((req, res, next) => {
187
+ handle(req, res).then((handled) => {
188
+ if (handled)
189
+ return;
190
+ if ((req.headers.accept ?? "").includes("text/html"))
191
+ injectOverlay(res, tag);
192
+ next();
193
+ }, next);
194
+ });
195
+ }
196
+ };
197
+ }
198
+ export {
199
+ bindingStudio,
200
+ injectOverlay
201
+ };
package/package.json CHANGED
@@ -1,53 +1,69 @@
1
- {
2
- "name": "api_binding_ui",
3
- "version": "2.0.2",
4
- "description": "API binding framework: bind any UI component to documented APIs at design time, run the bindings in production.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
13
- },
14
- "./studio": {
15
- "types": "./dist/studio/index.d.ts",
16
- "import": "./dist/studio/index.js"
17
- },
18
- "./studio.css": "./dist/studio/index.css",
19
- "./package.json": "./package.json"
20
- },
21
- "files": [
22
- "dist",
23
- "spec/FRAMEWORK.md",
24
- "README.md",
25
- "SKILL.md"
26
- ],
27
- "sideEffects": [
28
- "**/*.css"
29
- ],
30
- "scripts": {
31
- "dev": "bun --port=5174 ./index.html",
32
- "build": "rm -rf dist && bun build src/index.ts src/studio/index.ts --outdir dist --root src --target browser --splitting --sourcemap=linked --external react --external react-dom --external react/jsx-runtime --external yaml && node ./node_modules/typescript/bin/tsc -p tsconfig.build.json",
33
- "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p tsconfig.json",
34
- "test": "bun test",
35
- "prepublishOnly": "bun run typecheck && bun test && bun run build"
36
- },
37
- "peerDependencies": {
38
- "react": ">=18.0.0",
39
- "react-dom": ">=18.0.0"
40
- },
41
- "devDependencies": {
42
- "@types/node": "^22.13.0",
43
- "@types/react": "^19.2.0",
44
- "@types/react-dom": "^19.2.0",
45
- "react": "^19.2.0",
46
- "react-dom": "^19.2.0",
47
- "typescript": "^5.8.0",
48
- "vitest": "^3.0.0"
49
- },
50
- "dependencies": {
51
- "yaml": "^2.8.1"
52
- }
1
+ {
2
+ "name": "api_binding_ui",
3
+ "version": "3.1.0",
4
+ "description": "Design-time API binding: register screen components, bind them to documented APIs in a studio, and generate plain React Query hooks over an Orval client. Nothing ships in the app.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./studio": {
15
+ "types": "./dist/studio/index.d.ts",
16
+ "import": "./dist/studio/index.js"
17
+ },
18
+ "./studio.css": "./dist/studio/index.css",
19
+ "./package.json": "./package.json",
20
+ "./vite": {
21
+ "types": "./dist/vite/plugin.d.ts",
22
+ "import": "./dist/vite.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "spec/FRAMEWORK.md",
28
+ "README.md",
29
+ "SKILL.md"
30
+ ],
31
+ "sideEffects": [
32
+ "**/*.css"
33
+ ],
34
+ "scripts": {
35
+ "dev": "bun run build:app && node dist/cli.js studio src/dev/sample/bindings.json",
36
+ "build:app": "rm -rf dist/app && bun build ./index.html --outdir dist/app --minify",
37
+ "build": "rm -rf dist && bun build src/index.ts src/studio/index.ts --outdir dist --root src --target browser --splitting --sourcemap=linked --external react --external react-dom --external react/jsx-runtime --external yaml && bun build src/cli/cli.ts --outfile dist/cli.js --target node --external yaml --banner '#!/usr/bin/env node' && bun build src/vite/plugin.ts --outfile dist/vite.js --target node --external yaml && bun build src/overlay/main.ts --outfile dist/overlay.js --target browser --minify && bun run build:app && node ./node_modules/typescript/bin/tsc -p tsconfig.build.json",
38
+ "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p tsconfig.json",
39
+ "test": "bun test",
40
+ "prepublishOnly": "bun run typecheck && bun test && bun run build"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=18.0.0",
44
+ "react-dom": ">=18.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.13.0",
48
+ "@types/react": "^19.2.0",
49
+ "@types/react-dom": "^19.2.0",
50
+ "react": "^19.2.0",
51
+ "react-dom": "^19.2.0",
52
+ "typescript": "^5.8.0",
53
+ "vitest": "^3.0.0"
54
+ },
55
+ "dependencies": {
56
+ "yaml": "^2.8.1"
57
+ },
58
+ "bin": {
59
+ "api-binding": "dist/cli.js"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "react": {
63
+ "optional": true
64
+ },
65
+ "react-dom": {
66
+ "optional": true
67
+ }
68
+ }
53
69
  }