Client control blocks and loops, the dev-server rebuild recycle, the editor's tag and completion handling, and the island mount/HMR fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/csr
Navigation state preservation
Pages can opt into restoration across client navigation:
page Users {
navigation {
preserve = ["filters", "pagination", "scroll", "tabs", "expanded"]
}
}
Form-like categories restore named inputs, selects, and textareas. Password,
file, hidden, CSRF/token/secret/credential fields, and elements marked
data-no-preserve are never saved. For tab, expanded, or component UI state,
mark stable elements with data-wrn-preserve="key"; their value and ARIA
selected/expanded state are restored. State is scoped to pathname plus query.
Typed server actions
createActionClient<Input, Output>(route, name) supports programmatic calls.
Schema-backed WRN actions also export __wrnexusActionClients, whose input and
output are inferred automatically. Enhanced forms expose
data-wrn-action-state="pending|success|error" and dispatch bubbling
wrnexus:action:optimistic, :pending, :success, and :error events.
Success details contain returned data and invalidated cache tags; error details
contain field errors. Without JavaScript, the same form posts to its page and
receives a 303 redirect or accessible validation response.
The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms.
Part of the WrNexus framework — an SSR-first, Bun-native full-stack web framework.
Overview
@wrnexus/csr holds the three client runtimes that WrNexus 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.
Installation
bun add @wrnexus/csr
Private package — the machine must be authenticated to the
wrnexusnpm 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
| Export | Type | Served at | Contents |
|---|---|---|---|
REACTIVE_RUNTIME |
string |
/__wrnexus/reactive.js |
Reactive directive runtime |
NAV_RUNTIME |
string |
/__wrnexus/nav.js |
Client-side navigation runtime |
REALTIME_RUNTIME |
string |
/__wrnexus/realtime.js |
Realtime 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.
| Directive | Purpose |
|---|---|
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 while preserving interactive state |
Compiled conditional rendering and dynamic component cases omit inactive elements from the live
DOM. data-show is a visibility directive for stateful controls and keeps its element mounted.
Neither mechanism is authorization: never place secrets in client-rendered branches. Authorize on
the server and return only data the current request may access.
| data-for="item in list" (opt. index and key item.id) | Per-item rendering; stable keys preserve DOM identity during reorder |
| data-key="item.id" | Alternative key declaration for data-for templates |
| {{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:navigatedCustomEvent(detail.url) after each swap - Sends
x-wrnexus-nav: 1on 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.wrn:
wrn.room(name): Room // open (or reuse) a room connection
wrn.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:
| Attribute | On | Purpose |
|---|---|---|
data-room="<name>" |
container | Connect to room <name> |
data-room-user="<id>" |
container | Identify the connection (?user=<id>) |
data-room-log |
element | Where incoming messages are appended |
<template data-room-item="<type>"> |
template | Row template for messages of that type (empty = fallback) |
%field% |
inside template | Placeholder filled from the message field (text/attr only, HTML-escaped) |
data-room-status |
element | Reflects connection state text (connected/disconnected/error) |
data-room-status-class |
status element | Base class; a state variant (is-connected, …) is appended |
<form data-room-send> |
form | Submits named fields as a JSON message |
data-room-reset |
form field | Clears 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, showPassword: false">
<button data-on-click="count++">+1</button>
<span data-text="count"></span>
<p>Total: {{count}}</p>
<input type="{showPassword ? 'text' : 'password'}" />
<button
data-on-click="showPassword = !showPassword"
aria-label="{showPassword ? 'Hide password' : 'Show password'}"
>
Toggle password
</button>
</div>
<script src="/__wrnexus/reactive.js"></script>
State interpolation in ordinary attributes is reactive. The compiler keeps the
initial SSR value and emits an internal binding so attributes such as type,
aria-label, aria-pressed, class, and href update after state changes.
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 = wrn.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 WrNexus 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(nounsafe-eval), and DOM swaps useimportNode/attribute writes rather thaninnerHTML(Trusted-Types friendly). - Peer packages: rendered
.wrncomponents and the serving layer come from@wrnexus/core(the sole dependency); pages are rendered by the WrNexus dev/prod server.