Uniview

@uniview/react-runtime

Plugin-side runtime for bootstrapping React plugins in Web Workers and server environments

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

# React plugins
pnpm add @uniview/react-runtime

# Solid plugins
pnpm add @uniview/solid-runtime

Quick Start

Web Worker Plugin

// worker.ts
import { startWorkerPlugin } from "@uniview/react-runtime";
import App from "./App";

startWorkerPlugin({ App });
// App.tsx
import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div className="p-4">
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}
// worker.ts
import { startSolidWorkerPlugin } from "@uniview/solid-runtime";
import App from "./App";

startSolidWorkerPlugin({ App });
// App.tsx
import { createSignal } from "solid-js";

const App = () => {
  const [count, setCount] = createSignal(0);

  return (
    <div className="p-4">
      <p>Count: {count()}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
};

export default App;

The runtime handles all RPC communication with the host.

API

startWorkerPlugin

Bootstrap a plugin in a Web Worker with automatic RPC setup:

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

For advanced use cases, create a runtime with custom transport:

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<void>;
}

const runtime = createPluginRuntime({
  App: MyApp,
  hostApi: {
    updateTree: (tree) => sendToHost(tree),
    log: (level, args) => console.log(level, ...args),
  },
});

runtime.start();

Entry Points

The package provides multiple entry points for different environments:

EntryImportEnvironment
Default@uniview/react-runtimeWeb Worker, Main thread
WebSocket Client@uniview/react-runtime/ws-clientNode.js, Deno, Bun (connects to Bridge)
WebSocket Server@uniview/react-runtime/ws-serverNode.js, Deno, Bun (Deprecated)

For server-side plugins that connect to a Bridge server:

// server-plugin.ts
import { connectToHostServer } from "@uniview/react-runtime/ws-client";
import App from "./App";

connectToHostServer({
  App,
  serverUrl: "ws://localhost:3000",
  pluginId: "my-plugin",
});
// 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.

WebSocket Server Plugin

Deprecated: Running plugins as WebSocket servers is deprecated. Use @uniview/react-runtime/ws-client to connect to a Bridge server instead. This approach simplifies deployment and port management.

For server-side plugins that communicate over WebSocket:

// server-plugin.ts
import { startWSServerPlugin } from "@uniview/react-runtime/ws-server";
import App from "./App";

startWSServerPlugin({
  App,
  port: 3001,
});

Plugin Lifecycle

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

Bundle your plugin for worker execution:

// 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

Since plugins run in Web Workers or server environments:

AllowedNot Allowed
React hooksDOM APIs (document, window)
fetch()localStorage
console.log()Direct DOM manipulation
Async/awaitBrowser-specific APIs
Web Worker APIsNode.js APIs (in worker mode)

Handler Execution

Event handlers are serialized as IDs and executed via RPC:

// In your plugin
<button onClick={() => setCount(c => c + 1)}>Click</button>

// Becomes UINode:
{
  type: "button",
  props: {
    _onClickHandlerId: "handler_abc123"
  },
  children: ["Click"]
}

The runtime maintains a HandlerRegistry that maps IDs back to the original functions.

On this page