Files
WRNexusJS/packages/csr
ClintchizandClaude Opus 5 7ac6e08544 fix(csr): make dialog visibility testable and cover the focus trap
The focus trap and scroll lock shipped in 0.8.5 gated on
getBoundingClientRect, which the test DOM always reports as zero, so a dialog
never counted as open and none of that behaviour ran under test. focusableWithin
had the same measurement gate and would have found no items even once the
visibility check was fixed.

Both now use the hidden attribute and the data-show marker the components
already emit. Behaviour in a real browser is unchanged; the difference is that
it is now covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:40:30 +05:30
..
2026-08-07 15:08:05 +05:30
2026-08-02 23:18:51 +05:30

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

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

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 = 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 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 (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 WrNexus dev/prod server.