@openthink/ui-leaf 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +672 -0
- package/dist/cli.js.map +1 -0
- package/dist/dev-server-DapOoULX.d.ts +5 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +503 -0
- package/dist/index.js.map +1 -0
- package/dist/view.d.ts +16 -0
- package/dist/view.js +1 -0
- package/dist/view.js.map +1 -0
- package/package.json +66 -0
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/index.ts","../src/dev-server.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):\n//\n// STDIN\n// Line 1: config object (view, viewsRoot, data, mutations: [names], …)\n// Line 2+: mutation responses, one per line:\n// {\"type\":\"result\",\"id\":<n>,\"value\":<any>}\n// {\"type\":\"error\",\"id\":<n>,\"message\":\"<text>\"}\n//\n// STDOUT\n// {\"type\":\"ready\",\"url\":\"<url>\",\"port\":<n>} emitted once when the dev server is up\n// {\"type\":\"mutate\",\"id\":<n>,\"name\":\"<s>\",\"args\":<any>} emitted when a view triggers a mutation\n// {\"type\":\"closed\"} emitted on natural close\n// {\"type\":\"error\",\"message\":\"<text>\"} emitted on internal error\n//\n// Lifecycle\n// Exits 0 on natural close (view closed).\n// Exits 1 on internal error.\n// Closing stdin from the parent triggers shutdown (any pending\n// mutations are rejected).\n\nimport { createInterface } from \"node:readline\";\nimport { mount, type MountOptions } from \"./index.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; rsbuild's noise (which goes\n// through process.stdout.write) gets redirected to stderr by silent mode\n// 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\ninterface ConfigRequest {\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\ninterface MutateResult {\n type: \"result\";\n id: number;\n value?: unknown;\n}\n\ninterface MutateError {\n type: \"error\";\n id: number;\n message: string;\n}\n\ntype MutateResponse = MutateResult | MutateError;\n\nfunction emit(event: unknown): void {\n realStdoutWrite(`${JSON.stringify(event)}\\n`);\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 try {\n const config = JSON.parse(trimmed) as ConfigRequest;\n configResolve(config);\n } catch (err) {\n emit({\n type: \"error\",\n message: `failed to parse config JSON: ${err instanceof Error ? err.message : String(err)}`,\n });\n process.exit(1);\n }\n return;\n }\n\n // Mutation response.\n let msg: MutateResponse;\n try {\n msg = JSON.parse(trimmed) as MutateResponse;\n } catch {\n // Malformed line; ignore (consumer should be using the protocol).\n return;\n }\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 });\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; rsbuild 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 emit({ type: \"ready\", url: view.url, port: view.port });\n await view.closed;\n emit({ type: \"closed\" });\n process.exit(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 CspOption,\n type MutationHandler,\n type Shell,\n} from \"./dev-server.js\";\n\nexport type { CspOption, MutationHandler, Shell };\n\nexport interface MountOptions {\n /** View name. Resolves to <viewsRoot>/<view>.tsx. */\n view: string;\n /** JSON-serializable data passed to the view as a prop. */\n data: 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 * 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) that triggers shutdown after the startup grace\n * window. Defaults to 75000 — chosen to survive a single browser\n * background-tab throttle (browsers clamp setInterval in hidden tabs to\n * roughly once per minute). Lower it if you want faster shutdown on tab\n * close; raise it if your debugger pauses the page or your machine\n * sleeps mid-session.\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, eval for rsbuild HMR). View files can only\n * *add* further restrictions 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 / rsbuild 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.). Without this option, rsbuild's banner and build messages\n * collide with that protocol and silently corrupt it on the consumer\n * side.\n *\n * Setting `silent: true`:\n * - Sets rsbuild's logLevel to 'silent' (no banner, build, or\n * deprecation messages emitted by rsbuild)\n * - Redirects `process.stdout.write` to `process.stderr` for the\n * lifetime of the dev server, restored on close\n *\n * Tradeoff: any other code in the same process that writes to stdout\n * during the dev server's lifetime is also redirected. Hold the\n * captured `process.stdout.write` reference yourself if you need to\n * write to the *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 * If no client connects within (startupGraceMs + heartbeatTimeoutMs),\n * the server shuts down on its own.\n */\n startupGraceMs?: 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 when the view closes (heartbeat timeout) or close() is called. */\n closed: Promise<void>;\n /** Force-close the dev server early. */\n close: () => Promise<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 user closes the\n * browser tab.\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 * Multi-tab note: if the user opens the served URL in additional tabs (or\n * duplicates the tab), each tab heartbeats independently and the server\n * stays alive while *any* tab is open. Closing the original tab does not\n * shut down the CLI if a duplicate is still loaded.\n *\n * Ctrl+C: this function installs SIGINT and SIGTERM handlers that close\n * the dev server (and clean up its temp directory) 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 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 });\n\n const onSignal = (signal: NodeJS.Signals): void => {\n void (async () => {\n await server.close();\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(),\n close: server.close,\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 };\n}\n","// Spike: dev server with mutation bridge + heartbeat-based shutdown.\n// Builds on Spike #1; adds the bridge that makes ui-leaf actually useful.\n\nimport { randomBytes, timingSafeEqual as nodeTimingSafeEqual } from \"node:crypto\";\nimport { rmSync } from \"node:fs\";\nimport { mkdtemp, readdir, rm, stat, writeFile } from \"node:fs/promises\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { createRequire } from \"node:module\";\nimport { createServer as createTcpServer } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, resolve, sep } from \"node:path\";\nimport { createRsbuild } from \"@rsbuild/core\";\nimport { pluginReact } from \"@rsbuild/plugin-react\";\nimport open, { apps } from \"open\";\n\n// Resolve react / react-dom from ui-leaf's installed location using\n// Node's actual resolver. With hoisting (npm/pnpm/bun), these end up in\n// the consumer's top-level node_modules, NOT under ui-leaf/node_modules.\n// Aliasing the resolved directory paths in rspack lets the bundled view\n// always find react no matter where the package manager put it.\nconst uiLeafRequire = createRequire(import.meta.url);\nconst reactPath = dirname(uiLeafRequire.resolve(\"react/package.json\"));\nconst reactDomPath = dirname(uiLeafRequire.resolve(\"react-dom/package.json\"));\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 * Ask the OS for a free port and return it. Used when the consumer\n * requests `port: 0`. There's a small race window between close() and\n * rsbuild's bind, but in practice it's never been an issue for dev\n * tooling.\n */\nasync function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = createTcpServer();\n server.unref();\n server.on(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const addr = server.address();\n if (!addr || typeof addr !== \"object\") {\n server.close();\n reject(new Error(\"ui-leaf: failed to obtain a free port from the OS\"));\n return;\n }\n const port = addr.port;\n server.close(() => resolve(port));\n });\n });\n}\n\n// Stale-tempdir sweep threshold. Each mount writes index.html with the\n// consumer's data inlined; a crashed run leaves the dir behind until OS\n// rotation. 24h is well past any realistic single-session lifetime and\n// well short of the macOS reboot-only rotation cadence.\nconst STALE_TEMPDIR_AGE_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Best-effort removal of `ui-leaf-*` siblings in `tmpdir()` whose mtime\n * is older than the stale threshold. Fire-and-forget; never throws.\n * Filters by mtime so concurrent ui-leaf processes' active dirs are left\n * alone (they're created fresh per mount).\n */\nasync function sweepStaleTempDirs(): Promise<void> {\n try {\n const root = tmpdir();\n const entries = await readdir(root, { withFileTypes: true });\n const cutoff = Date.now() - STALE_TEMPDIR_AGE_MS;\n await Promise.all(\n entries\n .filter((e) => e.isDirectory() && e.name.startsWith(\"ui-leaf-\"))\n .map(async (e) => {\n const path = join(root, e.name);\n try {\n const info = await stat(path);\n if (info.mtimeMs < cutoff) {\n await rm(path, { recursive: true, force: true });\n }\n } catch {\n // Another process may be mid-cleanup, or the dir may belong\n // to a different UID; either way, skip silently.\n }\n }),\n );\n } catch {\n // tmpdir() unreadable is rare and not actionable here.\n }\n}\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 * eval/inline-scripts for rsbuild's HMR client). A future \"production\"\n * mode could ship a tighter preset once HMR isn't in the picture.\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-eval' '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\nexport interface DevServerOptions {\n view: string;\n data: 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 /** Heartbeat-stop window in ms. Browser silence longer than this triggers shutdown. */\n heartbeatTimeoutMs?: number;\n /** Grace period after server start before the heartbeat watcher is armed. */\n startupGraceMs?: 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 / rsbuild output to stdout. When true:\n * - rsbuildConfig.logLevel is set to 'silent' (no banner, build, or\n * deprecation messages)\n * - process.stdout.write is redirected to process.stderr for the\n * lifetime of the dev server, restored on close()\n *\n * Use when driving mount() programmatically and stdout is reserved for\n * a structured protocol (e.g. line-delimited JSON to a parent process).\n * Default: false.\n */\n silent?: boolean;\n}\n\nexport interface DevServer {\n url: string;\n port: number;\n /** Resolves when the view is closed (heartbeat timeout) or close() is called. */\n closed: Promise<void>;\n close: () => Promise<void>;\n}\n\nfunction 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\nasync function readJsonBody(req: IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = [];\n let total = 0;\n for await (const chunk of req) {\n const buf = chunk as Buffer;\n total += buf.length;\n // 1 MiB cap per request — protects against accidental huge payloads.\n if (total > 1024 * 1024) {\n throw new Error(\"request body exceeds 1 MiB limit\");\n }\n chunks.push(buf);\n }\n const text = Buffer.concat(chunks).toString(\"utf8\");\n return text ? JSON.parse(text) : undefined;\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 async function startDevServer(opts: DevServerOptions): Promise<DevServer> {\n const {\n view,\n data,\n viewsRoot,\n mutations = {},\n title = \"ui-leaf\",\n port,\n openBrowser = true,\n shell = \"tab\",\n heartbeatTimeoutMs = 75_000,\n startupGraceMs = 30_000,\n csp,\n allowedHosts,\n silent = false,\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 // Resolve port: 0 means \"let the OS pick\" — but rsbuild doesn't honor\n // port: 0 (it literally binds to 0 and reports back 0). Pre-allocate a\n // free port via Node's net layer and pass that explicit port to rsbuild\n // instead. Default 5810 if no port specified.\n const resolvedPort = port === 0 ? await findFreePort() : (port ?? 5810);\n\n // Programmatic consumers (esp. non-Node CLIs spawning ui-leaf as a\n // subprocess) often reserve stdout for a structured protocol. Belt-and-\n // -suspenders: rsbuild's logLevel:'silent' catches its own logger output,\n // and process.stdout.write is redirected to stderr to catch anything\n // that bypasses rsbuild's logger (banner before logger init, third-party\n // module writes, etc.).\n const restoreStdout: (() => void) | null = silent ? redirectStdoutToStderr() : null;\n\n // Hoisted so the outer catch can sweep them on a setup-time throw.\n let tempDir: string | null = null;\n let cleanupOnExit: (() => void) | null = 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 const viewsRootAbs = resolve(viewsRoot);\n const viewAbs = resolve(viewsRootAbs, `${view}.tsx`);\n if (!viewAbs.startsWith(viewsRootAbs + sep)) {\n throw new Error(\n `ui-leaf: view '${view}' resolves outside viewsRoot`,\n );\n }\n try {\n await stat(viewAbs);\n } catch {\n throw new Error(\n `ui-leaf: view '${view}' not found at ${viewAbs} (looked for .tsx; viewsRoot=${viewsRoot})`,\n );\n }\n\n const token = randomBytes(32).toString(\"hex\");\n tempDir = await mkdtemp(join(tmpdir(), \"ui-leaf-\"));\n\n // Synchronous fallback for any exit path that bypasses cleanup() —\n // uncaught throws and programmatic process.exit. SIGKILL, OOM-kill,\n // and power loss skip every Node hook, so the next startup's sweep is\n // the backstop for those. rmSync with force is idempotent when the\n // dir is already gone, so this no-ops safely if cleanup() ran first.\n const dirToSweep = tempDir;\n cleanupOnExit = (): void => {\n try {\n rmSync(dirToSweep, { recursive: true, force: true });\n } catch {\n // Exit handlers must not throw.\n }\n };\n process.on(\"exit\", cleanupOnExit);\n\n // Mop up any abandoned ui-leaf-* dirs from prior crashed runs that\n // SIGKILL'd or otherwise skipped both cleanup() and the exit handler.\n // Fire-and-forget so the sweep never blocks startup.\n void sweepStaleTempDirs();\n\n const entryPath = join(dirToSweep, \"entry.tsx\");\n await writeFile(\n entryPath,\n `import { createRoot } from \"react-dom/client\";\nimport View from ${JSON.stringify(viewAbs)};\n\nconst ctx = (globalThis).__UI_LEAF__ || {};\nconst data = ctx.data;\nconst token = ctx.token;\n\nasync function mutate(name, args) {\n const res = await fetch(\"/mutate\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token ? { Authorization: \"Bearer \" + token } : {}),\n },\n body: JSON.stringify({ name, args }),\n });\n const text = await res.text().catch(function () { return \"\"; });\n if (!res.ok) {\n var detail = text;\n try {\n var parsed = text ? JSON.parse(text) : null;\n if (parsed && typeof parsed === \"object\" && typeof parsed.error === \"string\") {\n detail = parsed.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() {\n try {\n await fetch(\"/heartbeat\", {\n method: \"POST\",\n headers: token ? { Authorization: \"Bearer \" + token } : {},\n });\n } catch {\n /* server may have shut down; ignore */\n }\n}\nsetInterval(heartbeat, 5000);\nheartbeat();\n\nconst el = document.getElementById(\"root\");\nif (!el) throw new Error(\"ui-leaf: #root element missing\");\ncreateRoot(el).render(<View data={data} mutate={mutate} />);\n`,\n );\n\n const dataInline = escapeForScriptTag(JSON.stringify(data));\n const tokenInline = JSON.stringify(token);\n const titleEscaped = title\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n const htmlPath = join(dirToSweep, \"index.html\");\n await writeFile(\n htmlPath,\n `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>${titleEscaped}</title>\n <script>window.__UI_LEAF__ = { data: ${dataInline}, token: ${tokenInline} };</script>\n </head>\n <body>\n <div id=\"root\"></div>\n </body>\n</html>\n`,\n );\n\n let lastHeartbeatAt = Date.now();\n let closeRequested = false;\n let resolveClosed: () => void = () => {};\n const closed = new Promise<void>((r) => {\n resolveClosed = r;\n });\n\n function checkAuth(req: IncomingMessage): boolean {\n const header = req.headers.authorization ?? \"\";\n const match = /^Bearer (.+)$/.exec(header);\n if (!match) return false;\n return timingSafeEqual(match[1]!, token);\n }\n\n function sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(body === undefined ? \"\" : JSON.stringify(body));\n }\n\n const rsbuild = await createRsbuild({\n cwd: dirToSweep,\n rsbuildConfig: {\n plugins: [pluginReact()],\n ...(silent ? { logLevel: \"silent\" as const } : {}),\n source: { entry: { index: entryPath } },\n // 5810 is unused by the major Node dev tools (vite=5173, parcel=1234,\n // webpack=8080, next/CRA=3000). rsbuild auto-bumps to the next free\n // port if 5810 is busy, so collisions are graceful.\n server: { port: resolvedPort, host: \"127.0.0.1\" },\n // Note: `dev.setupMiddlewares` is deprecated as of rsbuild 2.x in\n // favor of `server.setup`, but the new API has a different signature\n // and bypasses the rsbuild CSRF middleware in ways that break our\n // POST endpoints. Sticking with the deprecated path for v1.\n dev: {\n setupMiddlewares: [\n (middlewares) => {\n middlewares.unshift(async (req, res, next) => {\n // DNS-rebinding gate: reject anything not arriving with an\n // allowed Host (and Origin, when present) before any auth\n // or routing runs. Done in ui-leaf's own middleware rather\n // than relying on rsbuild so the property is explicit and\n // survives upstream API churn.\n const hostOk = isAllowedHost(req.headers.host, allowedHostSet);\n const originOk = isAllowedOrigin(req.headers.origin, allowedHostSet);\n if (!hostOk || !originOk) {\n const offender = !hostOk\n ? `Host \"${req.headers.host ?? \"(absent)\"}\"`\n : `Origin \"${req.headers.origin}\"`;\n res.statusCode = 403;\n res.setHeader(\"Content-Type\", \"text/plain; charset=utf-8\");\n res.end(\n `ui-leaf: refusing request with ${offender} — only the following hostnames are accepted to prevent DNS rebinding: ${allowedHostList}. Open the dev server at http://localhost:${resolvedPort}/ or http://127.0.0.1:${resolvedPort}/, or pass { allowedHosts: [\"my-alias\"] } to mount() to permit a custom alias.\\n`,\n );\n return;\n }\n const url = req.url ?? \"\";\n if (req.method === \"POST\" && url === \"/heartbeat\") {\n if (!checkAuth(req)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n return;\n }\n lastHeartbeatAt = Date.now();\n sendJson(res, 204, undefined);\n return;\n }\n if (req.method === \"POST\" && url === \"/mutate\") {\n if (!checkAuth(req)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n return;\n }\n let body: { name?: string; args?: unknown };\n try {\n body = (await readJsonBody(req)) as typeof body;\n } catch (err) {\n sendJson(res, 400, {\n error: err instanceof Error ? err.message : \"bad request\",\n });\n return;\n }\n const name = body?.name;\n if (typeof name !== \"string\" || name.length === 0) {\n sendJson(res, 400, { error: \"missing mutation name\" });\n return;\n }\n if (!Object.hasOwn(mutations, name)) {\n sendJson(res, 404, {\n error: `ui-leaf: no mutation handler registered for '${name}'. Add it to the mutations: { } map passed to mount().`,\n });\n return;\n }\n const handler = mutations[name]!;\n try {\n const result = await handler(body.args);\n sendJson(res, 200, result ?? null);\n } catch (err) {\n sendJson(res, 500, {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n next();\n });\n // CSP middleware unshifts AFTER the route handler so it ends up\n // at index 0 — runs first on every request, sets the header,\n // then yields to the route or rsbuild static handlers. No-op\n // when csp resolves to null (the \"off\" default).\n if (cspHeader) {\n middlewares.unshift((_req, res, next) => {\n res.setHeader(\"Content-Security-Policy\", cspHeader);\n next();\n });\n }\n },\n ],\n },\n html: { template: htmlPath },\n tools: {\n rspack: {\n resolve: {\n alias: {\n react: reactPath,\n \"react-dom\": reactDomPath,\n },\n },\n },\n },\n },\n });\n\n const devServer = await rsbuild.startDevServer();\n const actualPort = devServer.port;\n const url = `http://127.0.0.1:${actualPort}`;\n const startedAt = Date.now();\n\n let heartbeatWatcher: NodeJS.Timeout | undefined;\n\n const cleanup = async (): Promise<void> => {\n if (closeRequested) return;\n closeRequested = true;\n if (heartbeatWatcher) clearInterval(heartbeatWatcher);\n await devServer.server.close();\n await rm(dirToSweep, { recursive: true, force: true });\n // Listener is per-mount — unhook so long-lived hosts that mount many\n // views in sequence don't pile up exit handlers.\n if (cleanupOnExit) process.off(\"exit\", cleanupOnExit);\n if (restoreStdout) restoreStdout();\n resolveClosed();\n };\n\n heartbeatWatcher = setInterval(() => {\n const now = Date.now();\n if (now - startedAt < startupGraceMs) return;\n if (now - lastHeartbeatAt > heartbeatTimeoutMs) {\n void cleanup();\n }\n }, 1000);\n\n if (openBrowser) {\n if (shell === \"app\") {\n const launched = await openInAppMode(url);\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(url);\n }\n } else {\n await open(url);\n }\n }\n\n return {\n url,\n port: actualPort,\n closed,\n close: cleanup,\n };\n } catch (err) {\n // Setup failed before close() got wired up, so the caller has no\n // close to call. The user's catch may keep the process alive doing\n // other work, so sweep the (possibly populated) tempDir now and\n // unhook the exit fallback rather than waiting on process exit.\n if (tempDir) {\n try {\n rmSync(tempDir, { recursive: true, force: true });\n } catch {\n // Best-effort; the next startup's sweep will catch it.\n }\n }\n if (cleanupOnExit) process.off(\"exit\", cleanupOnExit);\n restoreStdout?.();\n throw err;\n }\n}\n"],"mappings":"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","names":["resolve","resolve","checkAuth","sendJson","url","resolve","createRequire","require","config","resolve"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { M as MutationHandler, S as Shell, C as CspOption } from './dev-server-DapOoULX.js';
|
|
2
|
+
|
|
3
|
+
interface MountOptions {
|
|
4
|
+
/** View name. Resolves to <viewsRoot>/<view>.tsx. */
|
|
5
|
+
view: string;
|
|
6
|
+
/** JSON-serializable data passed to the view as a prop. */
|
|
7
|
+
data: unknown;
|
|
8
|
+
/**
|
|
9
|
+
* Mutation handlers the view can call via mutate(name, args).
|
|
10
|
+
* Each handler can self-type its args and return:
|
|
11
|
+
*
|
|
12
|
+
* mutations: {
|
|
13
|
+
* recategorize: async (args: { id: string; category: string }) => {
|
|
14
|
+
* await db.recategorize(args.id, args.category);
|
|
15
|
+
* return { ok: true };
|
|
16
|
+
* },
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Each request body is capped at 1 MiB; oversized POSTs are rejected
|
|
20
|
+
* with a 400 and the view's mutate() promise rejects with a clear error.
|
|
21
|
+
*/
|
|
22
|
+
mutations?: Record<string, MutationHandler<any, any>>;
|
|
23
|
+
/** Root directory holding view .tsx files. Defaults to <cwd>/views. */
|
|
24
|
+
viewsRoot?: string;
|
|
25
|
+
/** Browser tab title. Defaults to "ui-leaf". */
|
|
26
|
+
title?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Port to bind. Defaults to 5810 — unused by the major Node dev tools.
|
|
29
|
+
* If the port is unavailable, ui-leaf bumps to the next free port and
|
|
30
|
+
* the actual bound port is reflected on the returned `url` and `port`.
|
|
31
|
+
* Override only if you need a stable URL (e.g. an external bookmark).
|
|
32
|
+
*/
|
|
33
|
+
port?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Open the browser when ready. Defaults to true. When false, mount()
|
|
36
|
+
* returns the URL on its resolved value so the caller can drive a
|
|
37
|
+
* headless browser, log the address, etc.
|
|
38
|
+
*/
|
|
39
|
+
openBrowser?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Browser shell. Defaults to "tab".
|
|
42
|
+
*
|
|
43
|
+
* - `"tab"` — open in the user's default browser as a regular tab.
|
|
44
|
+
* Works everywhere; URL bar is visible.
|
|
45
|
+
*
|
|
46
|
+
* - `"app"` — try Chromium's `--app` mode for a chromeless window
|
|
47
|
+
* (no URL bar, no tabs, looks like a desktop app). Available on
|
|
48
|
+
* Chrome, Edge, and Brave. If no Chromium browser is installed,
|
|
49
|
+
* ui-leaf falls back to "tab" with a stderr note. Safari and
|
|
50
|
+
* Firefox always fall back.
|
|
51
|
+
*
|
|
52
|
+
* Pair with the share-link pattern (see "Sharing views across users"
|
|
53
|
+
* in the README) when you want users to never see a localhost URL.
|
|
54
|
+
*/
|
|
55
|
+
shell?: Shell;
|
|
56
|
+
/**
|
|
57
|
+
* Abort to close the dev server early. The returned `closed` promise
|
|
58
|
+
* resolves either way; if you need to distinguish a signal-driven close
|
|
59
|
+
* from a natural tab-close, check `signal.aborted` after the await.
|
|
60
|
+
*/
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
/**
|
|
63
|
+
* Browser silence (ms) that triggers shutdown after the startup grace
|
|
64
|
+
* window. Defaults to 75000 — chosen to survive a single browser
|
|
65
|
+
* background-tab throttle (browsers clamp setInterval in hidden tabs to
|
|
66
|
+
* roughly once per minute). Lower it if you want faster shutdown on tab
|
|
67
|
+
* close; raise it if your debugger pauses the page or your machine
|
|
68
|
+
* sleeps mid-session.
|
|
69
|
+
*/
|
|
70
|
+
heartbeatTimeoutMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Content-Security-Policy enforcement. Defaults to "off".
|
|
73
|
+
*
|
|
74
|
+
* - `"off"` — no CSP header sent. Views can fetch arbitrary URLs and
|
|
75
|
+
* embed external resources freely. The data/mutations convention is
|
|
76
|
+
* honor-system.
|
|
77
|
+
*
|
|
78
|
+
* - `"strict"` — ui-leaf sends a balanced preset: locks `connect-src`
|
|
79
|
+
* to same-origin (the architectural lock — views cannot fetch
|
|
80
|
+
* external APIs, so all data flows through `data` and `mutations`),
|
|
81
|
+
* while permitting common needs (HTTPS images / fonts, inline
|
|
82
|
+
* styles for React, eval for rsbuild HMR). View files can only
|
|
83
|
+
* *add* further restrictions via meta tag, never remove them.
|
|
84
|
+
*
|
|
85
|
+
* - `string` — raw CSP header value for full control. Use when the
|
|
86
|
+
* "strict" preset doesn't fit (e.g. you need `connect-src` to
|
|
87
|
+
* include a Sentry endpoint).
|
|
88
|
+
*
|
|
89
|
+
* Trade-off: when set to "strict" or a custom string, a view file
|
|
90
|
+
* cannot relax the policy at runtime. Switching back requires changing
|
|
91
|
+
* the mount() call. That rigidity is a feature.
|
|
92
|
+
*/
|
|
93
|
+
csp?: CspOption;
|
|
94
|
+
/**
|
|
95
|
+
* Extra hostnames accepted in the request `Host` and `Origin` headers
|
|
96
|
+
* on top of the built-in loopback set (`localhost`, `127.0.0.1`, `[::1]`).
|
|
97
|
+
*
|
|
98
|
+
* The dev server gates every request on this set to defend against
|
|
99
|
+
* DNS-rebinding attacks; non-matching requests get HTTP 403. Use this
|
|
100
|
+
* escape hatch when you need to reach the dev server through a custom
|
|
101
|
+
* `/etc/hosts` alias (e.g. `["my-app.local"]`) or any other loopback
|
|
102
|
+
* name. Hostnames are matched case-insensitively, port-agnostic.
|
|
103
|
+
*
|
|
104
|
+
* Be deliberate: any hostname you add becomes a viable DNS-rebinding
|
|
105
|
+
* target. Don't add wildcards, public DNS names, or LAN hostnames you
|
|
106
|
+
* don't fully control.
|
|
107
|
+
*/
|
|
108
|
+
allowedHosts?: string[];
|
|
109
|
+
/**
|
|
110
|
+
* Suppress ui-leaf / rsbuild output to stdout. Default: false.
|
|
111
|
+
*
|
|
112
|
+
* When you drive `mount()` programmatically — e.g. as part of a Node
|
|
113
|
+
* bridge for a non-Node CLI that's spawned ui-leaf as a subprocess —
|
|
114
|
+
* stdout is usually reserved for a structured protocol (line-delimited
|
|
115
|
+
* JSON, etc.). Without this option, rsbuild's banner and build messages
|
|
116
|
+
* collide with that protocol and silently corrupt it on the consumer
|
|
117
|
+
* side.
|
|
118
|
+
*
|
|
119
|
+
* Setting `silent: true`:
|
|
120
|
+
* - Sets rsbuild's logLevel to 'silent' (no banner, build, or
|
|
121
|
+
* deprecation messages emitted by rsbuild)
|
|
122
|
+
* - Redirects `process.stdout.write` to `process.stderr` for the
|
|
123
|
+
* lifetime of the dev server, restored on close
|
|
124
|
+
*
|
|
125
|
+
* Tradeoff: any other code in the same process that writes to stdout
|
|
126
|
+
* during the dev server's lifetime is also redirected. Hold the
|
|
127
|
+
* captured `process.stdout.write` reference yourself if you need to
|
|
128
|
+
* write to the *real* stdout from the same process.
|
|
129
|
+
*/
|
|
130
|
+
silent?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Grace period (ms) after server start before the heartbeat watcher arms.
|
|
133
|
+
* Cold-loading clients sometimes take a few seconds to send their first
|
|
134
|
+
* heartbeat. Defaults to 30000.
|
|
135
|
+
*
|
|
136
|
+
* If no client connects within (startupGraceMs + heartbeatTimeoutMs),
|
|
137
|
+
* the server shuts down on its own.
|
|
138
|
+
*/
|
|
139
|
+
startupGraceMs?: number;
|
|
140
|
+
}
|
|
141
|
+
interface MountedView {
|
|
142
|
+
/** URL the view is reachable at (http://127.0.0.1:<port>). */
|
|
143
|
+
url: string;
|
|
144
|
+
/** Bound port. Useful when port: 0 was requested. */
|
|
145
|
+
port: number;
|
|
146
|
+
/** Resolves when the view closes (heartbeat timeout) or close() is called. */
|
|
147
|
+
closed: Promise<void>;
|
|
148
|
+
/** Force-close the dev server early. */
|
|
149
|
+
close: () => Promise<void>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Mount a customizable browser view from a CLI. Spins up a local dev server
|
|
153
|
+
* and renders the chosen view with the given data. Returns once the server
|
|
154
|
+
* is ready; await `result.closed` to block until the user closes the
|
|
155
|
+
* browser tab.
|
|
156
|
+
*
|
|
157
|
+
* Mutations triggered in the view are dispatched to the registered handlers
|
|
158
|
+
* here; the view never reaches the CLI's backing API directly.
|
|
159
|
+
*
|
|
160
|
+
* Multi-tab note: if the user opens the served URL in additional tabs (or
|
|
161
|
+
* duplicates the tab), each tab heartbeats independently and the server
|
|
162
|
+
* stays alive while *any* tab is open. Closing the original tab does not
|
|
163
|
+
* shut down the CLI if a duplicate is still loaded.
|
|
164
|
+
*
|
|
165
|
+
* Ctrl+C: this function installs SIGINT and SIGTERM handlers that close
|
|
166
|
+
* the dev server (and clean up its temp directory) before exiting.
|
|
167
|
+
*/
|
|
168
|
+
declare function mount(opts: MountOptions): Promise<MountedView>;
|
|
169
|
+
|
|
170
|
+
export { CspOption, type MountOptions, type MountedView, MutationHandler, Shell, mount };
|