# Architecture This document explains how Uniview works under the hood - the custom reconcilers (React and Solid), RPC communication, and rendering pipeline. High-Level Overview [#high-level-overview] Data Flow [#data-flow] Initialization [#initialization] When a host loads a plugin: State Update (User Interaction) [#state-update-user-interaction] When a user interacts with the rendered UI: **Full Tree Mode:** **Incremental Mode:** Props Update (Host-Initiated) [#props-update-host-initiated] When the host updates plugin props: Package Responsibilities [#package-responsibilities] @uniview/protocol [#univiewprotocol] The shared contract between plugin and host. **Exports:** * `UINode` - Serializable tree structure * `Mutation` - Incremental update mutations (appendChild, removeChild, setText, etc.) * `HostToPluginAPI` / `PluginToHostAPI` - RPC interfaces * `LAYOUT_TAGS` - Built-in HTML elements * `handlerIdProp()` - Handler ID convention * Zod validators for runtime validation ```typescript interface UINode { id: string; type: string; // "div", "Button", "Card", etc. props: JSONValue; // Serializable props children: (UINode | string)[]; } ``` @uniview/react-renderer [#univiewreact-renderer] Custom React reconciler that produces serializable trees. **Key Components:** * **HostConfig** - React reconciler configuration * **InternalNode** - In-memory tree representation * **serializeTree()** - Converts to protocol UINode * **MutationCollector** - Collects incremental mutations during commit * **HandlerRegistry** - Maps functions to IDs ```typescript import { render, serializeTree, HandlerRegistry } from "@uniview/react-renderer"; const registry = new HandlerRegistry(); const bridge = createRenderBridge(); bridge.subscribe((root) => { const tree = serializeTree(root, registry); // Send tree to host }); render(, bridge); ``` @uniview/solid-renderer [#univiewsolid-renderer] Solid universal renderer that produces the same serializable trees. Uses Babel with `babel-preset-solid` (`generate: "universal"`) to transform Solid JSX into renderer-agnostic calls. **Key Components:** * **createRenderer()** - Universal renderer (insert, spread, createElement) * **SolidNode** - In-memory tree representation (same shape as InternalNode) * **serializeTree()** - Converts to protocol UINode * **SolidMutationCollector** - Collects incremental mutations * **HandlerRegistry** - Maps functions to IDs Both renderers produce identical `UINode` output — hosts don't need to know which framework the plugin uses. Both reconcilers have no DOM dependencies. They work in Web Workers, Node.js, Deno, and Bun. @uniview/react-runtime & @uniview/solid-runtime [#univiewreact-runtime--univiewsolid-runtime] Plugin bootstrap and lifecycle management. Both packages follow the same API pattern. **React (`@uniview/react-runtime`):** ```typescript import { startWorkerPlugin } from "@uniview/react-runtime"; startWorkerPlugin({ App }); ``` **Solid (`@uniview/solid-runtime`):** ```typescript import { startSolidWorkerPlugin } from "@uniview/solid-runtime"; startSolidWorkerPlugin({ App }); ``` Both runtimes also provide WebSocket client entries for server-side plugins via `/ws-client` subpath exports. @uniview/host-sdk [#univiewhost-sdk] Framework-agnostic host infrastructure. **Exports:** * `PluginController` - Unified interface for all modes * `createWorkerController()` - Web Worker mode * `createWebSocketController()` - WebSocket mode * `createMainController()` - Main thread mode * `createComponentRegistry()` - Custom component mapping * `MutableTree` - Applies incremental mutations to UINode tree ```typescript const controller = createWorkerController({ pluginUrl: "/plugin.js", initialProps: { theme: "dark" }, }); controller.subscribe((tree) => { // Render tree }); await controller.connect(); ``` @uniview/host-svelte [#univiewhost-svelte] Svelte 5 rendering adapter. **Components:** * `PluginHost` - Main lifecycle component * `ComponentRenderer` - Recursive tree renderer ```svelte {#snippet loading()}

Loading...

{/snippet}
``` Key Design Decisions [#key-design-decisions] 1. Protocol is Product-Agnostic [#1-protocol-is-product-agnostic] The protocol defines `UINode` with `type: string`. No hardcoded component types. ```typescript // Protocol doesn't know about "Button" or "Card" interface UINode { type: string; // Any string - products define their own // ... } ``` Products register their own components: ```typescript registry.register("Button", MyButton); registry.register("Card", MyCard); registry.register("CustomWidget", MyWidget); ``` 2. Layout Tags as Primitives [#2-layout-tags-as-primitives] HTML-like elements are always available: ```typescript const LAYOUT_TAGS = [ "div", "span", "p", "section", "header", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "br", "hr", "button", "input", "form", "label", "a", // ... ]; ``` Hosts render these as native elements without registration. 3. Handler ID Convention [#3-handler-id-convention] Functions can't cross RPC boundaries. The convention: 1. Plugin registers handler: `onClick={() => ...}` → ID `"h_abc123"` 2. Serialization: `{ onClick: fn }` → `{ _onClickHandlerId: "h_abc123" }` 3. Host creates proxy: `onclick: () => controller.execute("h_abc123")` ```typescript // Protocol helpers import { handlerIdProp, isHandlerIdProp, extractEventName, } from "@uniview/protocol"; handlerIdProp("onClick"); // "_onClickHandlerId" isHandlerIdProp("_onClickHandlerId"); // true extractEventName("_onClickHandlerId"); // "onClick" ``` 4. Worker-First Architecture [#4-worker-first-architecture] Primary mode is Web Worker for security: * Plugin code runs in sandboxed Worker * No direct DOM access * Communication only via structured RPC * Host controls what gets rendered 5. Thin Framework Adapters [#5-thin-framework-adapters] All controller logic lives in `host-sdk`. Adapters only handle rendering: ``` host-sdk (400 lines) ← All the logic host-svelte (150 lines) ← Just Svelte rendering host-vue (150 lines) ← Just Vue rendering ``` Bridge Architecture (WebSocket Mode) [#bridge-architecture-websocket-mode] In WebSocket mode, Uniview uses a **Bridge Server** pattern to facilitate communication between the browser host and the plugin running in a server-side environment (Node.js, Deno, Bun). The Bridge Pattern [#the-bridge-pattern] Unlike the traditional approach where each plugin runs its own WebSocket server, Uniview uses a single Elysia-based bridge server (provided by `@uniview/bridge-server`). This simplifies deployment and networking by multiplexing all plugin connections through a single port. Key Characteristics [#key-characteristics] * **Plugins as Clients**: Plugins connect **as clients** to the bridge server. They do not need to manage their own server lifecycle or open public ports. * **Transparent Byte Forwarding**: The bridge server does not parse or inspect the RPC messages. It simply forwards raw bytes between the matched host and plugin pairs based on the `pluginId`. * **Simplified Deployment**: Only the bridge server needs a stable, reachable address. Plugins can run behind NAT or in ephemeral environments as long as they can reach the bridge. * **Unified Port**: All communication (multiple plugins, multiple hosts) happens over a single port (default `:3000`), making it easier to configure firewalls and reverse proxies. Security Model [#security-model] WebSocket mode and Main Thread mode do NOT provide sandbox isolation. Use Worker mode for untrusted plugins. Extension Points [#extension-points] Custom Components [#custom-components] Define product-specific primitives: ```typescript // Your product's API registry.register("CommandPalette", CommandPalette); registry.register("FileTree", FileTree); registry.register("Terminal", Terminal); ``` Custom Transports [#custom-transports] Support additional communication channels: ```typescript // Built-in createWorkerController({ pluginUrl }); createWebSocketController({ serverUrl }); // Potential extensions createIframeController({ frameUrl }); createElectronController({ processId }); ``` Custom APIs [#custom-apis] Expose additional functionality to plugins via RPC extensions: ```typescript // Host exposes file system API controller.expose({ readFile: (path) => fs.readFileSync(path), writeFile: (path, data) => fs.writeFileSync(path, data), }); // Plugin uses it const content = await hostApi.readFile("/config.json"); ``` # Getting Started This guide walks you through creating a simple interactive plugin and rendering it in a Svelte host. Prerequisites [#prerequisites] * Node.js 18+ * pnpm (recommended) or npm Installation [#installation] Install the core packages: ```bash # For React plugin development pnpm add @uniview/react-runtime @uniview/react-renderer @uniview/protocol react # For Solid plugin development pnpm add @uniview/solid-runtime @uniview/solid-renderer @uniview/protocol solid-js # For host development (Svelte) pnpm add @uniview/host-sdk @uniview/host-svelte @uniview/protocol ``` Creating a Plugin [#creating-a-plugin] Plugins can be written in **React** or **Solid**. This guide shows the React path — see below for Solid. 1. Write Your React Component [#1-write-your-react-component] Create a React component. Plugins can use standard React hooks, state, and standard HTML elements. ```tsx title="src/App.tsx" import { useState } from "react"; export default function App() { const [name, setName] = useState(""); const [submitted, setSubmitted] = useState(false); // Conditional rendering if (submitted) { return (

Welcome, {name}!

Your registration was successful.

); } return (

Registration Plugin

setName(e.target.value)} className="border p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none" placeholder="John Doe" />
); } ``` 2. Create the Worker Entry Point [#2-create-the-worker-entry-point] Bootstrap your plugin using the runtime: ```typescript title="src/worker.ts" import { startWorkerPlugin } from "@uniview/react-runtime"; import App from "./App"; startWorkerPlugin({ App }); ``` 3. Build the Plugin [#3-build-the-plugin] Bundle for browser (Web Worker target): ```typescript title="build.ts" // Using Bun await Bun.build({ entrypoints: ["./src/worker.ts"], outdir: "./dist", target: "browser", format: "esm", minify: true, }); ``` Or with other bundlers (esbuild, Vite, etc.): ```bash esbuild src/worker.ts --bundle --format=esm --outfile=dist/plugin.js ``` The plugin bundle must be self-contained with all dependencies included. Web Workers can't access the host's modules. Creating a Solid Plugin [#creating-a-solid-plugin] The same steps apply for Solid, with different packages and a required build step: ```tsx title="src/App.tsx" import { createSignal } from "solid-js"; const App = () => { const [count, setCount] = createSignal(0); return (

Count: {count()}

); }; export default App; ``` ```typescript title="src/worker.ts" import { startSolidWorkerPlugin } from "@uniview/solid-runtime"; import App from "./App"; startSolidWorkerPlugin({ App }); ``` Solid plugins require a Babel build step with `babel-preset-solid` (`generate: "universal"`) because Bun/Node can't natively run Solid's JSX transform. See `examples/plugin-solid-example/build.ts` for the full build configuration. When building Solid plugins for Bun/Node.js (`target: "bun"`), you must add `conditions: ["browser"]` to the build config. Without this, Bun resolves `solid-js` to its SSR build which has no reactivity. Creating a Host (Svelte) [#creating-a-host-svelte] 1. Set Up the Controller [#1-set-up-the-controller] ```svelte title="src/routes/+page.svelte"

Uniview Host Demo

{#snippet loading()}
Loading plugin environment...
{/snippet}
``` 2. Serve the Plugin [#2-serve-the-plugin] Place your built plugin in `static/plugins/` (for SvelteKit) or configure your dev server to serve it. 3. Run the App [#3-run-the-app] ```bash pnpm dev ``` Your plugin should now render inside the host! Custom Components [#custom-components] By default, plugins can use HTML elements (`div`, `button`, `input`, etc.). For custom components: Plugin Side [#plugin-side] Create wrapper components that emit custom types: ```tsx title="plugin-api/Button.tsx" import { createElement } from "react"; export function Button({ title, onClick, variant = "primary" }) { return createElement("Button", { title, onClick, variant }); } ``` Host Side [#host-side] Register renderers for custom types: ```svelte title="components/PluginButton.svelte" ``` ```typescript // In your page registry.register("Button", PluginButton); ``` Runtime Modes [#runtime-modes] Uniview supports three runtime modes: | Mode | Use Case | Security | | --------------- | --------------------------------- | ---------------- | | **Worker** | Production, untrusted plugins | Sandboxed | | **WebSocket** | Server-side plugins, Node.js APIs | Network isolated | | **Main Thread** | Development, debugging | None | For complex setups involving multiple modes, check out the [Advanced Host Guide](/docs/guides/advanced-host). Next Steps [#next-steps] Build a production-ready host with mode switching and dynamic loading Set up the WebSocket bridge for Node.js/server-side plugins Learn how to create and register custom components # Uniview Uniview is a universal plugin system that enables writing plugins in **React or Solid** that can be rendered by **any host framework** - Svelte, Vue, React, or custom implementations. Why Uniview? [#why-uniview] Modern applications often need extensibility through plugins. Uniview solves the challenge of: * **Framework Lock-in**: Write plugins once, render anywhere * **Security**: Sandboxed execution in Web Workers * **Flexibility**: Run plugins in browser, Node.js, Deno, or Bun * **Type Safety**: Full TypeScript support with RPC type checking Web Worker] <-->|RPC kkrpc
UINode tree| B[Host Svelte
or Vue, React] style A fill:#3b82f6,stroke:#1e40af,color:#fff style B fill:#10b981,stroke:#047857,color:#fff `} />
``` RPC (kkrpc) ┌───────────────────────┐ ◄──────────────────► ┌──────────────────┐ │ Plugin (React/Solid) │ UINode tree │ Host (Svelte) │ │ Web Worker │ │ or Vue, React │ └───────────────────────┘ └──────────────────┘ ```
Key Features [#key-features] Write plugins using React or Solid - hooks, signals, state, components Render in Svelte, Vue, React, or build your own adapter Run untrusted plugins safely in Web Workers Execute plugins in Node.js/Deno via WebSocket Render plugins to terminal without DOM (like React Native) Render plugins as native SwiftUI or AppKit applications Quick Example [#quick-example] ```tsx // App.tsx import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return (

Count: {count}

); } ```
```tsx // App.tsx import { createSignal } from "solid-js"; const App = () => { const [count, setCount] = createSignal(0); return (

Count: {count()}

); }; export default App; ```
**Host (Svelte):** ```svelte ``` Packages [#packages] | Package | Description | | ------------------------- | ----------------------------------------- | | `@uniview/protocol` | Core types, UINode schema, RPC interfaces | | `@uniview/react-renderer` | Custom React reconciler | | `@uniview/solid-renderer` | Solid universal renderer | | `@uniview/react-runtime` | React plugin bootstrap (Worker/Node.js) | | `@uniview/solid-runtime` | Solid plugin bootstrap (Worker/Node.js) | | `@uniview/host-sdk` | Framework-agnostic host controller | | `@uniview/host-svelte` | Svelte 5 rendering adapter | | `@uniview/tui-renderer` | Terminal UI renderer (non-DOM) | Inspiration [#inspiration] Uniview uses custom reconcilers (React and Solid) to serialize component trees into a framework-agnostic UINode format, enabling plugins to render on any host platform. Build your first plugin in 5 minutes Understand how Uniview works # Advanced Host Implementation This guide demonstrates how to build a robust Svelte host that can dynamically switch between Worker, WebSocket, and Main Thread modes. This pattern is useful for building "Universal" applications that support both local and remote plugins. The Universal Host Pattern [#the-universal-host-pattern] A production-ready host should handle: 1. **Dynamic Controller Creation**: Creating the right controller (`Worker`, `WebSocket`, `Main`) based on configuration. 2. **Resource Cleanup**: Disconnecting controllers when switching modes or unmounting. 3. **Loading States**: Showing feedback while the plugin initializes. 4. **Error Handling**: Gracefully handling connection failures. Implementation (Svelte 5) [#implementation-svelte-5] Here is a complete example of a host page that supports all three modes. ```svelte title="src/routes/+page.svelte"

Universal Host

Running in {runtimeMode} mode

{#if browser && controller && registry} {#key runtimeMode} {#snippet loading()}

Connecting to plugin environment...

{/snippet}
{/key} {:else}
Initializing host...
{/if}
``` Key Concepts [#key-concepts] Derived Controller Creation [#derived-controller-creation] We use `$derived.by` to reactively recreate the controller whenever `runtimeMode` changes. This ensures: 1. The old controller is properly cleaned up (via `$effect`). 2. The new controller is initialized with the correct configuration. 3. The `registry` is re-instantiated (or shared, depending on your needs). Keyed Re-rendering [#keyed-re-rendering] Wrapping `` in a `{#key runtimeMode}` block forces Svelte to destroy and recreate the host component when the mode changes. This is crucial because `PluginHost` maintains internal subscription state that needs to be reset for a fresh connection. ```svelte {#key runtimeMode} {/key} ``` Loading States [#loading-states] The `{#snippet loading()}` block allows you to provide a custom UI while the plugin is initializing. This is especially important for: * **Worker Mode**: Script loading and worker startup (approx. 50-200ms). * **WebSocket Mode**: Network connection latency (10-100ms+). Connection Lifecycle [#connection-lifecycle] 1. **Mount**: Host connects to the controller (`controller.connect()`). 2. **Handshake**: Controller establishes RPC link with plugin. 3. **Render**: Plugin sends initial tree → Host renders. 4. **Update**: User interaction → Host sends event → Plugin updates tree → Host re-renders. 5. **Unmount**: Host disconnects (`controller.disconnect()`), closing workers/sockets. # Benchmark The benchmark plugin helps you measure and compare performance between full-tree and incremental update modes. It's useful for understanding the trade-offs and validating performance improvements. Running the Benchmark [#running-the-benchmark] Quick Start [#quick-start] ```bash # Start the full demo environment cd examples/host-svelte-demo pnpm dev:all # Open benchmark with incremental mode open http://localhost:5173?demo=benchmark&update=incremental # Or full-tree mode for comparison open http://localhost:5173?demo=benchmark&update=full ``` URL Parameters [#url-parameters] The benchmark supports query parameters for quick access: | Parameter | Values | Description | | ----------- | ------------------------- | --------------------- | | `demo` | `benchmark` | Select benchmark demo | | `update` | `full` \| `incremental` | Update mode | | `framework` | `react` \| `solid` | Plugin framework | | `runtime` | `worker` \| `node-server` | Runtime mode | Example: `http://localhost:5173?demo=benchmark&framework=react&update=incremental&runtime=worker` Benchmark Configuration [#benchmark-configuration] Test Load [#test-load] The benchmark initializes with **1000 items** and allows up to **2000 items maximum**. Each item contains: * Unique ID * 20 words of lorem ipsum text (\~100-150 characters) This provides sufficient stress for modern computers while remaining responsive. Operations [#operations] **Manual Operations:** * **Add 10 Items** - Appends 10 new items to the list * **Remove 10 Items** - Removes last 10 items * **Update All Texts** - Changes text on all items (stress test) * **Update Single Item** - Modifies one random item **Auto-Benchmark:** * Runs 50 cycles alternating between add and modify * Auto-stops at 500 cycles or when reaching max items * Measures sustained performance under load Metrics Explained [#metrics-explained] Operation Metrics (Per Click) [#operation-metrics-per-click] These metrics measure performance from the user's perspective (each button click): | Metric | Description | Typical Values | | -------------------- | ------------------------------ | --------------------------- | | Operations performed | Total button clicks | User-dependent | | Last operation | Time for most recent operation | 5-15ms | | Avg time/operation | Mean operation latency | 6-10ms | | Messages in last op | RPC messages sent | 1-5 (incremental), 1 (full) | | Avg messages/op | Mean messages per operation | 1.5-3 (incremental) | | Avg bytes/op | Mean bandwidth per operation | 50-200KB | Message Metrics (Per Message) [#message-metrics-per-message] These metrics measure the RPC communication efficiency: | Metric | Description | Full Tree | Incremental | | -------------- | ---------------------- | --------- | ----------- | | Total messages | Cumulative RPC calls | Lower | Higher | | Total bytes | Cumulative bandwidth | Higher | Lower\* | | Bytes/message | Average message size | \~87KB | \~69KB | | Time/message | Serialization overhead | \~0.1μs | \~0.08μs | \*Note: Incremental mode may show higher total bytes in some scenarios due to message overhead. This is more noticeable with small trees. Performance Comparison [#performance-comparison] Typical Results (1000 items, React) [#typical-results-1000-items-react] **Full Tree Mode:** ``` Last operation: 8.10ms Total messages: 101 Total bytes: 8.76 MB Bytes/message: 86,697 Avg messages/op: 1.0 ``` **Incremental Mode:** ``` Last operation: 6.30ms Total messages: 202 Total bytes: 13.95 MB Bytes/message: 69,081 Avg messages/op: 2.0 ``` Analysis [#analysis] **When Incremental is Better:** * Large lists with frequent small updates * Adding/removing individual items * Text updates on existing nodes * Bandwidth-constrained environments **When Full Tree is Better:** * Small trees (\< 100 items) * Complete re-renders (theming, layout changes) * Simple apps with minimal updates * When message overhead exceeds payload savings How Incremental Updates Work [#how-incremental-updates-work] Mutation Types [#mutation-types] Instead of sending the entire tree, incremental mode sends mutations: ```typescript // Append new child type AppendChildMutation = { type: "appendChild"; parentId: string; node: UINode; }; // Update text content type SetTextMutation = { type: "setText"; textNodeId: string; text: string; }; // Update props type SetPropsMutation = { type: "setProps"; nodeId: string; props: Record; }; ``` Collection Process [#collection-process] 1. **React/Solid Reconciler** detects changes during render 2. **MutationCollector** records each change type 3. **End of commit** flushes mutations to RPC 4. **Host applies** mutations to existing tree Host-Side Application [#host-side-application] The host maintains a `MutableTree` that applies mutations: ```typescript const mutableTree = new MutableTree(); // On full tree update mutableTree.init(newTree); // On incremental update mutableTree.applyMutations(mutations); ``` The tree uses indexing for O(1) lookups: * `nodeIndex: Map` - Element lookups * `textIndex: Map` - Text node tracking Best Practices [#best-practices] Choosing Update Mode [#choosing-update-mode] **Use Incremental When:** * List has 100+ items * Frequent individual item updates * Network bandwidth is limited * Latency matters (real-time apps) **Use Full Tree When:** * Simple forms or small UIs * Complete re-renders are common * Debugging rendering issues * Plugin doesn't support incremental Optimizing Performance [#optimizing-performance] 1. **Batch Updates**: Group related state changes to reduce mutation count 2. **Stable IDs**: Ensure list items have stable keys for efficient diffing 3. **Avoid Deep Nesting**: Flatten tree structure where possible 4. **Measure First**: Use benchmark to validate assumptions Debugging [#debugging] **High message count?** * Check for unstable component keys * Look for unnecessary re-renders * Consider batching state updates **Large message sizes?** * Reduce prop payload sizes * Avoid passing large objects * Use references/IDs instead of data Implementation Details [#implementation-details] Plugin Side [#plugin-side] ```typescript // Enable incremental mode import { startWorkerPlugin } from "@uniview/react-runtime"; startWorkerPlugin({ App: MyBenchmarkApp, mode: "incremental", // "full" | "incremental" }); ``` Host Side [#host-side] Incremental mode is transparent to hosts. The controller handles both modes: ```typescript const controller = createWorkerController({ pluginUrl: "/plugins/benchmark-incremental.worker.js", }); // Works with both full and incremental plugins controller.subscribe((tree) => renderTree(tree)); ``` Future Improvements [#future-improvements] * **Batching**: Group rapid mutations into single RPC call * **Compression**: Delta compression for text updates * **Virtual Scrolling**: Render only visible items * **Memory Profiling**: Track heap usage during benchmarks # Bridge Server Setup To run plugins on the server (Node.js, Deno, or Bun), Uniview uses a **Bridge Server**. This server acts as a transparent WebSocket multiplexer, allowing multiple plugins and hosts to communicate over a single port. Why a Bridge Server? [#why-a-bridge-server] 1. **Single Port**: All plugins connect to one port (e.g., `:3000`). You don't need to open 50 ports for 50 plugins. 2. **No NAT Traversal**: Plugins connect *outbound* to the bridge. They can run behind firewalls or inside Docker containers without exposing ports. 3. **Protocol Agnostic**: The bridge just forwards raw messages. It doesn't need to know about Uniview protocols or updates. Implementation (Elysia) [#implementation-elysia] We recommend using [Elysia](https://elysiajs.com/) (on Bun) for the bridge server due to its performance and simplicity, but any WebSocket server will work. Here is the complete code for a production-ready bridge server: ```typescript title="server/index.ts" import { Elysia } from "elysia"; import { readFile } from "fs/promises"; import { join } from "path"; // In-memory connection map // pluginId -> { pluginWs, hostWs } const connections = new Map(); function normalizeMessage(message: unknown): string { let msgStr = typeof message === "string" ? message : message instanceof Buffer ? message.toString() : JSON.stringify(message); return msgStr; } const app = new Elysia() // Optional: Serve static worker files (if you want to host worker bundles too) .get("/:filename", async ({ params }) => { try { const file = await readFile(join("./dist", params.filename)); return new Response(file, { headers: { "Content-Type": "application/javascript" }, }); } catch { return new Response("Not found", { status: 404 }); } }) // 1. Plugin Endpoint: /plugins/:pluginId .ws("/plugins/:pluginId", { open(ws) { const pluginId = ws.data.params.pluginId; console.log(`[Bridge] Plugin connected: ${pluginId}`); if (!connections.has(pluginId)) { connections.set(pluginId, {}); } connections.get(pluginId)!.pluginWs = ws; }, message(ws, message) { const pluginId = ws.data.params.pluginId; const conn = connections.get(pluginId); // Forward message to Host if (conn?.hostWs) { conn.hostWs.send(normalizeMessage(message)); } }, close(ws) { const pluginId = ws.data.params.pluginId; const conn = connections.get(pluginId); if (conn) { conn.pluginWs = undefined; if (!conn.hostWs) connections.delete(pluginId); } console.log(`[Bridge] Plugin disconnected: ${pluginId}`); }, }) // 2. Host Endpoint: /host/:pluginId .ws("/host/:pluginId", { open(ws) { const pluginId = ws.data.params.pluginId; const conn = connections.get(pluginId); // Require plugin to be ready first if (!conn?.pluginWs) { ws.close(1000, "Plugin not ready"); return; } // Replace existing host connection if any if (conn.hostWs) { conn.hostWs.close(1000, "Replaced by new connection"); } conn.hostWs = ws; console.log(`[Bridge] Host connected: ${pluginId}`); }, message(ws, message) { const pluginId = ws.data.params.pluginId; const conn = connections.get(pluginId); // Forward message to Plugin if (conn?.pluginWs) { conn.pluginWs.send(normalizeMessage(message)); } }, close(ws) { const pluginId = ws.data.params.pluginId; const conn = connections.get(pluginId); if (conn) { conn.hostWs = undefined; if (!conn.pluginWs) connections.delete(pluginId); } console.log(`[Bridge] Host disconnected: ${pluginId}`); }, }) .listen(3000); console.log( `🌉 Bridge server listening on ${app.server?.hostname}:${app.server?.port}`, ); ``` Running the Server [#running-the-server] Using Bun: ```bash bun run server/index.ts ``` Connecting to the Bridge [#connecting-to-the-bridge] Plugin Side [#plugin-side] Connect your Node.js/Deno/Bun plugin to the bridge: ```typescript import { connectToHostServer } from "@uniview/react-runtime/ws-client"; import App from "./App"; connectToHostServer({ App, serverUrl: "ws://localhost:3000", pluginId: "my-plugin", // Must match unique ID }); ``` Host Side [#host-side] Connect your browser host to the bridge: ```typescript import { createWebSocketController } from "@uniview/host-sdk"; const controller = createWebSocketController({ serverUrl: "ws://localhost:3000", pluginId: "my-plugin", // Must match plugin's ID }); ``` # Custom Components Learn how to create custom components that plugins can use, giving you full control over styling and behavior. Overview [#overview] Uniview has two types of renderable elements: 1. **Layout Tags**: Built-in HTML elements (`div`, `button`, `input`, etc.) 2. **Custom Components**: Your framework-specific implementations When a plugin renders ` ``` Props Contract [#props-contract] Your component receives props from the plugin: ```tsx // Plugin renders: // Your component receives: { variant: "primary", onclick: () => controller.execute("handler_abc"), children: Snippet // Svelte 5 snippet for children } ``` Event handlers like `onClick` are automatically converted to lowercase (`onclick`) for Svelte compatibility. Component Patterns [#component-patterns] Card Component [#card-component] ```svelte
{#if title}

{title}

{/if} {#if description}

{description}

{/if}
{@render children?.()}
``` Input Component [#input-component] ```svelte
{#if label} {/if}
``` Toggle Component [#toggle-component] ```svelte ``` Event Handling [#event-handling] Events flow from plugin to host automatically: setCount(c + 1)}"] --> B["Serialized: { _onClickHandlerId: 'handler_123' }"] B --> C["Host proxy: onclick = () => controller.execute('handler_123')"] C --> D["Your component: {onclick} prop"] style A fill:#3b82f6,stroke:#1e40af,color:#fff style B fill:#8b5cf6,stroke:#6d28d9,color:#fff style C fill:#ec4899,stroke:#be185d,color:#fff style D fill:#10b981,stroke:#047857,color:#fff `} /> ``` Plugin: onClick={() => setCount(c + 1)} │ ▼ Serialized: { _onClickHandlerId: "handler_123" } │ ▼ Host proxy: onclick = () => controller.execute("handler_123") │ ▼ Your component: {onclick} prop ``` Passing Event Data [#passing-event-data] For events with data (like input changes): ```svelte oninput?.(e.currentTarget.value)} /> ``` ```tsx // Plugin side setName(value)} /> ``` Registration Patterns [#registration-patterns] Lazy Registration [#lazy-registration] Register components on-demand: ```typescript const registry = createComponentRegistry(); // Register core components immediately registry.register("Button", Button); registry.register("Input", Input); // Register others when needed async function loadAdvancedComponents() { const { DataTable } = await import("$lib/components/DataTable.svelte"); const { Chart } = await import("$lib/components/Chart.svelte"); registry.register("DataTable", DataTable); registry.register("Chart", Chart); } ``` Component Metadata [#component-metadata] Add metadata for documentation or tooling: ```typescript registry.register("Button", Button, { description: "Primary action button", props: { variant: { type: "string", default: "primary" }, size: { type: "string", default: "md" }, disabled: { type: "boolean", default: false }, }, }); ``` Checking Registration [#checking-registration] ```typescript // Check if component exists if (registry.has("Chart")) { // Chart is available } // List all registered components const components = registry.list(); // ['Button', 'Input', 'Card', ...] // Get component const ButtonComponent = registry.get("Button"); ``` Design System Integration [#design-system-integration] Using Tailwind CSS [#using-tailwind-css] ```svelte ``` Using shadcn/ui [#using-shadcnui] Wrap shadcn components for Uniview: ```svelte {@render children?.()} ``` Unknown Components [#unknown-components] When a plugin uses an unregistered component: ```typescript // In ComponentRenderer logic if (!registry.has(node.type)) { console.warn(`Unknown component: ${node.type}`); // Option 1: Render nothing // Option 2: Render error message // Option 3: Render as div with warning } ``` Consider creating a fallback component: ```svelte
Unknown component: {type}
``` Best Practices [#best-practices] Use primitive types (string, number, boolean) for props. Complex objects may not serialize correctly over RPC. Always provide sensible defaults. Plugins may not pass all expected props. Create a component catalog so plugin developers know what's available and how to use it. Test with empty children, missing handlers, and invalid prop values. # Native macOS Uniview plugins (React or Solid) can be rendered as native macOS applications. Two example hosts demonstrate different approaches: | Example | Framework | Update Strategy | Best For | | ------------------ | --------- | ------------------------- | ----------------------------- | | `host-macos-demo` | SwiftUI | Full view rebuild | Rapid prototyping, simple UIs | | `host-appkit-demo` | AppKit | Diff-based reconciliation | Production apps, complex UIs | Architecture [#architecture] Both examples use the same protocol stack: ``` Plugin (React/Solid) → Bridge Server → WebSocket → Native Host (Bun) (Elysia) (Swift) ``` The host connects to the bridge server as a WebSocket client, receives `UINode` trees (JSON), and renders them as native views. Protocol Flow [#protocol-flow] 1. **Plugin** runs React or Solid, produces a serialized `UINode` tree on every state change 2. **Bridge server** relays messages between plugin and host over WebSocket 3. **Host** receives the tree, maps element types to native views, handles user events 4. **Events** (clicks, input changes) are sent back to the plugin as RPC calls 5. **Plugin** runs the handler, React re-renders, new tree is sent — cycle repeats SwiftUI Host (host-macos-demo) [#swiftui-host-host-macos-demo] The simpler approach: converts `UINode` trees directly to SwiftUI views. ```swift // UINode → SwiftUI (full rebuild each update) struct UINodeRenderer: View { let node: UINode var body: some View { switch node.type { case "div": VStack { ForEach(node.children) { UINodeRenderer(node: $0) } } case "button": Button(node.text) { executeHandler(node.onClick) } // ... } } } ``` SwiftUI's declarative model means the entire view tree is rebuilt on each `updateTree` call. SwiftUI's own diffing handles the actual view updates. Running [#running] ```bash # Terminal 1 cd examples/bridge-server && bun run dev # Terminal 2 cd examples/plugin-example && bun run client:simple # Xcode open examples/host-macos-demo/HostMacOSDemo.xcodeproj # Cmd+R ``` AppKit Host (host-appkit-demo) [#appkit-host-host-appkit-demo] The production approach — imperative views with diff-based reconciliation. Uses a three-layer architecture: ``` UINode (JSON) → NodeViewModel (reference type) → NSView (imperative) ↑ TreeReconciler (id-based diffing) ``` Key Concepts [#key-concepts] **NodeViewModel** — intermediate layer between JSON data and native views: * Reference type (class) so views can hold a reference * `DirtyFields` bitfield tracks exactly what changed (type, props, text, children) * `weak var associatedView: NSView?` links to the rendered native view * `diff(against:)` compares two view models and sets dirty flags **TreeReconciler** — walks old and new view model trees, applying minimal mutations: * Matches nodes by stable ID (assigned by React reconciler) for O(1) lookup * Same type + same ID: update props in-place via `UpdatableNodeView` protocol * Different type: replace the entire NSView subtree * New ID: create and insert * Missing ID: remove from parent * Reorder: rearrange NSStackView's arranged subviews **UpdatableNodeView** — protocol for surgical prop updates: ```swift protocol UpdatableNodeView: NSView { func update(from oldModel: NodeViewModel, to newModel: NodeViewModel, ...) } ``` Each native view checks the dirty flags and only updates relevant properties: ```swift class ButtonNodeView: NSButton, UpdatableNodeView { func update(from old: NodeViewModel, to new: NodeViewModel, ...) { if old.dirtyFields.contains(.text) { self.title = new.textContent } if old.dirtyFields.contains(.props) { self.isEnabled = !new.props["disabled"] } } } ``` Component Mapping [#component-mapping] | UINode Type | NSView | Notes | | ------------------------------- | ---------------------------------- | ------------------------------------------- | | `div`, `section`, `form` | `ContainerView` (NSStackView) | Vertical default, horizontal with flex hint | | `p`, `span`, `h1`-`h6`, `label` | `TextNodeView` (NSTextField label) | Typography varies by type | | `button` | `ButtonNodeView` (NSButton) | target/action pattern | | `input` | `InputNodeView` (NSTextField) | NSTextFieldDelegate for onChange | | `Switch` | `SwitchNodeView` (NSSwitch) | Native macOS toggle switch | | `Toggle` | `ToggleNodeView` (NSButton) | Push on/push off button | | `ul`, `ol` | `ListContainerView` | NSStackView with left indent | Running [#running-1] ```bash # Terminal 1 cd examples/bridge-server && bun run dev # Terminal 2: simple or advanced demo cd examples/plugin-example && bun run client:simple # or: bun run client:advanced # Xcode open examples/host-appkit-demo/HostAppKitDemo.xcodeproj # Cmd+R, enter plugin ID "simple-demo" or "advanced-demo" ``` Building Your Own Native Host [#building-your-own-native-host] To build a native host for any platform, you need: 1. **WebSocket client** — connect to the bridge server at `ws://localhost:3000/host/{pluginId}` 2. **JSON-RPC handler** — implement the [kkrpc protocol](/docs/architecture) for request/response messages 3. **UINode decoder** — parse the `UINode` tree from JSON 4. **View factory** — map element types to native views 5. **Event handler** — send handler IDs back to the plugin via `executeHandler` RPC call The protocol is platform-agnostic. The same approach works for UIKit (iOS), WinUI (Windows), GTK (Linux), or any imperative UI framework. Handler ID Convention [#handler-id-convention] React event handlers can't be serialized over JSON. Instead, the reconciler replaces them with string IDs: ``` onClick={() => setCount(c + 1)} → { _onClickHandlerId: "handler_0" } onChange={(v) => setValue(v)} → { _onChangeHandlerId: "handler_1" } ``` When the user clicks a button, the host sends: ```json { "method": "executeHandler", "args": ["handler_0", []] } ``` The plugin resolves the handler ID to the original function and calls it. # Runtime Modes Uniview supports multiple runtime modes for different use cases. Each mode provides different trade-offs between isolation, performance, and capabilities. Overview [#overview] | Mode | Isolation | Environment | Use Case | | --------------- | ---------------- | ---------------- | --------------------------------- | | **Worker** | Full sandbox | Browser | Production, untrusted plugins | | **WebSocket** | Process boundary | Node.js/Deno/Bun | Server-side plugins, full runtime | | **Main Thread** | None | Browser | Development, debugging | Web Worker Mode [#web-worker-mode] The default and recommended mode for browser plugins. Characteristics [#characteristics] * **Full isolation**: Plugin cannot access DOM, `window`, or host memory * **Secure**: Safe to run untrusted third-party plugins * **Async only**: All communication via `postMessage` * **Browser-compatible**: Works in all modern browsers * **Update modes**: Supports both full-tree and incremental updates Setup [#setup] **Plugin side:** ```typescript // worker.ts import { startWorkerPlugin } from "@uniview/react-runtime"; import App from "./App"; startWorkerPlugin({ App, mode: "incremental", // "full" (default) or "incremental" }); ``` **Host side:** ```typescript import { createWorkerController } from "@uniview/host-sdk"; const controller = createWorkerController({ pluginUrl: "/plugins/my-plugin.js", initialProps: { theme: "dark" }, }); await controller.connect(); ``` Restrictions [#restrictions] Plugins in Worker mode cannot: * Access `window`, `document`, or DOM APIs * Use `localStorage` or `sessionStorage` * Import Node.js modules (`fs`, `path`, etc.) * Make synchronous calls to the host Allowed APIs [#allowed-apis] * `fetch()` for network requests * `console.log()` and other console methods * `setTimeout()`, `setInterval()` * Web Worker global APIs * All React hooks and patterns WebSocket Mode [#websocket-mode] For server-side plugins with full runtime access, using a Bridge architecture. Characteristics [#characteristics-1] * **Bridge Architecture**: Plugins connect as clients to a central bridge server * **Full runtime**: Access to file system, network, databases, and Node.js APIs * **Process isolation**: Plugin runs in a separate Node.js/Deno/Bun process * **Persistent**: Maintains long-running connections through the bridge * **Routing**: The bridge server routes traffic between specific plugin instances and hosts Bridge Server [#bridge-server] In the new architecture, a central **Bridge Server** (typically built with Elysia or similar) acts as a transparent byte forwarder. Instead of each plugin running its own WebSocket server, they connect to the bridge. * **Plugin Connection**: `ws://bridge:3000/plugins/:pluginId` * **Host Connection**: `ws://bridge:3000/host/:pluginId` The bridge server manages the pairing and ensures that RPC messages are forwarded correctly between the host and the plugin. Setup [#setup-1] **Plugin side (Node.js/Deno/Bun):** ```typescript // server-plugin.ts (Recommended) import { connectToHostServer } from "@uniview/react-runtime/ws-client"; import App from "./App"; connectToHostServer({ App, serverUrl: "ws://localhost:3000", // Bridge server pluginId: "my-plugin", }); ``` **Host side:** ```typescript import { createWebSocketController } from "@uniview/host-sdk"; const controller = createWebSocketController({ serverUrl: "ws://localhost:3000", // Bridge server pluginId: "my-plugin", // Required for bridge routing initialProps: { userId: "abc" }, }); await controller.connect(); ``` Use Cases [#use-cases] * Plugins that need database access or read/write files * Plugins that call external APIs with secrets * Plugins that need heavy computation (AI, image processing) * Multi-user collaborative plugins * Centralized management of multiple server-side plugins Architecture [#architecture] Svelte/React] end subgraph "Bridge Server (Elysia)" B[Bridge
Forwarder] end subgraph "Plugin Runtime" C[Plugin Client
Node.js/Deno/Bun] D[Database / FS] end A <-->|ws://.../host/:id| B B <-->|ws://.../plugins/:id| C C --> D style A fill:#3b82f6,stroke:#1e40af,color:#fff style B fill:#8b5cf6,stroke:#6d28d9,color:#fff style C fill:#10b981,stroke:#047857,color:#fff style D fill:#f59e0b,stroke:#d97706,color:#fff `} />
``` ┌─────────────────┐ WebSocket ┌─────────────────┐ WebSocket ┌─────────────────┐ │ Browser Host │ <───────────────────> │ Bridge Server │ <───────────────────> │ Plugin Client │ │ (Svelte/React) │ /host/:pluginId │ (Elysia) │ /plugins/:pluginId │ (Node.js/Deno) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Database, │ │ File System, │ │ External APIs │ └─────────────────┘ ```
Main Thread Mode [#main-thread-mode] For development and debugging only. Never use Main Thread mode in production. Plugins have full access to your application's memory and DOM. Characteristics [#characteristics-2] * **No isolation**: Plugin runs in same context as host * **Synchronous**: Direct function calls, no serialization overhead * **Full debugging**: Use browser DevTools normally * **Hot reload**: Works with Vite/webpack HMR Setup [#setup-2] ```typescript import { createMainController } from "@uniview/host-sdk"; import { SimpleDemo } from "@uniview/example-plugin"; const controller = createMainController({ App: SimpleDemo, initialProps: { debug: true }, }); await controller.connect(); ``` When to Use [#when-to-use] * Local development with HMR * Debugging plugin issues * Performance profiling * Testing new components Development Workflow [#development-workflow] ```typescript // In development, switch based on environment const controller = import.meta.env.DEV ? createMainController({ App: SimpleDemo }) : createWorkerController({ pluginUrl: "/plugins/simple.js" }); ``` Choosing a Mode [#choosing-a-mode] * Production browser deployments - Third-party or untrusted plugins - Security-sensitive applications - Standard web applications * Server-side plugin logic - Database or file system access - Heavy computation offloading - Multi-tenant architectures * Local development only - Debugging issues - Performance profiling - Never in production Mode Comparison [#mode-comparison] Communication Flow [#communication-flow] |postMessage| B1[Worker] B1 -->|onmessage| A1 end subgraph WebSocket Mode A2[Host] -->|ws.send| B2[Server] B2 -->|ws.send| A2 end subgraph Main Thread Mode A3[Host] <-->|direct function call| B3[Plugin] end style A1 fill:#3b82f6,stroke:#1e40af,color:#fff style B1 fill:#10b981,stroke:#047857,color:#fff style A2 fill:#3b82f6,stroke:#1e40af,color:#fff style B2 fill:#10b981,stroke:#047857,color:#fff style A3 fill:#3b82f6,stroke:#1e40af,color:#fff style B3 fill:#10b981,stroke:#047857,color:#fff `} /> **Worker Mode:** ``` Host → postMessage → Worker → onmessage Worker → postMessage → Host → onmessage ``` **WebSocket Mode:** ``` Host → ws.send() → Server → ws.onmessage Server → ws.send() → Host → ws.onmessage ``` **Main Thread Mode:** ``` Host → direct function call → Plugin Plugin → direct function call → Host ``` Latency [#latency] | Mode | Typical Latency | Notes | | ----------- | --------------- | ----------------------- | | Main Thread | \< 1ms | Direct calls | | Worker | 1-5ms | Serialization overhead | | WebSocket | 10-100ms | Network + serialization | Capabilities [#capabilities] | Capability | Worker | WebSocket | Main Thread | | ------------ | --------- | --------- | ----------- | | DOM Access | No | No | Yes | | File System | No | Yes | Yes | | Fetch API | Yes | Yes | Yes | | Node.js APIs | No | Yes | No | | Hot Reload | Limited | No | Yes | | Debugging | Difficult | Moderate | Easy | Migration Between Modes [#migration-between-modes] The `PluginController` interface is identical across all modes. To switch modes: 1. Change the controller creation 2. Update plugin build configuration 3. Deploy plugin to appropriate environment ```typescript // Before: Worker mode const controller = createWorkerController({ pluginUrl: "/plugins/app.js", }); // After: WebSocket mode const controller = createWebSocketController({ serverUrl: "ws://bridge-server:3000", pluginId: "my-plugin", }); // The rest of your code stays the same! controller.subscribe((tree) => renderTree(tree)); await controller.connect(); ``` # Terminal UI The `@uniview/tui-renderer` package provides a standalone React reconciler that renders directly to the terminal — no DOM, no browser, no plugin/RPC infrastructure needed. This is conceptually similar to React Native: React components are rendered to a non-DOM target using a custom reconciler. Quick Start [#quick-start] ```bash cd examples/tui-demo pnpm dev ``` How It Works [#how-it-works] ``` React Components → Custom Reconciler → In-Memory Tree → ANSI Terminal Output ``` The TUI renderer: 1. Uses `react-reconciler` to build an in-memory tree of terminal elements 2. Traverses the tree to compute layout (fixed positioning) 3. Renders to terminal using ANSI escape codes 4. Re-renders on state changes (full redraw) Available Components [#available-components] | Component | Description | | --------- | ------------------------------------------ | | `Box` | Container with optional border and padding | | `Text` | Styled text output (bold, color, etc.) | | `Button` | Highlighted text, selectable with keyboard | | `Input` | Single-line text input | | `Newline` | Line break | Example [#example] ```tsx import { useState } from "react"; import { Box, Text, Button } from "@uniview/tui-renderer"; export default function App() { const [count, setCount] = useState(0); return ( Counter: {count} ); } ``` Differences from Uniview Plugin System [#differences-from-uniview-plugin-system] The TUI renderer is a **standalone** package — it does not use the uniview plugin/host RPC architecture. It's a direct React-to-terminal renderer, useful for: * CLI tools built with React * Terminal dashboards * Learning how custom React reconcilers work * Demonstrating that React can target any output surface # @uniview/host-sdk The host-sdk package provides the infrastructure for loading and managing Uniview plugins, independent of any UI framework. Installation [#installation] ```bash pnpm add @uniview/host-sdk ``` Quick Start [#quick-start] ```typescript import { createWorkerController, createComponentRegistry, } from "@uniview/host-sdk"; // Create a registry for custom components const registry = createComponentRegistry(); registry.register("Button", MyButtonComponent); // Create a controller to load the plugin const controller = createWorkerController({ pluginUrl: "/plugins/my-plugin.js", initialProps: { userId: "123" }, }); // Subscribe to tree updates controller.subscribe((tree) => { console.log("New UI tree:", tree); // Render the tree using your framework }); // Connect to start the plugin await controller.connect(); ``` PluginController [#plugincontroller] The main interface for controlling plugins: ```typescript interface PluginController { // Lifecycle connect(): Promise; disconnect(): Promise; reload(): Promise; // Props updateProps(props: JSONValue): Promise; // Tree getTree(): UINode | null; subscribe(callback: (tree: UINode | null) => void): () => void; // Events execute(handlerId: HandlerId, args?: JSONValue[]): Promise; // Status getStatus(): { mode: HostMode; connected: boolean; lastError?: string; }; } ``` Controllers [#controllers] Worker Controller [#worker-controller] Load plugins in Web Workers for sandboxed execution: ```typescript import { createWorkerController } from "@uniview/host-sdk"; const controller = createWorkerController({ pluginUrl: "/plugins/dashboard.js", initialProps: { theme: "dark" }, }); await controller.connect(); ``` **Options:** | Option | Type | Description | | -------------- | ----------- | ------------------------------ | | `pluginUrl` | `string` | URL to the plugin bundle | | `initialProps` | `JSONValue` | Props passed to `initialize()` | WebSocket Controller [#websocket-controller] Connect to server-side plugins via WebSocket: ```typescript import { createWebSocketController } from "@uniview/host-sdk"; const controller = createWebSocketController({ serverUrl: "ws://localhost:3000", // Bridge server pluginId: "my-plugin", // Required - identifies which plugin to connect to initialProps: { userId: "abc" }, }); await controller.connect(); ``` **Options:** | Option | Type | Description | | -------------- | ----------- | -------------------------------------------------- | | `serverUrl` | `string` | Bridge server WebSocket URL | | `pluginId` | `string` | Plugin identifier (connects to `/host/{pluginId}`) | | `initialProps` | `JSONValue` | Props passed to `initialize()` | The host connects to `{serverUrl}/host/{pluginId}` where the Bridge server routes messages to the matching plugin. Main Thread Controller [#main-thread-controller] Run plugins in the main thread for development/debugging: ```typescript import { createMainController } from "@uniview/host-sdk"; import { SimpleDemo } from "@uniview/example-plugin"; const controller = createMainController({ App: SimpleDemo, initialProps: { debug: true }, }); await controller.connect(); ``` Main thread mode has no isolation. Use only for development. Component Registry [#component-registry] Map plugin component types to your framework's implementations: ```typescript import { createComponentRegistry } from "@uniview/host-sdk"; interface ComponentRegistry { register(type: string, component: T, metadata?: ComponentMetadata): void; get(type: string): T | undefined; has(type: string): boolean; list(): string[]; clear(): void; } const registry = createComponentRegistry(); // Register components registry.register("Button", Button); registry.register("Card", Card); registry.register("Modal", Modal); // Check availability if (registry.has("Button")) { const ButtonComponent = registry.get("Button"); } // List all registered console.log(registry.list()); // ['Button', 'Card', 'Modal'] ``` Layout Tags vs Custom Components [#layout-tags-vs-custom-components] * **Layout tags** (`div`, `span`, `p`, etc.) render as native elements * **Custom components** (PascalCase names) are looked up in the registry ```typescript import { isLayoutTag } from "@uniview/protocol"; function renderNode(node: UINode, registry: ComponentRegistry) { if (isLayoutTag(node.type)) { // Render as native element return createElement(node.type, node.props); } else if (registry.has(node.type)) { // Render registered component const Component = registry.get(node.type); return createElement(Component, node.props); } else { // Unknown component - show error console.warn(`Unknown component: ${node.type}`); } } ``` Event Execution [#event-execution] Handle events by executing handler IDs: ```typescript // When a user clicks a button in your rendered UI function handleClick(handlerId: string) { controller.execute(handlerId, []); } // With arguments (e.g., input change) function handleChange(handlerId: string, value: string) { controller.execute(handlerId, [value]); } ``` The handler ID is stored in props as `_on{Event}HandlerId`: ```typescript // UINode from plugin { type: "button", props: { _onClickHandlerId: "handler_abc123", className: "btn" }, children: ["Click me"] } ``` Tree Subscription [#tree-subscription] Subscribe to UI tree updates: ```typescript // Subscribe returns an unsubscribe function const unsubscribe = controller.subscribe((tree) => { if (tree) { renderUI(tree); } else { showEmptyState(); } }); // Clean up on unmount onDestroy(() => { unsubscribe(); controller.disconnect(); }); ``` Status Monitoring [#status-monitoring] Check controller status for UI feedback: ```typescript const status = controller.getStatus(); if (!status.connected) { showDisconnectedBanner(); } if (status.lastError) { showError(status.lastError); } console.log(`Mode: ${status.mode}`); // 'worker' | 'websocket' | 'main' ``` Framework Integration [#framework-integration] Svelte [#svelte] Use `@uniview/host-svelte` for ready-to-use components: ```svelte ``` React / Vue / Other [#react--vue--other] Implement your own renderer using the controller: ```typescript // 1. Create controller and registry const controller = createWorkerController({ pluginUrl: "/plugin.js" }); const registry = createComponentRegistry(); // 2. Subscribe to updates controller.subscribe((tree) => { // Re-render your UI with the new tree renderTree(tree, registry); }); // 3. Handle events function createEventHandler(handlerId: string) { return (...args: unknown[]) => { controller.execute(handlerId, args); }; } // 4. Connect on mount await controller.connect(); // 5. Disconnect on unmount controller.disconnect(); ``` Lifecycle [#lifecycle] createController createController --> connect connect --> subscribe subscribe --> updateProps subscribe --> execute subscribe --> disconnect updateProps --> subscribe execute --> subscribe disconnect --> [*] `} /> ``` createController() │ ▼ connect() ──────────────┐ │ │ ▼ │ subscribe(cb) ◄─────────┤ │ │ ▼ │ updateProps() ◄─────────┤ │ │ ▼ │ execute() ◄─────────────┤ │ │ ▼ │ disconnect() ───────────┘ ``` Always call `disconnect()` when done to clean up resources and prevent memory leaks. # @uniview/host-svelte The host-svelte package provides Svelte 5 components for rendering Uniview plugin UI trees using runes syntax. Installation [#installation] ```bash pnpm add @uniview/host-svelte @uniview/host-sdk @uniview/protocol ``` Quick Start [#quick-start] ```svelte {#snippet loading()}

Loading plugin...

{/snippet}
``` Components [#components] PluginHost [#pluginhost] Main component that manages plugin lifecycle and renders the UI tree. ```svelte {#snippet loading()}
Loading...
{/snippet}
``` **Props:** | Prop | Type | Description | | ------------ | ------------------- | ----------------------------------- | | `controller` | `PluginController` | Controller from `@uniview/host-sdk` | | `registry` | `ComponentRegistry` | Component registry for custom types | **Snippets:** | Snippet | Description | | --------- | ----------------------------------------- | | `loading` | Shown while waiting for first tree update | **Lifecycle:** 1. Connects to plugin on mount 2. Subscribes to tree updates 3. Re-renders on each tree change 4. Disconnects on destroy ComponentRenderer [#componentrenderer] Low-level component for rendering a single UINode. Used internally by PluginHost but available for custom rendering logic. ```svelte ``` ComponentRenderer requires context set by PluginHost. It reads `uniview:controller` and `uniview:registry` from Svelte context. Custom Components [#custom-components] Create Svelte 5 components that match plugin component types: ```svelte ``` Register in your host: ```typescript import { createComponentRegistry } from "@uniview/host-sdk"; import Button from "$lib/components/Button.svelte"; const registry = createComponentRegistry(); registry.register("Button", Button); ``` Event Handling [#event-handling] Events are automatically proxied from plugins to your components. How It Works [#how-it-works] 1. Plugin defines handler: `onClick={() => setCount(c => c + 1)}` 2. Serialized as: `{ _onClickHandlerId: "handler_abc123" }` 3. ComponentRenderer creates proxy: `onclick: () => controller.execute(id)` 4. Your component receives normal `onclick` prop Event Props Mapping [#event-props-mapping] | Plugin Prop | Svelte Prop | Handler ID Prop | | ----------- | ----------- | -------------------- | | `onClick` | `onclick` | `_onClickHandlerId` | | `onChange` | `onchange` | `_onChangeHandlerId` | | `onInput` | `oninput` | `_onInputHandlerId` | | `onSubmit` | `onsubmit` | `_onSubmitHandlerId` | Your components just receive normal event handlers - no special handling needed. Layout Tags [#layout-tags] These elements render as native Svelte elements via ``: ``` div, span, p, section, header, footer, h1, h2, h3, h4, h5, h6, ul, ol, li, br, hr, button, input, textarea, select, option, form, label, a ``` Custom component names (PascalCase) are looked up in the registry. Full Example [#full-example] ```svelte

Dashboard

{#snippet loading()}

Loading plugin...

{/snippet}
``` Svelte 5 Requirements [#svelte-5-requirements] This package uses Svelte 5 runes exclusively: | Feature | Syntax | | -------- | -------------------------------- | | Props | `let { prop } = $props()` | | State | `let value = $state(initial)` | | Derived | `let computed = $derived(expr)` | | Effects | `$effect(() => { ... })` | | Snippets | `{#snippet name()}...{/snippet}` | Svelte 4 syntax is not supported. Do not use `export let`, `$:`, or slot syntax. Context API [#context-api] PluginHost sets context for child components: ```typescript import { setContext } from "svelte"; // Set by PluginHost setContext("uniview:controller", controller); setContext("uniview:registry", registry); // Read in custom renderers import { getContext } from "svelte"; const controller = getContext("uniview:controller"); const registry = getContext("uniview:registry"); ``` Children Handling [#children-handling] UINode children can be either `UINode` objects or strings: ```svelte {#each node.children as child} {#if typeof child === 'string'} {child} {:else} {/if} {/each} ``` Never drop text children! Always handle the `string` case to render text content properly. # @uniview/protocol The protocol package defines the shared contract between plugins and hosts. Installation [#installation] ```bash pnpm add @uniview/protocol ``` UINode [#uinode] The serializable tree structure representing UI: ```typescript import type { UINode, JSONValue } from "@uniview/protocol"; interface UINode { id: string; type: string; props: Record; children: (UINode | string)[]; } ``` Layout Tags [#layout-tags] Built-in HTML-like elements that don't require registration: ```typescript import { LAYOUT_TAGS, isLayoutTag } from "@uniview/protocol"; // Available tags const tags = [ "div", "span", "p", "section", "header", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "br", "hr", "button", "input", "textarea", "select", "option", "form", "label", "a", ]; isLayoutTag("div"); // true isLayoutTag("Button"); // false ``` Event Props [#event-props] Utilities for handling event handler IDs: ```typescript import { EVENT_PROPS, handlerIdProp, isHandlerIdProp, extractEventName, } from "@uniview/protocol"; // Supported events EVENT_PROPS; // ['onClick', 'onChange', 'onInput', 'onSubmit', ...] // Convert event name to handler ID prop handlerIdProp("onClick"); // '_onClickHandlerId' // Check if a prop is a handler ID isHandlerIdProp("_onClickHandlerId"); // true isHandlerIdProp("className"); // false // Extract event name from handler ID prop extractEventName("_onClickHandlerId"); // 'onClick' ``` RPC Interfaces [#rpc-interfaces] Type definitions for plugin-host communication: ```typescript import type { HostToPluginAPI, PluginToHostAPI } from "@uniview/protocol"; // Methods host can call on plugin interface HostToPluginAPI { initialize(props?: JSONValue): Promise; updateProps(props: JSONValue): Promise; executeHandler(handlerId: string, args: JSONValue[]): Promise; destroy(): Promise; } // Methods plugin can call on host interface PluginToHostAPI { updateTree(tree: UINode | null): void; log(level: string, ...args: JSONValue[]): void; } ``` Validation [#validation] Zod schemas for runtime validation: ```typescript import { UINodeSchema, validateUINode } from "@uniview/protocol"; const tree = await fetchPluginTree(); // Validate at runtime const result = UINodeSchema.safeParse(tree); if (!result.success) { console.error("Invalid tree:", result.error); } ``` # @uniview/react-renderer The react-renderer package provides a custom React reconciler that produces serializable tree structures instead of DOM nodes. Installation [#installation] ```bash pnpm add @uniview/react-renderer react ``` Basic Usage [#basic-usage] ```typescript import { render, createRenderBridge, serializeTree, HandlerRegistry } from "@uniview/react-renderer"; // Create handler registry const registry = new HandlerRegistry(); // Create render bridge const bridge = createRenderBridge(); // Subscribe to tree updates bridge.subscribe((root) => { if (root) { const tree = serializeTree(root, registry); console.log('Tree updated:', tree); } }); // Render your React app render(, bridge); ``` HandlerRegistry [#handlerregistry] Manages the mapping between event handler functions and their IDs: ```typescript import { HandlerRegistry } from "@uniview/react-renderer"; const registry = new HandlerRegistry(); // Register a handler const id = registry.register(() => console.log("clicked")); // Returns something like "h_abc123" // Execute by ID await registry.execute(id, ["arg1", "arg2"]); // Clear all handlers (on plugin destroy) registry.clear(); ``` serializeTree [#serializetree] Converts the internal tree to protocol-compliant UINode: ```typescript import { serializeTree } from "@uniview/react-renderer"; bridge.subscribe((root) => { const tree = serializeTree(root, registry); // tree is now JSON-serializable // - Functions replaced with handler IDs // - Props validated for serializability }); ``` Internal Types [#internal-types] ```typescript // In-memory tree node (before serialization) interface InternalNode { id: string; type: string; props: Record; children: (InternalNode | TextNode)[]; parent: InternalNode | null; } // Text content node interface TextNode { _isTextNode: true; text: string; } ``` The reconciler has no DOM dependencies. It works in Web Workers, Node.js, Deno, and Bun. How It Works [#how-it-works] 1. React calls reconciler with element tree 2. Reconciler creates `InternalNode` for each element 3. Event handlers stored in `HandlerRegistry` 4. `serializeTree()` produces JSON-safe `UINode` 5. UINode sent to host via RPC # @uniview/react-runtime The runtime package bootstraps plugins in isolated environments (Web Workers, Node.js, Deno, Bun). There are two variants: `@uniview/react-runtime` for React plugins and `@uniview/solid-runtime` for Solid plugins. Both follow the same API pattern. Installation [#installation] ```bash # React plugins pnpm add @uniview/react-runtime # Solid plugins pnpm add @uniview/solid-runtime ``` Quick Start [#quick-start] Web Worker Plugin [#web-worker-plugin] ```typescript // worker.ts import { startWorkerPlugin } from "@uniview/react-runtime"; import App from "./App"; startWorkerPlugin({ App }); ``` ```tsx // App.tsx import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return (

Count: {count}

); } ```
```typescript // worker.ts import { startSolidWorkerPlugin } from "@uniview/solid-runtime"; import App from "./App"; startSolidWorkerPlugin({ App }); ``` ```tsx // App.tsx import { createSignal } from "solid-js"; const App = () => { const [count, setCount] = createSignal(0); return (

Count: {count()}

); }; export default App; ```
The runtime handles all RPC communication with the host. API [#api] startWorkerPlugin [#startworkerplugin] Bootstrap a plugin in a Web Worker with automatic RPC setup: ```typescript function startWorkerPlugin(options: { App: React.ComponentType }): void; ``` This function: 1. Creates an RPC channel using the Worker's `postMessage` 2. Exposes the `HostToPluginAPI` methods to the host 3. Sets up the React reconciler 4. Starts rendering when `initialize()` is called createPluginRuntime [#createpluginruntime] For advanced use cases, create a runtime with custom transport: ```typescript import { createPluginRuntime } from "@uniview/react-runtime"; import type { PluginToHostAPI } from "@uniview/protocol"; interface PluginRuntimeOptions { App: React.ComponentType; hostApi: PluginToHostAPI; onInitialize?: (props: JSONValue) => void; onUpdateProps?: (props: JSONValue) => void; } interface PluginRuntime { start(): void; stop(): void; executeHandler(handlerId: string, args: JSONValue[]): Promise; } const runtime = createPluginRuntime({ App: MyApp, hostApi: { updateTree: (tree) => sendToHost(tree), log: (level, args) => console.log(level, ...args), }, }); runtime.start(); ``` Entry Points [#entry-points] The package provides multiple entry points for different environments: | Entry | Import | Environment | | ---------------- | ---------------------------------- | --------------------------------------- | | Default | `@uniview/react-runtime` | Web Worker, Main thread | | WebSocket Client | `@uniview/react-runtime/ws-client` | Node.js, Deno, Bun (connects to Bridge) | WebSocket Client Plugin (Recommended) [#websocket-client-plugin-recommended] For server-side plugins that connect to a Bridge server: ```typescript // server-plugin.ts import { connectToHostServer } from "@uniview/react-runtime/ws-client"; import App from "./App"; connectToHostServer({ App, serverUrl: "ws://localhost:3000", pluginId: "my-plugin", }); ``` ```typescript // server-plugin.ts import { createSolidWebSocketPluginClient } from "@uniview/solid-runtime/ws-client"; import App from "./App"; createSolidWebSocketPluginClient({ App, serverUrl: "ws://localhost:3000", pluginId: "my-plugin", }); ``` Solid WebSocket plugins must be **built first** (Babel transform), then the built output is run with Bun. Use `conditions: ["browser"]` in the Bun build config to avoid resolving to solid-js's SSR build. The plugin connects to `{serverUrl}/plugins/{pluginId}` and waits for a host to connect. Plugin Lifecycle [#plugin-lifecycle] Initialization Flow [#initialization-flow] B[Runtime creates:
- React reconciler
- Handler registry
- Tree serializer] B --> C[React renders App
with initial props] C --> D[Tree serialized to
UINode + handler IDs] D --> E[hostApi.updateTree] `} /> Event Handling Flow [#event-handling-flow] B[Registry finds handler
by ID and executes it] B --> C[React state updates
trigger re-render] C --> D[hostApi.updateTree] `} />
``` Host calls initialize(props) │ ▼ ┌─────────────────────────┐ │ Runtime creates: │ │ - React reconciler │ │ - Handler registry │ │ - Tree serializer │ └───────────┬─────────────┘ │ ▼ ┌─────────────────────────┐ │ React renders App │ │ with initial props │ └───────────┬─────────────┘ │ ▼ ┌─────────────────────────┐ │ Tree serialized to │ │ UINode + handler IDs │ └───────────┬─────────────┘ │ ▼ hostApi.updateTree() ``` When events occur: ``` Host calls executeHandler(id, args) │ ▼ ┌─────────────────────────┐ │ Registry finds handler │ │ by ID and executes it │ └───────────┬─────────────┘ │ ▼ ┌─────────────────────────┐ │ React state updates │ │ trigger re-render │ └───────────┬─────────────┘ │ ▼ hostApi.updateTree() ```
Building Plugins [#building-plugins] Bundle your plugin for worker execution: ```typescript // tsdown.config.ts import { defineConfig } from "tsdown"; export default defineConfig({ entry: ["src/worker.ts"], format: ["esm"], platform: "browser", outDir: "dist", clean: true, }); ``` Plugins run in isolated environments. Do not use `window`, `document`, or other browser-only APIs. Environment Restrictions [#environment-restrictions] Since plugins run in Web Workers or server environments: | Allowed | Not Allowed | | --------------- | ------------------------------- | | React hooks | DOM APIs (`document`, `window`) | | `fetch()` | `localStorage` | | `console.log()` | Direct DOM manipulation | | Async/await | Browser-specific APIs | | Web Worker APIs | Node.js APIs (in worker mode) | Handler Execution [#handler-execution] Event handlers are serialized as IDs and executed via RPC: ```tsx // In your plugin // Becomes UINode: { type: "button", props: { _onClickHandlerId: "handler_abc123" }, children: ["Click"] } ``` The runtime maintains a `HandlerRegistry` that maps IDs back to the original functions.