page wrnexuscsr { seo { title = "@wrnexus/csr" description = "Reactive, navigation, and realtime browser runtimes." } view {
W WRNexusJS
Browse documentation
Frontend · Preview

@wrnexus/csr

Reactive, navigation, and realtime browser runtimes.

Private registry access required

This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:

bun add @wrnexus/csr@0.2.22

Request preview access. Never put registry tokens in source control.

The browser-side client runtime for WRNexusJS — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms.

Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.

Overview

@wrnexus/csr holds the three client runtimes that WRNexusJS serves to the browser. Components are authored as .wrn files and rendered on the server; this package provides the single, generic runtime that hydrates that HTML in the browser — there are no per-component browser bundles. Each runtime is exported as a plain-JS string (no build step, no imports) intended to be served verbatim from a well-known URL:

  • reactive at /__wrnexus/reactive.js — reactive directives (data-scope, data-text, data-for, …)
  • nav at /__wrnexus/nav.js — SPA-style client navigation with graceful fallback
  • realtime at /__wrnexus/realtime.js — WebSocket "rooms", declarative or programmatic

The package itself runs on the server (it just returns strings); the strings it returns run in the browser. A dev/prod server (see @wrnexus/core) is responsible for actually serving them.

bun add @wrnexus/csr
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).

API

All exports come from the package root (@wrnexus/csr). The runtime source is delivered as strings, so the "API" on the server side is small; the real surface is the browser directives/globals each string installs.

Runtime strings

ExportTypeServed atContents
REACTIVE_RUNTIMEstring/__wrnexus/reactive.jsReactive directive runtime
NAV_RUNTIMEstring/__wrnexus/nav.jsClient-side navigation runtime
REALTIME_RUNTIMEstring/__wrnexus/realtime.jsRealtime rooms runtime

Accessor functions

Convenience getters that return the same strings.

getReactiveRuntime(): string   // → REACTIVE_RUNTIME
getNavRuntime(): string        // → NAV_RUNTIME
getRealtimeRuntime(): string   // → REALTIME_RUNTIME

Browser: reactive directives

Applied to any subtree containing data-scope. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no unsafe-eval works.

DirectivePurpose
data-scope="count: 0, name: 'x'"Declare reactive state on a subtree
data-on-<event>="count++"Run a statement in scope on a DOM event
data-text="expr"Bind an element's textContent to an expression
data-show="expr"Toggle visibility (display) on truthiness
data-for="item in list" (opt. item, i in list)Per-item list rendering template
{{expr}} or {expr}Interpolation inside text nodes and attribute values
data-wrnexus-csr="id"Target for a generated CSR fetch binding (fetches /__wrnexus/csr?...)

Supported expression features: literals, identifiers, member access (a.b, a[b]), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (&& ||), unary (! - +), and ternary. Statements support ++/--, assignment operators (= += -= *= /= %=), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it.

Browser globals installed: window.__wrnexusHydrateScopes(root) and window.__wrnexusHydrateCsrFetches(root) — both idempotent, so re-running after a DOM swap or HMR morph is safe. Both run automatically on DOMContentLoaded.

Browser: navigation

Intercepts same-origin <a> clicks, fetches the target page, and swaps the #app container in place (via importNode — not innerHTML — so it works under a Trusted-Types CSP), updating history, title, and scroll, then re-hydrates. Cross-origin links, modified clicks, download/data-no-nav/rel="external"/target links, non-HTML responses, or a missing #app fall back to a full browser navigation.

  • Programmatic navigation: window.__wrnexusNavigate(url)
  • Emits a wrnexus:navigated CustomEvent (detail.url) after each swap
  • Sends x-wrnexus-nav: 1 on fetches so the server can return the page fragment
  • Appends any /__wrnexus/* runtime scripts the incoming page needs but the current document lacks

Browser: realtime rooms

Connects to /realtime/<name> over WebSocket (ws/wss chosen from location.protocol). Two usage modes.

Programmatic API via window.wire:

wire.room(name): Room          // open (or reuse) a room connection
wire.bindRooms(root?)          // (re)bind declarative [data-room] containers

interface Room {
  name: string;
  send(obj: object | string): Room;         // JSON-stringifies objects; queues until open
  on(type: string, cb): Room;               // filter by msg.type; "*" or a fn = all messages
  on(cb): Room;
  close(): Room;
}

Internal lifecycle messages are emitted to listeners as { type }: __open, __close, __error, and __raw (non-JSON frames, with data). Reconnect uses exponential backoff capped at 5s; queued sends flush on reconnect.

Declarative binding (zero JS) on a data-room="<name>" container:

AttributeOnPurpose
data-room="<name>"containerConnect to room <name>
data-room-user="<id>"containerIdentify the connection (?user=<id>)
data-room-logelementWhere incoming messages are appended
<template data-room-item="<type>">templateRow template for messages of that type (empty = fallback)
%field%inside templatePlaceholder filled from the message field (text/attr only, HTML-escaped)
data-room-statuselementReflects connection state text (connected/disconnected/error)
data-room-status-classstatus elementBase class; a state variant (is-connected, …) is appended
<form data-room-send>formSubmits named fields as a JSON message
data-room-resetform fieldClears that field after send

Rebinds on wrnexus:navigated and closes rooms whose container has left the page.

Usage

Server side — serve the runtime strings from your router (example with Bun.serve):

import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";

const routes: Record<string, string> = {
  "/__wrnexus/reactive.js": getReactiveRuntime(),
  "/__wrnexus/nav.js": getNavRuntime(),
  "/__wrnexus/realtime.js": getRealtimeRuntime(),
};

Bun.serve({
  fetch(req) {
    const body = routes[new URL(req.url).pathname];
    if (body) {
      return new Response(body, {
        headers: { "content-type": "text/javascript; charset=utf-8" },
      });
    }
    return new Response("Not found", { status: 404 });
  },
});

Browser side — server-rendered HTML that the reactive runtime hydrates:

<div data-scope="count: 0">
  <button data-on-click="count++">+1</button>
  <span data-text="count"></span>
  <p>Total: {{count}}</p>
</div>
<script src="/__wrnexus/reactive.js"></script>

A realtime chat, fully declarative:

<div data-room="lobby" data-room-user="ada">
  <div data-room-status></div>
  <ul data-room-log></ul>
  <template data-room-item="chat"><li>%user%: %text%</li></template>
  <form data-room-send>
    <input name="text" data-room-reset />
    <input type="hidden" name="type" value="chat" />
    <button>Send</button>
  </form>
</div>
<script src="/__wrnexus/realtime.js"></script>

Or drive a room from code:

const room = wire.room("lobby");
room.on("chat", (msg) => console.log(msg.user, msg.text));
room.send({ type: "chat", user: "ada", text: "hi" });

Requirements / Notes

  • Bun-only on the server (the package integrates with Bun-based WRNexusJS servers); the emitted strings are plain browser JS with no dependencies.
  • Browser runtimes are self-contained (no imports, no build step) and idempotent, so re-hydration after navigation or HMR is safe.
  • Designed for a strict CSP: the reactive expression evaluator avoids eval/new Function (no unsafe-eval), and DOM swaps use importNode/attribute writes rather than innerHTML (Trusted-Types friendly).
  • Peer packages: rendered .wrn components and the serving layer come from @wrnexus/core (the sole dependency); pages are rendered by the WRNexusJS dev/prod server.

Complete TypeScript API

This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.

/**
 * Browser reactive runtime (Point 2: reactive directives).
 *
 * Served verbatim at `/__wrnexus/reactive.js` for any page that contains a
 * `data-scope`. It is plain browser JS (no build step) and self-contained: it
 * inlines a tiny `signal()` so it has no imports to resolve.
 *
 * Supported directives (this is exactly what the `.wrn` compiler emits):
 *   data-scope="count: 0, name: 'x'"   declare reactive state on a subtree
 *   data-on-<event>="count++"          run a statement in scope on an event
 *   data-text="expr"                   element textContent follows an expression
 *   data-wrnexus-csr="id"                 target for generated CSR fetch bindings
 *   {{expr}} or {expr}                 interpolation inside text nodes
 *
 * Expressions are evaluated by a tiny parser instead of `eval`/`new Function`,
 * so production can use a strong CSP without `unsafe-eval`.
 */
declare const REACTIVE_RUNTIME: string;

/**
 * Client-side navigation runtime, served at `/__wrnexus/nav.js`.
 *
 * Progressive enhancement over normal links: intercepts same-origin `<a>`
 * clicks, fetches the target page's HTML, swaps the `#app` container in place,
 * updates history/title/scroll, ensures any framework runtimes the new page
 * needs are present, and re-hydrates. Anything unexpected (cross-origin,
 * modified click, non-HTML response, missing `#app`) falls back to a full
 * browser navigation, so behaviour degrades safely.
 *
 * Data "loaders": pages load their data on the server (SSR `api` bindings), so
 * the fetched HTML already contains fresh data — no separate client loader is
 * needed. Client-side (`csr`) bindings and reactive scopes re-hydrate after the
 * swap. Programmatic navigation is exposed as `window.__wrnexusNavigate(url)`.
 */
declare const NAV_RUNTIME: string;

/**
 * Client realtime runtime, served at `/__wrnexus/realtime.js`.
 *
 * Two ways to use it — no hand-written WebSocket code either way:
 *
 * 1. Declarative (zero JS). Put `data-room="<name>"` on a container; the runtime
 *    connects, appends incoming messages to `[data-room-log]` using a
 *    `<template data-room-item="<type>">` (fields via `%field%`, HTML-escaped),
 *    reflects connection state on `[data-room-status]`, and sends a
 *    `<form data-room-send>`'s named fields as JSON on submit (fields marked
 *    `data-room-reset` clear after send). Optional `data-room-user` identifies
 *    the connection.
 *
 * 2. Programmatic: `const room = wire.room("chat"); room.on("chat", fn);
 *    room.send({ type: "chat", text })`. Handles connect, JSON, reconnect.
 *
 * Rebinds on `wrnexus:navigated` (client-side nav) and closes rooms whose
 * container has left the page.
 */
declare const REALTIME_RUNTIME: string;

/**
 * @wrnexus/csr — the browser reactive runtime.
 *
 * Components are `.wrn` files rendered on the SERVER (see @wrnexus/dev-server)
 * and hydrated in the browser by this single, generic runtime — served once at
 * `/__wrnexus/reactive.js` for any page that contains a `data-scope`. There are
 * no per-component browser bundles: SSR stays cleanly separated from CSR.
 */

/** The reactive runtime served at `/__wrnexus/reactive.js` (plain browser JS). */
declare function getReactiveRuntime(): string;
/** The client-side navigation runtime served at `/__wrnexus/nav.js`. */
declare function getNavRuntime(): string;
/** The realtime client runtime served at `/__wrnexus/realtime.js`. */
declare function getRealtimeRuntime(): string;

export { NAV_RUNTIME, REACTIVE_RUNTIME, REALTIME_RUNTIME, getNavRuntime, getReactiveRuntime, getRealtimeRuntime };

Examples

Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.

Example 1

bun add @wrnexus/csr

Example 2

getReactiveRuntime(): string   // → REACTIVE_RUNTIME
getNavRuntime(): string        // → NAV_RUNTIME
getRealtimeRuntime(): string   // → REALTIME_RUNTIME

Example 3

wire.room(name): Room          // open (or reuse) a room connection
wire.bindRooms(root?)          // (re)bind declarative [data-room] containers

interface Room {
  name: string;
  send(obj: object | string): Room;         // JSON-stringifies objects; queues until open
  on(type: string, cb): Room;               // filter by msg.type; "*" or a fn = all messages
  on(cb): Room;
  close(): Room;
}

Example 4

import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";

const routes: Record<string, string> = {
  "/__wrnexus/reactive.js": getReactiveRuntime(),
  "/__wrnexus/nav.js": getNavRuntime(),
  "/__wrnexus/realtime.js": getRealtimeRuntime(),
};

Bun.serve({
  fetch(req) {
    const body = routes[new URL(req.url).pathname];
    if (body) {
      return new Response(body, {
        headers: { "content-type": "text/javascript; charset=utf-8" },
      });
    }
    return new Response("Not found", { status: 404 });
  },
});
} }