@openthink/ui-leaf 0.7.1 → 0.8.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 CHANGED
@@ -104,7 +104,7 @@ await mount({
104
104
  port, // optional, default: 5810 (auto-bumps if busy; pass 0 for OS-assigned)
105
105
  openBrowser, // optional, default: true
106
106
  shell, // optional, "tab" | "app", default: "tab"
107
- csp, // optional, default: "off" (see Hardening)
107
+ csp, // optional, default: "strict" (see Hardening)
108
108
  silent, // optional, default: false (see Programmatic use)
109
109
  signal, // optional AbortSignal
110
110
  heartbeatTimeoutMs, // optional, default: 5000
@@ -118,30 +118,32 @@ Returns `{ url, port, closed, close }`.
118
118
 
119
119
  ## Hardening: locking the data/mutation contract with CSP
120
120
 
121
- By default, the data/mutation routing is **convention, not enforcement** a view file is JavaScript in a browser tab and can `fetch()` anywhere it likes. Most consumers don't need more than that.
121
+ By default, ui-leaf **browser-enforces the broker principle** via `csp: "strict"`. The view physically cannot reach external endpoints `fetch()` to any non-loopback origin is blocked at the CSP layer, and HTML form submissions are locked to same-origin. All data flows through `data` and `mutations`.
122
122
 
123
- When you do want to enforce it (typically: views handle data sensitive enough that you don't want a forked view to be able to exfiltrate it), opt in via `csp`:
123
+ The default strict preset:
124
+
125
+ - **`connect-src 'self'`** — the architectural lock. Views cannot fetch external APIs.
126
+ - **`form-action 'self'`** — closes the form-submit exfiltration vector.
127
+ - **`img-src 'self' https: data:`** — permits HTTPS images and data URIs.
128
+ - **`font-src 'self' https: data:`** — permits CDN fonts.
129
+ - **`style-src 'self' 'unsafe-inline'`** and **`script-src 'self' 'unsafe-inline'`** — required for React.
130
+
131
+ Because the policy is sent as an HTTP response header, views cannot relax it at runtime. The only way to weaken the policy is to change the `mount()` call (i.e. fork the consumer CLI, not the view).
132
+
133
+ If you have a legitimate need for cross-origin access (e.g. views that talk to a companion API), opt out:
124
134
 
125
135
  ```ts
126
136
  mount({
127
137
  view: "report",
128
138
  data: { ... },
129
- csp: "strict", // or a custom CSP string for full control
139
+ csp: "off", // no CSP header; views can fetch arbitrary URLs
130
140
  });
131
141
  ```
132
142
 
133
- `csp: "strict"` ships a balanced preset that:
134
-
135
- - **Locks `connect-src` to same-origin** — the architectural lock. Views cannot fetch external APIs; all data flows through `data` and `mutations`.
136
- - **Permits HTTPS images and fonts** so views can load CDN assets normally.
137
- - **Allows inline styles and inline scripts** for React.
138
-
139
- Because the policy is sent as an HTTP response header, views cannot relax it at runtime. The only way to weaken the policy is to change the `mount()` call (i.e. fork the consumer CLI, not the view).
140
-
141
- If the preset is too strict for your case (e.g. you need to allow Sentry telemetry), pass a raw CSP string:
143
+ For partial relaxation (e.g. allow Sentry telemetry but keep everything else locked), pass a raw CSP string:
142
144
 
143
145
  ```ts
144
- csp: "default-src 'self'; connect-src 'self' https://sentry.io; img-src 'self' https:;"
146
+ csp: "default-src 'self'; connect-src 'self' https://sentry.io; form-action 'self'; img-src 'self' https:;"
145
147
  ```
146
148
 
147
149
  ### DNS-rebinding defence
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openthink/ui-leaf",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Customizable browser views, on demand, for any CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Matt Pardini",
@@ -384,6 +384,7 @@ async function openInAppMode(url) {
384
384
  var STRICT_CSP = [
385
385
  "default-src 'self'",
386
386
  "connect-src 'self'",
387
+ "form-action 'self'",
387
388
  "img-src 'self' data: https:",
388
389
  "font-src 'self' https: data:",
389
390
  "style-src 'self' 'unsafe-inline'",
@@ -439,7 +440,7 @@ async function startDevServer(opts) {
439
440
  shell = "tab",
440
441
  heartbeatTimeoutMs = 5e3,
441
442
  startupGraceMs = 3e4,
442
- csp,
443
+ csp = "strict",
443
444
  allowedHosts,
444
445
  silent = false,
445
446
  _opener,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/index.ts","../src/server.ts","../src/compile.ts","../src/internal/html.ts","../src/ipc.ts"],"sourcesContent":["#!/usr/bin/env node\n// ui-leaf CLI — language-neutral entry point for non-Node consumers.\n//\n// Protocol (stdio, line-delimited JSON; every message carries\n// `\"version\":\"1\"` as the first key):\n//\n// STDIN\n// Line 1: config object {\"version\":\"1\",\"view\":…,\"viewsRoot\":…,…}\n// Line 2+: one of:\n// Mutation responses (identified by the `id` field):\n// {\"version\":\"1\",\"type\":\"result\",\"id\":<n>,\"value\":<any>}\n// {\"version\":\"1\",\"type\":\"error\",\"id\":<n>,\"message\":\"<text>\"}\n// Live-update messages (no `id`):\n// {\"version\":\"1\",\"type\":\"update\",\"data\":<any>}\n// {\"version\":\"1\",\"type\":\"view\",\"source\":\"<tsx string>\"}\n// {\"version\":\"1\",\"type\":\"patch\",\"data\":<any>,\"view\":{\"source\":\"<tsx>\"}}\n// {\"version\":\"1\",\"type\":\"reopen\"}\n// {\"version\":\"1\",\"type\":\"close\"}\n//\n// STDOUT\n// {\"version\":\"1\",\"type\":\"ready\",\"url\":\"<url>\",\"port\":<n>}\n// {\"version\":\"1\",\"type\":\"mutate\",\"id\":<n>,\"name\":\"<s>\",\"args\":<any>}\n// {\"version\":\"1\",\"type\":\"disconnected\"}\n// {\"version\":\"1\",\"type\":\"reconnected\"}\n// {\"version\":\"1\",\"type\":\"closed\",\"reason\":\"caller\"|\"signal\"|\"error\"}\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"<text>\"}\n// {\"version\":\"1\",\"type\":\"error\",\"phase\":\"build\",\"message\":\"<text>\"}\n//\n// Version handling\n// A missing version field on any inbound message produces\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"missing version field\"}\n// A non-\"1\" version produces\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"unsupported protocol version: <x>\"}\n// Both errors on the config message exit 1; on subsequent messages\n// the bad line is dropped and the mount keeps running.\n//\n// Unknown post-config message types produce:\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"unknown message type: <x>\"}\n// The mount continues (non-fatal).\n//\n// view / patch compile failures preserve the previous view and produce:\n// {\"version\":\"1\",\"type\":\"error\",\"phase\":\"build\",\"message\":\"<text>\"}\n//\n// Inline view.source constraint (v1.0.0): the TSX source string is\n// treated as self-contained. Relative imports are not supported —\n// the string has no filesystem context to resolve them against. Bare-\n// package imports (react, react-dom) work via the internal alias plugin.\n//\n// Lifecycle\n// disconnected: browser tab heartbeat stopped; mount stays alive.\n// reconnected: browser reconnected after a disconnect.\n// closed: mount terminated; reason is caller|signal|error.\n// Exits 0 on closed with reason caller|signal; exits 1 on error.\n// Closing stdin from the parent triggers a caller close (exit 0).\n\nimport { createInterface } from \"node:readline\";\nimport { mount, type MountOptions } from \"./index.js\";\nimport {\n emit as serializeEvent,\n parseInbound,\n validateInboundShape,\n type Inbound,\n type InboundConfig,\n type OutboundEvent,\n} from \"./ipc.js\";\n\n// Capture the real stdout write BEFORE anything (especially mount() with\n// silent: true) gets a chance to redirect process.stdout. The binary's\n// protocol output uses this directly; bundler / dev-server noise (which\n// goes through process.stdout.write) gets redirected to stderr by silent\n// mode without affecting our protocol channel.\nconst realStdoutWrite = process.stdout.write.bind(process.stdout);\n\nconst args = process.argv.slice(2);\n\nif (args.length === 0 || args[0] === \"--help\" || args[0] === \"-h\") {\n process.stdout.write(\n [\n \"ui-leaf — Customizable browser views, on demand, for any CLI.\",\n \"\",\n \"Usage:\",\n \" ui-leaf mount Read a JSON config from stdin and mount a view.\",\n \" See https://github.com/OpenThinkAi/ui-leaf for\",\n \" the full stdio protocol spec.\",\n \"\",\n \" ui-leaf --version Print version.\",\n \" ui-leaf --help Print this message.\",\n \"\",\n ].join(\"\\n\"),\n );\n process.exit(0);\n}\n\nif (args[0] === \"--version\" || args[0] === \"-v\") {\n // Read version from package.json shipped alongside this binary.\n const { createRequire } = await import(\"node:module\");\n const require = createRequire(import.meta.url);\n const pkg = require(\"../package.json\") as { version: string };\n process.stdout.write(`${pkg.version}\\n`);\n process.exit(0);\n}\n\nif (args[0] !== \"mount\") {\n process.stderr.write(`ui-leaf: unknown command \"${args[0]}\"\\n`);\n process.exit(1);\n}\n\n// `ui-leaf mount --help` (or being run on a TTY without piped input)\n// would otherwise sit silently on stdin. Print the protocol pointer and\n// exit 0 so users discovering the binary land on docs, not a \"broken\"\n// exit code.\nif (args[1] === \"--help\" || args[1] === \"-h\" || process.stdin.isTTY) {\n process.stdout.write(\n [\n \"ui-leaf mount — read a JSON config from stdin and mount a view.\",\n \"\",\n \"Protocol: line-delimited JSON over stdio.\",\n \" stdin line 1 = config object\",\n \" lines 2+ = mutation responses {type:result|error,id,...}\",\n \" stdout {type:ready,url,port}, {type:mutate,id,name,args},\",\n \" {type:closed}, {type:error,message}\",\n \"\",\n \"Full spec: https://github.com/OpenThinkAi/ui-leaf#driving-ui-leaf-from-a-non-node-cli\",\n \"\",\n \"Example:\",\n ` echo '{\"view\":\"spec\",\"viewsRoot\":\"/abs/path\",\"data\":{}}' | ui-leaf mount`,\n \"\",\n ].join(\"\\n\"),\n );\n process.exit(0);\n}\n\ntry {\n await runMount();\n} catch (err) {\n emit({\n type: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n process.exit(1);\n}\n\ntype ConfigRequest = InboundConfig;\n\nfunction emit(event: OutboundEvent): void {\n realStdoutWrite(serializeEvent(event));\n}\n\nfunction stringifyVersion(got: unknown): string {\n if (typeof got === \"string\") return got;\n return JSON.stringify(got);\n}\n\nasync function runMount(): Promise<void> {\n const rl = createInterface({ input: process.stdin });\n let nextId = 0;\n const pending = new Map<\n number,\n { resolve: (value: unknown) => void; reject: (err: Error) => void }\n >();\n\n let configReceived = false;\n let configResolve!: (cfg: ConfigRequest) => void;\n let configReject!: (err: Error) => void;\n const configPromise = new Promise<ConfigRequest>((res, rej) => {\n configResolve = res;\n configReject = rej;\n });\n\n // Track the mounted view so the stdin-close handler can shut it down.\n // Set after `mount()` resolves; null until then.\n // biome-ignore lint/suspicious/noExplicitAny: MountedView shape inferred\n let mountedView: any = null;\n let stdinClosed = false;\n\n rl.on(\"line\", (line) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n if (!configReceived) {\n configReceived = true;\n const outcome = parseInbound<ConfigRequest>(trimmed);\n if (!outcome.ok) {\n // Config is the load-bearing first message. A bad version on the\n // config can't be recovered from, so per AC #5's optional clause\n // we emit the spec'd error and exit. Subsequent (post-config)\n // version violations are non-fatal — see below.\n if (outcome.kind === \"json\") {\n emit({\n type: \"error\",\n message: `failed to parse config JSON: ${outcome.reason}`,\n });\n } else if (outcome.kind === \"missing-version\") {\n emit({ type: \"error\", message: \"missing version field\" });\n } else {\n emit({\n type: \"error\",\n message: `unsupported protocol version: ${stringifyVersion(outcome.got)}`,\n });\n }\n process.exit(1);\n }\n const configValidation = validateInboundShape(outcome.msg, \"config\");\n if (!configValidation.ok) {\n emit({ type: \"error\", message: configValidation.reason });\n process.exit(1);\n }\n configResolve(outcome.msg);\n return;\n }\n\n // Post-config message: mutation response or live-update command.\n const outcome = parseInbound<Inbound>(trimmed);\n if (!outcome.ok) {\n if (outcome.kind === \"missing-version\") {\n emit({ type: \"error\", message: \"missing version field\" });\n } else if (outcome.kind === \"unsupported-version\") {\n emit({\n type: \"error\",\n message: `unsupported protocol version: ${stringifyVersion(outcome.got)}`,\n });\n }\n // Malformed JSON falls through silently.\n return;\n }\n const msg = outcome.msg;\n\n const msgValidation = validateInboundShape(msg, \"post-config\");\n if (!msgValidation.ok) {\n emit({ type: \"error\", message: msgValidation.reason });\n return;\n }\n\n // Mutation responses carry an `id` field — discriminate before checking `type`.\n if (\"id\" in msg && typeof msg.id === \"number\") {\n const p = pending.get(msg.id);\n if (!p) return;\n pending.delete(msg.id);\n if (msg.type === \"result\") p.resolve(msg.value);\n else if (msg.type === \"error\") p.reject(new Error(msg.message));\n return;\n }\n\n // Live-update commands — only dispatch if mountedView is ready.\n if (!mountedView) return;\n\n if (msg.type === \"update\") {\n mountedView.update(msg.data);\n return;\n }\n\n if (msg.type === \"view\") {\n void (async () => {\n const errors = await mountedView.swapView(msg.source);\n if (errors.length > 0) {\n emit({\n type: \"error\",\n phase: \"build\",\n message: errors.map((e: { message: string }) => e.message).join(\"; \"),\n });\n }\n })();\n return;\n }\n\n if (msg.type === \"patch\") {\n void (async () => {\n const errors = await mountedView.patch(msg.data, msg.view.source);\n if (errors.length > 0) {\n emit({\n type: \"error\",\n phase: \"build\",\n message: errors.map((e: { message: string }) => e.message).join(\"; \"),\n });\n }\n })();\n return;\n }\n\n if (msg.type === \"reopen\") {\n void mountedView.reopen().catch((err: unknown) => {\n emit({ type: \"error\", message: err instanceof Error ? err.message : String(err) });\n });\n return;\n }\n\n if (msg.type === \"close\") {\n void mountedView.close();\n return;\n }\n\n if (msg.type === \"ping\") {\n // Heartbeat from caller — silently acknowledged; no reply emitted.\n return;\n }\n });\n\n rl.on(\"close\", () => {\n stdinClosed = true;\n // Reject any pending mutations — the parent isn't going to respond.\n for (const { reject } of pending.values()) {\n reject(new Error(\"ui-leaf: stdin closed by parent before mutation responded\"));\n }\n pending.clear();\n // If we never received config, the parent dropped before doing\n // anything useful. Bail out with a non-zero exit so that's visible.\n if (!configReceived) {\n configReject(new Error(\"ui-leaf: stdin closed before config received\"));\n return;\n }\n // Otherwise, tear down the mounted view (if it exists yet) so the\n // process exits without waiting on the heartbeat timeout. The\n // view.closed promise resolves and runMount's normal exit path runs.\n if (mountedView) {\n void mountedView.close();\n }\n });\n\n const config = await configPromise;\n\n // Build mutations map: each declared name becomes a handler that emits\n // a mutate event on stdout and awaits a paired response on stdin.\n // biome-ignore lint/suspicious/noExplicitAny: handler signatures vary\n const mutations: Record<string, (args: any) => Promise<unknown>> = {};\n for (const name of config.mutations ?? []) {\n mutations[name] = (mutationArgs: unknown) => {\n const id = ++nextId;\n return new Promise<unknown>((resolve, reject) => {\n pending.set(id, { resolve, reject });\n emit({ type: \"mutate\", id, name, args: mutationArgs });\n });\n };\n }\n\n const mountOpts: MountOptions = {\n view: config.view,\n viewsRoot: config.viewsRoot,\n data: config.data,\n mutations,\n title: config.title,\n port: config.port,\n openBrowser: config.openBrowser,\n shell: config.shell,\n csp: config.csp,\n heartbeatTimeoutMs: config.heartbeatTimeoutMs,\n startupGraceMs: config.startupGraceMs,\n silent: true, // bridge owns stdout; bundler / dev-server output must stay silent\n };\n\n try {\n const view = await mount(mountOpts);\n mountedView = view;\n // If stdin closed while we were waiting on mount(), tear down right\n // away rather than hold the dev server open.\n if (stdinClosed) {\n void view.close();\n }\n view.on(\"disconnected\", () => emit({ type: \"disconnected\" }));\n view.on(\"reconnected\", () => emit({ type: \"reconnected\" }));\n emit({ type: \"ready\", url: view.url, port: view.port });\n const closeReason = await view.closed;\n emit({ type: \"closed\", reason: closeReason });\n process.exit(closeReason === \"error\" ? 1 : 0);\n } catch (err) {\n emit({\n type: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n process.exit(1);\n }\n}\n","// ui-leaf — Customizable browser views, on demand, for any CLI.\n// https://github.com/OpenThinkAi/ui-leaf\n\nimport { resolve } from \"node:path\";\nimport {\n startDevServer,\n type CloseReason,\n type CspOption,\n type DevServerEvent,\n type DevServerEventListener,\n type MutationHandler,\n type Shell,\n} from \"./server.js\";\nimport type { BuildError } from \"./compile.js\";\n\nexport type { BuildError, CloseReason, CspOption, DevServerEvent, DevServerEventListener, MutationHandler, Shell };\n\nexport interface MountOptions {\n /** View name. Resolves to <viewsRoot>/<view>.tsx. */\n view: string;\n /**\n * JSON-serializable data passed to the view as a prop.\n *\n * Privacy note: the data is compiled into the HTML served at the mount URL\n * and held in memory for the mount lifetime. Any same-UID local process\n * that can reach `127.0.0.1:<port>` can fetch `GET /` and read it — the\n * per-launch token guards `/mutate` against drive-by cross-origin requests\n * in the browser, not against other processes on the machine. For PHI, PCI,\n * financial records, or anything where a same-UID local reader is in your\n * threat model, use `dataLoader` instead — the loader's return value is\n * served at a token-gated `/api/data` endpoint and never appears in the HTML.\n */\n data?: unknown;\n /**\n * Async function that supplies sensitive data to the view without\n * including it in the served HTML. When provided, the loader is called\n * once during mount setup; its resolved value is served at a token-gated\n * `GET /api/data` endpoint (same per-launch token as `/mutate`) and the\n * view fetches it on first render before calling `createRoot().render()`.\n * The data never appears in the compiled HTML.\n *\n * Use this instead of `data` for PHI, PCI, financial records, or anything\n * else where in-HTML data exposure is in your threat model.\n *\n * Error semantics: if the loader rejects, the rejection propagates to the\n * `mount()` caller (no automatic retry). Errors surface at mount time,\n * matching the synchronous `data` path's behavior.\n *\n * Mutual exclusion: passing both `data` and `dataLoader` throws at\n * mount time.\n */\n dataLoader?: () => Promise<unknown>;\n /**\n * Mutation handlers the view can call via mutate(name, args).\n * Each handler can self-type its args and return:\n *\n * mutations: {\n * recategorize: async (args: { id: string; category: string }) => {\n * await db.recategorize(args.id, args.category);\n * return { ok: true };\n * },\n * }\n *\n * Each request body is capped at 1 MiB; oversized POSTs are rejected\n * with a 400 and the view's mutate() promise rejects with a clear error.\n */\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Root directory holding view .tsx files. Defaults to <cwd>/views. */\n viewsRoot?: string;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Port to bind. Defaults to 5810 — unused by the major Node dev tools.\n * If the port is unavailable, ui-leaf bumps to the next free port and\n * the actual bound port is reflected on the returned `url` and `port`.\n * Pass `0` to let the OS pick a free port directly.\n * Override only if you need a stable URL (e.g. an external bookmark).\n */\n port?: number;\n /**\n * Open the browser when ready. Defaults to true. When false, mount()\n * returns the URL on its resolved value so the caller can drive a\n * headless browser, log the address, etc.\n */\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - `\"tab\"` — open in the user's default browser as a regular tab.\n * Works everywhere; URL bar is visible.\n *\n * - `\"app\"` — try Chromium's `--app` mode for a chromeless window\n * (no URL bar, no tabs, looks like a desktop app). Available on\n * Chrome, Edge, and Brave. If no Chromium browser is installed,\n * ui-leaf falls back to \"tab\" with a stderr note. Safari and\n * Firefox always fall back.\n *\n * Pair with the share-link pattern (see \"Sharing views across users\"\n * in the README) when you want users to never see a localhost URL.\n */\n shell?: Shell;\n /**\n * Abort to close the dev server early. The returned `closed` promise\n * resolves either way; if you need to distinguish a signal-driven close\n * from a natural tab-close, check `signal.aborted` after the await.\n */\n signal?: AbortSignal;\n /**\n * Browser silence (ms) after which the mount emits `disconnected`.\n * Defaults to 5000 — tuned for the v1.0.0 subprocess-driver model where\n * a fast `disconnected` signal lets the caller decide whether to close,\n * reopen, or keep the mount alive. Raise it for sessions where the page\n * may legitimately pause (devtools paused on a breakpoint, machine\n * sleep, long background-tab throttling). Note: this no longer controls\n * when the mount terminates — only when the `disconnected` event fires.\n */\n heartbeatTimeoutMs?: number;\n /**\n * Content-Security-Policy enforcement. Defaults to \"off\".\n *\n * - `\"off\"` — no CSP header sent. Views can fetch arbitrary URLs and\n * embed external resources freely. The data/mutations convention is\n * honor-system.\n *\n * - `\"strict\"` — ui-leaf sends a balanced preset: locks `connect-src`\n * to same-origin (the architectural lock — views cannot fetch\n * external APIs, so all data flows through `data` and `mutations`),\n * while permitting common needs (HTTPS images / fonts, inline\n * styles for React). View files can only *add* further restrictions\n * via meta tag, never remove them.\n *\n * - `string` — raw CSP header value for full control. Use when the\n * \"strict\" preset doesn't fit (e.g. you need `connect-src` to\n * include a Sentry endpoint).\n *\n * Trade-off: when set to \"strict\" or a custom string, a view file\n * cannot relax the policy at runtime. Switching back requires changing\n * the mount() call. That rigidity is a feature.\n */\n csp?: CspOption;\n /**\n * Extra hostnames accepted in the request `Host` and `Origin` headers\n * on top of the built-in loopback set (`localhost`, `127.0.0.1`, `[::1]`).\n *\n * The dev server gates every request on this set to defend against\n * DNS-rebinding attacks; non-matching requests get HTTP 403. Use this\n * escape hatch when you need to reach the dev server through a custom\n * `/etc/hosts` alias (e.g. `[\"my-app.local\"]`) or any other loopback\n * name. Hostnames are matched case-insensitively, port-agnostic.\n *\n * Be deliberate: any hostname you add becomes a viable DNS-rebinding\n * target. Don't add wildcards, public DNS names, or LAN hostnames you\n * don't fully control.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. Default: false.\n *\n * When you drive `mount()` programmatically — e.g. as part of a Node\n * bridge for a non-Node CLI that's spawned ui-leaf as a subprocess —\n * stdout is usually reserved for a structured protocol (line-delimited\n * JSON, etc.). Setting `silent: true` redirects `process.stdout.write`\n * to `process.stderr` for the lifetime of the server, restored on close.\n *\n * Tradeoff: any other code in the same process that writes to stdout\n * during the server's lifetime is also redirected. Hold the captured\n * `process.stdout.write` reference yourself if you need to write to the\n * real stdout from the same process.\n */\n silent?: boolean;\n /**\n * Grace period (ms) after server start before the heartbeat watcher arms.\n * Cold-loading clients sometimes take a few seconds to send their first\n * heartbeat. Defaults to 30000.\n */\n startupGraceMs?: number;\n /**\n * Test seam: heartbeat watcher tick interval (ms). Defaults to 1000.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n}\n\nexport interface MountedView {\n /** URL the view is reachable at (http://127.0.0.1:<port>). */\n url: string;\n /** Bound port. Useful when port: 0 was requested. */\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n /** Force-close the dev server early. */\n close: () => Promise<void>;\n /**\n * Replace in-memory data and notify all `data-updated` listeners.\n * Preserves in-page React state — no recompile.\n */\n update: (data: unknown) => void;\n /**\n * Swap the view source on the fly. Triggers a recompile; on success replaces\n * the served HTML and notifies all `view-swapped` listeners. On compile\n * failure the previous HTML is preserved. Returns compile errors if any.\n */\n swapView: (source: string) => Promise<BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails neither\n * takes effect. Returns compile errors if any.\n */\n patch: (data: unknown, source: string) => Promise<BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /** Subscribe to a server-side event (data-updated | view-swapped). */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n /** Unsubscribe a previously-registered listener. */\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\n/**\n * Mount a customizable browser view from a CLI. Spins up a local dev server\n * and renders the chosen view with the given data. Returns once the server\n * is ready; await `result.closed` to block until the mount terminates.\n *\n * Mutations triggered in the view are dispatched to the registered handlers\n * here; the view never reaches the CLI's backing API directly.\n *\n * **Lifecycle.** Browser tab close (heartbeat silence) emits a `disconnected`\n * event on `result` but does NOT resolve `closed` or stop the server. The\n * mount only terminates — and `closed` resolves — when you call\n * `result.close()`, receive SIGINT/SIGTERM, or an internal error occurs.\n * Listen for `disconnected` and call `result.close()` yourself if you want\n * fast shutdown on tab close.\n *\n * **Multi-tab note.** The heartbeat is a single high-water mark across all\n * open tabs; `disconnected` fires only when all tabs go silent. Closing one\n * tab while another is open emits no event.\n *\n * Ctrl+C: this function installs SIGINT and SIGTERM handlers that close\n * the server before exiting.\n */\nexport async function mount(opts: MountOptions): Promise<MountedView> {\n const viewsRoot = opts.viewsRoot ?? resolve(process.cwd(), \"views\");\n\n const server = await startDevServer({\n view: opts.view,\n data: opts.data,\n dataLoader: opts.dataLoader,\n viewsRoot,\n mutations: opts.mutations,\n title: opts.title,\n port: opts.port,\n openBrowser: opts.openBrowser,\n shell: opts.shell,\n heartbeatTimeoutMs: opts.heartbeatTimeoutMs,\n startupGraceMs: opts.startupGraceMs,\n csp: opts.csp,\n allowedHosts: opts.allowedHosts,\n silent: opts.silent,\n _heartbeatCheckIntervalMs: opts._heartbeatCheckIntervalMs,\n });\n\n const onSignal = (signal: NodeJS.Signals): void => {\n void (async () => {\n await server.close(\"signal\");\n // Re-raise so default exit codes still apply.\n process.kill(process.pid, signal);\n })();\n };\n const sigint = (): void => onSignal(\"SIGINT\");\n const sigterm = (): void => onSignal(\"SIGTERM\");\n process.once(\"SIGINT\", sigint);\n process.once(\"SIGTERM\", sigterm);\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n await server.close();\n return {\n url: server.url,\n port: server.port,\n closed: Promise.resolve<CloseReason>(\"caller\"),\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n }\n opts.signal.addEventListener(\n \"abort\",\n () => void server.close(),\n { once: true },\n );\n }\n\n const closed = server.closed.finally(() => {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n });\n\n return {\n url: server.url,\n port: server.port,\n closed,\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n}\n","import { randomBytes, timingSafeEqual as nodeTimingSafeEqual } from \"node:crypto\";\nimport open, { apps } from \"open\";\nimport { compileView, compileSource } from \"./compile.js\";\nimport type { CloseReason } from \"./ipc.js\";\n\n// Module-level stdout redirect state. Captured ONCE at module load so\n// concurrent silent: true mounts share the same \"original\" reference and\n// restore-order doesn't matter. Refcounted so the last close restores.\nconst ORIGINAL_STDOUT_WRITE = process.stdout.write.bind(process.stdout);\nlet stdoutRedirectCount = 0;\n\n/**\n * Redirect process.stdout.write to process.stderr until the returned\n * function is called. Safe under concurrent silent mounts.\n */\nfunction redirectStdoutToStderr(): () => void {\n stdoutRedirectCount++;\n if (stdoutRedirectCount === 1) {\n // biome-ignore lint/suspicious/noExplicitAny: stdout.write has overloaded\n // signatures; forward exactly what comes in.\n process.stdout.write = ((chunk: any, enc?: any, cb?: any) =>\n process.stderr.write(chunk, enc, cb)) as typeof process.stdout.write;\n }\n let released = false;\n return () => {\n if (released) return;\n released = true;\n stdoutRedirectCount--;\n if (stdoutRedirectCount === 0) {\n process.stdout.write = ORIGINAL_STDOUT_WRITE;\n }\n };\n}\n\nexport type MutationHandler<TArgs = unknown, TResult = unknown> = (\n args: TArgs,\n) => TResult | Promise<TResult>;\n\n// `(string & {})` preserves the \"off\" / \"strict\" autocomplete suggestions\n// while still allowing arbitrary CSP strings. Plain string would collapse\n// the union and lose IntelliSense for the literals.\nexport type CspOption = \"off\" | \"strict\" | (string & {});\n\nexport type Shell = \"tab\" | \"app\";\n\n/**\n * Try to open `url` in a Chromium browser's --app mode (chromeless window:\n * no URL bar, no tabs). Returns true if a Chromium browser was found and\n * launched, false if no Chromium variant is installed (caller should fall\n * back to the default-browser tab).\n */\nasync function openInAppMode(url: string): Promise<boolean> {\n // Order: most-common Chromium variants first.\n const candidates = [apps.chrome, apps.edge, apps.brave];\n for (const app of candidates) {\n try {\n await open(url, { app: { name: app, arguments: [`--app=${url}`] } });\n return true;\n } catch {\n // Try next candidate; `open` throws if the binary isn't installed.\n }\n }\n return false;\n}\n\n/**\n * Strict preset: locks `connect-src` to same-origin (the architectural\n * lock that forces views to route mutations through the CLI), while\n * permitting common needs (HTTPS images/fonts, inline styles for React).\n * A future v1.x mode could tighten script-src once usage patterns are known.\n */\nconst STRICT_CSP = [\n \"default-src 'self'\",\n \"connect-src 'self'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self' https: data:\",\n \"style-src 'self' 'unsafe-inline'\",\n \"script-src 'self' 'unsafe-inline'\",\n].join(\"; \");\n\nfunction resolveCsp(opt: CspOption | undefined): string | null {\n if (!opt || opt === \"off\") return null;\n if (opt === \"strict\") return STRICT_CSP;\n return opt;\n}\n\nfunction timingSafeEqual(a: string, b: string): boolean {\n // Length check is not timing-safe but is fine — the token length is fixed\n // and known to attackers regardless. The byte compare must be timing-safe.\n if (a.length !== b.length) return false;\n return nodeTimingSafeEqual(Buffer.from(a, \"utf8\"), Buffer.from(b, \"utf8\"));\n}\n\nconst DEFAULT_LOOPBACK_HOSTNAMES = [\"127.0.0.1\", \"localhost\", \"::1\"] as const;\n\n// Extract the hostname portion of a Host header value, stripping the port.\n// IPv6 hosts arrive bracketed (`[::1]:5810`); plain hosts as `host:port`\n// or bare `host`. Returns lowercased hostname or null on shapes we don't\n// recognise (caller treats null as \"reject\").\nfunction parseHostHeader(value: string): string | null {\n const trimmed = value.trim();\n if (trimmed === \"\") return null;\n if (trimmed.startsWith(\"[\")) {\n const close = trimmed.indexOf(\"]\");\n if (close === -1) return null;\n return trimmed.slice(1, close).toLowerCase();\n }\n const colon = trimmed.indexOf(\":\");\n return (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();\n}\n\n// DNS-rebinding defence: every request must arrive with a Host header\n// pointing at one of the allowed names. Same gate applies to Origin when\n// the browser sends one. Absent Origin is fine — many legitimate\n// same-origin requests omit it. `Origin: null` is allowed because\n// sandboxed iframes and `file://` pages send it; the Host check still\n// constrains the network path so the Origin allowance isn't load-bearing.\nfunction isAllowedHost(value: string | undefined, allowed: Set<string>): boolean {\n const host = value === undefined ? null : parseHostHeader(value);\n return host !== null && allowed.has(host);\n}\n\nfunction isAllowedOrigin(value: string | undefined, allowed: Set<string>): boolean {\n if (value === undefined || value === \"\" || value === \"null\") return true;\n try {\n // WHATWG URL keeps the brackets on IPv6 hostnames (`[::1]`), but the\n // allow-list stores them stripped (matching parseHostHeader's output)\n // so origins and hosts compare consistently.\n let hostname = new URL(value).hostname.toLowerCase();\n if (hostname.startsWith(\"[\") && hostname.endsWith(\"]\")) {\n hostname = hostname.slice(1, -1);\n }\n return allowed.has(hostname);\n } catch {\n return false;\n }\n}\n\nexport interface DevServerOptions {\n view: string;\n data?: unknown;\n dataLoader?: () => Promise<unknown>;\n viewsRoot: string;\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n port?: number;\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - \"tab\" — open in user's default browser as a regular tab.\n * - \"app\" — try Chromium's --app mode (chromeless window). Falls back\n * to \"tab\" if no Chromium browser is installed (Chrome/Edge/Brave),\n * with a stderr note. Safari and Firefox always fall back.\n */\n shell?: Shell;\n /**\n * Browser silence (ms) after which the mount transitions to disconnected.\n * The mount does NOT terminate on disconnect — only explicit close/signal/error does.\n */\n heartbeatTimeoutMs?: number;\n /** Grace period after server start before the heartbeat watcher is armed. */\n startupGraceMs?: number;\n /**\n * Test seam: interval (ms) for the heartbeat watcher tick. Defaults to 1000.\n * Lower values let tests observe disconnect transitions without sleeping ~1s.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n /** Content-Security-Policy enforcement. See MountOptions.csp. */\n csp?: CspOption;\n /**\n * Extra hostnames (beyond `localhost`, `127.0.0.1`, `[::1]`) accepted in\n * the request `Host` and `Origin` headers. Use to allow a custom\n * `/etc/hosts` alias or another loopback name; values are matched by\n * hostname only (port-agnostic). Anything outside this set + the\n * loopback defaults is rejected with HTTP 403 to defend against\n * DNS-rebinding attacks. Default: empty.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. When true, process.stdout.write is\n * redirected to process.stderr for the lifetime of the server, restored\n * on close(). Use when driving mount() programmatically and stdout is\n * reserved for a structured protocol (e.g. line-delimited JSON).\n * Default: false.\n */\n silent?: boolean;\n /**\n * Test seam: replace the browser-open implementation. When provided,\n * called instead of `open(url)` for both the initial open and `reopen()`.\n * Never set this in production; use `openBrowser: false` instead.\n */\n _opener?: (url: string) => Promise<void>;\n}\n\nexport type { CloseReason };\n\nexport type DevServerEvent = \"data-updated\" | \"view-swapped\" | \"disconnected\" | \"reconnected\";\nexport type DevServerEventListener = () => void;\n\ntype ConnectionState = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface DevServer {\n url: string;\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n close: (reason?: CloseReason) => Promise<void>;\n /**\n * Replace in-memory data and emit a `data-updated` event to all\n * registered listeners. Does not recompile the view.\n */\n update: (data: unknown) => void;\n /**\n * Recompile the view from an inline TSX source string and replace the\n * in-memory HTML. Emits `view-swapped` on success; preserves the previous\n * HTML on compile failure. Returns errors array (empty = success).\n */\n swapView: (source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails,\n * neither takes effect. Returns errors array (empty = success).\n */\n patch: (data: unknown, source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /**\n * Subscribe to a server-side event. Listeners are called synchronously\n * after each mutation completes.\n *\n * Events:\n * \"data-updated\" — fired by update() and patch()\n * \"view-swapped\" — fired by swapView() and patch()\n */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\nexport async function startDevServer(opts: DevServerOptions): Promise<DevServer> {\n const {\n view,\n data,\n dataLoader,\n viewsRoot,\n mutations = {},\n title = \"ui-leaf\",\n port,\n openBrowser = true,\n shell = \"tab\",\n heartbeatTimeoutMs = 5_000,\n startupGraceMs = 30_000,\n csp,\n allowedHosts,\n silent = false,\n _opener,\n _heartbeatCheckIntervalMs = 1000,\n } = opts;\n const cspHeader = resolveCsp(csp);\n const allowedHostSet = new Set<string>(DEFAULT_LOOPBACK_HOSTNAMES);\n for (const h of allowedHosts ?? []) allowedHostSet.add(h.toLowerCase());\n const allowedHostList = [...allowedHostSet].join(\", \");\n\n // Programmatic consumers (esp. non-Node CLIs spawning ui-leaf as a\n // subprocess) often reserve stdout for a structured protocol. Redirect\n // process.stdout.write to stderr to catch anything that bypasses our\n // own output path.\n const restoreStdout: (() => void) | null = silent ? redirectStdoutToStderr() : null;\n\n try {\n if (view.includes(\"/\") || view.includes(\"\\\\\")) {\n throw new Error(\n `ui-leaf: view '${view}' must be a bare identifier with no path separators`,\n );\n }\n\n if (data !== undefined && dataLoader) {\n throw new Error(\"ui-leaf: pass data or dataLoader, not both\");\n }\n\n const token = randomBytes(32).toString(\"hex\");\n\n // Eagerly invoke the loader before starting the server. The resolved\n // value lives only in this closure — it is never written to disk. If the\n // loader rejects, the setup-failure catch below restores stdout before\n // re-throwing.\n let loadedData: unknown;\n if (dataLoader) {\n loadedData = await dataLoader();\n }\n\n // Compile the view once at mount time; hold the resulting HTML in memory.\n const result = await compileView({\n entry: view,\n viewsRoot,\n data: dataLoader ? null : data,\n title,\n csp: cspHeader ?? undefined,\n token,\n dataLoader: !!dataLoader,\n });\n\n if (result.errors.length > 0) {\n const msg = result.errors.map((e) => e.message).join(\"; \");\n throw new Error(`ui-leaf: view compilation failed: ${msg}`);\n }\n\n // Mutable view state: the / handler reads from this on every request.\n // update(), swapView(), patch() mutate it in place.\n const viewState = { html: result.html, data: dataLoader ? loadedData : data };\n\n // Minimal event broker. Pre-seeded so fireEvent's get() always returns a Set.\n const listeners = new Map<DevServerEvent, Set<DevServerEventListener>>([\n [\"data-updated\", new Set()],\n [\"view-swapped\", new Set()],\n [\"disconnected\", new Set()],\n [\"reconnected\", new Set()],\n ]);\n function fireEvent(event: DevServerEvent): void {\n for (const fn of listeners.get(event)!) fn();\n }\n\n const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();\n const sseEncoder = new TextEncoder();\n\n function broadcast(event: Record<string, unknown>): void {\n const frame = sseEncoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n for (const controller of sseClients) {\n try {\n controller.enqueue(frame);\n } catch {\n sseClients.delete(controller);\n }\n }\n }\n\n let lastHeartbeatAt = Date.now();\n let closeRequested = false;\n let connectionState: ConnectionState = \"connecting\";\n let resolveClosed: (reason: CloseReason) => void = () => {};\n const closed = new Promise<CloseReason>((r) => {\n resolveClosed = r;\n });\n\n const bunPort = port === undefined ? 5810 : port; // port: 0 → OS picks\n let actualPort = bunPort;\n\n const handler = (req: Request): Response | Promise<Response> => {\n const host = req.headers.get(\"host\") ?? undefined;\n const origin = req.headers.get(\"origin\") ?? undefined;\n\n // DNS-rebinding gate: reject any request (including WebSocket upgrade\n // attempts) that does not arrive with an allowed Host. When Origin is\n // present, it must also be in the allowed set.\n const hostOk = isAllowedHost(host, allowedHostSet);\n const originOk = isAllowedOrigin(origin, allowedHostSet);\n if (!hostOk || !originOk) {\n const offender = !hostOk\n ? `Host \"${host ?? \"(absent)\"}\"`\n : `Origin \"${origin}\"`;\n return new Response(\n `ui-leaf: refusing request with ${offender} — only the following hostnames are accepted to prevent DNS rebinding: ${allowedHostList}. Open the server at http://localhost:${actualPort}/ or http://127.0.0.1:${actualPort}/, or pass { allowedHosts: [\"my-alias\"] } to mount() to permit a custom alias.\\n`,\n { status: 403, headers: { \"Content-Type\": \"text/plain; charset=utf-8\" } },\n );\n }\n\n const headers: Record<string, string> = {};\n if (cspHeader) {\n headers[\"Content-Security-Policy\"] = cspHeader;\n }\n\n const url = new URL(req.url);\n const path = url.pathname;\n const method = req.method;\n\n if (method === \"GET\" && path === \"/\") {\n return new Response(viewState.html, {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n }\n\n if (method === \"POST\" && path === \"/heartbeat\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n lastHeartbeatAt = Date.now();\n if (connectionState === \"disconnected\") {\n connectionState = \"connected\";\n fireEvent(\"reconnected\");\n } else if (connectionState === \"connecting\") {\n connectionState = \"connected\";\n }\n return new Response(\"\", { status: 204, headers });\n }\n\n if (method === \"POST\" && path === \"/mutate\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return handleMutate(req, mutations, headers);\n }\n\n if (method === \"GET\" && path === \"/api/data\") {\n if (!dataLoader) {\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return new Response(JSON.stringify(viewState.data !== undefined ? viewState.data : null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (method === \"GET\" && path === \"/events\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n let sseController!: ReadableStreamDefaultController<Uint8Array>;\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n sseController = controller;\n sseClients.add(controller);\n // Enqueue an SSE comment immediately so Bun flushes response headers\n // before any broadcast event arrives (empty streams block header send).\n controller.enqueue(sseEncoder.encode(\": connected\\n\\n\"));\n req.signal?.addEventListener(\"abort\", () => {\n sseClients.delete(sseController);\n try { sseController.close(); } catch { /* already closed */ }\n });\n },\n cancel() {\n sseClients.delete(sseController);\n },\n });\n return new Response(stream, {\n status: 200,\n headers: {\n ...headers,\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n },\n });\n }\n\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n };\n\n let heartbeatWatcher: NodeJS.Timeout | undefined;\n\n // `bunServer` is assigned immediately after this declaration by the IIFE\n // below. The `!` assertion is safe: cleanup is never called during server\n // construction, only after the server is running.\n let bunServer!: ReturnType<typeof Bun.serve>;\n\n const cleanup = async (reason: CloseReason): Promise<void> => {\n if (closeRequested) return;\n closeRequested = true;\n if (heartbeatWatcher) clearInterval(heartbeatWatcher);\n broadcast({ type: \"closing\", reason });\n for (const controller of sseClients) {\n try { controller.close(); } catch { /* already closed */ }\n }\n sseClients.clear();\n // Graceful stop: waits for in-flight writes (including the closing SSE\n // event) to flush before tearing down TCP connections.\n await bunServer.stop();\n if (restoreStdout) restoreStdout();\n resolveClosed(reason);\n };\n\n // Auto-bump: if bunPort is busy, try bunPort+1 … up to MAX_PORT_ATTEMPTS.\n // port: 0 goes straight to Bun (OS assigns a free port; never EADDRINUSE).\n // The Bun error callback fires for socket errors AND for unhandled throws in\n // the fetch handler. Either case routes through cleanup(\"error\") so the mount\n // terminates cleanly rather than hanging. This means a single buggy request\n // handler is fatal — intentional: unhandled errors indicate broken invariants.\n const serverErrorHandler = (_err: Error): Response => {\n void cleanup(\"error\");\n return new Response(JSON.stringify({ error: \"internal server error\" }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n };\n bunServer = (() => {\n if (bunPort === 0) {\n return Bun.serve({ hostname: \"127.0.0.1\", port: 0, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n }\n const MAX_PORT_ATTEMPTS = 10;\n for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {\n try {\n return Bun.serve({ hostname: \"127.0.0.1\", port: bunPort + i, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n } catch (err) {\n const isAddrinuse = err instanceof Error && err.message.includes(\"EADDRINUSE\");\n if (!isAddrinuse || i === MAX_PORT_ATTEMPTS - 1) {\n if (isAddrinuse) {\n throw new Error(\n `ui-leaf: ports ${bunPort}–${bunPort + MAX_PORT_ATTEMPTS - 1} are all in use. Pass { port: 0 } to mount() for an OS-assigned port.`,\n );\n }\n throw err;\n }\n }\n }\n throw new Error(\"unreachable\"); // TypeScript: loop always returns or throws\n })();\n actualPort = bunServer.port ?? bunPort;\n const url = `http://127.0.0.1:${actualPort}`;\n const startedAt = Date.now();\n\n heartbeatWatcher = setInterval(() => {\n if (closeRequested) return;\n const now = Date.now();\n if (now - startedAt < startupGraceMs) return;\n if (now - lastHeartbeatAt > heartbeatTimeoutMs) {\n if (connectionState !== \"disconnected\") {\n connectionState = \"disconnected\";\n fireEvent(\"disconnected\");\n }\n }\n }, _heartbeatCheckIntervalMs);\n\n // The URL passed to the browser includes the token as a hash fragment so it\n // is never sent to the server (browsers strip fragments before HTTP requests).\n // The public `url` returned to consumers stays fragment-free.\n const openUrl = `${url}/#token=${token}`;\n\n // Browser-open implementation, or the test-seam override if one was supplied.\n const doOpen: () => Promise<void> = _opener\n ? () => _opener(openUrl)\n : async () => {\n if (shell === \"app\") {\n const launched = await openInAppMode(openUrl);\n if (!launched) {\n process.stderr.write(\n `ui-leaf: shell:\"app\" requested but no Chromium browser found; falling back to default browser tab.\\n`,\n );\n await open(openUrl);\n }\n } else {\n await open(openUrl);\n }\n };\n\n if (openBrowser) {\n await doOpen();\n }\n\n return {\n url,\n port: actualPort,\n closed,\n close: (reason: CloseReason = \"caller\") => cleanup(reason),\n on(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.add(listener);\n },\n off(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.delete(listener);\n },\n update(newData: unknown): void {\n viewState.data = newData;\n broadcast({ type: \"data-updated\", data: newData });\n fireEvent(\"data-updated\");\n },\n async swapView(source: string): Promise<import(\"./compile.js\").BuildError[]> {\n const r = await compileSource({\n source,\n data: viewState.data,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n viewState.html = r.html;\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"view-swapped\");\n return [];\n },\n async patch(newData: unknown, source: string): Promise<import(\"./compile.js\").BuildError[]> {\n // Compile first with newData so the HTML embeds the incoming data.\n const r = await compileSource({\n source,\n data: newData,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n // Only mutate state on compile success (atomicity guarantee).\n viewState.data = newData;\n viewState.html = r.html;\n broadcast({ type: \"data-updated\", data: newData });\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"data-updated\");\n fireEvent(\"view-swapped\");\n return [];\n },\n async reopen(): Promise<void> {\n await doOpen();\n },\n };\n } catch (err) {\n restoreStdout?.();\n throw err;\n }\n}\n\n// Custom header (not Authorization: Bearer) so any cross-origin fetch triggers\n// a CORS preflight, which browsers block for non-same-origin callers without\n// an explicit CORS allow list. This closes the simple-form-POST / no-preflight\n// attack vector against the localhost dev server.\nfunction checkAuth(req: Request, token: string): boolean {\n const value = req.headers.get(\"x-ui-leaf-token\") ?? \"\";\n if (!value) return false;\n return timingSafeEqual(value, token);\n}\n\nasync function handleMutate(\n req: Request,\n mutations: Record<string, MutationHandler<any, any>>,\n headers: Record<string, string>,\n): Promise<Response> {\n // 1 MiB cap: Content-Length precheck short-circuits chunked / large bodies\n // before req.text() buffers them. req.text() still buffers the whole body\n // if Content-Length is absent or underreported — acceptable for this\n // loopback-only server, where the auth gate already runs first.\n const contentLength = req.headers.get(\"content-length\");\n if (contentLength && Number.parseInt(contentLength, 10) > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n let body: { name?: string; args?: unknown };\n try {\n const text = await req.text();\n if (text.length > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n body = (text ? JSON.parse(text) : undefined) as typeof body;\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : \"bad request\" }),\n { status: 400, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const name = body?.name;\n if (typeof name !== \"string\" || name.length === 0) {\n return new Response(JSON.stringify({ error: \"missing mutation name\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (!Object.hasOwn(mutations, name)) {\n return new Response(\n JSON.stringify({\n error: `ui-leaf: no mutation handler registered for '${name}'. Add it to the mutations: { } map passed to mount().`,\n }),\n { status: 404, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const handler = mutations[name]!;\n try {\n const result = await handler(body.args);\n return new Response(JSON.stringify(result ?? null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),\n { status: 500, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n}\n","import { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { join, resolve, sep } from \"node:path\";\nimport { mkdtemp, rm, stat, writeFile } from \"node:fs/promises\";\nimport type { BunPlugin } from \"bun\";\nimport { escapeForScriptTag } from \"./internal/html.js\";\n\n// Resolve React imports at module load — works under bun test / bun run.\n// NOTE: under bun build --compile (binary mode), createRequire() resolves from\n// the binary's embedded virtual filesystem. AGT-131 (cross-compile script)\n// will need a Bun.build plugin or Bun embedded-files to ensure React is\n// reachable inside the compiled binary. Flagging here so AGT-131 is not blindsided.\nconst requireFromHere = createRequire(import.meta.url);\n\n// BunPlugin that rewrites bare react/react-dom imports to absolute paths\n// under ui-leaf's installed node_modules. Ensures the bundled view always\n// finds the same React instance regardless of the consumer's package-manager\n// hoisting, and prevents duplicate React instances across views.\nconst reactAliasPlugin: BunPlugin = {\n name: \"ui-leaf-react-alias\",\n setup(build) {\n // Matches: react, react/jsx-runtime, react/jsx-dev-runtime,\n // react-dom, react-dom/client, react-dom/profiling, etc.\n build.onResolve({ filter: /^react($|\\/|-dom($|\\/))/ }, (args) => {\n try {\n return { path: requireFromHere.resolve(args.path) };\n } catch {\n return {\n path: args.path,\n errors: [{ text: `ui-leaf: failed to resolve ${args.path}` }],\n };\n }\n });\n },\n};\n\nexport interface BuildError {\n file: string;\n line: number;\n column: number;\n message: string;\n}\n\nexport interface CompileOptions {\n /** View name or path relative to viewsRoot (e.g. \"dashboard\" or \"dashboard.tsx\"). */\n entry: string;\n /** Root directory holding .tsx view files. */\n viewsRoot: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. Ignored when dataLoader is true. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Raw CSP string to emit as a <meta http-equiv=\"Content-Security-Policy\"> tag.\n * Undefined / absent means no CSP meta tag is emitted.\n */\n csp?: string;\n /**\n * Extra allowed hostnames (beyond loopback defaults). Accepted in the\n * option bag for API symmetry with DevServerOptions; has no compile-time\n * effect — the runtime DNS-rebinding gate lives in the server.\n */\n allowedHosts?: string[];\n /**\n * Per-launch auth token. Accepted for API symmetry with DevServerOptions;\n * the token is no longer embedded in HTML — it is delivered via the URL\n * fragment and read by the inline bootstrap script.\n * @deprecated No-op since v1.0.0 — token delivery is handled by startDevServer.\n */\n token?: string;\n /**\n * When true, generate an entry that fetches data from GET /api/data at\n * render time rather than reading it from window.__UI_LEAF__.data. The\n * compiled HTML bootstrap omits the data field (only token is included).\n * Use when data is sensitive and must not be written to the HTML file.\n */\n dataLoader?: boolean;\n}\n\n/**\n * Options for compiling an inline TSX source string.\n *\n * v1.0.0 constraint: `source` is treated as a self-contained TSX string.\n * Relative imports are not supported — the string has no filesystem context\n * to resolve them against. Bare-package imports (react, react-dom) work via\n * the react-alias plugin. This is the intended contract for IPC-driven\n * view hot-swaps (AI-generated self-contained components).\n */\nexport interface CompileSourceOptions {\n /** Raw TSX source string to compile. Must be a self-contained component. */\n source: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /** Raw CSP string. Undefined / absent means no CSP meta tag. */\n csp?: string;\n /**\n * Per-launch auth token. Accepted for API symmetry; not embedded in HTML —\n * see CompileOptions.token.\n * @deprecated No-op since v1.0.0.\n */\n token?: string;\n}\n\nexport interface CompileResult {\n html: string;\n errors: BuildError[];\n}\n\n// Friendly message rendered when the page is reloaded without the token fragment.\nconst SESSION_ENDED_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>Session ended — re-launch the CLI to continue.</p></div>';\n\n// Overlay rendered when the mount terminates. v1.x extension point: replaceable\n// via a consumer-supplied template slot (deferred per plan-approval decision).\nconst CLOSED_OVERLAY_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>This view has closed.</p></div>';\n\n// Shared bridge injected into every compiled entry: mutation + heartbeat.\nconst SHARED_BRIDGE = `\nasync function mutate(name: string, args?: unknown): Promise<unknown> {\n const res = await fetch(\"/mutate\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token ? { \"X-UI-Leaf-Token\": token } : {}),\n },\n body: JSON.stringify({ name, args }),\n });\n const text = await res.text().catch(() => \"\");\n if (!res.ok) {\n let detail = text;\n try {\n const parsed: unknown = text ? JSON.parse(text) : null;\n if (parsed !== null && typeof parsed === \"object\" && \"error\" in parsed && typeof (parsed as { error: unknown }).error === \"string\") {\n detail = (parsed as { error: string }).error;\n }\n } catch { /* keep raw text */ }\n throw new Error(\"ui-leaf: mutation '\" + name + \"' failed (\" + res.status + \"): \" + detail);\n }\n return text ? JSON.parse(text) : undefined;\n}\n\nasync function heartbeat(): Promise<void> {\n try {\n await fetch(\"/heartbeat\", {\n method: \"POST\",\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n } catch { /* server may have shut down; ignore */ }\n}\nsetInterval(heartbeat, 5000);\nheartbeat();\n\nfunction subscribeEvents(onEvent: (ev: { type: string; [k: string]: unknown }) => void): void {\n let delay = 250;\n const budget = 30_000;\n const started = Date.now();\n let done = false;\n\n async function connect(): Promise<void> {\n try {\n const res = await fetch(\"/events\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok || !res.body) throw new Error(\"bad status \" + res.status);\n delay = 250;\n const reader = res.body.getReader();\n const dec = new TextDecoder(\"utf-8\");\n let buf = \"\";\n while (true) {\n const { done: streamDone, value } = await reader.read();\n if (streamDone) break;\n buf += dec.decode(value, { stream: true });\n let idx: number;\n while ((idx = buf.indexOf(\"\\\\n\\\\n\")) !== -1) {\n const chunk = buf.slice(0, idx);\n buf = buf.slice(idx + 2);\n for (const line of chunk.split(\"\\\\n\")) {\n if (line.startsWith(\"data:\")) {\n try {\n const ev = JSON.parse(line.slice(5).trimStart()) as { type: string; [k: string]: unknown };\n if (ev.type === \"closing\") done = true;\n onEvent(ev);\n } catch { /* skip malformed event */ }\n }\n }\n }\n if (done) return;\n }\n } catch {\n if (done) return;\n }\n if (done) return;\n if (Date.now() - started > budget) {\n onEvent({ type: \"closing\", reason: \"error\" });\n return;\n }\n await new Promise<void>((r) => setTimeout(r, delay));\n delay = Math.min(delay * 2, 5_000);\n void connect();\n }\n\n void connect();\n}`;\n\n/** Run Bun.build on `entryPath` and return the raw JS output or errors. */\nasync function runBunBuild(entryPath: string): Promise<{ js: string } | { errors: BuildError[] }> {\n let buildOutput: Awaited<ReturnType<typeof Bun.build>>;\n try {\n buildOutput = await Bun.build({\n entrypoints: [entryPath],\n target: \"browser\",\n format: \"esm\",\n minify: false,\n sourcemap: \"none\",\n plugins: [reactAliasPlugin],\n });\n } catch (err) {\n if (err instanceof AggregateError) {\n type BunBuildMsg = { message: string; position?: { file?: string; line?: number; column?: number } | null };\n const errors: BuildError[] = (err.errors as BunBuildMsg[]).map((e) => ({\n file: e.position?.file ?? \"<unknown>\",\n line: e.position?.line ?? 0,\n column: e.position?.column ?? 0,\n message: e.message,\n }));\n return { errors };\n }\n throw err;\n }\n const output = buildOutput.outputs[0];\n if (!output) {\n return {\n errors: [{ file: \"<unknown>\", line: 0, column: 0, message: \"ui-leaf: Bun.build produced no output\" }],\n };\n }\n return { js: await output.text() };\n}\n\n/** Assemble the final HTML page from compiled JS and options. */\nfunction assembleHtml(opts: {\n js: string;\n title: string;\n csp: string | undefined;\n data: unknown;\n dataLoader: boolean;\n}): string {\n const { js, title, csp, data, dataLoader } = opts;\n // Escape </script> sequences to prevent script-tag break-out.\n const safeJs = js.replace(/<\\/script>/gi, \"<\\\\/script>\");\n\n const titleEscaped = title\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n\n const cspMeta = csp\n ? ` <meta http-equiv=\"Content-Security-Policy\" content=\"${csp.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\")}\" />\\n`\n : \"\";\n\n // Double-stringify data: outer JSON.stringify produces a JSON string, then\n // escapeForScriptTag ensures </script> and U+2028/U+2029 can't break out.\n const dataInit = dataLoader\n ? \"window.__UI_LEAF__ = {};\"\n : `window.__UI_LEAF__ = { data: JSON.parse(${escapeForScriptTag(JSON.stringify(JSON.stringify(data ?? null)))}) };`;\n\n // Bootstrap: reads token from URL fragment, stashes it on __UI_LEAF__.token,\n // then immediately clears the fragment from the URL bar so the token is\n // never visible in history. On reload (fragment gone), sets sessionEnded so\n // the bundled module can render a friendly recovery message instead of\n // attempting unauthenticated fetches.\n // decodeURIComponent is wrapped in try/catch: a malformed %-sequence would\n // otherwise throw and kill the bootstrap silently; the catch falls through\n // to sessionEnded so the user gets the recovery screen instead of a blank page.\n const bootstrapScript = `${dataInit}\n(function(){var m=/[#&]token=([^&#]*)/.exec(window.location.hash);if(m){try{window.__UI_LEAF__.token=decodeURIComponent(m[1]);history.replaceState(null,\"\",window.location.pathname+window.location.search);}catch(e){window.__UI_LEAF__.sessionEnded=true;}}else{window.__UI_LEAF__.sessionEnded=true;}})();`;\n\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>${titleEscaped}</title>\n${cspMeta} <!-- ui-leaf bootstrap -->\n <script>${bootstrapScript}</script>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\">${safeJs}</script>\n </body>\n</html>`;\n}\n\nexport async function compileView(opts: CompileOptions): Promise<CompileResult> {\n const {\n entry,\n viewsRoot,\n data,\n title = \"ui-leaf\",\n csp,\n // allowedHosts and token have no compile-time effect; accepted for API symmetry.\n allowedHosts: _allowedHosts,\n token: _token,\n dataLoader = false,\n } = opts;\n\n const viewsRootAbs = resolve(viewsRoot);\n const hasExt = /\\.[a-z]+$/i.test(entry);\n const viewAbs = resolve(viewsRootAbs, hasExt ? entry : `${entry}.tsx`);\n if (!viewAbs.startsWith(viewsRootAbs + sep)) {\n return {\n html: \"\",\n errors: [\n {\n file: \"<unknown>\",\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' resolves outside viewsRoot`,\n },\n ],\n };\n }\n try {\n await stat(viewAbs);\n } catch {\n return {\n html: \"\",\n errors: [\n {\n file: viewAbs,\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' not found at ${viewAbs}`,\n },\n ],\n };\n }\n\n // Generate a temp entry that imports the resolved view, mounts React via\n // createRoot, and wires the mutation/heartbeat bridge.\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-compile-\"));\n try {\n const entryPath = join(tempDir, \"entry.tsx\");\n\n const entryContent = dataLoader\n ? `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n${SHARED_BRIDGE}\n\n async function bootstrap(): Promise<void> {\n const res = await fetch(\"/api/data\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\"ui-leaf: /api/data fetch failed (\" + res.status + \"): \" + text);\n }\n let currentData: unknown = await res.json();\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n }\n bootstrap();\n}\n`\n : `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Compile an inline TSX source string into a full HTML page.\n *\n * The source is treated as a self-contained component; relative imports are\n * not supported (v1.0.0 constraint — the string has no filesystem context).\n * Bare-package imports (react, react-dom) work via the react-alias plugin.\n */\nexport async function compileSource(opts: CompileSourceOptions): Promise<CompileResult> {\n const { source, data, title = \"ui-leaf\", csp, token: _token } = opts;\n\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-src-\"));\n try {\n // Write the caller's tsx as the view file, then write a thin entry wrapper.\n const viewPath = join(tempDir, \"view.tsx\");\n const entryPath = join(tempDir, \"entry.tsx\");\n\n await writeFile(viewPath, source);\n\n const entryContent = `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewPath)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader: false }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n","export function escapeForScriptTag(json: string): string {\n // Defend against </script> break-out and U+2028/U+2029 line terminators\n // that JSON.stringify emits raw but JS string literals don't accept.\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n","// ui-leaf stdio IPC protocol — single source of truth for the line-delimited\n// JSON shape exchanged between the binary and its caller.\n//\n// Versioning policy (design.md §8.2): every message carries a `version`\n// literal. Additive changes (new optional fields, new message types) keep\n// the same version; only contract breaks bump it. v1.0.0 ships \"1\".\n\nexport const PROTOCOL_VERSION = \"1\" as const;\nexport type ProtocolVersion = typeof PROTOCOL_VERSION;\n\n// Outbound: messages the binary writes on stdout.\nexport type OutboundReady = {\n version: ProtocolVersion;\n type: \"ready\";\n url: string;\n port: number;\n};\n\nexport type OutboundMutate = {\n version: ProtocolVersion;\n type: \"mutate\";\n id: number;\n name: string;\n args: unknown;\n};\n\nexport type CloseReason = \"caller\" | \"signal\" | \"error\";\n\nexport type OutboundDisconnected = {\n version: ProtocolVersion;\n type: \"disconnected\";\n};\n\nexport type OutboundReconnected = {\n version: ProtocolVersion;\n type: \"reconnected\";\n};\n\nexport type OutboundClosed = {\n version: ProtocolVersion;\n type: \"closed\";\n reason: CloseReason;\n};\n\nexport type OutboundError = {\n version: ProtocolVersion;\n type: \"error\";\n /** Optional phase tag, e.g. \"build\" for view/patch compile failures. */\n phase?: string;\n message: string;\n};\n\nexport type Outbound =\n | OutboundReady\n | OutboundMutate\n | OutboundClosed\n | OutboundDisconnected\n | OutboundReconnected\n | OutboundError;\n\n// Inbound: messages the binary reads on stdin (line 1 = config, lines 2+ =\n// mutation responses). Both shapes carry the version field.\nexport type InboundConfig = {\n version: ProtocolVersion;\n view: string;\n viewsRoot: string;\n data?: unknown;\n mutations?: string[];\n title?: string;\n port?: number;\n openBrowser?: boolean;\n shell?: \"tab\" | \"app\";\n csp?: string;\n heartbeatTimeoutMs?: number;\n startupGraceMs?: number;\n};\n\nexport type InboundMutateResult = {\n version: ProtocolVersion;\n type: \"result\";\n id: number;\n value?: unknown;\n};\n\nexport type InboundMutateError = {\n version: ProtocolVersion;\n type: \"error\";\n id: number;\n message: string;\n};\n\nexport type InboundMutateResponse = InboundMutateResult | InboundMutateError;\n\n// New inbound message types (v1.0.0): live-update handlers.\n\n/** Replace in-memory data and emit a data-updated SSE event. */\nexport type InboundUpdate = {\n version: ProtocolVersion;\n type: \"update\";\n data: unknown;\n};\n\n/** Swap the view source on-the-fly; triggers a recompile and view-swapped SSE event. */\nexport type InboundView = {\n version: ProtocolVersion;\n type: \"view\";\n source: string;\n};\n\n/**\n * Atomically replace both data and view source. If the compile fails, neither\n * takes effect and the previous state is preserved.\n */\nexport type InboundPatch = {\n version: ProtocolVersion;\n type: \"patch\";\n data: unknown;\n view: { source: string };\n};\n\n/** Re-invoke open(url) to launch a fresh browser tab at the same URL. */\nexport type InboundReopen = {\n version: ProtocolVersion;\n type: \"reopen\";\n};\n\n/** Terminate the mount cleanly (caller-initiated close). */\nexport type InboundClose = {\n version: ProtocolVersion;\n type: \"close\";\n};\n\n/** Caller heartbeat. The binary silently acknowledges; no reply is emitted. */\nexport type InboundPing = {\n version: ProtocolVersion;\n type: \"ping\";\n};\n\n/**\n * Discriminated union of all valid post-config inbound messages. Discriminate\n * on `type`; mutation responses are identified by the presence of an `id` field.\n */\nexport type Inbound =\n | InboundMutateResponse\n | InboundUpdate\n | InboundView\n | InboundPatch\n | InboundReopen\n | InboundClose\n | InboundPing;\n\n// `Omit<U, K>` collapses a discriminated union by intersecting the\n// remaining keys; the distributive form preserves the variants so the\n// `emit()` argument can be {type:\"ready\",url,port} OR {type:\"error\",message}\n// etc., not the (impossible) intersection.\nexport type OutboundEvent = Outbound extends infer T\n ? T extends Outbound\n ? Omit<T, \"version\">\n : never\n : never;\n\n// Serialise an outbound event with `version` as the first key. The\n// `version` field is added here so call sites can't forget — the helper's\n// argument type strips it. JSON.stringify preserves insertion order for\n// non-integer string keys (ES2015+), so the resulting line begins\n// `{\"version\":\"1\",…`.\nexport function emit(event: OutboundEvent): string {\n const stamped = { version: PROTOCOL_VERSION, ...event };\n return `${JSON.stringify(stamped)}\\n`;\n}\n\n// Parse a stdin line. Returns a discriminated outcome so the caller can\n// route version violations to the spec'd error reply (AC #2) rather than\n// silently dropping or crashing.\nexport type ParseOutcome<T> =\n | { ok: true; msg: T }\n | { ok: false; kind: \"json\"; reason: string }\n | { ok: false; kind: \"missing-version\" }\n | { ok: false; kind: \"unsupported-version\"; got: unknown };\n\nexport type ValidateOutcome = { ok: true } | { ok: false; reason: string };\n\n/**\n * Structural validator for inbound messages. Called after parseInbound() confirms\n * version and JSON shape; this function checks per-type required fields.\n *\n * kind=\"config\" → validates InboundConfig required fields (view, viewsRoot).\n * kind=\"post-config\" → validates InboundMessage variants by type discriminant.\n *\n * On failure the caller should emit {type:\"error\",message:reason} and continue\n * (or exit 1 for config failures, per the protocol spec).\n */\nexport function validateInboundShape(\n msg: unknown,\n kind: \"config\" | \"post-config\",\n): ValidateOutcome {\n if (typeof msg !== \"object\" || msg === null) {\n return { ok: false, reason: \"message is not an object\" };\n }\n const m = msg as Record<string, unknown>;\n\n if (kind === \"config\") {\n if (typeof m.view !== \"string\" || m.view === \"\") {\n return { ok: false, reason: 'config requires a non-empty string \"view\"' };\n }\n if (typeof m.viewsRoot !== \"string\" || m.viewsRoot === \"\") {\n return { ok: false, reason: 'config requires a non-empty string \"viewsRoot\"' };\n }\n if (\"mutations\" in m && m.mutations !== undefined) {\n if (\n !Array.isArray(m.mutations) ||\n !(m.mutations as unknown[]).every((x) => typeof x === \"string\")\n ) {\n return { ok: false, reason: \"config.mutations must be an array of strings\" };\n }\n }\n if (\"port\" in m && m.port !== undefined && typeof m.port !== \"number\") {\n return { ok: false, reason: \"config.port must be a number\" };\n }\n if (\n \"openBrowser\" in m &&\n m.openBrowser !== undefined &&\n typeof m.openBrowser !== \"boolean\"\n ) {\n return { ok: false, reason: \"config.openBrowser must be a boolean\" };\n }\n if (\n \"shell\" in m &&\n m.shell !== undefined &&\n m.shell !== \"tab\" &&\n m.shell !== \"app\"\n ) {\n return { ok: false, reason: 'config.shell must be \"tab\" or \"app\"' };\n }\n if (\n \"heartbeatTimeoutMs\" in m &&\n m.heartbeatTimeoutMs !== undefined &&\n typeof m.heartbeatTimeoutMs !== \"number\"\n ) {\n return { ok: false, reason: \"config.heartbeatTimeoutMs must be a number\" };\n }\n if (\n \"startupGraceMs\" in m &&\n m.startupGraceMs !== undefined &&\n typeof m.startupGraceMs !== \"number\"\n ) {\n return { ok: false, reason: \"config.startupGraceMs must be a number\" };\n }\n return { ok: true };\n }\n\n // post-config: discriminate on type first for result/error (which also carry id),\n // then on type alone for live-update commands.\n const type = m.type;\n if (typeof type !== \"string\") {\n return { ok: false, reason: '\"type\" field must be a string' };\n }\n\n // Mutation responses: type is \"result\" or \"error\", always carry a numeric id.\n if (type === \"result\" || type === \"error\") {\n if (typeof m.id !== \"number\") {\n return { ok: false, reason: `\"${type}\" requires a numeric \"id\" field` };\n }\n if (type === \"error\" && typeof m.message !== \"string\") {\n return { ok: false, reason: '\"error\" requires a string \"message\" field' };\n }\n return { ok: true };\n }\n\n // Live-update commands.\n switch (type) {\n case \"update\":\n if (!Object.hasOwn(m, \"data\")) {\n return { ok: false, reason: '\"update\" requires a \"data\" field' };\n }\n return { ok: true };\n case \"view\":\n if (typeof m.source !== \"string\") {\n return { ok: false, reason: '\"view\" requires a string \"source\" field' };\n }\n return { ok: true };\n case \"patch\":\n if (!Object.hasOwn(m, \"data\")) {\n return { ok: false, reason: '\"patch\" requires a \"data\" field' };\n }\n if (\n typeof m.view !== \"object\" ||\n m.view === null ||\n typeof (m.view as Record<string, unknown>).source !== \"string\"\n ) {\n return { ok: false, reason: '\"patch\" requires a string \"view.source\" field' };\n }\n return { ok: true };\n case \"reopen\":\n case \"close\":\n case \"ping\":\n return { ok: true };\n default:\n return { ok: false, reason: `unknown message type: \"${type}\"` };\n }\n}\n\nexport function parseInbound<T extends { version: ProtocolVersion }>(\n line: string,\n): ParseOutcome<T> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch (err) {\n return {\n ok: false,\n kind: \"json\",\n reason: err instanceof Error ? err.message : String(err),\n };\n }\n if (typeof parsed !== \"object\" || parsed === null) {\n return { ok: false, kind: \"missing-version\" };\n }\n if (!Object.hasOwn(parsed, \"version\")) {\n return { ok: false, kind: \"missing-version\" };\n }\n const version = (parsed as { version: unknown }).version;\n if (version !== PROTOCOL_VERSION) {\n return { ok: false, kind: \"unsupported-version\", got: version };\n }\n return { ok: true, msg: parsed as T };\n}\n"],"mappings":"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","names":["resolve","args","fireEvent","broadcast","url","resolve","createRequire","require","emit","outcome","resolve"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/index.ts","../src/server.ts","../src/compile.ts","../src/internal/html.ts","../src/ipc.ts"],"sourcesContent":["#!/usr/bin/env node\n// ui-leaf CLI — language-neutral entry point for non-Node consumers.\n//\n// Protocol (stdio, line-delimited JSON; every message carries\n// `\"version\":\"1\"` as the first key):\n//\n// STDIN\n// Line 1: config object {\"version\":\"1\",\"view\":…,\"viewsRoot\":…,…}\n// Line 2+: one of:\n// Mutation responses (identified by the `id` field):\n// {\"version\":\"1\",\"type\":\"result\",\"id\":<n>,\"value\":<any>}\n// {\"version\":\"1\",\"type\":\"error\",\"id\":<n>,\"message\":\"<text>\"}\n// Live-update messages (no `id`):\n// {\"version\":\"1\",\"type\":\"update\",\"data\":<any>}\n// {\"version\":\"1\",\"type\":\"view\",\"source\":\"<tsx string>\"}\n// {\"version\":\"1\",\"type\":\"patch\",\"data\":<any>,\"view\":{\"source\":\"<tsx>\"}}\n// {\"version\":\"1\",\"type\":\"reopen\"}\n// {\"version\":\"1\",\"type\":\"close\"}\n//\n// STDOUT\n// {\"version\":\"1\",\"type\":\"ready\",\"url\":\"<url>\",\"port\":<n>}\n// {\"version\":\"1\",\"type\":\"mutate\",\"id\":<n>,\"name\":\"<s>\",\"args\":<any>}\n// {\"version\":\"1\",\"type\":\"disconnected\"}\n// {\"version\":\"1\",\"type\":\"reconnected\"}\n// {\"version\":\"1\",\"type\":\"closed\",\"reason\":\"caller\"|\"signal\"|\"error\"}\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"<text>\"}\n// {\"version\":\"1\",\"type\":\"error\",\"phase\":\"build\",\"message\":\"<text>\"}\n//\n// Version handling\n// A missing version field on any inbound message produces\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"missing version field\"}\n// A non-\"1\" version produces\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"unsupported protocol version: <x>\"}\n// Both errors on the config message exit 1; on subsequent messages\n// the bad line is dropped and the mount keeps running.\n//\n// Unknown post-config message types produce:\n// {\"version\":\"1\",\"type\":\"error\",\"message\":\"unknown message type: <x>\"}\n// The mount continues (non-fatal).\n//\n// view / patch compile failures preserve the previous view and produce:\n// {\"version\":\"1\",\"type\":\"error\",\"phase\":\"build\",\"message\":\"<text>\"}\n//\n// Inline view.source constraint (v1.0.0): the TSX source string is\n// treated as self-contained. Relative imports are not supported —\n// the string has no filesystem context to resolve them against. Bare-\n// package imports (react, react-dom) work via the internal alias plugin.\n//\n// Lifecycle\n// disconnected: browser tab heartbeat stopped; mount stays alive.\n// reconnected: browser reconnected after a disconnect.\n// closed: mount terminated; reason is caller|signal|error.\n// Exits 0 on closed with reason caller|signal; exits 1 on error.\n// Closing stdin from the parent triggers a caller close (exit 0).\n\nimport { createInterface } from \"node:readline\";\nimport { mount, type MountOptions } from \"./index.js\";\nimport {\n emit as serializeEvent,\n parseInbound,\n validateInboundShape,\n type Inbound,\n type InboundConfig,\n type OutboundEvent,\n} from \"./ipc.js\";\n\n// Capture the real stdout write BEFORE anything (especially mount() with\n// silent: true) gets a chance to redirect process.stdout. The binary's\n// protocol output uses this directly; bundler / dev-server noise (which\n// goes through process.stdout.write) gets redirected to stderr by silent\n// mode without affecting our protocol channel.\nconst realStdoutWrite = process.stdout.write.bind(process.stdout);\n\nconst args = process.argv.slice(2);\n\nif (args.length === 0 || args[0] === \"--help\" || args[0] === \"-h\") {\n process.stdout.write(\n [\n \"ui-leaf — Customizable browser views, on demand, for any CLI.\",\n \"\",\n \"Usage:\",\n \" ui-leaf mount Read a JSON config from stdin and mount a view.\",\n \" See https://github.com/OpenThinkAi/ui-leaf for\",\n \" the full stdio protocol spec.\",\n \"\",\n \" ui-leaf --version Print version.\",\n \" ui-leaf --help Print this message.\",\n \"\",\n ].join(\"\\n\"),\n );\n process.exit(0);\n}\n\nif (args[0] === \"--version\" || args[0] === \"-v\") {\n // Read version from package.json shipped alongside this binary.\n const { createRequire } = await import(\"node:module\");\n const require = createRequire(import.meta.url);\n const pkg = require(\"../package.json\") as { version: string };\n process.stdout.write(`${pkg.version}\\n`);\n process.exit(0);\n}\n\nif (args[0] !== \"mount\") {\n process.stderr.write(`ui-leaf: unknown command \"${args[0]}\"\\n`);\n process.exit(1);\n}\n\n// `ui-leaf mount --help` (or being run on a TTY without piped input)\n// would otherwise sit silently on stdin. Print the protocol pointer and\n// exit 0 so users discovering the binary land on docs, not a \"broken\"\n// exit code.\nif (args[1] === \"--help\" || args[1] === \"-h\" || process.stdin.isTTY) {\n process.stdout.write(\n [\n \"ui-leaf mount — read a JSON config from stdin and mount a view.\",\n \"\",\n \"Protocol: line-delimited JSON over stdio.\",\n \" stdin line 1 = config object\",\n \" lines 2+ = mutation responses {type:result|error,id,...}\",\n \" stdout {type:ready,url,port}, {type:mutate,id,name,args},\",\n \" {type:closed}, {type:error,message}\",\n \"\",\n \"Full spec: https://github.com/OpenThinkAi/ui-leaf#driving-ui-leaf-from-a-non-node-cli\",\n \"\",\n \"Example:\",\n ` echo '{\"view\":\"spec\",\"viewsRoot\":\"/abs/path\",\"data\":{}}' | ui-leaf mount`,\n \"\",\n ].join(\"\\n\"),\n );\n process.exit(0);\n}\n\ntry {\n await runMount();\n} catch (err) {\n emit({\n type: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n process.exit(1);\n}\n\ntype ConfigRequest = InboundConfig;\n\nfunction emit(event: OutboundEvent): void {\n realStdoutWrite(serializeEvent(event));\n}\n\nfunction stringifyVersion(got: unknown): string {\n if (typeof got === \"string\") return got;\n return JSON.stringify(got);\n}\n\nasync function runMount(): Promise<void> {\n const rl = createInterface({ input: process.stdin });\n let nextId = 0;\n const pending = new Map<\n number,\n { resolve: (value: unknown) => void; reject: (err: Error) => void }\n >();\n\n let configReceived = false;\n let configResolve!: (cfg: ConfigRequest) => void;\n let configReject!: (err: Error) => void;\n const configPromise = new Promise<ConfigRequest>((res, rej) => {\n configResolve = res;\n configReject = rej;\n });\n\n // Track the mounted view so the stdin-close handler can shut it down.\n // Set after `mount()` resolves; null until then.\n // biome-ignore lint/suspicious/noExplicitAny: MountedView shape inferred\n let mountedView: any = null;\n let stdinClosed = false;\n\n rl.on(\"line\", (line) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n if (!configReceived) {\n configReceived = true;\n const outcome = parseInbound<ConfigRequest>(trimmed);\n if (!outcome.ok) {\n // Config is the load-bearing first message. A bad version on the\n // config can't be recovered from, so per AC #5's optional clause\n // we emit the spec'd error and exit. Subsequent (post-config)\n // version violations are non-fatal — see below.\n if (outcome.kind === \"json\") {\n emit({\n type: \"error\",\n message: `failed to parse config JSON: ${outcome.reason}`,\n });\n } else if (outcome.kind === \"missing-version\") {\n emit({ type: \"error\", message: \"missing version field\" });\n } else {\n emit({\n type: \"error\",\n message: `unsupported protocol version: ${stringifyVersion(outcome.got)}`,\n });\n }\n process.exit(1);\n }\n const configValidation = validateInboundShape(outcome.msg, \"config\");\n if (!configValidation.ok) {\n emit({ type: \"error\", message: configValidation.reason });\n process.exit(1);\n }\n configResolve(outcome.msg);\n return;\n }\n\n // Post-config message: mutation response or live-update command.\n const outcome = parseInbound<Inbound>(trimmed);\n if (!outcome.ok) {\n if (outcome.kind === \"missing-version\") {\n emit({ type: \"error\", message: \"missing version field\" });\n } else if (outcome.kind === \"unsupported-version\") {\n emit({\n type: \"error\",\n message: `unsupported protocol version: ${stringifyVersion(outcome.got)}`,\n });\n }\n // Malformed JSON falls through silently.\n return;\n }\n const msg = outcome.msg;\n\n const msgValidation = validateInboundShape(msg, \"post-config\");\n if (!msgValidation.ok) {\n emit({ type: \"error\", message: msgValidation.reason });\n return;\n }\n\n // Mutation responses carry an `id` field — discriminate before checking `type`.\n if (\"id\" in msg && typeof msg.id === \"number\") {\n const p = pending.get(msg.id);\n if (!p) return;\n pending.delete(msg.id);\n if (msg.type === \"result\") p.resolve(msg.value);\n else if (msg.type === \"error\") p.reject(new Error(msg.message));\n return;\n }\n\n // Live-update commands — only dispatch if mountedView is ready.\n if (!mountedView) return;\n\n if (msg.type === \"update\") {\n mountedView.update(msg.data);\n return;\n }\n\n if (msg.type === \"view\") {\n void (async () => {\n const errors = await mountedView.swapView(msg.source);\n if (errors.length > 0) {\n emit({\n type: \"error\",\n phase: \"build\",\n message: errors.map((e: { message: string }) => e.message).join(\"; \"),\n });\n }\n })();\n return;\n }\n\n if (msg.type === \"patch\") {\n void (async () => {\n const errors = await mountedView.patch(msg.data, msg.view.source);\n if (errors.length > 0) {\n emit({\n type: \"error\",\n phase: \"build\",\n message: errors.map((e: { message: string }) => e.message).join(\"; \"),\n });\n }\n })();\n return;\n }\n\n if (msg.type === \"reopen\") {\n void mountedView.reopen().catch((err: unknown) => {\n emit({ type: \"error\", message: err instanceof Error ? err.message : String(err) });\n });\n return;\n }\n\n if (msg.type === \"close\") {\n void mountedView.close();\n return;\n }\n\n if (msg.type === \"ping\") {\n // Heartbeat from caller — silently acknowledged; no reply emitted.\n return;\n }\n });\n\n rl.on(\"close\", () => {\n stdinClosed = true;\n // Reject any pending mutations — the parent isn't going to respond.\n for (const { reject } of pending.values()) {\n reject(new Error(\"ui-leaf: stdin closed by parent before mutation responded\"));\n }\n pending.clear();\n // If we never received config, the parent dropped before doing\n // anything useful. Bail out with a non-zero exit so that's visible.\n if (!configReceived) {\n configReject(new Error(\"ui-leaf: stdin closed before config received\"));\n return;\n }\n // Otherwise, tear down the mounted view (if it exists yet) so the\n // process exits without waiting on the heartbeat timeout. The\n // view.closed promise resolves and runMount's normal exit path runs.\n if (mountedView) {\n void mountedView.close();\n }\n });\n\n const config = await configPromise;\n\n // Build mutations map: each declared name becomes a handler that emits\n // a mutate event on stdout and awaits a paired response on stdin.\n // biome-ignore lint/suspicious/noExplicitAny: handler signatures vary\n const mutations: Record<string, (args: any) => Promise<unknown>> = {};\n for (const name of config.mutations ?? []) {\n mutations[name] = (mutationArgs: unknown) => {\n const id = ++nextId;\n return new Promise<unknown>((resolve, reject) => {\n pending.set(id, { resolve, reject });\n emit({ type: \"mutate\", id, name, args: mutationArgs });\n });\n };\n }\n\n const mountOpts: MountOptions = {\n view: config.view,\n viewsRoot: config.viewsRoot,\n data: config.data,\n mutations,\n title: config.title,\n port: config.port,\n openBrowser: config.openBrowser,\n shell: config.shell,\n csp: config.csp,\n heartbeatTimeoutMs: config.heartbeatTimeoutMs,\n startupGraceMs: config.startupGraceMs,\n silent: true, // bridge owns stdout; bundler / dev-server output must stay silent\n };\n\n try {\n const view = await mount(mountOpts);\n mountedView = view;\n // If stdin closed while we were waiting on mount(), tear down right\n // away rather than hold the dev server open.\n if (stdinClosed) {\n void view.close();\n }\n view.on(\"disconnected\", () => emit({ type: \"disconnected\" }));\n view.on(\"reconnected\", () => emit({ type: \"reconnected\" }));\n emit({ type: \"ready\", url: view.url, port: view.port });\n const closeReason = await view.closed;\n emit({ type: \"closed\", reason: closeReason });\n process.exit(closeReason === \"error\" ? 1 : 0);\n } catch (err) {\n emit({\n type: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n process.exit(1);\n }\n}\n","// ui-leaf — Customizable browser views, on demand, for any CLI.\n// https://github.com/OpenThinkAi/ui-leaf\n\nimport { resolve } from \"node:path\";\nimport {\n startDevServer,\n type CloseReason,\n type CspOption,\n type DevServerEvent,\n type DevServerEventListener,\n type MutationHandler,\n type Shell,\n} from \"./server.js\";\nimport type { BuildError } from \"./compile.js\";\n\nexport type { BuildError, CloseReason, CspOption, DevServerEvent, DevServerEventListener, MutationHandler, Shell };\n\nexport interface MountOptions {\n /** View name. Resolves to <viewsRoot>/<view>.tsx. */\n view: string;\n /**\n * JSON-serializable data passed to the view as a prop.\n *\n * Privacy note: the data is compiled into the HTML served at the mount URL\n * and held in memory for the mount lifetime. Any same-UID local process\n * that can reach `127.0.0.1:<port>` can fetch `GET /` and read it — the\n * per-launch token guards `/mutate` against drive-by cross-origin requests\n * in the browser, not against other processes on the machine. For PHI, PCI,\n * financial records, or anything where a same-UID local reader is in your\n * threat model, use `dataLoader` instead — the loader's return value is\n * served at a token-gated `/api/data` endpoint and never appears in the HTML.\n */\n data?: unknown;\n /**\n * Async function that supplies sensitive data to the view without\n * including it in the served HTML. When provided, the loader is called\n * once during mount setup; its resolved value is served at a token-gated\n * `GET /api/data` endpoint (same per-launch token as `/mutate`) and the\n * view fetches it on first render before calling `createRoot().render()`.\n * The data never appears in the compiled HTML.\n *\n * Use this instead of `data` for PHI, PCI, financial records, or anything\n * else where in-HTML data exposure is in your threat model.\n *\n * Error semantics: if the loader rejects, the rejection propagates to the\n * `mount()` caller (no automatic retry). Errors surface at mount time,\n * matching the synchronous `data` path's behavior.\n *\n * Mutual exclusion: passing both `data` and `dataLoader` throws at\n * mount time.\n */\n dataLoader?: () => Promise<unknown>;\n /**\n * Mutation handlers the view can call via mutate(name, args).\n * Each handler can self-type its args and return:\n *\n * mutations: {\n * recategorize: async (args: { id: string; category: string }) => {\n * await db.recategorize(args.id, args.category);\n * return { ok: true };\n * },\n * }\n *\n * Each request body is capped at 1 MiB; oversized POSTs are rejected\n * with a 400 and the view's mutate() promise rejects with a clear error.\n */\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Root directory holding view .tsx files. Defaults to <cwd>/views. */\n viewsRoot?: string;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Port to bind. Defaults to 5810 — unused by the major Node dev tools.\n * If the port is unavailable, ui-leaf bumps to the next free port and\n * the actual bound port is reflected on the returned `url` and `port`.\n * Pass `0` to let the OS pick a free port directly.\n * Override only if you need a stable URL (e.g. an external bookmark).\n */\n port?: number;\n /**\n * Open the browser when ready. Defaults to true. When false, mount()\n * returns the URL on its resolved value so the caller can drive a\n * headless browser, log the address, etc.\n */\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - `\"tab\"` — open in the user's default browser as a regular tab.\n * Works everywhere; URL bar is visible.\n *\n * - `\"app\"` — try Chromium's `--app` mode for a chromeless window\n * (no URL bar, no tabs, looks like a desktop app). Available on\n * Chrome, Edge, and Brave. If no Chromium browser is installed,\n * ui-leaf falls back to \"tab\" with a stderr note. Safari and\n * Firefox always fall back.\n *\n * Pair with the share-link pattern (see \"Sharing views across users\"\n * in the README) when you want users to never see a localhost URL.\n */\n shell?: Shell;\n /**\n * Abort to close the dev server early. The returned `closed` promise\n * resolves either way; if you need to distinguish a signal-driven close\n * from a natural tab-close, check `signal.aborted` after the await.\n */\n signal?: AbortSignal;\n /**\n * Browser silence (ms) after which the mount emits `disconnected`.\n * Defaults to 5000 — tuned for the v1.0.0 subprocess-driver model where\n * a fast `disconnected` signal lets the caller decide whether to close,\n * reopen, or keep the mount alive. Raise it for sessions where the page\n * may legitimately pause (devtools paused on a breakpoint, machine\n * sleep, long background-tab throttling). Note: this no longer controls\n * when the mount terminates — only when the `disconnected` event fires.\n */\n heartbeatTimeoutMs?: number;\n /**\n * Content-Security-Policy enforcement. Defaults to `\"strict\"`.\n *\n * - `\"strict\"` — ui-leaf sends a balanced preset that browser-enforces\n * the broker principle: `connect-src 'self'` prevents views from\n * fetching external APIs (all data flows through `data` and\n * `mutations`); `form-action 'self'` closes the form-submit\n * exfiltration vector. HTTPS images/fonts and inline styles for\n * React are permitted. View files can only *add* further restrictions\n * via meta tag, never remove them.\n *\n * - `\"off\"` — no CSP header sent. Views can fetch arbitrary URLs and\n * submit forms to any origin. The data/mutations contract is\n * honor-system only. Use this if you have a legitimate need for\n * cross-origin access and accept the trade-off.\n *\n * - `string` — raw CSP header value for full control. Use when the\n * \"strict\" preset doesn't fit (e.g. you need `connect-src` to\n * include a Sentry endpoint).\n *\n * Trade-off: when set to \"strict\" or a custom string, a view file\n * cannot relax the policy at runtime. Switching back requires changing\n * the mount() call. That rigidity is a feature.\n */\n csp?: CspOption;\n /**\n * Extra hostnames accepted in the request `Host` and `Origin` headers\n * on top of the built-in loopback set (`localhost`, `127.0.0.1`, `[::1]`).\n *\n * The dev server gates every request on this set to defend against\n * DNS-rebinding attacks; non-matching requests get HTTP 403. Use this\n * escape hatch when you need to reach the dev server through a custom\n * `/etc/hosts` alias (e.g. `[\"my-app.local\"]`) or any other loopback\n * name. Hostnames are matched case-insensitively, port-agnostic.\n *\n * Be deliberate: any hostname you add becomes a viable DNS-rebinding\n * target. Don't add wildcards, public DNS names, or LAN hostnames you\n * don't fully control.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. Default: false.\n *\n * When you drive `mount()` programmatically — e.g. as part of a Node\n * bridge for a non-Node CLI that's spawned ui-leaf as a subprocess —\n * stdout is usually reserved for a structured protocol (line-delimited\n * JSON, etc.). Setting `silent: true` redirects `process.stdout.write`\n * to `process.stderr` for the lifetime of the server, restored on close.\n *\n * Tradeoff: any other code in the same process that writes to stdout\n * during the server's lifetime is also redirected. Hold the captured\n * `process.stdout.write` reference yourself if you need to write to the\n * real stdout from the same process.\n */\n silent?: boolean;\n /**\n * Grace period (ms) after server start before the heartbeat watcher arms.\n * Cold-loading clients sometimes take a few seconds to send their first\n * heartbeat. Defaults to 30000.\n */\n startupGraceMs?: number;\n /**\n * Test seam: heartbeat watcher tick interval (ms). Defaults to 1000.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n}\n\nexport interface MountedView {\n /** URL the view is reachable at (http://127.0.0.1:<port>). */\n url: string;\n /** Bound port. Useful when port: 0 was requested. */\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n /** Force-close the dev server early. */\n close: () => Promise<void>;\n /**\n * Replace in-memory data and notify all `data-updated` listeners.\n * Preserves in-page React state — no recompile.\n */\n update: (data: unknown) => void;\n /**\n * Swap the view source on the fly. Triggers a recompile; on success replaces\n * the served HTML and notifies all `view-swapped` listeners. On compile\n * failure the previous HTML is preserved. Returns compile errors if any.\n */\n swapView: (source: string) => Promise<BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails neither\n * takes effect. Returns compile errors if any.\n */\n patch: (data: unknown, source: string) => Promise<BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /** Subscribe to a server-side event (data-updated | view-swapped). */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n /** Unsubscribe a previously-registered listener. */\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\n/**\n * Mount a customizable browser view from a CLI. Spins up a local dev server\n * and renders the chosen view with the given data. Returns once the server\n * is ready; await `result.closed` to block until the mount terminates.\n *\n * Mutations triggered in the view are dispatched to the registered handlers\n * here; the view never reaches the CLI's backing API directly.\n *\n * **Lifecycle.** Browser tab close (heartbeat silence) emits a `disconnected`\n * event on `result` but does NOT resolve `closed` or stop the server. The\n * mount only terminates — and `closed` resolves — when you call\n * `result.close()`, receive SIGINT/SIGTERM, or an internal error occurs.\n * Listen for `disconnected` and call `result.close()` yourself if you want\n * fast shutdown on tab close.\n *\n * **Multi-tab note.** The heartbeat is a single high-water mark across all\n * open tabs; `disconnected` fires only when all tabs go silent. Closing one\n * tab while another is open emits no event.\n *\n * Ctrl+C: this function installs SIGINT and SIGTERM handlers that close\n * the server before exiting.\n */\nexport async function mount(opts: MountOptions): Promise<MountedView> {\n const viewsRoot = opts.viewsRoot ?? resolve(process.cwd(), \"views\");\n\n const server = await startDevServer({\n view: opts.view,\n data: opts.data,\n dataLoader: opts.dataLoader,\n viewsRoot,\n mutations: opts.mutations,\n title: opts.title,\n port: opts.port,\n openBrowser: opts.openBrowser,\n shell: opts.shell,\n heartbeatTimeoutMs: opts.heartbeatTimeoutMs,\n startupGraceMs: opts.startupGraceMs,\n csp: opts.csp,\n allowedHosts: opts.allowedHosts,\n silent: opts.silent,\n _heartbeatCheckIntervalMs: opts._heartbeatCheckIntervalMs,\n });\n\n const onSignal = (signal: NodeJS.Signals): void => {\n void (async () => {\n await server.close(\"signal\");\n // Re-raise so default exit codes still apply.\n process.kill(process.pid, signal);\n })();\n };\n const sigint = (): void => onSignal(\"SIGINT\");\n const sigterm = (): void => onSignal(\"SIGTERM\");\n process.once(\"SIGINT\", sigint);\n process.once(\"SIGTERM\", sigterm);\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n await server.close();\n return {\n url: server.url,\n port: server.port,\n closed: Promise.resolve<CloseReason>(\"caller\"),\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n }\n opts.signal.addEventListener(\n \"abort\",\n () => void server.close(),\n { once: true },\n );\n }\n\n const closed = server.closed.finally(() => {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n });\n\n return {\n url: server.url,\n port: server.port,\n closed,\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n}\n","import { randomBytes, timingSafeEqual as nodeTimingSafeEqual } from \"node:crypto\";\nimport open, { apps } from \"open\";\nimport { compileView, compileSource } from \"./compile.js\";\nimport type { CloseReason } from \"./ipc.js\";\n\n// Module-level stdout redirect state. Captured ONCE at module load so\n// concurrent silent: true mounts share the same \"original\" reference and\n// restore-order doesn't matter. Refcounted so the last close restores.\nconst ORIGINAL_STDOUT_WRITE = process.stdout.write.bind(process.stdout);\nlet stdoutRedirectCount = 0;\n\n/**\n * Redirect process.stdout.write to process.stderr until the returned\n * function is called. Safe under concurrent silent mounts.\n */\nfunction redirectStdoutToStderr(): () => void {\n stdoutRedirectCount++;\n if (stdoutRedirectCount === 1) {\n // biome-ignore lint/suspicious/noExplicitAny: stdout.write has overloaded\n // signatures; forward exactly what comes in.\n process.stdout.write = ((chunk: any, enc?: any, cb?: any) =>\n process.stderr.write(chunk, enc, cb)) as typeof process.stdout.write;\n }\n let released = false;\n return () => {\n if (released) return;\n released = true;\n stdoutRedirectCount--;\n if (stdoutRedirectCount === 0) {\n process.stdout.write = ORIGINAL_STDOUT_WRITE;\n }\n };\n}\n\nexport type MutationHandler<TArgs = unknown, TResult = unknown> = (\n args: TArgs,\n) => TResult | Promise<TResult>;\n\n// `(string & {})` preserves the \"off\" / \"strict\" autocomplete suggestions\n// while still allowing arbitrary CSP strings. Plain string would collapse\n// the union and lose IntelliSense for the literals.\nexport type CspOption = \"off\" | \"strict\" | (string & {});\n\nexport type Shell = \"tab\" | \"app\";\n\n/**\n * Try to open `url` in a Chromium browser's --app mode (chromeless window:\n * no URL bar, no tabs). Returns true if a Chromium browser was found and\n * launched, false if no Chromium variant is installed (caller should fall\n * back to the default-browser tab).\n */\nasync function openInAppMode(url: string): Promise<boolean> {\n // Order: most-common Chromium variants first.\n const candidates = [apps.chrome, apps.edge, apps.brave];\n for (const app of candidates) {\n try {\n await open(url, { app: { name: app, arguments: [`--app=${url}`] } });\n return true;\n } catch {\n // Try next candidate; `open` throws if the binary isn't installed.\n }\n }\n return false;\n}\n\n/**\n * Strict preset: locks `connect-src` to same-origin (the architectural\n * lock that forces views to route mutations through the CLI), while\n * permitting common needs (HTTPS images/fonts, inline styles for React).\n * A future v1.x mode could tighten script-src once usage patterns are known.\n */\nconst STRICT_CSP = [\n \"default-src 'self'\",\n \"connect-src 'self'\",\n \"form-action 'self'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self' https: data:\",\n \"style-src 'self' 'unsafe-inline'\",\n \"script-src 'self' 'unsafe-inline'\",\n].join(\"; \");\n\nfunction resolveCsp(opt: CspOption | undefined): string | null {\n if (!opt || opt === \"off\") return null;\n if (opt === \"strict\") return STRICT_CSP;\n return opt;\n}\n\nfunction timingSafeEqual(a: string, b: string): boolean {\n // Length check is not timing-safe but is fine — the token length is fixed\n // and known to attackers regardless. The byte compare must be timing-safe.\n if (a.length !== b.length) return false;\n return nodeTimingSafeEqual(Buffer.from(a, \"utf8\"), Buffer.from(b, \"utf8\"));\n}\n\nconst DEFAULT_LOOPBACK_HOSTNAMES = [\"127.0.0.1\", \"localhost\", \"::1\"] as const;\n\n// Extract the hostname portion of a Host header value, stripping the port.\n// IPv6 hosts arrive bracketed (`[::1]:5810`); plain hosts as `host:port`\n// or bare `host`. Returns lowercased hostname or null on shapes we don't\n// recognise (caller treats null as \"reject\").\nfunction parseHostHeader(value: string): string | null {\n const trimmed = value.trim();\n if (trimmed === \"\") return null;\n if (trimmed.startsWith(\"[\")) {\n const close = trimmed.indexOf(\"]\");\n if (close === -1) return null;\n return trimmed.slice(1, close).toLowerCase();\n }\n const colon = trimmed.indexOf(\":\");\n return (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();\n}\n\n// DNS-rebinding defence: every request must arrive with a Host header\n// pointing at one of the allowed names. Same gate applies to Origin when\n// the browser sends one. Absent Origin is fine — many legitimate\n// same-origin requests omit it. `Origin: null` is allowed because\n// sandboxed iframes and `file://` pages send it; the Host check still\n// constrains the network path so the Origin allowance isn't load-bearing.\nfunction isAllowedHost(value: string | undefined, allowed: Set<string>): boolean {\n const host = value === undefined ? null : parseHostHeader(value);\n return host !== null && allowed.has(host);\n}\n\nfunction isAllowedOrigin(value: string | undefined, allowed: Set<string>): boolean {\n if (value === undefined || value === \"\" || value === \"null\") return true;\n try {\n // WHATWG URL keeps the brackets on IPv6 hostnames (`[::1]`), but the\n // allow-list stores them stripped (matching parseHostHeader's output)\n // so origins and hosts compare consistently.\n let hostname = new URL(value).hostname.toLowerCase();\n if (hostname.startsWith(\"[\") && hostname.endsWith(\"]\")) {\n hostname = hostname.slice(1, -1);\n }\n return allowed.has(hostname);\n } catch {\n return false;\n }\n}\n\nexport interface DevServerOptions {\n view: string;\n data?: unknown;\n dataLoader?: () => Promise<unknown>;\n viewsRoot: string;\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n port?: number;\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - \"tab\" — open in user's default browser as a regular tab.\n * - \"app\" — try Chromium's --app mode (chromeless window). Falls back\n * to \"tab\" if no Chromium browser is installed (Chrome/Edge/Brave),\n * with a stderr note. Safari and Firefox always fall back.\n */\n shell?: Shell;\n /**\n * Browser silence (ms) after which the mount transitions to disconnected.\n * The mount does NOT terminate on disconnect — only explicit close/signal/error does.\n */\n heartbeatTimeoutMs?: number;\n /** Grace period after server start before the heartbeat watcher is armed. */\n startupGraceMs?: number;\n /**\n * Test seam: interval (ms) for the heartbeat watcher tick. Defaults to 1000.\n * Lower values let tests observe disconnect transitions without sleeping ~1s.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n /** Content-Security-Policy enforcement. See MountOptions.csp. */\n csp?: CspOption;\n /**\n * Extra hostnames (beyond `localhost`, `127.0.0.1`, `[::1]`) accepted in\n * the request `Host` and `Origin` headers. Use to allow a custom\n * `/etc/hosts` alias or another loopback name; values are matched by\n * hostname only (port-agnostic). Anything outside this set + the\n * loopback defaults is rejected with HTTP 403 to defend against\n * DNS-rebinding attacks. Default: empty.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. When true, process.stdout.write is\n * redirected to process.stderr for the lifetime of the server, restored\n * on close(). Use when driving mount() programmatically and stdout is\n * reserved for a structured protocol (e.g. line-delimited JSON).\n * Default: false.\n */\n silent?: boolean;\n /**\n * Test seam: replace the browser-open implementation. When provided,\n * called instead of `open(url)` for both the initial open and `reopen()`.\n * Never set this in production; use `openBrowser: false` instead.\n */\n _opener?: (url: string) => Promise<void>;\n}\n\nexport type { CloseReason };\n\nexport type DevServerEvent = \"data-updated\" | \"view-swapped\" | \"disconnected\" | \"reconnected\";\nexport type DevServerEventListener = () => void;\n\ntype ConnectionState = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface DevServer {\n url: string;\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n close: (reason?: CloseReason) => Promise<void>;\n /**\n * Replace in-memory data and emit a `data-updated` event to all\n * registered listeners. Does not recompile the view.\n */\n update: (data: unknown) => void;\n /**\n * Recompile the view from an inline TSX source string and replace the\n * in-memory HTML. Emits `view-swapped` on success; preserves the previous\n * HTML on compile failure. Returns errors array (empty = success).\n */\n swapView: (source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails,\n * neither takes effect. Returns errors array (empty = success).\n */\n patch: (data: unknown, source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /**\n * Subscribe to a server-side event. Listeners are called synchronously\n * after each mutation completes.\n *\n * Events:\n * \"data-updated\" — fired by update() and patch()\n * \"view-swapped\" — fired by swapView() and patch()\n */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\nexport async function startDevServer(opts: DevServerOptions): Promise<DevServer> {\n const {\n view,\n data,\n dataLoader,\n viewsRoot,\n mutations = {},\n title = \"ui-leaf\",\n port,\n openBrowser = true,\n shell = \"tab\",\n heartbeatTimeoutMs = 5_000,\n startupGraceMs = 30_000,\n csp = \"strict\",\n allowedHosts,\n silent = false,\n _opener,\n _heartbeatCheckIntervalMs = 1000,\n } = opts;\n const cspHeader = resolveCsp(csp);\n const allowedHostSet = new Set<string>(DEFAULT_LOOPBACK_HOSTNAMES);\n for (const h of allowedHosts ?? []) allowedHostSet.add(h.toLowerCase());\n const allowedHostList = [...allowedHostSet].join(\", \");\n\n // Programmatic consumers (esp. non-Node CLIs spawning ui-leaf as a\n // subprocess) often reserve stdout for a structured protocol. Redirect\n // process.stdout.write to stderr to catch anything that bypasses our\n // own output path.\n const restoreStdout: (() => void) | null = silent ? redirectStdoutToStderr() : null;\n\n try {\n if (view.includes(\"/\") || view.includes(\"\\\\\")) {\n throw new Error(\n `ui-leaf: view '${view}' must be a bare identifier with no path separators`,\n );\n }\n\n if (data !== undefined && dataLoader) {\n throw new Error(\"ui-leaf: pass data or dataLoader, not both\");\n }\n\n const token = randomBytes(32).toString(\"hex\");\n\n // Eagerly invoke the loader before starting the server. The resolved\n // value lives only in this closure — it is never written to disk. If the\n // loader rejects, the setup-failure catch below restores stdout before\n // re-throwing.\n let loadedData: unknown;\n if (dataLoader) {\n loadedData = await dataLoader();\n }\n\n // Compile the view once at mount time; hold the resulting HTML in memory.\n const result = await compileView({\n entry: view,\n viewsRoot,\n data: dataLoader ? null : data,\n title,\n csp: cspHeader ?? undefined,\n token,\n dataLoader: !!dataLoader,\n });\n\n if (result.errors.length > 0) {\n const msg = result.errors.map((e) => e.message).join(\"; \");\n throw new Error(`ui-leaf: view compilation failed: ${msg}`);\n }\n\n // Mutable view state: the / handler reads from this on every request.\n // update(), swapView(), patch() mutate it in place.\n const viewState = { html: result.html, data: dataLoader ? loadedData : data };\n\n // Minimal event broker. Pre-seeded so fireEvent's get() always returns a Set.\n const listeners = new Map<DevServerEvent, Set<DevServerEventListener>>([\n [\"data-updated\", new Set()],\n [\"view-swapped\", new Set()],\n [\"disconnected\", new Set()],\n [\"reconnected\", new Set()],\n ]);\n function fireEvent(event: DevServerEvent): void {\n for (const fn of listeners.get(event)!) fn();\n }\n\n const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();\n const sseEncoder = new TextEncoder();\n\n function broadcast(event: Record<string, unknown>): void {\n const frame = sseEncoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n for (const controller of sseClients) {\n try {\n controller.enqueue(frame);\n } catch {\n sseClients.delete(controller);\n }\n }\n }\n\n let lastHeartbeatAt = Date.now();\n let closeRequested = false;\n let connectionState: ConnectionState = \"connecting\";\n let resolveClosed: (reason: CloseReason) => void = () => {};\n const closed = new Promise<CloseReason>((r) => {\n resolveClosed = r;\n });\n\n const bunPort = port === undefined ? 5810 : port; // port: 0 → OS picks\n let actualPort = bunPort;\n\n const handler = (req: Request): Response | Promise<Response> => {\n const host = req.headers.get(\"host\") ?? undefined;\n const origin = req.headers.get(\"origin\") ?? undefined;\n\n // DNS-rebinding gate: reject any request (including WebSocket upgrade\n // attempts) that does not arrive with an allowed Host. When Origin is\n // present, it must also be in the allowed set.\n const hostOk = isAllowedHost(host, allowedHostSet);\n const originOk = isAllowedOrigin(origin, allowedHostSet);\n if (!hostOk || !originOk) {\n const offender = !hostOk\n ? `Host \"${host ?? \"(absent)\"}\"`\n : `Origin \"${origin}\"`;\n return new Response(\n `ui-leaf: refusing request with ${offender} — only the following hostnames are accepted to prevent DNS rebinding: ${allowedHostList}. Open the server at http://localhost:${actualPort}/ or http://127.0.0.1:${actualPort}/, or pass { allowedHosts: [\"my-alias\"] } to mount() to permit a custom alias.\\n`,\n { status: 403, headers: { \"Content-Type\": \"text/plain; charset=utf-8\" } },\n );\n }\n\n const headers: Record<string, string> = {};\n if (cspHeader) {\n headers[\"Content-Security-Policy\"] = cspHeader;\n }\n\n const url = new URL(req.url);\n const path = url.pathname;\n const method = req.method;\n\n if (method === \"GET\" && path === \"/\") {\n return new Response(viewState.html, {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n }\n\n if (method === \"POST\" && path === \"/heartbeat\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n lastHeartbeatAt = Date.now();\n if (connectionState === \"disconnected\") {\n connectionState = \"connected\";\n fireEvent(\"reconnected\");\n } else if (connectionState === \"connecting\") {\n connectionState = \"connected\";\n }\n return new Response(\"\", { status: 204, headers });\n }\n\n if (method === \"POST\" && path === \"/mutate\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return handleMutate(req, mutations, headers);\n }\n\n if (method === \"GET\" && path === \"/api/data\") {\n if (!dataLoader) {\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return new Response(JSON.stringify(viewState.data !== undefined ? viewState.data : null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (method === \"GET\" && path === \"/events\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n let sseController!: ReadableStreamDefaultController<Uint8Array>;\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n sseController = controller;\n sseClients.add(controller);\n // Enqueue an SSE comment immediately so Bun flushes response headers\n // before any broadcast event arrives (empty streams block header send).\n controller.enqueue(sseEncoder.encode(\": connected\\n\\n\"));\n req.signal?.addEventListener(\"abort\", () => {\n sseClients.delete(sseController);\n try { sseController.close(); } catch { /* already closed */ }\n });\n },\n cancel() {\n sseClients.delete(sseController);\n },\n });\n return new Response(stream, {\n status: 200,\n headers: {\n ...headers,\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n },\n });\n }\n\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n };\n\n let heartbeatWatcher: NodeJS.Timeout | undefined;\n\n // `bunServer` is assigned immediately after this declaration by the IIFE\n // below. The `!` assertion is safe: cleanup is never called during server\n // construction, only after the server is running.\n let bunServer!: ReturnType<typeof Bun.serve>;\n\n const cleanup = async (reason: CloseReason): Promise<void> => {\n if (closeRequested) return;\n closeRequested = true;\n if (heartbeatWatcher) clearInterval(heartbeatWatcher);\n broadcast({ type: \"closing\", reason });\n for (const controller of sseClients) {\n try { controller.close(); } catch { /* already closed */ }\n }\n sseClients.clear();\n // Graceful stop: waits for in-flight writes (including the closing SSE\n // event) to flush before tearing down TCP connections.\n await bunServer.stop();\n if (restoreStdout) restoreStdout();\n resolveClosed(reason);\n };\n\n // Auto-bump: if bunPort is busy, try bunPort+1 … up to MAX_PORT_ATTEMPTS.\n // port: 0 goes straight to Bun (OS assigns a free port; never EADDRINUSE).\n // The Bun error callback fires for socket errors AND for unhandled throws in\n // the fetch handler. Either case routes through cleanup(\"error\") so the mount\n // terminates cleanly rather than hanging. This means a single buggy request\n // handler is fatal — intentional: unhandled errors indicate broken invariants.\n const serverErrorHandler = (_err: Error): Response => {\n void cleanup(\"error\");\n return new Response(JSON.stringify({ error: \"internal server error\" }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n };\n bunServer = (() => {\n if (bunPort === 0) {\n return Bun.serve({ hostname: \"127.0.0.1\", port: 0, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n }\n const MAX_PORT_ATTEMPTS = 10;\n for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {\n try {\n return Bun.serve({ hostname: \"127.0.0.1\", port: bunPort + i, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n } catch (err) {\n const isAddrinuse = err instanceof Error && err.message.includes(\"EADDRINUSE\");\n if (!isAddrinuse || i === MAX_PORT_ATTEMPTS - 1) {\n if (isAddrinuse) {\n throw new Error(\n `ui-leaf: ports ${bunPort}–${bunPort + MAX_PORT_ATTEMPTS - 1} are all in use. Pass { port: 0 } to mount() for an OS-assigned port.`,\n );\n }\n throw err;\n }\n }\n }\n throw new Error(\"unreachable\"); // TypeScript: loop always returns or throws\n })();\n actualPort = bunServer.port ?? bunPort;\n const url = `http://127.0.0.1:${actualPort}`;\n const startedAt = Date.now();\n\n heartbeatWatcher = setInterval(() => {\n if (closeRequested) return;\n const now = Date.now();\n if (now - startedAt < startupGraceMs) return;\n if (now - lastHeartbeatAt > heartbeatTimeoutMs) {\n if (connectionState !== \"disconnected\") {\n connectionState = \"disconnected\";\n fireEvent(\"disconnected\");\n }\n }\n }, _heartbeatCheckIntervalMs);\n\n // The URL passed to the browser includes the token as a hash fragment so it\n // is never sent to the server (browsers strip fragments before HTTP requests).\n // The public `url` returned to consumers stays fragment-free.\n const openUrl = `${url}/#token=${token}`;\n\n // Browser-open implementation, or the test-seam override if one was supplied.\n const doOpen: () => Promise<void> = _opener\n ? () => _opener(openUrl)\n : async () => {\n if (shell === \"app\") {\n const launched = await openInAppMode(openUrl);\n if (!launched) {\n process.stderr.write(\n `ui-leaf: shell:\"app\" requested but no Chromium browser found; falling back to default browser tab.\\n`,\n );\n await open(openUrl);\n }\n } else {\n await open(openUrl);\n }\n };\n\n if (openBrowser) {\n await doOpen();\n }\n\n return {\n url,\n port: actualPort,\n closed,\n close: (reason: CloseReason = \"caller\") => cleanup(reason),\n on(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.add(listener);\n },\n off(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.delete(listener);\n },\n update(newData: unknown): void {\n viewState.data = newData;\n broadcast({ type: \"data-updated\", data: newData });\n fireEvent(\"data-updated\");\n },\n async swapView(source: string): Promise<import(\"./compile.js\").BuildError[]> {\n const r = await compileSource({\n source,\n data: viewState.data,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n viewState.html = r.html;\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"view-swapped\");\n return [];\n },\n async patch(newData: unknown, source: string): Promise<import(\"./compile.js\").BuildError[]> {\n // Compile first with newData so the HTML embeds the incoming data.\n const r = await compileSource({\n source,\n data: newData,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n // Only mutate state on compile success (atomicity guarantee).\n viewState.data = newData;\n viewState.html = r.html;\n broadcast({ type: \"data-updated\", data: newData });\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"data-updated\");\n fireEvent(\"view-swapped\");\n return [];\n },\n async reopen(): Promise<void> {\n await doOpen();\n },\n };\n } catch (err) {\n restoreStdout?.();\n throw err;\n }\n}\n\n// Custom header (not Authorization: Bearer) so any cross-origin fetch triggers\n// a CORS preflight, which browsers block for non-same-origin callers without\n// an explicit CORS allow list. This closes the simple-form-POST / no-preflight\n// attack vector against the localhost dev server.\nfunction checkAuth(req: Request, token: string): boolean {\n const value = req.headers.get(\"x-ui-leaf-token\") ?? \"\";\n if (!value) return false;\n return timingSafeEqual(value, token);\n}\n\nasync function handleMutate(\n req: Request,\n mutations: Record<string, MutationHandler<any, any>>,\n headers: Record<string, string>,\n): Promise<Response> {\n // 1 MiB cap: Content-Length precheck short-circuits chunked / large bodies\n // before req.text() buffers them. req.text() still buffers the whole body\n // if Content-Length is absent or underreported — acceptable for this\n // loopback-only server, where the auth gate already runs first.\n const contentLength = req.headers.get(\"content-length\");\n if (contentLength && Number.parseInt(contentLength, 10) > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n let body: { name?: string; args?: unknown };\n try {\n const text = await req.text();\n if (text.length > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n body = (text ? JSON.parse(text) : undefined) as typeof body;\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : \"bad request\" }),\n { status: 400, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const name = body?.name;\n if (typeof name !== \"string\" || name.length === 0) {\n return new Response(JSON.stringify({ error: \"missing mutation name\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (!Object.hasOwn(mutations, name)) {\n return new Response(\n JSON.stringify({\n error: `ui-leaf: no mutation handler registered for '${name}'. Add it to the mutations: { } map passed to mount().`,\n }),\n { status: 404, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const handler = mutations[name]!;\n try {\n const result = await handler(body.args);\n return new Response(JSON.stringify(result ?? null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),\n { status: 500, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n}\n","import { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { join, resolve, sep } from \"node:path\";\nimport { mkdtemp, rm, stat, writeFile } from \"node:fs/promises\";\nimport type { BunPlugin } from \"bun\";\nimport { escapeForScriptTag } from \"./internal/html.js\";\n\n// Resolve React imports at module load — works under bun test / bun run.\n// NOTE: under bun build --compile (binary mode), createRequire() resolves from\n// the binary's embedded virtual filesystem. AGT-131 (cross-compile script)\n// will need a Bun.build plugin or Bun embedded-files to ensure React is\n// reachable inside the compiled binary. Flagging here so AGT-131 is not blindsided.\nconst requireFromHere = createRequire(import.meta.url);\n\n// BunPlugin that rewrites bare react/react-dom imports to absolute paths\n// under ui-leaf's installed node_modules. Ensures the bundled view always\n// finds the same React instance regardless of the consumer's package-manager\n// hoisting, and prevents duplicate React instances across views.\nconst reactAliasPlugin: BunPlugin = {\n name: \"ui-leaf-react-alias\",\n setup(build) {\n // Matches: react, react/jsx-runtime, react/jsx-dev-runtime,\n // react-dom, react-dom/client, react-dom/profiling, etc.\n build.onResolve({ filter: /^react($|\\/|-dom($|\\/))/ }, (args) => {\n try {\n return { path: requireFromHere.resolve(args.path) };\n } catch {\n return {\n path: args.path,\n errors: [{ text: `ui-leaf: failed to resolve ${args.path}` }],\n };\n }\n });\n },\n};\n\nexport interface BuildError {\n file: string;\n line: number;\n column: number;\n message: string;\n}\n\nexport interface CompileOptions {\n /** View name or path relative to viewsRoot (e.g. \"dashboard\" or \"dashboard.tsx\"). */\n entry: string;\n /** Root directory holding .tsx view files. */\n viewsRoot: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. Ignored when dataLoader is true. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Raw CSP string to emit as a <meta http-equiv=\"Content-Security-Policy\"> tag.\n * Undefined / absent means no CSP meta tag is emitted.\n */\n csp?: string;\n /**\n * Extra allowed hostnames (beyond loopback defaults). Accepted in the\n * option bag for API symmetry with DevServerOptions; has no compile-time\n * effect — the runtime DNS-rebinding gate lives in the server.\n */\n allowedHosts?: string[];\n /**\n * Per-launch auth token. Accepted for API symmetry with DevServerOptions;\n * the token is no longer embedded in HTML — it is delivered via the URL\n * fragment and read by the inline bootstrap script.\n * @deprecated No-op since v1.0.0 — token delivery is handled by startDevServer.\n */\n token?: string;\n /**\n * When true, generate an entry that fetches data from GET /api/data at\n * render time rather than reading it from window.__UI_LEAF__.data. The\n * compiled HTML bootstrap omits the data field (only token is included).\n * Use when data is sensitive and must not be written to the HTML file.\n */\n dataLoader?: boolean;\n}\n\n/**\n * Options for compiling an inline TSX source string.\n *\n * v1.0.0 constraint: `source` is treated as a self-contained TSX string.\n * Relative imports are not supported — the string has no filesystem context\n * to resolve them against. Bare-package imports (react, react-dom) work via\n * the react-alias plugin. This is the intended contract for IPC-driven\n * view hot-swaps (AI-generated self-contained components).\n */\nexport interface CompileSourceOptions {\n /** Raw TSX source string to compile. Must be a self-contained component. */\n source: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /** Raw CSP string. Undefined / absent means no CSP meta tag. */\n csp?: string;\n /**\n * Per-launch auth token. Accepted for API symmetry; not embedded in HTML —\n * see CompileOptions.token.\n * @deprecated No-op since v1.0.0.\n */\n token?: string;\n}\n\nexport interface CompileResult {\n html: string;\n errors: BuildError[];\n}\n\n// Friendly message rendered when the page is reloaded without the token fragment.\nconst SESSION_ENDED_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>Session ended — re-launch the CLI to continue.</p></div>';\n\n// Overlay rendered when the mount terminates. v1.x extension point: replaceable\n// via a consumer-supplied template slot (deferred per plan-approval decision).\nconst CLOSED_OVERLAY_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>This view has closed.</p></div>';\n\n// Shared bridge injected into every compiled entry: mutation + heartbeat.\nconst SHARED_BRIDGE = `\nasync function mutate(name: string, args?: unknown): Promise<unknown> {\n const res = await fetch(\"/mutate\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token ? { \"X-UI-Leaf-Token\": token } : {}),\n },\n body: JSON.stringify({ name, args }),\n });\n const text = await res.text().catch(() => \"\");\n if (!res.ok) {\n let detail = text;\n try {\n const parsed: unknown = text ? JSON.parse(text) : null;\n if (parsed !== null && typeof parsed === \"object\" && \"error\" in parsed && typeof (parsed as { error: unknown }).error === \"string\") {\n detail = (parsed as { error: string }).error;\n }\n } catch { /* keep raw text */ }\n throw new Error(\"ui-leaf: mutation '\" + name + \"' failed (\" + res.status + \"): \" + detail);\n }\n return text ? JSON.parse(text) : undefined;\n}\n\nasync function heartbeat(): Promise<void> {\n try {\n await fetch(\"/heartbeat\", {\n method: \"POST\",\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n } catch { /* server may have shut down; ignore */ }\n}\nsetInterval(heartbeat, 5000);\nheartbeat();\n\nfunction subscribeEvents(onEvent: (ev: { type: string; [k: string]: unknown }) => void): void {\n let delay = 250;\n const budget = 30_000;\n const started = Date.now();\n let done = false;\n\n async function connect(): Promise<void> {\n try {\n const res = await fetch(\"/events\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok || !res.body) throw new Error(\"bad status \" + res.status);\n delay = 250;\n const reader = res.body.getReader();\n const dec = new TextDecoder(\"utf-8\");\n let buf = \"\";\n while (true) {\n const { done: streamDone, value } = await reader.read();\n if (streamDone) break;\n buf += dec.decode(value, { stream: true });\n let idx: number;\n while ((idx = buf.indexOf(\"\\\\n\\\\n\")) !== -1) {\n const chunk = buf.slice(0, idx);\n buf = buf.slice(idx + 2);\n for (const line of chunk.split(\"\\\\n\")) {\n if (line.startsWith(\"data:\")) {\n try {\n const ev = JSON.parse(line.slice(5).trimStart()) as { type: string; [k: string]: unknown };\n if (ev.type === \"closing\") done = true;\n onEvent(ev);\n } catch { /* skip malformed event */ }\n }\n }\n }\n if (done) return;\n }\n } catch {\n if (done) return;\n }\n if (done) return;\n if (Date.now() - started > budget) {\n onEvent({ type: \"closing\", reason: \"error\" });\n return;\n }\n await new Promise<void>((r) => setTimeout(r, delay));\n delay = Math.min(delay * 2, 5_000);\n void connect();\n }\n\n void connect();\n}`;\n\n/** Run Bun.build on `entryPath` and return the raw JS output or errors. */\nasync function runBunBuild(entryPath: string): Promise<{ js: string } | { errors: BuildError[] }> {\n let buildOutput: Awaited<ReturnType<typeof Bun.build>>;\n try {\n buildOutput = await Bun.build({\n entrypoints: [entryPath],\n target: \"browser\",\n format: \"esm\",\n minify: false,\n sourcemap: \"none\",\n plugins: [reactAliasPlugin],\n });\n } catch (err) {\n if (err instanceof AggregateError) {\n type BunBuildMsg = { message: string; position?: { file?: string; line?: number; column?: number } | null };\n const errors: BuildError[] = (err.errors as BunBuildMsg[]).map((e) => ({\n file: e.position?.file ?? \"<unknown>\",\n line: e.position?.line ?? 0,\n column: e.position?.column ?? 0,\n message: e.message,\n }));\n return { errors };\n }\n throw err;\n }\n const output = buildOutput.outputs[0];\n if (!output) {\n return {\n errors: [{ file: \"<unknown>\", line: 0, column: 0, message: \"ui-leaf: Bun.build produced no output\" }],\n };\n }\n return { js: await output.text() };\n}\n\n/** Assemble the final HTML page from compiled JS and options. */\nfunction assembleHtml(opts: {\n js: string;\n title: string;\n csp: string | undefined;\n data: unknown;\n dataLoader: boolean;\n}): string {\n const { js, title, csp, data, dataLoader } = opts;\n // Escape </script> sequences to prevent script-tag break-out.\n const safeJs = js.replace(/<\\/script>/gi, \"<\\\\/script>\");\n\n const titleEscaped = title\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n\n const cspMeta = csp\n ? ` <meta http-equiv=\"Content-Security-Policy\" content=\"${csp.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\")}\" />\\n`\n : \"\";\n\n // Double-stringify data: outer JSON.stringify produces a JSON string, then\n // escapeForScriptTag ensures </script> and U+2028/U+2029 can't break out.\n const dataInit = dataLoader\n ? \"window.__UI_LEAF__ = {};\"\n : `window.__UI_LEAF__ = { data: JSON.parse(${escapeForScriptTag(JSON.stringify(JSON.stringify(data ?? null)))}) };`;\n\n // Bootstrap: reads token from URL fragment, stashes it on __UI_LEAF__.token,\n // then immediately clears the fragment from the URL bar so the token is\n // never visible in history. On reload (fragment gone), sets sessionEnded so\n // the bundled module can render a friendly recovery message instead of\n // attempting unauthenticated fetches.\n // decodeURIComponent is wrapped in try/catch: a malformed %-sequence would\n // otherwise throw and kill the bootstrap silently; the catch falls through\n // to sessionEnded so the user gets the recovery screen instead of a blank page.\n const bootstrapScript = `${dataInit}\n(function(){var m=/[#&]token=([^&#]*)/.exec(window.location.hash);if(m){try{window.__UI_LEAF__.token=decodeURIComponent(m[1]);history.replaceState(null,\"\",window.location.pathname+window.location.search);}catch(e){window.__UI_LEAF__.sessionEnded=true;}}else{window.__UI_LEAF__.sessionEnded=true;}})();`;\n\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>${titleEscaped}</title>\n${cspMeta} <!-- ui-leaf bootstrap -->\n <script>${bootstrapScript}</script>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\">${safeJs}</script>\n </body>\n</html>`;\n}\n\nexport async function compileView(opts: CompileOptions): Promise<CompileResult> {\n const {\n entry,\n viewsRoot,\n data,\n title = \"ui-leaf\",\n csp,\n // allowedHosts and token have no compile-time effect; accepted for API symmetry.\n allowedHosts: _allowedHosts,\n token: _token,\n dataLoader = false,\n } = opts;\n\n const viewsRootAbs = resolve(viewsRoot);\n const hasExt = /\\.[a-z]+$/i.test(entry);\n const viewAbs = resolve(viewsRootAbs, hasExt ? entry : `${entry}.tsx`);\n if (!viewAbs.startsWith(viewsRootAbs + sep)) {\n return {\n html: \"\",\n errors: [\n {\n file: \"<unknown>\",\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' resolves outside viewsRoot`,\n },\n ],\n };\n }\n try {\n await stat(viewAbs);\n } catch {\n return {\n html: \"\",\n errors: [\n {\n file: viewAbs,\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' not found at ${viewAbs}`,\n },\n ],\n };\n }\n\n // Generate a temp entry that imports the resolved view, mounts React via\n // createRoot, and wires the mutation/heartbeat bridge.\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-compile-\"));\n try {\n const entryPath = join(tempDir, \"entry.tsx\");\n\n const entryContent = dataLoader\n ? `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n${SHARED_BRIDGE}\n\n async function bootstrap(): Promise<void> {\n const res = await fetch(\"/api/data\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\"ui-leaf: /api/data fetch failed (\" + res.status + \"): \" + text);\n }\n let currentData: unknown = await res.json();\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n }\n bootstrap();\n}\n`\n : `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Compile an inline TSX source string into a full HTML page.\n *\n * The source is treated as a self-contained component; relative imports are\n * not supported (v1.0.0 constraint — the string has no filesystem context).\n * Bare-package imports (react, react-dom) work via the react-alias plugin.\n */\nexport async function compileSource(opts: CompileSourceOptions): Promise<CompileResult> {\n const { source, data, title = \"ui-leaf\", csp, token: _token } = opts;\n\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-src-\"));\n try {\n // Write the caller's tsx as the view file, then write a thin entry wrapper.\n const viewPath = join(tempDir, \"view.tsx\");\n const entryPath = join(tempDir, \"entry.tsx\");\n\n await writeFile(viewPath, source);\n\n const entryContent = `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewPath)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader: false }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n","export function escapeForScriptTag(json: string): string {\n // Defend against </script> break-out and U+2028/U+2029 line terminators\n // that JSON.stringify emits raw but JS string literals don't accept.\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n","// ui-leaf stdio IPC protocol — single source of truth for the line-delimited\n// JSON shape exchanged between the binary and its caller.\n//\n// Versioning policy (design.md §8.2): every message carries a `version`\n// literal. Additive changes (new optional fields, new message types) keep\n// the same version; only contract breaks bump it. v1.0.0 ships \"1\".\n\nexport const PROTOCOL_VERSION = \"1\" as const;\nexport type ProtocolVersion = typeof PROTOCOL_VERSION;\n\n// Outbound: messages the binary writes on stdout.\nexport type OutboundReady = {\n version: ProtocolVersion;\n type: \"ready\";\n url: string;\n port: number;\n};\n\nexport type OutboundMutate = {\n version: ProtocolVersion;\n type: \"mutate\";\n id: number;\n name: string;\n args: unknown;\n};\n\nexport type CloseReason = \"caller\" | \"signal\" | \"error\";\n\nexport type OutboundDisconnected = {\n version: ProtocolVersion;\n type: \"disconnected\";\n};\n\nexport type OutboundReconnected = {\n version: ProtocolVersion;\n type: \"reconnected\";\n};\n\nexport type OutboundClosed = {\n version: ProtocolVersion;\n type: \"closed\";\n reason: CloseReason;\n};\n\nexport type OutboundError = {\n version: ProtocolVersion;\n type: \"error\";\n /** Optional phase tag, e.g. \"build\" for view/patch compile failures. */\n phase?: string;\n message: string;\n};\n\nexport type Outbound =\n | OutboundReady\n | OutboundMutate\n | OutboundClosed\n | OutboundDisconnected\n | OutboundReconnected\n | OutboundError;\n\n// Inbound: messages the binary reads on stdin (line 1 = config, lines 2+ =\n// mutation responses). Both shapes carry the version field.\nexport type InboundConfig = {\n version: ProtocolVersion;\n view: string;\n viewsRoot: string;\n data?: unknown;\n mutations?: string[];\n title?: string;\n port?: number;\n openBrowser?: boolean;\n shell?: \"tab\" | \"app\";\n csp?: string;\n heartbeatTimeoutMs?: number;\n startupGraceMs?: number;\n};\n\nexport type InboundMutateResult = {\n version: ProtocolVersion;\n type: \"result\";\n id: number;\n value?: unknown;\n};\n\nexport type InboundMutateError = {\n version: ProtocolVersion;\n type: \"error\";\n id: number;\n message: string;\n};\n\nexport type InboundMutateResponse = InboundMutateResult | InboundMutateError;\n\n// New inbound message types (v1.0.0): live-update handlers.\n\n/** Replace in-memory data and emit a data-updated SSE event. */\nexport type InboundUpdate = {\n version: ProtocolVersion;\n type: \"update\";\n data: unknown;\n};\n\n/** Swap the view source on-the-fly; triggers a recompile and view-swapped SSE event. */\nexport type InboundView = {\n version: ProtocolVersion;\n type: \"view\";\n source: string;\n};\n\n/**\n * Atomically replace both data and view source. If the compile fails, neither\n * takes effect and the previous state is preserved.\n */\nexport type InboundPatch = {\n version: ProtocolVersion;\n type: \"patch\";\n data: unknown;\n view: { source: string };\n};\n\n/** Re-invoke open(url) to launch a fresh browser tab at the same URL. */\nexport type InboundReopen = {\n version: ProtocolVersion;\n type: \"reopen\";\n};\n\n/** Terminate the mount cleanly (caller-initiated close). */\nexport type InboundClose = {\n version: ProtocolVersion;\n type: \"close\";\n};\n\n/** Caller heartbeat. The binary silently acknowledges; no reply is emitted. */\nexport type InboundPing = {\n version: ProtocolVersion;\n type: \"ping\";\n};\n\n/**\n * Discriminated union of all valid post-config inbound messages. Discriminate\n * on `type`; mutation responses are identified by the presence of an `id` field.\n */\nexport type Inbound =\n | InboundMutateResponse\n | InboundUpdate\n | InboundView\n | InboundPatch\n | InboundReopen\n | InboundClose\n | InboundPing;\n\n// `Omit<U, K>` collapses a discriminated union by intersecting the\n// remaining keys; the distributive form preserves the variants so the\n// `emit()` argument can be {type:\"ready\",url,port} OR {type:\"error\",message}\n// etc., not the (impossible) intersection.\nexport type OutboundEvent = Outbound extends infer T\n ? T extends Outbound\n ? Omit<T, \"version\">\n : never\n : never;\n\n// Serialise an outbound event with `version` as the first key. The\n// `version` field is added here so call sites can't forget — the helper's\n// argument type strips it. JSON.stringify preserves insertion order for\n// non-integer string keys (ES2015+), so the resulting line begins\n// `{\"version\":\"1\",…`.\nexport function emit(event: OutboundEvent): string {\n const stamped = { version: PROTOCOL_VERSION, ...event };\n return `${JSON.stringify(stamped)}\\n`;\n}\n\n// Parse a stdin line. Returns a discriminated outcome so the caller can\n// route version violations to the spec'd error reply (AC #2) rather than\n// silently dropping or crashing.\nexport type ParseOutcome<T> =\n | { ok: true; msg: T }\n | { ok: false; kind: \"json\"; reason: string }\n | { ok: false; kind: \"missing-version\" }\n | { ok: false; kind: \"unsupported-version\"; got: unknown };\n\nexport type ValidateOutcome = { ok: true } | { ok: false; reason: string };\n\n/**\n * Structural validator for inbound messages. Called after parseInbound() confirms\n * version and JSON shape; this function checks per-type required fields.\n *\n * kind=\"config\" → validates InboundConfig required fields (view, viewsRoot).\n * kind=\"post-config\" → validates InboundMessage variants by type discriminant.\n *\n * On failure the caller should emit {type:\"error\",message:reason} and continue\n * (or exit 1 for config failures, per the protocol spec).\n */\nexport function validateInboundShape(\n msg: unknown,\n kind: \"config\" | \"post-config\",\n): ValidateOutcome {\n if (typeof msg !== \"object\" || msg === null) {\n return { ok: false, reason: \"message is not an object\" };\n }\n const m = msg as Record<string, unknown>;\n\n if (kind === \"config\") {\n if (typeof m.view !== \"string\" || m.view === \"\") {\n return { ok: false, reason: 'config requires a non-empty string \"view\"' };\n }\n if (typeof m.viewsRoot !== \"string\" || m.viewsRoot === \"\") {\n return { ok: false, reason: 'config requires a non-empty string \"viewsRoot\"' };\n }\n if (\"mutations\" in m && m.mutations !== undefined) {\n if (\n !Array.isArray(m.mutations) ||\n !(m.mutations as unknown[]).every((x) => typeof x === \"string\")\n ) {\n return { ok: false, reason: \"config.mutations must be an array of strings\" };\n }\n }\n if (\"port\" in m && m.port !== undefined && typeof m.port !== \"number\") {\n return { ok: false, reason: \"config.port must be a number\" };\n }\n if (\n \"openBrowser\" in m &&\n m.openBrowser !== undefined &&\n typeof m.openBrowser !== \"boolean\"\n ) {\n return { ok: false, reason: \"config.openBrowser must be a boolean\" };\n }\n if (\n \"shell\" in m &&\n m.shell !== undefined &&\n m.shell !== \"tab\" &&\n m.shell !== \"app\"\n ) {\n return { ok: false, reason: 'config.shell must be \"tab\" or \"app\"' };\n }\n if (\n \"heartbeatTimeoutMs\" in m &&\n m.heartbeatTimeoutMs !== undefined &&\n typeof m.heartbeatTimeoutMs !== \"number\"\n ) {\n return { ok: false, reason: \"config.heartbeatTimeoutMs must be a number\" };\n }\n if (\n \"startupGraceMs\" in m &&\n m.startupGraceMs !== undefined &&\n typeof m.startupGraceMs !== \"number\"\n ) {\n return { ok: false, reason: \"config.startupGraceMs must be a number\" };\n }\n return { ok: true };\n }\n\n // post-config: discriminate on type first for result/error (which also carry id),\n // then on type alone for live-update commands.\n const type = m.type;\n if (typeof type !== \"string\") {\n return { ok: false, reason: '\"type\" field must be a string' };\n }\n\n // Mutation responses: type is \"result\" or \"error\", always carry a numeric id.\n if (type === \"result\" || type === \"error\") {\n if (typeof m.id !== \"number\") {\n return { ok: false, reason: `\"${type}\" requires a numeric \"id\" field` };\n }\n if (type === \"error\" && typeof m.message !== \"string\") {\n return { ok: false, reason: '\"error\" requires a string \"message\" field' };\n }\n return { ok: true };\n }\n\n // Live-update commands.\n switch (type) {\n case \"update\":\n if (!Object.hasOwn(m, \"data\")) {\n return { ok: false, reason: '\"update\" requires a \"data\" field' };\n }\n return { ok: true };\n case \"view\":\n if (typeof m.source !== \"string\") {\n return { ok: false, reason: '\"view\" requires a string \"source\" field' };\n }\n return { ok: true };\n case \"patch\":\n if (!Object.hasOwn(m, \"data\")) {\n return { ok: false, reason: '\"patch\" requires a \"data\" field' };\n }\n if (\n typeof m.view !== \"object\" ||\n m.view === null ||\n typeof (m.view as Record<string, unknown>).source !== \"string\"\n ) {\n return { ok: false, reason: '\"patch\" requires a string \"view.source\" field' };\n }\n return { ok: true };\n case \"reopen\":\n case \"close\":\n case \"ping\":\n return { ok: true };\n default:\n return { ok: false, reason: `unknown message type: \"${type}\"` };\n }\n}\n\nexport function parseInbound<T extends { version: ProtocolVersion }>(\n line: string,\n): ParseOutcome<T> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch (err) {\n return {\n ok: false,\n kind: \"json\",\n reason: err instanceof Error ? err.message : String(err),\n };\n }\n if (typeof parsed !== \"object\" || parsed === null) {\n return { ok: false, kind: \"missing-version\" };\n }\n if (!Object.hasOwn(parsed, \"version\")) {\n return { ok: false, kind: \"missing-version\" };\n }\n const version = (parsed as { version: unknown }).version;\n if (version !== PROTOCOL_VERSION) {\n return { ok: false, kind: \"unsupported-version\", got: version };\n }\n return { ok: true, msg: parsed as T };\n}\n"],"mappings":"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","names":["resolve","args","fireEvent","broadcast","url","resolve","createRequire","require","emit","outcome","resolve"]}
@@ -110,18 +110,20 @@ interface MountOptions {
110
110
  */
111
111
  heartbeatTimeoutMs?: number;
112
112
  /**
113
- * Content-Security-Policy enforcement. Defaults to "off".
113
+ * Content-Security-Policy enforcement. Defaults to `"strict"`.
114
+ *
115
+ * - `"strict"` — ui-leaf sends a balanced preset that browser-enforces
116
+ * the broker principle: `connect-src 'self'` prevents views from
117
+ * fetching external APIs (all data flows through `data` and
118
+ * `mutations`); `form-action 'self'` closes the form-submit
119
+ * exfiltration vector. HTTPS images/fonts and inline styles for
120
+ * React are permitted. View files can only *add* further restrictions
121
+ * via meta tag, never remove them.
114
122
  *
115
123
  * - `"off"` — no CSP header sent. Views can fetch arbitrary URLs and
116
- * embed external resources freely. The data/mutations convention is
117
- * honor-system.
118
- *
119
- * - `"strict"` — ui-leaf sends a balanced preset: locks `connect-src`
120
- * to same-origin (the architectural lock — views cannot fetch
121
- * external APIs, so all data flows through `data` and `mutations`),
122
- * while permitting common needs (HTTPS images / fonts, inline
123
- * styles for React). View files can only *add* further restrictions
124
- * via meta tag, never remove them.
124
+ * submit forms to any origin. The data/mutations contract is
125
+ * honor-system only. Use this if you have a legitimate need for
126
+ * cross-origin access and accept the trade-off.
125
127
  *
126
128
  * - `string` — raw CSP header value for full control. Use when the
127
129
  * "strict" preset doesn't fit (e.g. you need `connect-src` to
@@ -379,6 +379,7 @@ async function openInAppMode(url) {
379
379
  var STRICT_CSP = [
380
380
  "default-src 'self'",
381
381
  "connect-src 'self'",
382
+ "form-action 'self'",
382
383
  "img-src 'self' data: https:",
383
384
  "font-src 'self' https: data:",
384
385
  "style-src 'self' 'unsafe-inline'",
@@ -434,7 +435,7 @@ async function startDevServer(opts) {
434
435
  shell = "tab",
435
436
  heartbeatTimeoutMs = 5e3,
436
437
  startupGraceMs = 3e4,
437
- csp,
438
+ csp = "strict",
438
439
  allowedHosts,
439
440
  silent = false,
440
441
  _opener,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/compile.ts","../src/internal/html.ts"],"sourcesContent":["// ui-leaf — Customizable browser views, on demand, for any CLI.\n// https://github.com/OpenThinkAi/ui-leaf\n\nimport { resolve } from \"node:path\";\nimport {\n startDevServer,\n type CloseReason,\n type CspOption,\n type DevServerEvent,\n type DevServerEventListener,\n type MutationHandler,\n type Shell,\n} from \"./server.js\";\nimport type { BuildError } from \"./compile.js\";\n\nexport type { BuildError, CloseReason, CspOption, DevServerEvent, DevServerEventListener, MutationHandler, Shell };\n\nexport interface MountOptions {\n /** View name. Resolves to <viewsRoot>/<view>.tsx. */\n view: string;\n /**\n * JSON-serializable data passed to the view as a prop.\n *\n * Privacy note: the data is compiled into the HTML served at the mount URL\n * and held in memory for the mount lifetime. Any same-UID local process\n * that can reach `127.0.0.1:<port>` can fetch `GET /` and read it — the\n * per-launch token guards `/mutate` against drive-by cross-origin requests\n * in the browser, not against other processes on the machine. For PHI, PCI,\n * financial records, or anything where a same-UID local reader is in your\n * threat model, use `dataLoader` instead — the loader's return value is\n * served at a token-gated `/api/data` endpoint and never appears in the HTML.\n */\n data?: unknown;\n /**\n * Async function that supplies sensitive data to the view without\n * including it in the served HTML. When provided, the loader is called\n * once during mount setup; its resolved value is served at a token-gated\n * `GET /api/data` endpoint (same per-launch token as `/mutate`) and the\n * view fetches it on first render before calling `createRoot().render()`.\n * The data never appears in the compiled HTML.\n *\n * Use this instead of `data` for PHI, PCI, financial records, or anything\n * else where in-HTML data exposure is in your threat model.\n *\n * Error semantics: if the loader rejects, the rejection propagates to the\n * `mount()` caller (no automatic retry). Errors surface at mount time,\n * matching the synchronous `data` path's behavior.\n *\n * Mutual exclusion: passing both `data` and `dataLoader` throws at\n * mount time.\n */\n dataLoader?: () => Promise<unknown>;\n /**\n * Mutation handlers the view can call via mutate(name, args).\n * Each handler can self-type its args and return:\n *\n * mutations: {\n * recategorize: async (args: { id: string; category: string }) => {\n * await db.recategorize(args.id, args.category);\n * return { ok: true };\n * },\n * }\n *\n * Each request body is capped at 1 MiB; oversized POSTs are rejected\n * with a 400 and the view's mutate() promise rejects with a clear error.\n */\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Root directory holding view .tsx files. Defaults to <cwd>/views. */\n viewsRoot?: string;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Port to bind. Defaults to 5810 — unused by the major Node dev tools.\n * If the port is unavailable, ui-leaf bumps to the next free port and\n * the actual bound port is reflected on the returned `url` and `port`.\n * Pass `0` to let the OS pick a free port directly.\n * Override only if you need a stable URL (e.g. an external bookmark).\n */\n port?: number;\n /**\n * Open the browser when ready. Defaults to true. When false, mount()\n * returns the URL on its resolved value so the caller can drive a\n * headless browser, log the address, etc.\n */\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - `\"tab\"` — open in the user's default browser as a regular tab.\n * Works everywhere; URL bar is visible.\n *\n * - `\"app\"` — try Chromium's `--app` mode for a chromeless window\n * (no URL bar, no tabs, looks like a desktop app). Available on\n * Chrome, Edge, and Brave. If no Chromium browser is installed,\n * ui-leaf falls back to \"tab\" with a stderr note. Safari and\n * Firefox always fall back.\n *\n * Pair with the share-link pattern (see \"Sharing views across users\"\n * in the README) when you want users to never see a localhost URL.\n */\n shell?: Shell;\n /**\n * Abort to close the dev server early. The returned `closed` promise\n * resolves either way; if you need to distinguish a signal-driven close\n * from a natural tab-close, check `signal.aborted` after the await.\n */\n signal?: AbortSignal;\n /**\n * Browser silence (ms) after which the mount emits `disconnected`.\n * Defaults to 5000 — tuned for the v1.0.0 subprocess-driver model where\n * a fast `disconnected` signal lets the caller decide whether to close,\n * reopen, or keep the mount alive. Raise it for sessions where the page\n * may legitimately pause (devtools paused on a breakpoint, machine\n * sleep, long background-tab throttling). Note: this no longer controls\n * when the mount terminates — only when the `disconnected` event fires.\n */\n heartbeatTimeoutMs?: number;\n /**\n * Content-Security-Policy enforcement. Defaults to \"off\".\n *\n * - `\"off\"` — no CSP header sent. Views can fetch arbitrary URLs and\n * embed external resources freely. The data/mutations convention is\n * honor-system.\n *\n * - `\"strict\"` — ui-leaf sends a balanced preset: locks `connect-src`\n * to same-origin (the architectural lock — views cannot fetch\n * external APIs, so all data flows through `data` and `mutations`),\n * while permitting common needs (HTTPS images / fonts, inline\n * styles for React). View files can only *add* further restrictions\n * via meta tag, never remove them.\n *\n * - `string` — raw CSP header value for full control. Use when the\n * \"strict\" preset doesn't fit (e.g. you need `connect-src` to\n * include a Sentry endpoint).\n *\n * Trade-off: when set to \"strict\" or a custom string, a view file\n * cannot relax the policy at runtime. Switching back requires changing\n * the mount() call. That rigidity is a feature.\n */\n csp?: CspOption;\n /**\n * Extra hostnames accepted in the request `Host` and `Origin` headers\n * on top of the built-in loopback set (`localhost`, `127.0.0.1`, `[::1]`).\n *\n * The dev server gates every request on this set to defend against\n * DNS-rebinding attacks; non-matching requests get HTTP 403. Use this\n * escape hatch when you need to reach the dev server through a custom\n * `/etc/hosts` alias (e.g. `[\"my-app.local\"]`) or any other loopback\n * name. Hostnames are matched case-insensitively, port-agnostic.\n *\n * Be deliberate: any hostname you add becomes a viable DNS-rebinding\n * target. Don't add wildcards, public DNS names, or LAN hostnames you\n * don't fully control.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. Default: false.\n *\n * When you drive `mount()` programmatically — e.g. as part of a Node\n * bridge for a non-Node CLI that's spawned ui-leaf as a subprocess —\n * stdout is usually reserved for a structured protocol (line-delimited\n * JSON, etc.). Setting `silent: true` redirects `process.stdout.write`\n * to `process.stderr` for the lifetime of the server, restored on close.\n *\n * Tradeoff: any other code in the same process that writes to stdout\n * during the server's lifetime is also redirected. Hold the captured\n * `process.stdout.write` reference yourself if you need to write to the\n * real stdout from the same process.\n */\n silent?: boolean;\n /**\n * Grace period (ms) after server start before the heartbeat watcher arms.\n * Cold-loading clients sometimes take a few seconds to send their first\n * heartbeat. Defaults to 30000.\n */\n startupGraceMs?: number;\n /**\n * Test seam: heartbeat watcher tick interval (ms). Defaults to 1000.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n}\n\nexport interface MountedView {\n /** URL the view is reachable at (http://127.0.0.1:<port>). */\n url: string;\n /** Bound port. Useful when port: 0 was requested. */\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n /** Force-close the dev server early. */\n close: () => Promise<void>;\n /**\n * Replace in-memory data and notify all `data-updated` listeners.\n * Preserves in-page React state — no recompile.\n */\n update: (data: unknown) => void;\n /**\n * Swap the view source on the fly. Triggers a recompile; on success replaces\n * the served HTML and notifies all `view-swapped` listeners. On compile\n * failure the previous HTML is preserved. Returns compile errors if any.\n */\n swapView: (source: string) => Promise<BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails neither\n * takes effect. Returns compile errors if any.\n */\n patch: (data: unknown, source: string) => Promise<BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /** Subscribe to a server-side event (data-updated | view-swapped). */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n /** Unsubscribe a previously-registered listener. */\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\n/**\n * Mount a customizable browser view from a CLI. Spins up a local dev server\n * and renders the chosen view with the given data. Returns once the server\n * is ready; await `result.closed` to block until the mount terminates.\n *\n * Mutations triggered in the view are dispatched to the registered handlers\n * here; the view never reaches the CLI's backing API directly.\n *\n * **Lifecycle.** Browser tab close (heartbeat silence) emits a `disconnected`\n * event on `result` but does NOT resolve `closed` or stop the server. The\n * mount only terminates — and `closed` resolves — when you call\n * `result.close()`, receive SIGINT/SIGTERM, or an internal error occurs.\n * Listen for `disconnected` and call `result.close()` yourself if you want\n * fast shutdown on tab close.\n *\n * **Multi-tab note.** The heartbeat is a single high-water mark across all\n * open tabs; `disconnected` fires only when all tabs go silent. Closing one\n * tab while another is open emits no event.\n *\n * Ctrl+C: this function installs SIGINT and SIGTERM handlers that close\n * the server before exiting.\n */\nexport async function mount(opts: MountOptions): Promise<MountedView> {\n const viewsRoot = opts.viewsRoot ?? resolve(process.cwd(), \"views\");\n\n const server = await startDevServer({\n view: opts.view,\n data: opts.data,\n dataLoader: opts.dataLoader,\n viewsRoot,\n mutations: opts.mutations,\n title: opts.title,\n port: opts.port,\n openBrowser: opts.openBrowser,\n shell: opts.shell,\n heartbeatTimeoutMs: opts.heartbeatTimeoutMs,\n startupGraceMs: opts.startupGraceMs,\n csp: opts.csp,\n allowedHosts: opts.allowedHosts,\n silent: opts.silent,\n _heartbeatCheckIntervalMs: opts._heartbeatCheckIntervalMs,\n });\n\n const onSignal = (signal: NodeJS.Signals): void => {\n void (async () => {\n await server.close(\"signal\");\n // Re-raise so default exit codes still apply.\n process.kill(process.pid, signal);\n })();\n };\n const sigint = (): void => onSignal(\"SIGINT\");\n const sigterm = (): void => onSignal(\"SIGTERM\");\n process.once(\"SIGINT\", sigint);\n process.once(\"SIGTERM\", sigterm);\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n await server.close();\n return {\n url: server.url,\n port: server.port,\n closed: Promise.resolve<CloseReason>(\"caller\"),\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n }\n opts.signal.addEventListener(\n \"abort\",\n () => void server.close(),\n { once: true },\n );\n }\n\n const closed = server.closed.finally(() => {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n });\n\n return {\n url: server.url,\n port: server.port,\n closed,\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n}\n","import { randomBytes, timingSafeEqual as nodeTimingSafeEqual } from \"node:crypto\";\nimport open, { apps } from \"open\";\nimport { compileView, compileSource } from \"./compile.js\";\nimport type { CloseReason } from \"./ipc.js\";\n\n// Module-level stdout redirect state. Captured ONCE at module load so\n// concurrent silent: true mounts share the same \"original\" reference and\n// restore-order doesn't matter. Refcounted so the last close restores.\nconst ORIGINAL_STDOUT_WRITE = process.stdout.write.bind(process.stdout);\nlet stdoutRedirectCount = 0;\n\n/**\n * Redirect process.stdout.write to process.stderr until the returned\n * function is called. Safe under concurrent silent mounts.\n */\nfunction redirectStdoutToStderr(): () => void {\n stdoutRedirectCount++;\n if (stdoutRedirectCount === 1) {\n // biome-ignore lint/suspicious/noExplicitAny: stdout.write has overloaded\n // signatures; forward exactly what comes in.\n process.stdout.write = ((chunk: any, enc?: any, cb?: any) =>\n process.stderr.write(chunk, enc, cb)) as typeof process.stdout.write;\n }\n let released = false;\n return () => {\n if (released) return;\n released = true;\n stdoutRedirectCount--;\n if (stdoutRedirectCount === 0) {\n process.stdout.write = ORIGINAL_STDOUT_WRITE;\n }\n };\n}\n\nexport type MutationHandler<TArgs = unknown, TResult = unknown> = (\n args: TArgs,\n) => TResult | Promise<TResult>;\n\n// `(string & {})` preserves the \"off\" / \"strict\" autocomplete suggestions\n// while still allowing arbitrary CSP strings. Plain string would collapse\n// the union and lose IntelliSense for the literals.\nexport type CspOption = \"off\" | \"strict\" | (string & {});\n\nexport type Shell = \"tab\" | \"app\";\n\n/**\n * Try to open `url` in a Chromium browser's --app mode (chromeless window:\n * no URL bar, no tabs). Returns true if a Chromium browser was found and\n * launched, false if no Chromium variant is installed (caller should fall\n * back to the default-browser tab).\n */\nasync function openInAppMode(url: string): Promise<boolean> {\n // Order: most-common Chromium variants first.\n const candidates = [apps.chrome, apps.edge, apps.brave];\n for (const app of candidates) {\n try {\n await open(url, { app: { name: app, arguments: [`--app=${url}`] } });\n return true;\n } catch {\n // Try next candidate; `open` throws if the binary isn't installed.\n }\n }\n return false;\n}\n\n/**\n * Strict preset: locks `connect-src` to same-origin (the architectural\n * lock that forces views to route mutations through the CLI), while\n * permitting common needs (HTTPS images/fonts, inline styles for React).\n * A future v1.x mode could tighten script-src once usage patterns are known.\n */\nconst STRICT_CSP = [\n \"default-src 'self'\",\n \"connect-src 'self'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self' https: data:\",\n \"style-src 'self' 'unsafe-inline'\",\n \"script-src 'self' 'unsafe-inline'\",\n].join(\"; \");\n\nfunction resolveCsp(opt: CspOption | undefined): string | null {\n if (!opt || opt === \"off\") return null;\n if (opt === \"strict\") return STRICT_CSP;\n return opt;\n}\n\nfunction timingSafeEqual(a: string, b: string): boolean {\n // Length check is not timing-safe but is fine — the token length is fixed\n // and known to attackers regardless. The byte compare must be timing-safe.\n if (a.length !== b.length) return false;\n return nodeTimingSafeEqual(Buffer.from(a, \"utf8\"), Buffer.from(b, \"utf8\"));\n}\n\nconst DEFAULT_LOOPBACK_HOSTNAMES = [\"127.0.0.1\", \"localhost\", \"::1\"] as const;\n\n// Extract the hostname portion of a Host header value, stripping the port.\n// IPv6 hosts arrive bracketed (`[::1]:5810`); plain hosts as `host:port`\n// or bare `host`. Returns lowercased hostname or null on shapes we don't\n// recognise (caller treats null as \"reject\").\nfunction parseHostHeader(value: string): string | null {\n const trimmed = value.trim();\n if (trimmed === \"\") return null;\n if (trimmed.startsWith(\"[\")) {\n const close = trimmed.indexOf(\"]\");\n if (close === -1) return null;\n return trimmed.slice(1, close).toLowerCase();\n }\n const colon = trimmed.indexOf(\":\");\n return (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();\n}\n\n// DNS-rebinding defence: every request must arrive with a Host header\n// pointing at one of the allowed names. Same gate applies to Origin when\n// the browser sends one. Absent Origin is fine — many legitimate\n// same-origin requests omit it. `Origin: null` is allowed because\n// sandboxed iframes and `file://` pages send it; the Host check still\n// constrains the network path so the Origin allowance isn't load-bearing.\nfunction isAllowedHost(value: string | undefined, allowed: Set<string>): boolean {\n const host = value === undefined ? null : parseHostHeader(value);\n return host !== null && allowed.has(host);\n}\n\nfunction isAllowedOrigin(value: string | undefined, allowed: Set<string>): boolean {\n if (value === undefined || value === \"\" || value === \"null\") return true;\n try {\n // WHATWG URL keeps the brackets on IPv6 hostnames (`[::1]`), but the\n // allow-list stores them stripped (matching parseHostHeader's output)\n // so origins and hosts compare consistently.\n let hostname = new URL(value).hostname.toLowerCase();\n if (hostname.startsWith(\"[\") && hostname.endsWith(\"]\")) {\n hostname = hostname.slice(1, -1);\n }\n return allowed.has(hostname);\n } catch {\n return false;\n }\n}\n\nexport interface DevServerOptions {\n view: string;\n data?: unknown;\n dataLoader?: () => Promise<unknown>;\n viewsRoot: string;\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n port?: number;\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - \"tab\" — open in user's default browser as a regular tab.\n * - \"app\" — try Chromium's --app mode (chromeless window). Falls back\n * to \"tab\" if no Chromium browser is installed (Chrome/Edge/Brave),\n * with a stderr note. Safari and Firefox always fall back.\n */\n shell?: Shell;\n /**\n * Browser silence (ms) after which the mount transitions to disconnected.\n * The mount does NOT terminate on disconnect — only explicit close/signal/error does.\n */\n heartbeatTimeoutMs?: number;\n /** Grace period after server start before the heartbeat watcher is armed. */\n startupGraceMs?: number;\n /**\n * Test seam: interval (ms) for the heartbeat watcher tick. Defaults to 1000.\n * Lower values let tests observe disconnect transitions without sleeping ~1s.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n /** Content-Security-Policy enforcement. See MountOptions.csp. */\n csp?: CspOption;\n /**\n * Extra hostnames (beyond `localhost`, `127.0.0.1`, `[::1]`) accepted in\n * the request `Host` and `Origin` headers. Use to allow a custom\n * `/etc/hosts` alias or another loopback name; values are matched by\n * hostname only (port-agnostic). Anything outside this set + the\n * loopback defaults is rejected with HTTP 403 to defend against\n * DNS-rebinding attacks. Default: empty.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. When true, process.stdout.write is\n * redirected to process.stderr for the lifetime of the server, restored\n * on close(). Use when driving mount() programmatically and stdout is\n * reserved for a structured protocol (e.g. line-delimited JSON).\n * Default: false.\n */\n silent?: boolean;\n /**\n * Test seam: replace the browser-open implementation. When provided,\n * called instead of `open(url)` for both the initial open and `reopen()`.\n * Never set this in production; use `openBrowser: false` instead.\n */\n _opener?: (url: string) => Promise<void>;\n}\n\nexport type { CloseReason };\n\nexport type DevServerEvent = \"data-updated\" | \"view-swapped\" | \"disconnected\" | \"reconnected\";\nexport type DevServerEventListener = () => void;\n\ntype ConnectionState = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface DevServer {\n url: string;\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n close: (reason?: CloseReason) => Promise<void>;\n /**\n * Replace in-memory data and emit a `data-updated` event to all\n * registered listeners. Does not recompile the view.\n */\n update: (data: unknown) => void;\n /**\n * Recompile the view from an inline TSX source string and replace the\n * in-memory HTML. Emits `view-swapped` on success; preserves the previous\n * HTML on compile failure. Returns errors array (empty = success).\n */\n swapView: (source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails,\n * neither takes effect. Returns errors array (empty = success).\n */\n patch: (data: unknown, source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /**\n * Subscribe to a server-side event. Listeners are called synchronously\n * after each mutation completes.\n *\n * Events:\n * \"data-updated\" — fired by update() and patch()\n * \"view-swapped\" — fired by swapView() and patch()\n */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\nexport async function startDevServer(opts: DevServerOptions): Promise<DevServer> {\n const {\n view,\n data,\n dataLoader,\n viewsRoot,\n mutations = {},\n title = \"ui-leaf\",\n port,\n openBrowser = true,\n shell = \"tab\",\n heartbeatTimeoutMs = 5_000,\n startupGraceMs = 30_000,\n csp,\n allowedHosts,\n silent = false,\n _opener,\n _heartbeatCheckIntervalMs = 1000,\n } = opts;\n const cspHeader = resolveCsp(csp);\n const allowedHostSet = new Set<string>(DEFAULT_LOOPBACK_HOSTNAMES);\n for (const h of allowedHosts ?? []) allowedHostSet.add(h.toLowerCase());\n const allowedHostList = [...allowedHostSet].join(\", \");\n\n // Programmatic consumers (esp. non-Node CLIs spawning ui-leaf as a\n // subprocess) often reserve stdout for a structured protocol. Redirect\n // process.stdout.write to stderr to catch anything that bypasses our\n // own output path.\n const restoreStdout: (() => void) | null = silent ? redirectStdoutToStderr() : null;\n\n try {\n if (view.includes(\"/\") || view.includes(\"\\\\\")) {\n throw new Error(\n `ui-leaf: view '${view}' must be a bare identifier with no path separators`,\n );\n }\n\n if (data !== undefined && dataLoader) {\n throw new Error(\"ui-leaf: pass data or dataLoader, not both\");\n }\n\n const token = randomBytes(32).toString(\"hex\");\n\n // Eagerly invoke the loader before starting the server. The resolved\n // value lives only in this closure — it is never written to disk. If the\n // loader rejects, the setup-failure catch below restores stdout before\n // re-throwing.\n let loadedData: unknown;\n if (dataLoader) {\n loadedData = await dataLoader();\n }\n\n // Compile the view once at mount time; hold the resulting HTML in memory.\n const result = await compileView({\n entry: view,\n viewsRoot,\n data: dataLoader ? null : data,\n title,\n csp: cspHeader ?? undefined,\n token,\n dataLoader: !!dataLoader,\n });\n\n if (result.errors.length > 0) {\n const msg = result.errors.map((e) => e.message).join(\"; \");\n throw new Error(`ui-leaf: view compilation failed: ${msg}`);\n }\n\n // Mutable view state: the / handler reads from this on every request.\n // update(), swapView(), patch() mutate it in place.\n const viewState = { html: result.html, data: dataLoader ? loadedData : data };\n\n // Minimal event broker. Pre-seeded so fireEvent's get() always returns a Set.\n const listeners = new Map<DevServerEvent, Set<DevServerEventListener>>([\n [\"data-updated\", new Set()],\n [\"view-swapped\", new Set()],\n [\"disconnected\", new Set()],\n [\"reconnected\", new Set()],\n ]);\n function fireEvent(event: DevServerEvent): void {\n for (const fn of listeners.get(event)!) fn();\n }\n\n const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();\n const sseEncoder = new TextEncoder();\n\n function broadcast(event: Record<string, unknown>): void {\n const frame = sseEncoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n for (const controller of sseClients) {\n try {\n controller.enqueue(frame);\n } catch {\n sseClients.delete(controller);\n }\n }\n }\n\n let lastHeartbeatAt = Date.now();\n let closeRequested = false;\n let connectionState: ConnectionState = \"connecting\";\n let resolveClosed: (reason: CloseReason) => void = () => {};\n const closed = new Promise<CloseReason>((r) => {\n resolveClosed = r;\n });\n\n const bunPort = port === undefined ? 5810 : port; // port: 0 → OS picks\n let actualPort = bunPort;\n\n const handler = (req: Request): Response | Promise<Response> => {\n const host = req.headers.get(\"host\") ?? undefined;\n const origin = req.headers.get(\"origin\") ?? undefined;\n\n // DNS-rebinding gate: reject any request (including WebSocket upgrade\n // attempts) that does not arrive with an allowed Host. When Origin is\n // present, it must also be in the allowed set.\n const hostOk = isAllowedHost(host, allowedHostSet);\n const originOk = isAllowedOrigin(origin, allowedHostSet);\n if (!hostOk || !originOk) {\n const offender = !hostOk\n ? `Host \"${host ?? \"(absent)\"}\"`\n : `Origin \"${origin}\"`;\n return new Response(\n `ui-leaf: refusing request with ${offender} — only the following hostnames are accepted to prevent DNS rebinding: ${allowedHostList}. Open the server at http://localhost:${actualPort}/ or http://127.0.0.1:${actualPort}/, or pass { allowedHosts: [\"my-alias\"] } to mount() to permit a custom alias.\\n`,\n { status: 403, headers: { \"Content-Type\": \"text/plain; charset=utf-8\" } },\n );\n }\n\n const headers: Record<string, string> = {};\n if (cspHeader) {\n headers[\"Content-Security-Policy\"] = cspHeader;\n }\n\n const url = new URL(req.url);\n const path = url.pathname;\n const method = req.method;\n\n if (method === \"GET\" && path === \"/\") {\n return new Response(viewState.html, {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n }\n\n if (method === \"POST\" && path === \"/heartbeat\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n lastHeartbeatAt = Date.now();\n if (connectionState === \"disconnected\") {\n connectionState = \"connected\";\n fireEvent(\"reconnected\");\n } else if (connectionState === \"connecting\") {\n connectionState = \"connected\";\n }\n return new Response(\"\", { status: 204, headers });\n }\n\n if (method === \"POST\" && path === \"/mutate\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return handleMutate(req, mutations, headers);\n }\n\n if (method === \"GET\" && path === \"/api/data\") {\n if (!dataLoader) {\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return new Response(JSON.stringify(viewState.data !== undefined ? viewState.data : null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (method === \"GET\" && path === \"/events\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n let sseController!: ReadableStreamDefaultController<Uint8Array>;\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n sseController = controller;\n sseClients.add(controller);\n // Enqueue an SSE comment immediately so Bun flushes response headers\n // before any broadcast event arrives (empty streams block header send).\n controller.enqueue(sseEncoder.encode(\": connected\\n\\n\"));\n req.signal?.addEventListener(\"abort\", () => {\n sseClients.delete(sseController);\n try { sseController.close(); } catch { /* already closed */ }\n });\n },\n cancel() {\n sseClients.delete(sseController);\n },\n });\n return new Response(stream, {\n status: 200,\n headers: {\n ...headers,\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n },\n });\n }\n\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n };\n\n let heartbeatWatcher: NodeJS.Timeout | undefined;\n\n // `bunServer` is assigned immediately after this declaration by the IIFE\n // below. The `!` assertion is safe: cleanup is never called during server\n // construction, only after the server is running.\n let bunServer!: ReturnType<typeof Bun.serve>;\n\n const cleanup = async (reason: CloseReason): Promise<void> => {\n if (closeRequested) return;\n closeRequested = true;\n if (heartbeatWatcher) clearInterval(heartbeatWatcher);\n broadcast({ type: \"closing\", reason });\n for (const controller of sseClients) {\n try { controller.close(); } catch { /* already closed */ }\n }\n sseClients.clear();\n // Graceful stop: waits for in-flight writes (including the closing SSE\n // event) to flush before tearing down TCP connections.\n await bunServer.stop();\n if (restoreStdout) restoreStdout();\n resolveClosed(reason);\n };\n\n // Auto-bump: if bunPort is busy, try bunPort+1 … up to MAX_PORT_ATTEMPTS.\n // port: 0 goes straight to Bun (OS assigns a free port; never EADDRINUSE).\n // The Bun error callback fires for socket errors AND for unhandled throws in\n // the fetch handler. Either case routes through cleanup(\"error\") so the mount\n // terminates cleanly rather than hanging. This means a single buggy request\n // handler is fatal — intentional: unhandled errors indicate broken invariants.\n const serverErrorHandler = (_err: Error): Response => {\n void cleanup(\"error\");\n return new Response(JSON.stringify({ error: \"internal server error\" }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n };\n bunServer = (() => {\n if (bunPort === 0) {\n return Bun.serve({ hostname: \"127.0.0.1\", port: 0, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n }\n const MAX_PORT_ATTEMPTS = 10;\n for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {\n try {\n return Bun.serve({ hostname: \"127.0.0.1\", port: bunPort + i, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n } catch (err) {\n const isAddrinuse = err instanceof Error && err.message.includes(\"EADDRINUSE\");\n if (!isAddrinuse || i === MAX_PORT_ATTEMPTS - 1) {\n if (isAddrinuse) {\n throw new Error(\n `ui-leaf: ports ${bunPort}–${bunPort + MAX_PORT_ATTEMPTS - 1} are all in use. Pass { port: 0 } to mount() for an OS-assigned port.`,\n );\n }\n throw err;\n }\n }\n }\n throw new Error(\"unreachable\"); // TypeScript: loop always returns or throws\n })();\n actualPort = bunServer.port ?? bunPort;\n const url = `http://127.0.0.1:${actualPort}`;\n const startedAt = Date.now();\n\n heartbeatWatcher = setInterval(() => {\n if (closeRequested) return;\n const now = Date.now();\n if (now - startedAt < startupGraceMs) return;\n if (now - lastHeartbeatAt > heartbeatTimeoutMs) {\n if (connectionState !== \"disconnected\") {\n connectionState = \"disconnected\";\n fireEvent(\"disconnected\");\n }\n }\n }, _heartbeatCheckIntervalMs);\n\n // The URL passed to the browser includes the token as a hash fragment so it\n // is never sent to the server (browsers strip fragments before HTTP requests).\n // The public `url` returned to consumers stays fragment-free.\n const openUrl = `${url}/#token=${token}`;\n\n // Browser-open implementation, or the test-seam override if one was supplied.\n const doOpen: () => Promise<void> = _opener\n ? () => _opener(openUrl)\n : async () => {\n if (shell === \"app\") {\n const launched = await openInAppMode(openUrl);\n if (!launched) {\n process.stderr.write(\n `ui-leaf: shell:\"app\" requested but no Chromium browser found; falling back to default browser tab.\\n`,\n );\n await open(openUrl);\n }\n } else {\n await open(openUrl);\n }\n };\n\n if (openBrowser) {\n await doOpen();\n }\n\n return {\n url,\n port: actualPort,\n closed,\n close: (reason: CloseReason = \"caller\") => cleanup(reason),\n on(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.add(listener);\n },\n off(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.delete(listener);\n },\n update(newData: unknown): void {\n viewState.data = newData;\n broadcast({ type: \"data-updated\", data: newData });\n fireEvent(\"data-updated\");\n },\n async swapView(source: string): Promise<import(\"./compile.js\").BuildError[]> {\n const r = await compileSource({\n source,\n data: viewState.data,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n viewState.html = r.html;\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"view-swapped\");\n return [];\n },\n async patch(newData: unknown, source: string): Promise<import(\"./compile.js\").BuildError[]> {\n // Compile first with newData so the HTML embeds the incoming data.\n const r = await compileSource({\n source,\n data: newData,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n // Only mutate state on compile success (atomicity guarantee).\n viewState.data = newData;\n viewState.html = r.html;\n broadcast({ type: \"data-updated\", data: newData });\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"data-updated\");\n fireEvent(\"view-swapped\");\n return [];\n },\n async reopen(): Promise<void> {\n await doOpen();\n },\n };\n } catch (err) {\n restoreStdout?.();\n throw err;\n }\n}\n\n// Custom header (not Authorization: Bearer) so any cross-origin fetch triggers\n// a CORS preflight, which browsers block for non-same-origin callers without\n// an explicit CORS allow list. This closes the simple-form-POST / no-preflight\n// attack vector against the localhost dev server.\nfunction checkAuth(req: Request, token: string): boolean {\n const value = req.headers.get(\"x-ui-leaf-token\") ?? \"\";\n if (!value) return false;\n return timingSafeEqual(value, token);\n}\n\nasync function handleMutate(\n req: Request,\n mutations: Record<string, MutationHandler<any, any>>,\n headers: Record<string, string>,\n): Promise<Response> {\n // 1 MiB cap: Content-Length precheck short-circuits chunked / large bodies\n // before req.text() buffers them. req.text() still buffers the whole body\n // if Content-Length is absent or underreported — acceptable for this\n // loopback-only server, where the auth gate already runs first.\n const contentLength = req.headers.get(\"content-length\");\n if (contentLength && Number.parseInt(contentLength, 10) > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n let body: { name?: string; args?: unknown };\n try {\n const text = await req.text();\n if (text.length > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n body = (text ? JSON.parse(text) : undefined) as typeof body;\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : \"bad request\" }),\n { status: 400, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const name = body?.name;\n if (typeof name !== \"string\" || name.length === 0) {\n return new Response(JSON.stringify({ error: \"missing mutation name\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (!Object.hasOwn(mutations, name)) {\n return new Response(\n JSON.stringify({\n error: `ui-leaf: no mutation handler registered for '${name}'. Add it to the mutations: { } map passed to mount().`,\n }),\n { status: 404, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const handler = mutations[name]!;\n try {\n const result = await handler(body.args);\n return new Response(JSON.stringify(result ?? null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),\n { status: 500, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n}\n","import { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { join, resolve, sep } from \"node:path\";\nimport { mkdtemp, rm, stat, writeFile } from \"node:fs/promises\";\nimport type { BunPlugin } from \"bun\";\nimport { escapeForScriptTag } from \"./internal/html.js\";\n\n// Resolve React imports at module load — works under bun test / bun run.\n// NOTE: under bun build --compile (binary mode), createRequire() resolves from\n// the binary's embedded virtual filesystem. AGT-131 (cross-compile script)\n// will need a Bun.build plugin or Bun embedded-files to ensure React is\n// reachable inside the compiled binary. Flagging here so AGT-131 is not blindsided.\nconst requireFromHere = createRequire(import.meta.url);\n\n// BunPlugin that rewrites bare react/react-dom imports to absolute paths\n// under ui-leaf's installed node_modules. Ensures the bundled view always\n// finds the same React instance regardless of the consumer's package-manager\n// hoisting, and prevents duplicate React instances across views.\nconst reactAliasPlugin: BunPlugin = {\n name: \"ui-leaf-react-alias\",\n setup(build) {\n // Matches: react, react/jsx-runtime, react/jsx-dev-runtime,\n // react-dom, react-dom/client, react-dom/profiling, etc.\n build.onResolve({ filter: /^react($|\\/|-dom($|\\/))/ }, (args) => {\n try {\n return { path: requireFromHere.resolve(args.path) };\n } catch {\n return {\n path: args.path,\n errors: [{ text: `ui-leaf: failed to resolve ${args.path}` }],\n };\n }\n });\n },\n};\n\nexport interface BuildError {\n file: string;\n line: number;\n column: number;\n message: string;\n}\n\nexport interface CompileOptions {\n /** View name or path relative to viewsRoot (e.g. \"dashboard\" or \"dashboard.tsx\"). */\n entry: string;\n /** Root directory holding .tsx view files. */\n viewsRoot: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. Ignored when dataLoader is true. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Raw CSP string to emit as a <meta http-equiv=\"Content-Security-Policy\"> tag.\n * Undefined / absent means no CSP meta tag is emitted.\n */\n csp?: string;\n /**\n * Extra allowed hostnames (beyond loopback defaults). Accepted in the\n * option bag for API symmetry with DevServerOptions; has no compile-time\n * effect — the runtime DNS-rebinding gate lives in the server.\n */\n allowedHosts?: string[];\n /**\n * Per-launch auth token. Accepted for API symmetry with DevServerOptions;\n * the token is no longer embedded in HTML — it is delivered via the URL\n * fragment and read by the inline bootstrap script.\n * @deprecated No-op since v1.0.0 — token delivery is handled by startDevServer.\n */\n token?: string;\n /**\n * When true, generate an entry that fetches data from GET /api/data at\n * render time rather than reading it from window.__UI_LEAF__.data. The\n * compiled HTML bootstrap omits the data field (only token is included).\n * Use when data is sensitive and must not be written to the HTML file.\n */\n dataLoader?: boolean;\n}\n\n/**\n * Options for compiling an inline TSX source string.\n *\n * v1.0.0 constraint: `source` is treated as a self-contained TSX string.\n * Relative imports are not supported — the string has no filesystem context\n * to resolve them against. Bare-package imports (react, react-dom) work via\n * the react-alias plugin. This is the intended contract for IPC-driven\n * view hot-swaps (AI-generated self-contained components).\n */\nexport interface CompileSourceOptions {\n /** Raw TSX source string to compile. Must be a self-contained component. */\n source: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /** Raw CSP string. Undefined / absent means no CSP meta tag. */\n csp?: string;\n /**\n * Per-launch auth token. Accepted for API symmetry; not embedded in HTML —\n * see CompileOptions.token.\n * @deprecated No-op since v1.0.0.\n */\n token?: string;\n}\n\nexport interface CompileResult {\n html: string;\n errors: BuildError[];\n}\n\n// Friendly message rendered when the page is reloaded without the token fragment.\nconst SESSION_ENDED_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>Session ended — re-launch the CLI to continue.</p></div>';\n\n// Overlay rendered when the mount terminates. v1.x extension point: replaceable\n// via a consumer-supplied template slot (deferred per plan-approval decision).\nconst CLOSED_OVERLAY_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>This view has closed.</p></div>';\n\n// Shared bridge injected into every compiled entry: mutation + heartbeat.\nconst SHARED_BRIDGE = `\nasync function mutate(name: string, args?: unknown): Promise<unknown> {\n const res = await fetch(\"/mutate\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token ? { \"X-UI-Leaf-Token\": token } : {}),\n },\n body: JSON.stringify({ name, args }),\n });\n const text = await res.text().catch(() => \"\");\n if (!res.ok) {\n let detail = text;\n try {\n const parsed: unknown = text ? JSON.parse(text) : null;\n if (parsed !== null && typeof parsed === \"object\" && \"error\" in parsed && typeof (parsed as { error: unknown }).error === \"string\") {\n detail = (parsed as { error: string }).error;\n }\n } catch { /* keep raw text */ }\n throw new Error(\"ui-leaf: mutation '\" + name + \"' failed (\" + res.status + \"): \" + detail);\n }\n return text ? JSON.parse(text) : undefined;\n}\n\nasync function heartbeat(): Promise<void> {\n try {\n await fetch(\"/heartbeat\", {\n method: \"POST\",\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n } catch { /* server may have shut down; ignore */ }\n}\nsetInterval(heartbeat, 5000);\nheartbeat();\n\nfunction subscribeEvents(onEvent: (ev: { type: string; [k: string]: unknown }) => void): void {\n let delay = 250;\n const budget = 30_000;\n const started = Date.now();\n let done = false;\n\n async function connect(): Promise<void> {\n try {\n const res = await fetch(\"/events\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok || !res.body) throw new Error(\"bad status \" + res.status);\n delay = 250;\n const reader = res.body.getReader();\n const dec = new TextDecoder(\"utf-8\");\n let buf = \"\";\n while (true) {\n const { done: streamDone, value } = await reader.read();\n if (streamDone) break;\n buf += dec.decode(value, { stream: true });\n let idx: number;\n while ((idx = buf.indexOf(\"\\\\n\\\\n\")) !== -1) {\n const chunk = buf.slice(0, idx);\n buf = buf.slice(idx + 2);\n for (const line of chunk.split(\"\\\\n\")) {\n if (line.startsWith(\"data:\")) {\n try {\n const ev = JSON.parse(line.slice(5).trimStart()) as { type: string; [k: string]: unknown };\n if (ev.type === \"closing\") done = true;\n onEvent(ev);\n } catch { /* skip malformed event */ }\n }\n }\n }\n if (done) return;\n }\n } catch {\n if (done) return;\n }\n if (done) return;\n if (Date.now() - started > budget) {\n onEvent({ type: \"closing\", reason: \"error\" });\n return;\n }\n await new Promise<void>((r) => setTimeout(r, delay));\n delay = Math.min(delay * 2, 5_000);\n void connect();\n }\n\n void connect();\n}`;\n\n/** Run Bun.build on `entryPath` and return the raw JS output or errors. */\nasync function runBunBuild(entryPath: string): Promise<{ js: string } | { errors: BuildError[] }> {\n let buildOutput: Awaited<ReturnType<typeof Bun.build>>;\n try {\n buildOutput = await Bun.build({\n entrypoints: [entryPath],\n target: \"browser\",\n format: \"esm\",\n minify: false,\n sourcemap: \"none\",\n plugins: [reactAliasPlugin],\n });\n } catch (err) {\n if (err instanceof AggregateError) {\n type BunBuildMsg = { message: string; position?: { file?: string; line?: number; column?: number } | null };\n const errors: BuildError[] = (err.errors as BunBuildMsg[]).map((e) => ({\n file: e.position?.file ?? \"<unknown>\",\n line: e.position?.line ?? 0,\n column: e.position?.column ?? 0,\n message: e.message,\n }));\n return { errors };\n }\n throw err;\n }\n const output = buildOutput.outputs[0];\n if (!output) {\n return {\n errors: [{ file: \"<unknown>\", line: 0, column: 0, message: \"ui-leaf: Bun.build produced no output\" }],\n };\n }\n return { js: await output.text() };\n}\n\n/** Assemble the final HTML page from compiled JS and options. */\nfunction assembleHtml(opts: {\n js: string;\n title: string;\n csp: string | undefined;\n data: unknown;\n dataLoader: boolean;\n}): string {\n const { js, title, csp, data, dataLoader } = opts;\n // Escape </script> sequences to prevent script-tag break-out.\n const safeJs = js.replace(/<\\/script>/gi, \"<\\\\/script>\");\n\n const titleEscaped = title\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n\n const cspMeta = csp\n ? ` <meta http-equiv=\"Content-Security-Policy\" content=\"${csp.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\")}\" />\\n`\n : \"\";\n\n // Double-stringify data: outer JSON.stringify produces a JSON string, then\n // escapeForScriptTag ensures </script> and U+2028/U+2029 can't break out.\n const dataInit = dataLoader\n ? \"window.__UI_LEAF__ = {};\"\n : `window.__UI_LEAF__ = { data: JSON.parse(${escapeForScriptTag(JSON.stringify(JSON.stringify(data ?? null)))}) };`;\n\n // Bootstrap: reads token from URL fragment, stashes it on __UI_LEAF__.token,\n // then immediately clears the fragment from the URL bar so the token is\n // never visible in history. On reload (fragment gone), sets sessionEnded so\n // the bundled module can render a friendly recovery message instead of\n // attempting unauthenticated fetches.\n // decodeURIComponent is wrapped in try/catch: a malformed %-sequence would\n // otherwise throw and kill the bootstrap silently; the catch falls through\n // to sessionEnded so the user gets the recovery screen instead of a blank page.\n const bootstrapScript = `${dataInit}\n(function(){var m=/[#&]token=([^&#]*)/.exec(window.location.hash);if(m){try{window.__UI_LEAF__.token=decodeURIComponent(m[1]);history.replaceState(null,\"\",window.location.pathname+window.location.search);}catch(e){window.__UI_LEAF__.sessionEnded=true;}}else{window.__UI_LEAF__.sessionEnded=true;}})();`;\n\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>${titleEscaped}</title>\n${cspMeta} <!-- ui-leaf bootstrap -->\n <script>${bootstrapScript}</script>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\">${safeJs}</script>\n </body>\n</html>`;\n}\n\nexport async function compileView(opts: CompileOptions): Promise<CompileResult> {\n const {\n entry,\n viewsRoot,\n data,\n title = \"ui-leaf\",\n csp,\n // allowedHosts and token have no compile-time effect; accepted for API symmetry.\n allowedHosts: _allowedHosts,\n token: _token,\n dataLoader = false,\n } = opts;\n\n const viewsRootAbs = resolve(viewsRoot);\n const hasExt = /\\.[a-z]+$/i.test(entry);\n const viewAbs = resolve(viewsRootAbs, hasExt ? entry : `${entry}.tsx`);\n if (!viewAbs.startsWith(viewsRootAbs + sep)) {\n return {\n html: \"\",\n errors: [\n {\n file: \"<unknown>\",\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' resolves outside viewsRoot`,\n },\n ],\n };\n }\n try {\n await stat(viewAbs);\n } catch {\n return {\n html: \"\",\n errors: [\n {\n file: viewAbs,\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' not found at ${viewAbs}`,\n },\n ],\n };\n }\n\n // Generate a temp entry that imports the resolved view, mounts React via\n // createRoot, and wires the mutation/heartbeat bridge.\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-compile-\"));\n try {\n const entryPath = join(tempDir, \"entry.tsx\");\n\n const entryContent = dataLoader\n ? `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n${SHARED_BRIDGE}\n\n async function bootstrap(): Promise<void> {\n const res = await fetch(\"/api/data\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\"ui-leaf: /api/data fetch failed (\" + res.status + \"): \" + text);\n }\n let currentData: unknown = await res.json();\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n }\n bootstrap();\n}\n`\n : `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Compile an inline TSX source string into a full HTML page.\n *\n * The source is treated as a self-contained component; relative imports are\n * not supported (v1.0.0 constraint — the string has no filesystem context).\n * Bare-package imports (react, react-dom) work via the react-alias plugin.\n */\nexport async function compileSource(opts: CompileSourceOptions): Promise<CompileResult> {\n const { source, data, title = \"ui-leaf\", csp, token: _token } = opts;\n\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-src-\"));\n try {\n // Write the caller's tsx as the view file, then write a thin entry wrapper.\n const viewPath = join(tempDir, \"view.tsx\");\n const entryPath = join(tempDir, \"entry.tsx\");\n\n await writeFile(viewPath, source);\n\n const entryContent = `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewPath)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader: false }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n","export function escapeForScriptTag(json: string): string {\n // Defend against </script> break-out and U+2028/U+2029 line terminators\n // that JSON.stringify emits raw but JS string literals don't accept.\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n"],"mappings":"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","names":["resolve","fireEvent","broadcast","url","resolve"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/compile.ts","../src/internal/html.ts"],"sourcesContent":["// ui-leaf — Customizable browser views, on demand, for any CLI.\n// https://github.com/OpenThinkAi/ui-leaf\n\nimport { resolve } from \"node:path\";\nimport {\n startDevServer,\n type CloseReason,\n type CspOption,\n type DevServerEvent,\n type DevServerEventListener,\n type MutationHandler,\n type Shell,\n} from \"./server.js\";\nimport type { BuildError } from \"./compile.js\";\n\nexport type { BuildError, CloseReason, CspOption, DevServerEvent, DevServerEventListener, MutationHandler, Shell };\n\nexport interface MountOptions {\n /** View name. Resolves to <viewsRoot>/<view>.tsx. */\n view: string;\n /**\n * JSON-serializable data passed to the view as a prop.\n *\n * Privacy note: the data is compiled into the HTML served at the mount URL\n * and held in memory for the mount lifetime. Any same-UID local process\n * that can reach `127.0.0.1:<port>` can fetch `GET /` and read it — the\n * per-launch token guards `/mutate` against drive-by cross-origin requests\n * in the browser, not against other processes on the machine. For PHI, PCI,\n * financial records, or anything where a same-UID local reader is in your\n * threat model, use `dataLoader` instead — the loader's return value is\n * served at a token-gated `/api/data` endpoint and never appears in the HTML.\n */\n data?: unknown;\n /**\n * Async function that supplies sensitive data to the view without\n * including it in the served HTML. When provided, the loader is called\n * once during mount setup; its resolved value is served at a token-gated\n * `GET /api/data` endpoint (same per-launch token as `/mutate`) and the\n * view fetches it on first render before calling `createRoot().render()`.\n * The data never appears in the compiled HTML.\n *\n * Use this instead of `data` for PHI, PCI, financial records, or anything\n * else where in-HTML data exposure is in your threat model.\n *\n * Error semantics: if the loader rejects, the rejection propagates to the\n * `mount()` caller (no automatic retry). Errors surface at mount time,\n * matching the synchronous `data` path's behavior.\n *\n * Mutual exclusion: passing both `data` and `dataLoader` throws at\n * mount time.\n */\n dataLoader?: () => Promise<unknown>;\n /**\n * Mutation handlers the view can call via mutate(name, args).\n * Each handler can self-type its args and return:\n *\n * mutations: {\n * recategorize: async (args: { id: string; category: string }) => {\n * await db.recategorize(args.id, args.category);\n * return { ok: true };\n * },\n * }\n *\n * Each request body is capped at 1 MiB; oversized POSTs are rejected\n * with a 400 and the view's mutate() promise rejects with a clear error.\n */\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Root directory holding view .tsx files. Defaults to <cwd>/views. */\n viewsRoot?: string;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Port to bind. Defaults to 5810 — unused by the major Node dev tools.\n * If the port is unavailable, ui-leaf bumps to the next free port and\n * the actual bound port is reflected on the returned `url` and `port`.\n * Pass `0` to let the OS pick a free port directly.\n * Override only if you need a stable URL (e.g. an external bookmark).\n */\n port?: number;\n /**\n * Open the browser when ready. Defaults to true. When false, mount()\n * returns the URL on its resolved value so the caller can drive a\n * headless browser, log the address, etc.\n */\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - `\"tab\"` — open in the user's default browser as a regular tab.\n * Works everywhere; URL bar is visible.\n *\n * - `\"app\"` — try Chromium's `--app` mode for a chromeless window\n * (no URL bar, no tabs, looks like a desktop app). Available on\n * Chrome, Edge, and Brave. If no Chromium browser is installed,\n * ui-leaf falls back to \"tab\" with a stderr note. Safari and\n * Firefox always fall back.\n *\n * Pair with the share-link pattern (see \"Sharing views across users\"\n * in the README) when you want users to never see a localhost URL.\n */\n shell?: Shell;\n /**\n * Abort to close the dev server early. The returned `closed` promise\n * resolves either way; if you need to distinguish a signal-driven close\n * from a natural tab-close, check `signal.aborted` after the await.\n */\n signal?: AbortSignal;\n /**\n * Browser silence (ms) after which the mount emits `disconnected`.\n * Defaults to 5000 — tuned for the v1.0.0 subprocess-driver model where\n * a fast `disconnected` signal lets the caller decide whether to close,\n * reopen, or keep the mount alive. Raise it for sessions where the page\n * may legitimately pause (devtools paused on a breakpoint, machine\n * sleep, long background-tab throttling). Note: this no longer controls\n * when the mount terminates — only when the `disconnected` event fires.\n */\n heartbeatTimeoutMs?: number;\n /**\n * Content-Security-Policy enforcement. Defaults to `\"strict\"`.\n *\n * - `\"strict\"` — ui-leaf sends a balanced preset that browser-enforces\n * the broker principle: `connect-src 'self'` prevents views from\n * fetching external APIs (all data flows through `data` and\n * `mutations`); `form-action 'self'` closes the form-submit\n * exfiltration vector. HTTPS images/fonts and inline styles for\n * React are permitted. View files can only *add* further restrictions\n * via meta tag, never remove them.\n *\n * - `\"off\"` — no CSP header sent. Views can fetch arbitrary URLs and\n * submit forms to any origin. The data/mutations contract is\n * honor-system only. Use this if you have a legitimate need for\n * cross-origin access and accept the trade-off.\n *\n * - `string` — raw CSP header value for full control. Use when the\n * \"strict\" preset doesn't fit (e.g. you need `connect-src` to\n * include a Sentry endpoint).\n *\n * Trade-off: when set to \"strict\" or a custom string, a view file\n * cannot relax the policy at runtime. Switching back requires changing\n * the mount() call. That rigidity is a feature.\n */\n csp?: CspOption;\n /**\n * Extra hostnames accepted in the request `Host` and `Origin` headers\n * on top of the built-in loopback set (`localhost`, `127.0.0.1`, `[::1]`).\n *\n * The dev server gates every request on this set to defend against\n * DNS-rebinding attacks; non-matching requests get HTTP 403. Use this\n * escape hatch when you need to reach the dev server through a custom\n * `/etc/hosts` alias (e.g. `[\"my-app.local\"]`) or any other loopback\n * name. Hostnames are matched case-insensitively, port-agnostic.\n *\n * Be deliberate: any hostname you add becomes a viable DNS-rebinding\n * target. Don't add wildcards, public DNS names, or LAN hostnames you\n * don't fully control.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. Default: false.\n *\n * When you drive `mount()` programmatically — e.g. as part of a Node\n * bridge for a non-Node CLI that's spawned ui-leaf as a subprocess —\n * stdout is usually reserved for a structured protocol (line-delimited\n * JSON, etc.). Setting `silent: true` redirects `process.stdout.write`\n * to `process.stderr` for the lifetime of the server, restored on close.\n *\n * Tradeoff: any other code in the same process that writes to stdout\n * during the server's lifetime is also redirected. Hold the captured\n * `process.stdout.write` reference yourself if you need to write to the\n * real stdout from the same process.\n */\n silent?: boolean;\n /**\n * Grace period (ms) after server start before the heartbeat watcher arms.\n * Cold-loading clients sometimes take a few seconds to send their first\n * heartbeat. Defaults to 30000.\n */\n startupGraceMs?: number;\n /**\n * Test seam: heartbeat watcher tick interval (ms). Defaults to 1000.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n}\n\nexport interface MountedView {\n /** URL the view is reachable at (http://127.0.0.1:<port>). */\n url: string;\n /** Bound port. Useful when port: 0 was requested. */\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n /** Force-close the dev server early. */\n close: () => Promise<void>;\n /**\n * Replace in-memory data and notify all `data-updated` listeners.\n * Preserves in-page React state — no recompile.\n */\n update: (data: unknown) => void;\n /**\n * Swap the view source on the fly. Triggers a recompile; on success replaces\n * the served HTML and notifies all `view-swapped` listeners. On compile\n * failure the previous HTML is preserved. Returns compile errors if any.\n */\n swapView: (source: string) => Promise<BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails neither\n * takes effect. Returns compile errors if any.\n */\n patch: (data: unknown, source: string) => Promise<BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /** Subscribe to a server-side event (data-updated | view-swapped). */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n /** Unsubscribe a previously-registered listener. */\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\n/**\n * Mount a customizable browser view from a CLI. Spins up a local dev server\n * and renders the chosen view with the given data. Returns once the server\n * is ready; await `result.closed` to block until the mount terminates.\n *\n * Mutations triggered in the view are dispatched to the registered handlers\n * here; the view never reaches the CLI's backing API directly.\n *\n * **Lifecycle.** Browser tab close (heartbeat silence) emits a `disconnected`\n * event on `result` but does NOT resolve `closed` or stop the server. The\n * mount only terminates — and `closed` resolves — when you call\n * `result.close()`, receive SIGINT/SIGTERM, or an internal error occurs.\n * Listen for `disconnected` and call `result.close()` yourself if you want\n * fast shutdown on tab close.\n *\n * **Multi-tab note.** The heartbeat is a single high-water mark across all\n * open tabs; `disconnected` fires only when all tabs go silent. Closing one\n * tab while another is open emits no event.\n *\n * Ctrl+C: this function installs SIGINT and SIGTERM handlers that close\n * the server before exiting.\n */\nexport async function mount(opts: MountOptions): Promise<MountedView> {\n const viewsRoot = opts.viewsRoot ?? resolve(process.cwd(), \"views\");\n\n const server = await startDevServer({\n view: opts.view,\n data: opts.data,\n dataLoader: opts.dataLoader,\n viewsRoot,\n mutations: opts.mutations,\n title: opts.title,\n port: opts.port,\n openBrowser: opts.openBrowser,\n shell: opts.shell,\n heartbeatTimeoutMs: opts.heartbeatTimeoutMs,\n startupGraceMs: opts.startupGraceMs,\n csp: opts.csp,\n allowedHosts: opts.allowedHosts,\n silent: opts.silent,\n _heartbeatCheckIntervalMs: opts._heartbeatCheckIntervalMs,\n });\n\n const onSignal = (signal: NodeJS.Signals): void => {\n void (async () => {\n await server.close(\"signal\");\n // Re-raise so default exit codes still apply.\n process.kill(process.pid, signal);\n })();\n };\n const sigint = (): void => onSignal(\"SIGINT\");\n const sigterm = (): void => onSignal(\"SIGTERM\");\n process.once(\"SIGINT\", sigint);\n process.once(\"SIGTERM\", sigterm);\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n await server.close();\n return {\n url: server.url,\n port: server.port,\n closed: Promise.resolve<CloseReason>(\"caller\"),\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n }\n opts.signal.addEventListener(\n \"abort\",\n () => void server.close(),\n { once: true },\n );\n }\n\n const closed = server.closed.finally(() => {\n process.off(\"SIGINT\", sigint);\n process.off(\"SIGTERM\", sigterm);\n });\n\n return {\n url: server.url,\n port: server.port,\n closed,\n close: () => server.close(),\n update: server.update.bind(server),\n swapView: (source: string) => server.swapView(source),\n patch: (data: unknown, source: string) => server.patch(data, source),\n reopen: server.reopen.bind(server),\n on: server.on.bind(server),\n off: server.off.bind(server),\n };\n}\n","import { randomBytes, timingSafeEqual as nodeTimingSafeEqual } from \"node:crypto\";\nimport open, { apps } from \"open\";\nimport { compileView, compileSource } from \"./compile.js\";\nimport type { CloseReason } from \"./ipc.js\";\n\n// Module-level stdout redirect state. Captured ONCE at module load so\n// concurrent silent: true mounts share the same \"original\" reference and\n// restore-order doesn't matter. Refcounted so the last close restores.\nconst ORIGINAL_STDOUT_WRITE = process.stdout.write.bind(process.stdout);\nlet stdoutRedirectCount = 0;\n\n/**\n * Redirect process.stdout.write to process.stderr until the returned\n * function is called. Safe under concurrent silent mounts.\n */\nfunction redirectStdoutToStderr(): () => void {\n stdoutRedirectCount++;\n if (stdoutRedirectCount === 1) {\n // biome-ignore lint/suspicious/noExplicitAny: stdout.write has overloaded\n // signatures; forward exactly what comes in.\n process.stdout.write = ((chunk: any, enc?: any, cb?: any) =>\n process.stderr.write(chunk, enc, cb)) as typeof process.stdout.write;\n }\n let released = false;\n return () => {\n if (released) return;\n released = true;\n stdoutRedirectCount--;\n if (stdoutRedirectCount === 0) {\n process.stdout.write = ORIGINAL_STDOUT_WRITE;\n }\n };\n}\n\nexport type MutationHandler<TArgs = unknown, TResult = unknown> = (\n args: TArgs,\n) => TResult | Promise<TResult>;\n\n// `(string & {})` preserves the \"off\" / \"strict\" autocomplete suggestions\n// while still allowing arbitrary CSP strings. Plain string would collapse\n// the union and lose IntelliSense for the literals.\nexport type CspOption = \"off\" | \"strict\" | (string & {});\n\nexport type Shell = \"tab\" | \"app\";\n\n/**\n * Try to open `url` in a Chromium browser's --app mode (chromeless window:\n * no URL bar, no tabs). Returns true if a Chromium browser was found and\n * launched, false if no Chromium variant is installed (caller should fall\n * back to the default-browser tab).\n */\nasync function openInAppMode(url: string): Promise<boolean> {\n // Order: most-common Chromium variants first.\n const candidates = [apps.chrome, apps.edge, apps.brave];\n for (const app of candidates) {\n try {\n await open(url, { app: { name: app, arguments: [`--app=${url}`] } });\n return true;\n } catch {\n // Try next candidate; `open` throws if the binary isn't installed.\n }\n }\n return false;\n}\n\n/**\n * Strict preset: locks `connect-src` to same-origin (the architectural\n * lock that forces views to route mutations through the CLI), while\n * permitting common needs (HTTPS images/fonts, inline styles for React).\n * A future v1.x mode could tighten script-src once usage patterns are known.\n */\nconst STRICT_CSP = [\n \"default-src 'self'\",\n \"connect-src 'self'\",\n \"form-action 'self'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self' https: data:\",\n \"style-src 'self' 'unsafe-inline'\",\n \"script-src 'self' 'unsafe-inline'\",\n].join(\"; \");\n\nfunction resolveCsp(opt: CspOption | undefined): string | null {\n if (!opt || opt === \"off\") return null;\n if (opt === \"strict\") return STRICT_CSP;\n return opt;\n}\n\nfunction timingSafeEqual(a: string, b: string): boolean {\n // Length check is not timing-safe but is fine — the token length is fixed\n // and known to attackers regardless. The byte compare must be timing-safe.\n if (a.length !== b.length) return false;\n return nodeTimingSafeEqual(Buffer.from(a, \"utf8\"), Buffer.from(b, \"utf8\"));\n}\n\nconst DEFAULT_LOOPBACK_HOSTNAMES = [\"127.0.0.1\", \"localhost\", \"::1\"] as const;\n\n// Extract the hostname portion of a Host header value, stripping the port.\n// IPv6 hosts arrive bracketed (`[::1]:5810`); plain hosts as `host:port`\n// or bare `host`. Returns lowercased hostname or null on shapes we don't\n// recognise (caller treats null as \"reject\").\nfunction parseHostHeader(value: string): string | null {\n const trimmed = value.trim();\n if (trimmed === \"\") return null;\n if (trimmed.startsWith(\"[\")) {\n const close = trimmed.indexOf(\"]\");\n if (close === -1) return null;\n return trimmed.slice(1, close).toLowerCase();\n }\n const colon = trimmed.indexOf(\":\");\n return (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();\n}\n\n// DNS-rebinding defence: every request must arrive with a Host header\n// pointing at one of the allowed names. Same gate applies to Origin when\n// the browser sends one. Absent Origin is fine — many legitimate\n// same-origin requests omit it. `Origin: null` is allowed because\n// sandboxed iframes and `file://` pages send it; the Host check still\n// constrains the network path so the Origin allowance isn't load-bearing.\nfunction isAllowedHost(value: string | undefined, allowed: Set<string>): boolean {\n const host = value === undefined ? null : parseHostHeader(value);\n return host !== null && allowed.has(host);\n}\n\nfunction isAllowedOrigin(value: string | undefined, allowed: Set<string>): boolean {\n if (value === undefined || value === \"\" || value === \"null\") return true;\n try {\n // WHATWG URL keeps the brackets on IPv6 hostnames (`[::1]`), but the\n // allow-list stores them stripped (matching parseHostHeader's output)\n // so origins and hosts compare consistently.\n let hostname = new URL(value).hostname.toLowerCase();\n if (hostname.startsWith(\"[\") && hostname.endsWith(\"]\")) {\n hostname = hostname.slice(1, -1);\n }\n return allowed.has(hostname);\n } catch {\n return false;\n }\n}\n\nexport interface DevServerOptions {\n view: string;\n data?: unknown;\n dataLoader?: () => Promise<unknown>;\n viewsRoot: string;\n // biome-ignore lint/suspicious/noExplicitAny: each handler has its own\n // arg/return types; the map can't share one shape.\n mutations?: Record<string, MutationHandler<any, any>>;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n port?: number;\n openBrowser?: boolean;\n /**\n * Browser shell. Defaults to \"tab\".\n *\n * - \"tab\" — open in user's default browser as a regular tab.\n * - \"app\" — try Chromium's --app mode (chromeless window). Falls back\n * to \"tab\" if no Chromium browser is installed (Chrome/Edge/Brave),\n * with a stderr note. Safari and Firefox always fall back.\n */\n shell?: Shell;\n /**\n * Browser silence (ms) after which the mount transitions to disconnected.\n * The mount does NOT terminate on disconnect — only explicit close/signal/error does.\n */\n heartbeatTimeoutMs?: number;\n /** Grace period after server start before the heartbeat watcher is armed. */\n startupGraceMs?: number;\n /**\n * Test seam: interval (ms) for the heartbeat watcher tick. Defaults to 1000.\n * Lower values let tests observe disconnect transitions without sleeping ~1s.\n * Never set this in production.\n */\n _heartbeatCheckIntervalMs?: number;\n /** Content-Security-Policy enforcement. See MountOptions.csp. */\n csp?: CspOption;\n /**\n * Extra hostnames (beyond `localhost`, `127.0.0.1`, `[::1]`) accepted in\n * the request `Host` and `Origin` headers. Use to allow a custom\n * `/etc/hosts` alias or another loopback name; values are matched by\n * hostname only (port-agnostic). Anything outside this set + the\n * loopback defaults is rejected with HTTP 403 to defend against\n * DNS-rebinding attacks. Default: empty.\n */\n allowedHosts?: string[];\n /**\n * Suppress ui-leaf output to stdout. When true, process.stdout.write is\n * redirected to process.stderr for the lifetime of the server, restored\n * on close(). Use when driving mount() programmatically and stdout is\n * reserved for a structured protocol (e.g. line-delimited JSON).\n * Default: false.\n */\n silent?: boolean;\n /**\n * Test seam: replace the browser-open implementation. When provided,\n * called instead of `open(url)` for both the initial open and `reopen()`.\n * Never set this in production; use `openBrowser: false` instead.\n */\n _opener?: (url: string) => Promise<void>;\n}\n\nexport type { CloseReason };\n\nexport type DevServerEvent = \"data-updated\" | \"view-swapped\" | \"disconnected\" | \"reconnected\";\nexport type DevServerEventListener = () => void;\n\ntype ConnectionState = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface DevServer {\n url: string;\n port: number;\n /** Resolves with the close reason when the mount terminates. */\n closed: Promise<CloseReason>;\n close: (reason?: CloseReason) => Promise<void>;\n /**\n * Replace in-memory data and emit a `data-updated` event to all\n * registered listeners. Does not recompile the view.\n */\n update: (data: unknown) => void;\n /**\n * Recompile the view from an inline TSX source string and replace the\n * in-memory HTML. Emits `view-swapped` on success; preserves the previous\n * HTML on compile failure. Returns errors array (empty = success).\n */\n swapView: (source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Atomically replace both data and view source. If compilation fails,\n * neither takes effect. Returns errors array (empty = success).\n */\n patch: (data: unknown, source: string) => Promise<import(\"./compile.js\").BuildError[]>;\n /**\n * Re-invoke the browser-open function to launch a fresh tab at the same URL.\n * Always opens a new tab — if one is already connected, a duplicate opens.\n */\n reopen: () => Promise<void>;\n /**\n * Subscribe to a server-side event. Listeners are called synchronously\n * after each mutation completes.\n *\n * Events:\n * \"data-updated\" — fired by update() and patch()\n * \"view-swapped\" — fired by swapView() and patch()\n */\n on: (event: DevServerEvent, listener: DevServerEventListener) => void;\n off: (event: DevServerEvent, listener: DevServerEventListener) => void;\n}\n\nexport async function startDevServer(opts: DevServerOptions): Promise<DevServer> {\n const {\n view,\n data,\n dataLoader,\n viewsRoot,\n mutations = {},\n title = \"ui-leaf\",\n port,\n openBrowser = true,\n shell = \"tab\",\n heartbeatTimeoutMs = 5_000,\n startupGraceMs = 30_000,\n csp = \"strict\",\n allowedHosts,\n silent = false,\n _opener,\n _heartbeatCheckIntervalMs = 1000,\n } = opts;\n const cspHeader = resolveCsp(csp);\n const allowedHostSet = new Set<string>(DEFAULT_LOOPBACK_HOSTNAMES);\n for (const h of allowedHosts ?? []) allowedHostSet.add(h.toLowerCase());\n const allowedHostList = [...allowedHostSet].join(\", \");\n\n // Programmatic consumers (esp. non-Node CLIs spawning ui-leaf as a\n // subprocess) often reserve stdout for a structured protocol. Redirect\n // process.stdout.write to stderr to catch anything that bypasses our\n // own output path.\n const restoreStdout: (() => void) | null = silent ? redirectStdoutToStderr() : null;\n\n try {\n if (view.includes(\"/\") || view.includes(\"\\\\\")) {\n throw new Error(\n `ui-leaf: view '${view}' must be a bare identifier with no path separators`,\n );\n }\n\n if (data !== undefined && dataLoader) {\n throw new Error(\"ui-leaf: pass data or dataLoader, not both\");\n }\n\n const token = randomBytes(32).toString(\"hex\");\n\n // Eagerly invoke the loader before starting the server. The resolved\n // value lives only in this closure — it is never written to disk. If the\n // loader rejects, the setup-failure catch below restores stdout before\n // re-throwing.\n let loadedData: unknown;\n if (dataLoader) {\n loadedData = await dataLoader();\n }\n\n // Compile the view once at mount time; hold the resulting HTML in memory.\n const result = await compileView({\n entry: view,\n viewsRoot,\n data: dataLoader ? null : data,\n title,\n csp: cspHeader ?? undefined,\n token,\n dataLoader: !!dataLoader,\n });\n\n if (result.errors.length > 0) {\n const msg = result.errors.map((e) => e.message).join(\"; \");\n throw new Error(`ui-leaf: view compilation failed: ${msg}`);\n }\n\n // Mutable view state: the / handler reads from this on every request.\n // update(), swapView(), patch() mutate it in place.\n const viewState = { html: result.html, data: dataLoader ? loadedData : data };\n\n // Minimal event broker. Pre-seeded so fireEvent's get() always returns a Set.\n const listeners = new Map<DevServerEvent, Set<DevServerEventListener>>([\n [\"data-updated\", new Set()],\n [\"view-swapped\", new Set()],\n [\"disconnected\", new Set()],\n [\"reconnected\", new Set()],\n ]);\n function fireEvent(event: DevServerEvent): void {\n for (const fn of listeners.get(event)!) fn();\n }\n\n const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();\n const sseEncoder = new TextEncoder();\n\n function broadcast(event: Record<string, unknown>): void {\n const frame = sseEncoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n for (const controller of sseClients) {\n try {\n controller.enqueue(frame);\n } catch {\n sseClients.delete(controller);\n }\n }\n }\n\n let lastHeartbeatAt = Date.now();\n let closeRequested = false;\n let connectionState: ConnectionState = \"connecting\";\n let resolveClosed: (reason: CloseReason) => void = () => {};\n const closed = new Promise<CloseReason>((r) => {\n resolveClosed = r;\n });\n\n const bunPort = port === undefined ? 5810 : port; // port: 0 → OS picks\n let actualPort = bunPort;\n\n const handler = (req: Request): Response | Promise<Response> => {\n const host = req.headers.get(\"host\") ?? undefined;\n const origin = req.headers.get(\"origin\") ?? undefined;\n\n // DNS-rebinding gate: reject any request (including WebSocket upgrade\n // attempts) that does not arrive with an allowed Host. When Origin is\n // present, it must also be in the allowed set.\n const hostOk = isAllowedHost(host, allowedHostSet);\n const originOk = isAllowedOrigin(origin, allowedHostSet);\n if (!hostOk || !originOk) {\n const offender = !hostOk\n ? `Host \"${host ?? \"(absent)\"}\"`\n : `Origin \"${origin}\"`;\n return new Response(\n `ui-leaf: refusing request with ${offender} — only the following hostnames are accepted to prevent DNS rebinding: ${allowedHostList}. Open the server at http://localhost:${actualPort}/ or http://127.0.0.1:${actualPort}/, or pass { allowedHosts: [\"my-alias\"] } to mount() to permit a custom alias.\\n`,\n { status: 403, headers: { \"Content-Type\": \"text/plain; charset=utf-8\" } },\n );\n }\n\n const headers: Record<string, string> = {};\n if (cspHeader) {\n headers[\"Content-Security-Policy\"] = cspHeader;\n }\n\n const url = new URL(req.url);\n const path = url.pathname;\n const method = req.method;\n\n if (method === \"GET\" && path === \"/\") {\n return new Response(viewState.html, {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n }\n\n if (method === \"POST\" && path === \"/heartbeat\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n lastHeartbeatAt = Date.now();\n if (connectionState === \"disconnected\") {\n connectionState = \"connected\";\n fireEvent(\"reconnected\");\n } else if (connectionState === \"connecting\") {\n connectionState = \"connected\";\n }\n return new Response(\"\", { status: 204, headers });\n }\n\n if (method === \"POST\" && path === \"/mutate\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return handleMutate(req, mutations, headers);\n }\n\n if (method === \"GET\" && path === \"/api/data\") {\n if (!dataLoader) {\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n return new Response(JSON.stringify(viewState.data !== undefined ? viewState.data : null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (method === \"GET\" && path === \"/events\") {\n if (!checkAuth(req, token)) {\n return new Response(\"\", { status: 401, headers });\n }\n let sseController!: ReadableStreamDefaultController<Uint8Array>;\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n sseController = controller;\n sseClients.add(controller);\n // Enqueue an SSE comment immediately so Bun flushes response headers\n // before any broadcast event arrives (empty streams block header send).\n controller.enqueue(sseEncoder.encode(\": connected\\n\\n\"));\n req.signal?.addEventListener(\"abort\", () => {\n sseClients.delete(sseController);\n try { sseController.close(); } catch { /* already closed */ }\n });\n },\n cancel() {\n sseClients.delete(sseController);\n },\n });\n return new Response(stream, {\n status: 200,\n headers: {\n ...headers,\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n },\n });\n }\n\n return new Response(JSON.stringify({ error: \"not found\" }), {\n status: 404,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n };\n\n let heartbeatWatcher: NodeJS.Timeout | undefined;\n\n // `bunServer` is assigned immediately after this declaration by the IIFE\n // below. The `!` assertion is safe: cleanup is never called during server\n // construction, only after the server is running.\n let bunServer!: ReturnType<typeof Bun.serve>;\n\n const cleanup = async (reason: CloseReason): Promise<void> => {\n if (closeRequested) return;\n closeRequested = true;\n if (heartbeatWatcher) clearInterval(heartbeatWatcher);\n broadcast({ type: \"closing\", reason });\n for (const controller of sseClients) {\n try { controller.close(); } catch { /* already closed */ }\n }\n sseClients.clear();\n // Graceful stop: waits for in-flight writes (including the closing SSE\n // event) to flush before tearing down TCP connections.\n await bunServer.stop();\n if (restoreStdout) restoreStdout();\n resolveClosed(reason);\n };\n\n // Auto-bump: if bunPort is busy, try bunPort+1 … up to MAX_PORT_ATTEMPTS.\n // port: 0 goes straight to Bun (OS assigns a free port; never EADDRINUSE).\n // The Bun error callback fires for socket errors AND for unhandled throws in\n // the fetch handler. Either case routes through cleanup(\"error\") so the mount\n // terminates cleanly rather than hanging. This means a single buggy request\n // handler is fatal — intentional: unhandled errors indicate broken invariants.\n const serverErrorHandler = (_err: Error): Response => {\n void cleanup(\"error\");\n return new Response(JSON.stringify({ error: \"internal server error\" }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n };\n bunServer = (() => {\n if (bunPort === 0) {\n return Bun.serve({ hostname: \"127.0.0.1\", port: 0, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n }\n const MAX_PORT_ATTEMPTS = 10;\n for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {\n try {\n return Bun.serve({ hostname: \"127.0.0.1\", port: bunPort + i, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });\n } catch (err) {\n const isAddrinuse = err instanceof Error && err.message.includes(\"EADDRINUSE\");\n if (!isAddrinuse || i === MAX_PORT_ATTEMPTS - 1) {\n if (isAddrinuse) {\n throw new Error(\n `ui-leaf: ports ${bunPort}–${bunPort + MAX_PORT_ATTEMPTS - 1} are all in use. Pass { port: 0 } to mount() for an OS-assigned port.`,\n );\n }\n throw err;\n }\n }\n }\n throw new Error(\"unreachable\"); // TypeScript: loop always returns or throws\n })();\n actualPort = bunServer.port ?? bunPort;\n const url = `http://127.0.0.1:${actualPort}`;\n const startedAt = Date.now();\n\n heartbeatWatcher = setInterval(() => {\n if (closeRequested) return;\n const now = Date.now();\n if (now - startedAt < startupGraceMs) return;\n if (now - lastHeartbeatAt > heartbeatTimeoutMs) {\n if (connectionState !== \"disconnected\") {\n connectionState = \"disconnected\";\n fireEvent(\"disconnected\");\n }\n }\n }, _heartbeatCheckIntervalMs);\n\n // The URL passed to the browser includes the token as a hash fragment so it\n // is never sent to the server (browsers strip fragments before HTTP requests).\n // The public `url` returned to consumers stays fragment-free.\n const openUrl = `${url}/#token=${token}`;\n\n // Browser-open implementation, or the test-seam override if one was supplied.\n const doOpen: () => Promise<void> = _opener\n ? () => _opener(openUrl)\n : async () => {\n if (shell === \"app\") {\n const launched = await openInAppMode(openUrl);\n if (!launched) {\n process.stderr.write(\n `ui-leaf: shell:\"app\" requested but no Chromium browser found; falling back to default browser tab.\\n`,\n );\n await open(openUrl);\n }\n } else {\n await open(openUrl);\n }\n };\n\n if (openBrowser) {\n await doOpen();\n }\n\n return {\n url,\n port: actualPort,\n closed,\n close: (reason: CloseReason = \"caller\") => cleanup(reason),\n on(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.add(listener);\n },\n off(event: DevServerEvent, listener: DevServerEventListener): void {\n listeners.get(event)?.delete(listener);\n },\n update(newData: unknown): void {\n viewState.data = newData;\n broadcast({ type: \"data-updated\", data: newData });\n fireEvent(\"data-updated\");\n },\n async swapView(source: string): Promise<import(\"./compile.js\").BuildError[]> {\n const r = await compileSource({\n source,\n data: viewState.data,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n viewState.html = r.html;\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"view-swapped\");\n return [];\n },\n async patch(newData: unknown, source: string): Promise<import(\"./compile.js\").BuildError[]> {\n // Compile first with newData so the HTML embeds the incoming data.\n const r = await compileSource({\n source,\n data: newData,\n title,\n csp: cspHeader ?? undefined,\n token,\n });\n if (r.errors.length > 0) return r.errors;\n // Only mutate state on compile success (atomicity guarantee).\n viewState.data = newData;\n viewState.html = r.html;\n broadcast({ type: \"data-updated\", data: newData });\n broadcast({ type: \"view-swapped\" });\n fireEvent(\"data-updated\");\n fireEvent(\"view-swapped\");\n return [];\n },\n async reopen(): Promise<void> {\n await doOpen();\n },\n };\n } catch (err) {\n restoreStdout?.();\n throw err;\n }\n}\n\n// Custom header (not Authorization: Bearer) so any cross-origin fetch triggers\n// a CORS preflight, which browsers block for non-same-origin callers without\n// an explicit CORS allow list. This closes the simple-form-POST / no-preflight\n// attack vector against the localhost dev server.\nfunction checkAuth(req: Request, token: string): boolean {\n const value = req.headers.get(\"x-ui-leaf-token\") ?? \"\";\n if (!value) return false;\n return timingSafeEqual(value, token);\n}\n\nasync function handleMutate(\n req: Request,\n mutations: Record<string, MutationHandler<any, any>>,\n headers: Record<string, string>,\n): Promise<Response> {\n // 1 MiB cap: Content-Length precheck short-circuits chunked / large bodies\n // before req.text() buffers them. req.text() still buffers the whole body\n // if Content-Length is absent or underreported — acceptable for this\n // loopback-only server, where the auth gate already runs first.\n const contentLength = req.headers.get(\"content-length\");\n if (contentLength && Number.parseInt(contentLength, 10) > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n let body: { name?: string; args?: unknown };\n try {\n const text = await req.text();\n if (text.length > 1024 * 1024) {\n return new Response(JSON.stringify({ error: \"request body exceeds 1 MiB limit\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n body = (text ? JSON.parse(text) : undefined) as typeof body;\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : \"bad request\" }),\n { status: 400, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const name = body?.name;\n if (typeof name !== \"string\" || name.length === 0) {\n return new Response(JSON.stringify({ error: \"missing mutation name\" }), {\n status: 400,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n }\n\n if (!Object.hasOwn(mutations, name)) {\n return new Response(\n JSON.stringify({\n error: `ui-leaf: no mutation handler registered for '${name}'. Add it to the mutations: { } map passed to mount().`,\n }),\n { status: 404, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n\n const handler = mutations[name]!;\n try {\n const result = await handler(body.args);\n return new Response(JSON.stringify(result ?? null), {\n status: 200,\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n return new Response(\n JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),\n { status: 500, headers: { ...headers, \"Content-Type\": \"application/json\" } },\n );\n }\n}\n","import { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { join, resolve, sep } from \"node:path\";\nimport { mkdtemp, rm, stat, writeFile } from \"node:fs/promises\";\nimport type { BunPlugin } from \"bun\";\nimport { escapeForScriptTag } from \"./internal/html.js\";\n\n// Resolve React imports at module load — works under bun test / bun run.\n// NOTE: under bun build --compile (binary mode), createRequire() resolves from\n// the binary's embedded virtual filesystem. AGT-131 (cross-compile script)\n// will need a Bun.build plugin or Bun embedded-files to ensure React is\n// reachable inside the compiled binary. Flagging here so AGT-131 is not blindsided.\nconst requireFromHere = createRequire(import.meta.url);\n\n// BunPlugin that rewrites bare react/react-dom imports to absolute paths\n// under ui-leaf's installed node_modules. Ensures the bundled view always\n// finds the same React instance regardless of the consumer's package-manager\n// hoisting, and prevents duplicate React instances across views.\nconst reactAliasPlugin: BunPlugin = {\n name: \"ui-leaf-react-alias\",\n setup(build) {\n // Matches: react, react/jsx-runtime, react/jsx-dev-runtime,\n // react-dom, react-dom/client, react-dom/profiling, etc.\n build.onResolve({ filter: /^react($|\\/|-dom($|\\/))/ }, (args) => {\n try {\n return { path: requireFromHere.resolve(args.path) };\n } catch {\n return {\n path: args.path,\n errors: [{ text: `ui-leaf: failed to resolve ${args.path}` }],\n };\n }\n });\n },\n};\n\nexport interface BuildError {\n file: string;\n line: number;\n column: number;\n message: string;\n}\n\nexport interface CompileOptions {\n /** View name or path relative to viewsRoot (e.g. \"dashboard\" or \"dashboard.tsx\"). */\n entry: string;\n /** Root directory holding .tsx view files. */\n viewsRoot: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. Ignored when dataLoader is true. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /**\n * Raw CSP string to emit as a <meta http-equiv=\"Content-Security-Policy\"> tag.\n * Undefined / absent means no CSP meta tag is emitted.\n */\n csp?: string;\n /**\n * Extra allowed hostnames (beyond loopback defaults). Accepted in the\n * option bag for API symmetry with DevServerOptions; has no compile-time\n * effect — the runtime DNS-rebinding gate lives in the server.\n */\n allowedHosts?: string[];\n /**\n * Per-launch auth token. Accepted for API symmetry with DevServerOptions;\n * the token is no longer embedded in HTML — it is delivered via the URL\n * fragment and read by the inline bootstrap script.\n * @deprecated No-op since v1.0.0 — token delivery is handled by startDevServer.\n */\n token?: string;\n /**\n * When true, generate an entry that fetches data from GET /api/data at\n * render time rather than reading it from window.__UI_LEAF__.data. The\n * compiled HTML bootstrap omits the data field (only token is included).\n * Use when data is sensitive and must not be written to the HTML file.\n */\n dataLoader?: boolean;\n}\n\n/**\n * Options for compiling an inline TSX source string.\n *\n * v1.0.0 constraint: `source` is treated as a self-contained TSX string.\n * Relative imports are not supported — the string has no filesystem context\n * to resolve them against. Bare-package imports (react, react-dom) work via\n * the react-alias plugin. This is the intended contract for IPC-driven\n * view hot-swaps (AI-generated self-contained components).\n */\nexport interface CompileSourceOptions {\n /** Raw TSX source string to compile. Must be a self-contained component. */\n source: string;\n /** JSON-serializable data injected as window.__UI_LEAF__.data. */\n data?: unknown;\n /** Browser tab title. Defaults to \"ui-leaf\". */\n title?: string;\n /** Raw CSP string. Undefined / absent means no CSP meta tag. */\n csp?: string;\n /**\n * Per-launch auth token. Accepted for API symmetry; not embedded in HTML —\n * see CompileOptions.token.\n * @deprecated No-op since v1.0.0.\n */\n token?: string;\n}\n\nexport interface CompileResult {\n html: string;\n errors: BuildError[];\n}\n\n// Friendly message rendered when the page is reloaded without the token fragment.\nconst SESSION_ENDED_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>Session ended — re-launch the CLI to continue.</p></div>';\n\n// Overlay rendered when the mount terminates. v1.x extension point: replaceable\n// via a consumer-supplied template slot (deferred per plan-approval decision).\nconst CLOSED_OVERLAY_HTML =\n '<div style=\"font-family:sans-serif;padding:2em;color:#555\"><p>This view has closed.</p></div>';\n\n// Shared bridge injected into every compiled entry: mutation + heartbeat.\nconst SHARED_BRIDGE = `\nasync function mutate(name: string, args?: unknown): Promise<unknown> {\n const res = await fetch(\"/mutate\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token ? { \"X-UI-Leaf-Token\": token } : {}),\n },\n body: JSON.stringify({ name, args }),\n });\n const text = await res.text().catch(() => \"\");\n if (!res.ok) {\n let detail = text;\n try {\n const parsed: unknown = text ? JSON.parse(text) : null;\n if (parsed !== null && typeof parsed === \"object\" && \"error\" in parsed && typeof (parsed as { error: unknown }).error === \"string\") {\n detail = (parsed as { error: string }).error;\n }\n } catch { /* keep raw text */ }\n throw new Error(\"ui-leaf: mutation '\" + name + \"' failed (\" + res.status + \"): \" + detail);\n }\n return text ? JSON.parse(text) : undefined;\n}\n\nasync function heartbeat(): Promise<void> {\n try {\n await fetch(\"/heartbeat\", {\n method: \"POST\",\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n } catch { /* server may have shut down; ignore */ }\n}\nsetInterval(heartbeat, 5000);\nheartbeat();\n\nfunction subscribeEvents(onEvent: (ev: { type: string; [k: string]: unknown }) => void): void {\n let delay = 250;\n const budget = 30_000;\n const started = Date.now();\n let done = false;\n\n async function connect(): Promise<void> {\n try {\n const res = await fetch(\"/events\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok || !res.body) throw new Error(\"bad status \" + res.status);\n delay = 250;\n const reader = res.body.getReader();\n const dec = new TextDecoder(\"utf-8\");\n let buf = \"\";\n while (true) {\n const { done: streamDone, value } = await reader.read();\n if (streamDone) break;\n buf += dec.decode(value, { stream: true });\n let idx: number;\n while ((idx = buf.indexOf(\"\\\\n\\\\n\")) !== -1) {\n const chunk = buf.slice(0, idx);\n buf = buf.slice(idx + 2);\n for (const line of chunk.split(\"\\\\n\")) {\n if (line.startsWith(\"data:\")) {\n try {\n const ev = JSON.parse(line.slice(5).trimStart()) as { type: string; [k: string]: unknown };\n if (ev.type === \"closing\") done = true;\n onEvent(ev);\n } catch { /* skip malformed event */ }\n }\n }\n }\n if (done) return;\n }\n } catch {\n if (done) return;\n }\n if (done) return;\n if (Date.now() - started > budget) {\n onEvent({ type: \"closing\", reason: \"error\" });\n return;\n }\n await new Promise<void>((r) => setTimeout(r, delay));\n delay = Math.min(delay * 2, 5_000);\n void connect();\n }\n\n void connect();\n}`;\n\n/** Run Bun.build on `entryPath` and return the raw JS output or errors. */\nasync function runBunBuild(entryPath: string): Promise<{ js: string } | { errors: BuildError[] }> {\n let buildOutput: Awaited<ReturnType<typeof Bun.build>>;\n try {\n buildOutput = await Bun.build({\n entrypoints: [entryPath],\n target: \"browser\",\n format: \"esm\",\n minify: false,\n sourcemap: \"none\",\n plugins: [reactAliasPlugin],\n });\n } catch (err) {\n if (err instanceof AggregateError) {\n type BunBuildMsg = { message: string; position?: { file?: string; line?: number; column?: number } | null };\n const errors: BuildError[] = (err.errors as BunBuildMsg[]).map((e) => ({\n file: e.position?.file ?? \"<unknown>\",\n line: e.position?.line ?? 0,\n column: e.position?.column ?? 0,\n message: e.message,\n }));\n return { errors };\n }\n throw err;\n }\n const output = buildOutput.outputs[0];\n if (!output) {\n return {\n errors: [{ file: \"<unknown>\", line: 0, column: 0, message: \"ui-leaf: Bun.build produced no output\" }],\n };\n }\n return { js: await output.text() };\n}\n\n/** Assemble the final HTML page from compiled JS and options. */\nfunction assembleHtml(opts: {\n js: string;\n title: string;\n csp: string | undefined;\n data: unknown;\n dataLoader: boolean;\n}): string {\n const { js, title, csp, data, dataLoader } = opts;\n // Escape </script> sequences to prevent script-tag break-out.\n const safeJs = js.replace(/<\\/script>/gi, \"<\\\\/script>\");\n\n const titleEscaped = title\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n\n const cspMeta = csp\n ? ` <meta http-equiv=\"Content-Security-Policy\" content=\"${csp.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\")}\" />\\n`\n : \"\";\n\n // Double-stringify data: outer JSON.stringify produces a JSON string, then\n // escapeForScriptTag ensures </script> and U+2028/U+2029 can't break out.\n const dataInit = dataLoader\n ? \"window.__UI_LEAF__ = {};\"\n : `window.__UI_LEAF__ = { data: JSON.parse(${escapeForScriptTag(JSON.stringify(JSON.stringify(data ?? null)))}) };`;\n\n // Bootstrap: reads token from URL fragment, stashes it on __UI_LEAF__.token,\n // then immediately clears the fragment from the URL bar so the token is\n // never visible in history. On reload (fragment gone), sets sessionEnded so\n // the bundled module can render a friendly recovery message instead of\n // attempting unauthenticated fetches.\n // decodeURIComponent is wrapped in try/catch: a malformed %-sequence would\n // otherwise throw and kill the bootstrap silently; the catch falls through\n // to sessionEnded so the user gets the recovery screen instead of a blank page.\n const bootstrapScript = `${dataInit}\n(function(){var m=/[#&]token=([^&#]*)/.exec(window.location.hash);if(m){try{window.__UI_LEAF__.token=decodeURIComponent(m[1]);history.replaceState(null,\"\",window.location.pathname+window.location.search);}catch(e){window.__UI_LEAF__.sessionEnded=true;}}else{window.__UI_LEAF__.sessionEnded=true;}})();`;\n\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>${titleEscaped}</title>\n${cspMeta} <!-- ui-leaf bootstrap -->\n <script>${bootstrapScript}</script>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\">${safeJs}</script>\n </body>\n</html>`;\n}\n\nexport async function compileView(opts: CompileOptions): Promise<CompileResult> {\n const {\n entry,\n viewsRoot,\n data,\n title = \"ui-leaf\",\n csp,\n // allowedHosts and token have no compile-time effect; accepted for API symmetry.\n allowedHosts: _allowedHosts,\n token: _token,\n dataLoader = false,\n } = opts;\n\n const viewsRootAbs = resolve(viewsRoot);\n const hasExt = /\\.[a-z]+$/i.test(entry);\n const viewAbs = resolve(viewsRootAbs, hasExt ? entry : `${entry}.tsx`);\n if (!viewAbs.startsWith(viewsRootAbs + sep)) {\n return {\n html: \"\",\n errors: [\n {\n file: \"<unknown>\",\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' resolves outside viewsRoot`,\n },\n ],\n };\n }\n try {\n await stat(viewAbs);\n } catch {\n return {\n html: \"\",\n errors: [\n {\n file: viewAbs,\n line: 0,\n column: 0,\n message: `ui-leaf: view '${entry}' not found at ${viewAbs}`,\n },\n ],\n };\n }\n\n // Generate a temp entry that imports the resolved view, mounts React via\n // createRoot, and wires the mutation/heartbeat bridge.\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-compile-\"));\n try {\n const entryPath = join(tempDir, \"entry.tsx\");\n\n const entryContent = dataLoader\n ? `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n${SHARED_BRIDGE}\n\n async function bootstrap(): Promise<void> {\n const res = await fetch(\"/api/data\", {\n headers: token ? { \"X-UI-Leaf-Token\": token } : {},\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\"ui-leaf: /api/data fetch failed (\" + res.status + \"): \" + text);\n }\n let currentData: unknown = await res.json();\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n }\n bootstrap();\n}\n`\n : `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Compile an inline TSX source string into a full HTML page.\n *\n * The source is treated as a self-contained component; relative imports are\n * not supported (v1.0.0 constraint — the string has no filesystem context).\n * Bare-package imports (react, react-dom) work via the react-alias plugin.\n */\nexport async function compileSource(opts: CompileSourceOptions): Promise<CompileResult> {\n const { source, data, title = \"ui-leaf\", csp, token: _token } = opts;\n\n const tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-src-\"));\n try {\n // Write the caller's tsx as the view file, then write a thin entry wrapper.\n const viewPath = join(tempDir, \"view.tsx\");\n const entryPath = join(tempDir, \"entry.tsx\");\n\n await writeFile(viewPath, source);\n\n const entryContent = `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewPath)};\n\nconst ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};\nconst token = ctx.token;\n\nif (ctx.sessionEnded) {\n const root = document.getElementById(\"root\");\n if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};\n} else {\n let currentData: unknown = ctx.data;\n${SHARED_BRIDGE}\n\n const el = document.getElementById(\"root\");\n if (!el) throw new Error(\"ui-leaf: #root element missing\");\n const root = createRoot(el);\n root.render(<View data={currentData} mutate={mutate} />);\n subscribeEvents((ev) => {\n if (ev.type === \"data-updated\") {\n currentData = ev.data;\n root.render(<View data={currentData} mutate={mutate} />);\n } else if (ev.type === \"view-swapped\") {\n window.location.reload();\n } else if (ev.type === \"closing\") {\n el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};\n }\n });\n}\n`;\n\n await writeFile(entryPath, entryContent);\n\n const buildResult = await runBunBuild(entryPath);\n if (\"errors\" in buildResult) return { html: \"\", errors: buildResult.errors };\n\n return {\n html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader: false }),\n errors: [],\n };\n } finally {\n await rm(tempDir, { recursive: true, force: true });\n }\n}\n","export function escapeForScriptTag(json: string): string {\n // Defend against </script> break-out and U+2028/U+2029 line terminators\n // that JSON.stringify emits raw but JS string literals don't accept.\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n"],"mappings":"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","names":["resolve","fireEvent","broadcast","url","resolve"]}