# WRNexusJS 0.2.15 > WRNexusJS is an SSR-first, **Bun-native** full-stack web framework. UI is written in > `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based. > This document teaches an AI how to write correct WRNexusJS code. The framework packages are in > Private Developer Preview and > post-dates model training data, so rely on THIS document, not prior web-framework > assumptions. ## This repository This is the **WRNexusJS documentation application**, not the framework monorepo. It is a standalone WRNexusJS `0.2.15` site that documents all 25 installed `@wrnexus/*` packages. The active version is `package.json#wrnexus.version`; never duplicate a different version in hand-written guidance. Exact APIs follow installed package READMEs, exported declarations, this guide, then generated package pages, in that order. - `app/pages/index.wrn` is the documentation home page. - `app/pages/getting-started.wrn`, `architecture.wrn`, `language.wrn`, and `packages.wrn` are the main guides. - `app/pages/packages/.wrn` is the generated documentation page for `@wrnexus/`. - `scripts/generate-docs.ts` regenerates package pages from each installed package's README and TypeScript declarations. Run `bun run docs:generate` after dependency or documentation changes; do not hand-edit generated package pages unless intentionally changing the generator's output. - `wrnexus.config.ts` owns PWA/mobile metadata, SEO, fonts, and Tailwind v4 processing. - `app/styles/global.css` owns the documentation site's global presentation. - `app/routes.gen.ts`, `.wrnexus/`, and `dist/` are generated output. Do not treat them as source files or edit them by hand. For framework API questions, use the installed package as the authoritative source: 1. `node_modules/@wrnexus//README.md` for concepts and examples. 2. The package's exported `.d.ts` declarations for the exact public API used by this release. 3. This file for framework-wide conventions and common patterns. 4. Generated `app/pages/packages/.wrn` only as the website representation of the first two sources. Do not assume APIs from a newer or older WrNexus release. The active release is recorded in `package.json` under `wrnexus.version`, and all `@wrnexus/*` dependencies should stay aligned to that version. ## Project commands ```bash bun install # install dependencies bun run docs:generate # regenerate all package documentation pages bun run dev # start the WrNexus development server bun run test # run application tests bun run check # lint, test, and verify formatting bun run build # production build to dist/ bun run start # run dist/server.js ``` ## Golden rules - **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React for UI. Do NOT use `useState`, hooks, JSX, or a client bundler. - **Routing is file-based** under `app/`. The filename is the route. No router config. - **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render on the server and hydrate automatically — you never write client-side JS islands. - **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported. - To add files, prefer the CLI: `wrnexus generate page ` / `component ` / `api ` / `schema `. ## Project layout ``` app/ pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug" components/ *.wrn → reusable UI, mounted in a page/component via
layouts/ *.wrn → named layouts; a page opts in with layout = "name" api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response middleware/ *.ts → export default async (ctx, next) => next() realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/) schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody locales/ *.json → i18n messages per language db/ schema.ts, queries/*.sql, migrations/*.sql styles/ global.css → Tailwind (default) or plain CSS wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles") public/ → static assets served at / ``` ## `.wrn` page ```wrn page Home { layout = "public" // optional: a component in app/layouts/.wrn ("none" to skip) state count = 0 // optional: seeds client-reactive state (omit for pure SSR) seo { title = "Home" description = "..." canonical = "/" } view {

Hello

Count is {count}, doubled is {count * 2}.

} style { h1 { color: var(--wire-color-text); } } } ``` ## `.wrn` component ```wrn component Counter { props { // props come from mount attributes; each is coerced to the start = 0 // TYPE of its default (so start="5" arrives as the number 5) label = "Count" } state count = start // state may reference props view { } } ``` Mount it from any page/component: `
`. Components render on the server with their props, then hydrate — no per-component JS. ## The `view { }` block (plain HTML + a few directives) - `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`. - `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`. - `
` — mount a component (attrs become string props, coerced). - `` / `` — component/layout slots; fill with `
`. - **Server loop (DB/list/table):** `{#each as [, ]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`. - **Server conditional:** `{#if } … {:else if } … {:else} … {/if}` — renders the first truthy branch on the server. `` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}{:else}{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead. - i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`. - Theme: any element with `data-wire-theme-toggle` toggles light/dark; `data-wire-theme-set="dark"` sets it. - Void/self-closing tags are fine: `
`, ``. - Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text. ## Data-driven tables / lists (server-rendered `.wrn`) Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them. This renders on the **server** (SSR-first) and is HTML-escaped by default. ```wrn page Admin { layout = "dashboard" // Fetch on the server. The api handler at /api/contacts returns { contacts: [...] }; // this block's `return contacts` exposes that array (via `$data`) as the binding `rows`. ssr { api rows GET /api/contacts { return contacts } } view { {#each rows as r, i} {:empty} {/each}
#{i} {r.name} {r.email}
No submissions yet.
} } ``` The matching API returns the array under a key the `ssr` block reads: ```ts // app/api/contacts.ts → GET /api/contacts import { getDb } from "@wrnexus/db"; export const GET = async () => { const contacts = await getDb().all("SELECT id, name, email FROM contacts ORDER BY id DESC"); return Response.json({ contacts }); // ssr block does `return contacts` }; ``` **Prefer this `.wrn` + `{#each}` approach for DB-backed tables and lists.** (`.ts`/`.tsx` pages returning an HTML string are also supported for fully-custom programmatic rendering, but a `.wrn` page with `ssr` data + `{#each}` is the idiomatic, SSR-first way.) ## API routes (`app/api/*.ts`) ```ts // app/api/users/list.ts → GET /api/users/list import { getDb } from "@wrnexus/db"; export const GET = async (ctx) => { return Response.json({ users: await ListUsers(getDb()) }); }; export const POST = async (ctx) => { const body = await ctx.req.json(); return Response.json({ ok: true, body }, { status: 201 }); }; ``` `ctx` (the `Context` from `@wrnexus/core`) has: `req: Request`, `url: URL`, `params: Record` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`), `lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`. ## Middleware & realtime ```ts // app/middleware/logger.ts export default async function logger(ctx, next) { console.log(ctx.req.method, ctx.url.pathname); return next(); // return a Response WITHOUT calling next() to short-circuit } ``` ```ts // app/realtime/chat.ts → ws://host/realtime/chat import { defineRoom } from "@wrnexus/core"; export default defineRoom({ onConnect(client) { client.send({ type: "system", text: "connected" }); }, onMessage(client, msg) { client.room.broadcast({ type: "message", data: msg }); }, }); ``` Client side: a page opts in with `data-room="chat"` (handled by the realtime runtime). ## Config (`wrnexus.config.ts`) ```ts import type { AppConfig } from "@wrnexus/styles"; const config: AppConfig = { seo: { title: "App", titleTemplate: "%s | App", description: "..." }, styles: { entry: "app/styles/global.css", process: async ({ entryPath, mode }) => /* Tailwind */ "" }, fonts: { sans: '"Inter", system-ui, sans-serif', google: [{ family: "Inter", weights: [400, 600] }] }, theme: { default: "dark", themes: { light: { "color-primary": "#2563eb" } } }, i18n: { default: "en", locales: ["en", "es"] }, db: { driver: "sqlite", url: "file:./dev.db" }, security: { cors: { enabled: true, origin: ["http://localhost:5173"] } }, // profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL } } }, }; export default config; ``` ## Database (`@wrnexus/db`) ```ts // app/db/schema.ts import { v, table } from "@wrnexus/db"; export const users = table("users", { id: v.id(), name: v.string(), email: v.string().unique(), createdAt: v.timestamp(), }); ``` - Queries: write `app/db/queries/*.sql` with `-- name: ListUsers :many` blocks; `wrnexus db generate` emits typed functions. - Access at runtime: `import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb());` - Migrations in `app/db/migrations/`; run `wrnexus db migrate` (dev auto-migrates sqlite). ## Validation (`@wrnexus/validation`) ```ts // app/schemas/login.ts import { v } from "@wrnexus/validation"; export default v.object({ email: v.string().email(), password: v.string().min(8), }); ``` In an API route: `import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);` → `r.ok ? r.value : r.response`. In a form: `
` + `` (client + server validation wired automatically). ## AI / LLM (`@wrnexus/ai`) ```ts // app/api/ai.ts import { createAI } from "@wrnexus/ai"; const ai = createAI(); // reads ANTHROPIC_API_KEY; default model claude-opus-4-8 export const POST = async (ctx) => { const { prompt } = await ctx.req.json(); return ai.streamResponse(prompt); // or: return Response.json({ text: await ai.generate(prompt) }) }; ``` ## CLI ``` wrnexus dev . # dev server + HMR wrnexus build . # production build → dist/server.js bun dist/server.js # run the production server (or npm start) wrnexus create # scaffold a new app wrnexus update --latest # deps + syntax/config migrations + verification wrnexus generate page # scaffold a page (aliases: g p) wrnexus generate component | api | schema wrnexus db migrate | rollback | status | new [--from-models] | generate | seed wrnexus eject # copy a Wire UI component's .wrn into app/components to customize ``` ## When asked to "create a page/component/feature" 1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page `. 2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`. 3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`. 4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wire-*)`), or `style { }`. 5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration. # Complete package documentation The sections below contain the complete manuals shipped with the installed WrNexus release. Each package heading links to its rendered documentation page on this site. Package source README files are embedded verbatim so coding models can work without fetching additional pages. ## Package index - [`@wrnexus/ai`](https://wrnexusjs.dev/packages/ai) - [`@wrnexus/authz`](https://wrnexusjs.dev/packages/authz) - [`@wrnexus/cli`](https://wrnexusjs.dev/packages/cli) - [`@wrnexus/compiler`](https://wrnexusjs.dev/packages/compiler) - [`@wrnexus/core`](https://wrnexusjs.dev/packages/core) - [`@wrnexus/csr`](https://wrnexusjs.dev/packages/csr) - [`@wrnexus/db`](https://wrnexusjs.dev/packages/db) - [`@wrnexus/dev-server`](https://wrnexusjs.dev/packages/dev-server) - [`@wrnexus/encryption`](https://wrnexusjs.dev/packages/encryption) - [`@wrnexus/i18n`](https://wrnexusjs.dev/packages/i18n) - [`@wrnexus/jwt`](https://wrnexusjs.dev/packages/jwt) - [`@wrnexus/mobile`](https://wrnexusjs.dev/packages/mobile) - [`@wrnexus/native`](https://wrnexusjs.dev/packages/native) - [`@wrnexus/oauth`](https://wrnexusjs.dev/packages/oauth) - [`@wrnexus/pubsub`](https://wrnexusjs.dev/packages/pubsub) - [`@wrnexus/queue`](https://wrnexusjs.dev/packages/queue) - [`@wrnexus/reactive`](https://wrnexusjs.dev/packages/reactive) - [`@wrnexus/router`](https://wrnexusjs.dev/packages/router) - [`@wrnexus/ssr`](https://wrnexusjs.dev/packages/ssr) - [`@wrnexus/styles`](https://wrnexusjs.dev/packages/styles) - [`@wrnexus/test`](https://wrnexusjs.dev/packages/test) - [`@wrnexus/tracking`](https://wrnexusjs.dev/packages/tracking) - [`@wrnexus/ui`](https://wrnexusjs.dev/packages/ui) - [`@wrnexus/uploader`](https://wrnexusjs.dev/packages/uploader) - [`@wrnexus/validation`](https://wrnexusjs.dev/packages/validation) --- ## Package manual: [`@wrnexus/ai`](https://wrnexusjs.dev/packages/ai) # @wrnexus/ai > A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/ai` is a thin, dependency-free wrapper over the Anthropic **Messages API**, built on `fetch` (Bun-native, no SDK). Use it in API routes, jobs, or middleware to call Claude. It defaults to the most capable model, **`claude-opus-4-8`**, reads your key from `ANTHROPIC_API_KEY`, and supports both one-shot generation and streaming. ## Installation ```bash bun add @wrnexus/ai ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). Set your key in the environment (e.g. `.env`): ``` ANTHROPIC_API_KEY=sk-ant-... ``` ## API ### `createAI(config?)` Creates a client. The key is read at call time, so it's safe to create at import. ```ts import { createAI } from "@wrnexus/ai"; const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version }) ``` `AIConfig` fields (all optional): | Field | Default | Description | | ----------- | --------------------------- | -------------------------- | | `apiKey` | `ANTHROPIC_API_KEY` | Anthropic API key | | `model` | `"claude-opus-4-8"` | Model id | | `maxTokens` | `4096` | Default max output tokens | | `baseURL` | `https://api.anthropic.com` | API base URL | | `version` | `"2023-06-01"` | `anthropic-version` header | ### `ai.generate(prompt, opts?): Promise` One-shot text generation. `prompt` is a string or a `Message[]` history. ```ts const text = await ai.generate("Write a haiku about Bun."); const reply = await ai.generate( [ { role: "user", content: "My name is Ada." }, { role: "assistant", content: "Hi Ada!" }, { role: "user", content: "What's my name?" }, ], { system: "You are concise." }, ); ``` ### `ai.stream(prompt, opts?): AsyncGenerator` Yields text deltas as they arrive. ```ts for await (const chunk of ai.stream("Tell me a story.")) { process.stdout.write(chunk); } ``` ### `ai.streamResponse(prompt, opts?): Response` Returns a streaming `text/plain` `Response` — drop it straight into an API route. ```ts // app/api/chat.ts import { createAI } from "@wrnexus/ai"; const ai = createAI(); export const POST = async (ctx) => { const { prompt } = await ctx.req.json(); return ai.streamResponse(prompt); }; ``` ### `GenerateOptions` | Option | Type | Description | | ----------- | ------------------------------------------------- | ---------------------------------------------------- | | `system` | `string` | System prompt | | `model` | `string` | Override the model for this call | | `maxTokens` | `number` | Override max output tokens | | `thinking` | `boolean` | Enable adaptive extended thinking (deeper reasoning) | | `effort` | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | Reasoning effort / token spend | | `messages` | `Message[]` | Full history — supersedes `prompt` | | `signal` | `AbortSignal` | Cancel the request | > `temperature` / `top_p` are intentionally **not** exposed — the current Claude > models reject them (400). Steer output with prompting instead. ### `AIError` Thrown on non-2xx responses or a model refusal. Carries `.status` and `.type` (e.g. `"authentication_error"`, `"rate_limit_error"`, `"refusal"`). ```ts import { AIError } from "@wrnexus/ai"; try { await ai.generate("..."); } catch (e) { if (e instanceof AIError && e.type === "rate_limit_error") { /* back off */ } } ``` ## Usage ```ts // app/api/summarize.ts — summarize posted text import { createAI } from "@wrnexus/ai"; const ai = createAI(); export const POST = async (ctx) => { const { text } = await ctx.req.json().catch(() => ({})); if (!text) return Response.json({ error: "Provide 'text'." }, { status: 400 }); const summary = await ai.generate(`Summarize in one sentence:\n\n${text}`, { system: "You are a precise summarizer.", }); return Response.json({ summary }); }; ``` ## Requirements / Notes - **Bun-only.** Uses `fetch`, `ReadableStream`, `TextDecoder`/`TextEncoder`, and reads `ANTHROPIC_API_KEY` from `Bun.env` (falls back to `process.env`). - **Zero dependencies** — no `@anthropic-ai/sdk`; talks to the Messages API directly. - Defaults to `claude-opus-4-8`. Pass `{ model }` for a different model (e.g. `"claude-sonnet-5"` for speed/cost, `"claude-haiku-4-5"` for the fastest). --- ## Package manual: [`@wrnexus/authz`](https://wrnexusjs.dev/packages/authz) # @wrnexus/authz > Composable authorization for WrNexus — role-based (RBAC), policy-based (PBAC), and attribute-based (ABAC) access control that reduces to a boolean check plus an `authorize()` guard. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/authz` is a small, server-side authorization toolkit. It gives you three interchangeable models — RBAC (roles → permissions), PBAC (policy predicates), and ABAC (attribute matchers) — that all collapse to a `boolean | Promise` decision. Wrap any decision in a `Middleware` guard (`authorize`, `requireRole`, `requirePermission`) to protect WrNexus routes. Reach for it whenever a route or action needs to be gated on who the user is, what roles they hold, or attributes of the user and the resource. It plugs into `@wrnexus/core` by reading `ctx.user` as the authorization subject. ## Installation ```bash bun add @wrnexus/authz ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API The package has a single entry point (`@wrnexus/authz`) exporting the following. ### Types | Symbol | Description | | ---------------------------------- | --------------------------------------------------------------------------------------------- | | `Subject` | The authorized principal: `{ id?: string; roles?: string[]; [attribute: string]: unknown }`. | | `Rbac` | An RBAC checker: `{ can(subject, permission): boolean; permissionsFor(roles): Set }`. | | `Policy` | A predicate `(subject: S, resource?: R) => boolean \| Promise`. | ### RBAC #### `defineRbac(roles: Record): Rbac` Builds an RBAC checker from a role → permissions map. Supported permission forms: - `"*"` — grants every permission. - `"ns:*"` — namespace wildcard (e.g. `"post:*"` grants `"post:write"`). - `"role:"` — inherits all permissions of another role (resolved recursively, cycle-safe). The returned `Rbac` provides: - `can(subject, permission)` — `true` if any of `subject.roles` grants `permission` (honouring `*` and namespace wildcards). Returns `false` when the subject has no roles. - `permissionsFor(roles)` — the resolved `Set` of all permissions granted to a set of roles. #### `hasRole(subject: Subject | undefined, ...required: string[]): boolean` `true` if the subject holds **all** of the given roles. ### PBAC / ABAC combinators - `any(...policies: Policy[]): Policy` — allow if **any** policy passes (OR); awaits async policies. - `all(...policies: Policy[]): Policy` — allow only if **all** policies pass (AND); awaits async policies. - `attr(name: string, match: unknown | ((value: unknown) => boolean)): Policy` — ABAC helper that allows when `subject[name]` equals `match`, or when `match` is a function, when `match(value)` is truthy. ### Guards (middleware) Each guard returns a `@wrnexus/core` `Middleware`. A denied request short-circuits with `Response.json({ ok: false, error: "Forbidden" }, { status: 403 })`. - `authorize(policy: (ctx: Context) => boolean | Promise): Middleware` — runs `policy` against the request `Context`; calls `next()` when it resolves truthy, otherwise returns 403. - `requireRole(...roles: string[]): Middleware` — allows when `ctx.user` holds **any** of the listed roles. - `requirePermission(rbac: Rbac, permission: string): Middleware` — allows when `rbac.can(ctx.user, permission)` is `true`. ## Usage ### RBAC ```ts import { defineRbac, hasRole } from "@wrnexus/authz"; const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"], viewer: ["post:read"], // role inheritance: lead gets everything an editor has, plus post:publish lead: ["role:editor", "post:publish"], }); const user = { id: "u1", roles: ["editor"] }; rbac.can(user, "post:write"); // true rbac.can(user, "post:delete"); // false rbac.permissionsFor(["lead"]); // Set { "post:read", "post:write", "post:publish" } hasRole(user, "editor"); // true ``` ### Guarding routes ```ts import { authorize, requireRole, requirePermission, defineRbac } from "@wrnexus/authz"; const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] }); // Only admins or editors app.get("/dashboard", requireRole("admin", "editor"), handler); // Requires a specific permission app.post("/posts", requirePermission(rbac, "post:write"), handler); // Arbitrary policy over the request context app.delete( "/posts/:id", authorize((ctx) => hasRole(ctx.user, "admin")), handler, ); ``` ### PBAC / ABAC policies ```ts import { any, all, attr, authorize, type Policy } from "@wrnexus/authz"; interface User { id: string; department?: string; roles?: string[]; } interface Post { authorId: string; } // Ownership policy (subject + resource) const ownsPost: Policy = (u, post) => u.id === post?.authorId; // ABAC: attribute equality, or a predicate const inEngineering = attr("department", "engineering"); const isVerified = attr("verified", (v) => v === true); // Compose: allow if the user owns the post OR is in engineering AND verified const canEdit = any(ownsPost, all(inEngineering, isVerified)); app.put( "/posts/:id", authorize((ctx) => canEdit(ctx.user as User, loadPost(ctx))), handler, ); ``` ## Requirements / Notes - **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported. - Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`. - Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise` (e.g. for a database ownership check). --- ## Package manual: [`@wrnexus/cli`](https://wrnexusjs.dev/packages/cli) # @wrnexus/cli > The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/cli` provides the `wrnexus` executable — the single entry point for developing a WrNexus app. It runs the HMR dev server, produces a self-contained production build, scaffolds apps/pages/components, drives database migrations, regenerates typed routes and queries, runs tests, and manages configuration profiles. It also scaffolds multi-app monorepos and serves them behind a domain-routing gateway. This is a CLI/build-time package (it shells out to the Bun binary for the dev child and tests) and it also exports the workspace config types via a subpath. ## Installation ```bash bun add @wrnexus/cli ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). Once installed, invoke it from an app directory: ```bash bunx wrnexus dev # or add scripts: "dev": "wrnexus dev .", "build": "wrnexus build ." ``` ## Commands Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=` (see [Profiles](#profiles)). | Command | Purpose | | ------------------------------------- | ---------------------------------------------------------------------- | | `wrnexus dev [app-dir] [--port=3000]` | Start the development server with live reload / HMR. | | `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. | | `wrnexus create ` | Scaffold a new single app from an inline template. | | `wrnexus workspace ` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. | | `wrnexus generate ` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. | | `wrnexus generate mobile` | Scaffold a Capacitor shell for iOS and Android. | | `wrnexus mobile add ` | Install Capacitor plugins and sync native projects. | | `wrnexus eject ` | Copy Wire UI component `.wrn` sources into `app/components/`. | | `wrnexus db ` | Database migrations and tooling (see [db](#wrnexus-db)). | | `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). | | `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. | | `wrnexus help` | Print usage. | `wrnexus g` is an alias for `wrnexus generate`. ### `wrnexus dev` Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`). ```bash wrnexus dev . --port=8080 ``` ### `wrnexus build` Emits into `/dist/`: - `server.js` — a single, minified, self-contained Bun server with a **static** manifest of every page / api / realtime / middleware / component / layout module (no runtime filesystem scan or on-the-fly bundling). - `reactive.js`, `theme.css`, `theme.js`, `ui.css`, and (if present) `styles.css` — hashed, minified browser assets. - `public/` — copied verbatim. Before bundling, it regenerates typed queries for the default and every named database. Run the output with: ```bash bun dist/server.js # PORT env var optional # Generated apps also provide: npm start # Build and start together: npm run production ``` ### `wrnexus create` Scaffolds a new app from an inline (dependency-free) template — `package.json`, `.gitignore`, config, and starter `app/` files. Use `npm run dev` during development, `npm run build && npm start` for production, or `npm run production` to build and start in one command. The generated production server currently requires Bun even when npm is used to manage packages and scripts. ### `wrnexus update` `wrnexus update --latest` performs a complete project upgrade. It hands control to the exact target CLI, backs up important project files under `.wrnexus/update-backups/`, updates every `@wrnexus/*` dependency, refreshes framework-owned references, and applies every versioned syntax/config/file migration between the project version and target version. After installation it runs the project's `check` and `build` scripts; the new version is recorded only after verification succeeds. Use `--dry-run` to preview an upgrade or `--no-verify` when verification is intentionally handled elsewhere. Migrations never overwrite user-owned configuration wholesale: each release must provide a focused, idempotent transformation for any changed syntax or config contract. ```bash wrnexus create my-app ``` ### `wrnexus generate` Scaffolds a single file from a template, refusing to overwrite an existing file. Types (with aliases): `page`/`p`, `component`/`c`, `api`/`a`, `schema`/`s`. Nested names create nested paths. ```bash wrnexus generate page about # app/pages/about.wrn wrnexus generate component user-card # app/components/user-card.wrn wrnexus generate api users/list # app/api/users/list.ts wrnexus generate schema signup # app/schemas/signup.ts wrnexus generate routes # regenerate app/routes.gen.ts wrnexus generate docker # Dockerfile + compose + .dockerignore wrnexus generate mobile --mode=webview --app-id=com.example.app --app-name="Example" --url=https://app.example.com wrnexus generate mobile --mode=native ``` The mobile generator creates a separate `mobile/` package and reads `config.mobile.mode`. `webview` creates a Capacitor shell that renders the hosted WrNexus application. `native` creates a WebView-free Expo/React Native app whose screens call the shared backend through `mobile/src/wrnexus.ts`. Native screens do not render `.wrn` HTML. In either mode, run `bun install` in `mobile/`; iOS device builds require macOS and Xcode. Install official or community Capacitor plugins through the root CLI: ```bash wrnexus mobile add @capacitor/camera @capacitor/haptics wrnexus mobile sync wrnexus mobile assets # generate native icons from config.mobile.icon ``` In native mode, `mobile add` runs `expo install` and `mobile sync` runs Expo prebuild. In WebView mode they retain the Capacitor install/sync behavior. `wrnexus mobile compile` maps portable `app/pages/**/*.wrn` pages to Expo Router TSX routes. Native `bun run start` invokes this compilation automatically. Browser code can access installed plugins through the SSR-safe `@wrnexus/mobile` bridge. The command adds each plugin to both the WrNexus app (JavaScript proxy) and `mobile/` (native synchronization). `wrnexus mobile sync` also configures Android so only true network failures use the local connection-error screen. HTTP errors such as 404 and 500 keep their WrNexus response pages. ### `wrnexus eject` Copies a Wire UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app. ```bash wrnexus eject button card modal ``` ### `wrnexus db` Database migrations and tooling. Without a flag, commands target the **default** database (`db` in `wrnexus.config.ts`, files under `app/db/`). Pass `--db=` to target a named database (`databases.`, files under `app/db//`). | Subcommand | Purpose | | ------------------------------- | ----------------------------------------------------------------------------------- | | `db new [--from-models]` | Scaffold a migration; `--from-models` derives it from the TS models in `schema.ts`. | | `db migrate` | Apply all pending migrations. | | `db rollback` | Revert the last applied migration. | | `db status` | List applied / pending migrations. | | `db generate` | Regenerate typed queries (`queries/*.sql` → `queries.gen.ts`). | | `db seed` | Run the database's `seed.ts` (default export / `seed` function). | | `db studio [table]` | Inspect tables — list row counts, or dump the first 50 rows of one table. | ```bash wrnexus db new create_users --from-models wrnexus db migrate wrnexus db studio users wrnexus db status --db=analytics ``` ### `wrnexus workspace` and `wrnexus gateway` `workspace ` scaffolds a monorepo: several WrNexus apps under `apps/*` and shared libraries under `packages/*`, plus a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). ```bash wrnexus workspace acme wrnexus gateway --port=3000 ``` ### `wrnexus test` Runs the app's tests with `bun test`. Defaults to the `test` profile (config + `.env.test`). Pass `--watch` to re-run on change; extra flags pass straight through to `bun test`. ```bash wrnexus test . --watch ``` ## Profiles Pass `--profile=` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.`, `.env..local`) into `process.env`. ```bash wrnexus dev --profile=uat wrnexus profiles # ● development (config, .env.development) # ○ production # ○ uat (config, .env.uat) ``` ## Subpath exports `@wrnexus/cli/workspace` exposes the workspace configuration types used by `wrnexus.workspace.ts`: ```ts import type { WorkspaceConfig, WorkspaceApp } from "@wrnexus/cli/workspace"; const config: WorkspaceConfig = { security: { trustedHostsOnly: true, headers: true, accessLog: true }, apps: [{ name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] }], }; export default config; ``` ## Requirements / Notes - **Bun-only.** The CLI runs on Bun, spawns the Bun binary for the dev child and `bun test`, and the production build uses `Bun.build`. Node is not supported. - Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn` → `.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable Wire UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`. - Reads `wrnexus.config.ts` for `db` / `databases`, `theme`, `styles`, `seo`, `security`, `i18n`, and `profiles`, and `wrnexus.workspace.ts` for the gateway. --- ## Package manual: [`@wrnexus/compiler`](https://wrnexusjs.dev/packages/compiler) # @wrnexus/compiler > Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page. ## Installation ```bash bun add @wrnexus/compiler ``` > 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/compiler`). ### `compileWireFile(source: string): string` Compile `.wrn` source to a TypeScript module string. Throws `ParseError` on invalid input. The output is prefixed with a `// compiled from .wrn` comment. ### `compile(source: string): CompileResult` Richer entry point that returns the generated code, the AST, and any diagnostics. ```ts interface CompileResult { code: string; ast: PageAst; diagnostics: string[]; } ``` On a `ParseError` it pushes the message into `diagnostics` and re-throws. ### `parse(source: string): PageAst` Run the lexer + recursive-descent parser and return the AST. Throws `ParseError` (lexer `LexError`s are caught and rethrown as `ParseError`). ### `generate(ast: PageAst): string` Lower a `PageAst` to TypeScript. `page` ASTs become a default-export page component (plus `meta`, optional `layout`, `__wrnexusApi`/method handlers, `websocket`, and SSR/CSR data bindings); `component` ASTs become a module exporting `render(props)` and `__wrnexusComponent`. ### `Lexer` On-demand lexer for `.wrn`. Yields structural tokens and exposes raw-span readers for the parser. ```ts class Lexer { pos: number; constructor(src: string); next(): Token; // consume next structural token peek(): Token; // look ahead without consuming readPath(): string; // route path, e.g. /users/[id] readToLineEnd(): string; // rest of line (state/prop initializers) readBalancedBraces(): string; // inner text of a { ... } block, string-aware } ``` `Token` is `{ type: TokenType; value: string; pos: number }`, where `TokenType` is one of `ident`, `string`, `lbrace`, `rbrace`, `lparen`, `rparen`, `at`, `eq`, `comma`, `eof`. ### Errors | Class | Thrown by | Meaning | | ------------ | ------------------------------------------------- | --------------------------------------------------------------- | | `ParseError` | `parse`, `compile`, `compileWireFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. | | `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. | ### AST types Exported type-only symbols describing the parsed tree: | Type | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PageAst` | Root node: `kind` (`"page" \| "component"`), `name`, optional `layout`, `props`, `states`, `seo`, `view`, `styles`, `functions`, `dataApis`, `modeFunctions`, `apis`, `realtimes`. | | `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. | | `Attr` | `{ name; value; event; boolean? }` — `event` marks `@event` bindings. | | `StateDecl` | `{ name; expr }` — a `state x = ` declaration. | | `SeoBlock` | `Record` from the `seo { ... }` block. | | `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. | | `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. | | `DataMode` | `"ssr" \| "client"`. | | `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. | | `RealtimeBlock` | `{ name; handlers }` — a `realtime { on evt(args) { ... } }` block. | ## Usage Compile a page: ```ts import { compileWireFile } from "@wrnexus/compiler"; const ts = compileWireFile(` page Home { state count = 0 seo { title = "Home" description = "Welcome" } view { } } `); // ts is a TypeScript module: exports `meta`, and a default page component // returning an HTML string, wrapped in a data-scope for the reactive runtime. ``` Inspect the AST and diagnostics: ```ts import { compile, ParseError } from "@wrnexus/compiler"; try { const { code, ast, diagnostics } = compile(source); console.log(ast.kind, ast.name, ast.states.length); } catch (err) { if (err instanceof ParseError) console.error(err.message); } ``` Drive the parse/codegen stages directly: ```ts import { parse, generate } from "@wrnexus/compiler"; const ast = parse(componentSource); // ast.kind === "component" const module = generate(ast); // exports render(props) + __wrnexusComponent ``` Use the lexer standalone: ```ts import { Lexer } from "@wrnexus/compiler"; const lx = new Lexer("page Home {"); lx.next(); // { type: "ident", value: "page", pos: 0 } lx.next(); // { type: "ident", value: "Home", pos: 5 } lx.next(); // { type: "lbrace", value: "{", pos: 10 } ``` ## The `.wrn` language (as parsed) A file opens with `page ` or `component ` followed by a `{ ... }` body containing zero or more members: - `layout = ""` — selects `app/layouts/.wrn` (pages only). - `props { name = ... }` — component props; each default's type drives coercion. - `state = ` — reactive state seeded from a raw JS expression. - `view { }` — plain HTML with `{expr}` interpolation, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and ``. - `seo { key = "value" ... }` — metadata merged into the generated `meta`. - `style { }` — inlined page/component stylesheet (repeatable). - `functions { }` — shared server-side helpers (repeatable). - `api { }` — route handler, lowered to a `METHOD` export (repeatable). - `ssr { ... }` / `client { ... }` — data blocks holding `api { ... }` bindings and their own `functions { ... }`. - `realtime { on () { } ... }` — websocket handlers, lowered to a `websocket` export. `view` markup is parsed by a lenient dedicated HTML parser (`parseHtmlView`); HTML void elements (`
`, ``, …) take no closing tag. Line comments (`//`) are skipped by the lexer. ## Requirements / Notes - Pure TypeScript with no runtime dependencies; runs under **Bun** as part of the WrNexus toolchain (Node is not supported). - Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wire*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader. --- ## Package manual: [`@wrnexus/core`](https://wrnexusjs.dev/packages/core) # @wrnexus/core > The framework core: the request `Context`, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WrNexus package builds on. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/core` is the shared foundation of WrNexus. It defines the `Context` object that flows through every middleware, page, and API route, plus the `Middleware`/`Next` contract they implement. On top of that it ships the building blocks a real app needs: cookie-backed sessions, password auth, CSRF protection, rate limiting, request logging, HTTP + in-memory caching, file uploads, streaming/SSE responses, WebSocket "rooms", security headers/CORS, and a server-side JSX runtime that renders to HTML strings. Everything here is **server-side** and Bun-native (it uses `Bun.password`, `Bun.write`, the web-standard `Request`/`Response`, and `crypto`). You depend on it directly and transitively through the rest of the framework. ## Installation ```bash bun add @wrnexus/core ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API ### Context & middleware — `@wrnexus/core` The `Context` (`ctx`) is the single value passed to middleware and handlers. | Export | Kind | Description | | ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------ | | `Context` | type | Per-request object: `req`, `url`, `lang`, `t`, `params`, `locals`, `user?`, `ip?`, `cookies`, `session`, `localStorage`. | | `Next` | type | `() => Promise \| Response` — invokes the next middleware/handler. | | `Middleware` | type | `(ctx, next) => Promise \| Response`. Return `next()` to continue, or a `Response` to short-circuit. | | `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (wires up cookies, session, localStorage snapshot). | | `withContextHeaders(ctx, res)` | fn | Apply accumulated headers (e.g. `Set-Cookie`) from the context onto a response. | | `PageComponent` | type | `(ctx) => string \| Promise` — a page module's default export. | | `PageMeta` / `SeoConfig` | type | `` metadata: `title`, `description`, `canonical`, `robots`, `image`, `twitterCard`, `themeColor`, … | | `TFunction` | type | `(key, params?) => string` — translate a key for `ctx.lang`, interpolating `{param}` placeholders. | Key `Context` fields: - `ctx.locals` — per-request scratch space for passing values between middleware. - `ctx.user` — the authenticated user (populated by `sessionAuth`/`logIn`), or `null`. - `ctx.ip` — the direct socket peer IP (not spoofable via headers). - `ctx.cookies` / `ctx.session` / `ctx.localStorage` — see **Storage** below. ### Authentication — `@wrnexus/core` Passwords are hashed with argon2id via `Bun.password`; sessions ride the cookie-backed `SessionStore`. | Export | Signature | Notes | | -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `hashPassword(password)` | `(string) => Promise` | argon2id hash to store. | | `verifyPassword(password, hash)` | `(string, string) => Promise` | Constant-safe; returns `false` on bad/empty hash. | | `logIn(ctx, user)` | `(Context, U) => void` | Regenerates the session id (fixation defense), stores the user, sets `ctx.user`. | | `logOut(ctx)` | `(Context) => void` | Clears the session and `ctx.user`. | | `getUser(ctx)` | `(Context) => U \| null` | Current user from `ctx.user`, falling back to the session. | | `sessionAuth()` | `() => Middleware` | Hydrates `ctx.user` from the session each request. Register early. | | `requireAuth(options?)` | `(RequireAuthOptions?) => Middleware` | Guard: API/fetch requests get `401 JSON`, page navigations get `302` to `loginPath` (default `/login`) with `?next=`. | | `SESSION_USER_KEY` | `"user"` | Session key holding the user. | `RequireAuthOptions`: `{ loginPath?: string }`. ### CSRF — `@wrnexus/core` Double-submit cookie pattern: a readable `wire-csrf` cookie is echoed in an `x-csrf-token` header on unsafe requests. | Export | Signature | Notes | | ----------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `csrfToken(ctx)` | `(Context) => string` | Ensures the CSRF cookie exists and returns its token. | | `verifyCsrf(ctx)` | `(Context) => boolean` | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). | | `csrfProtection()` | `() => Middleware` | 403s unsafe requests with a missing/mismatched token. | | `CSRF_COOKIE` / `CSRF_HEADER` | `"wire-csrf"` / `"x-csrf-token"` | Cookie & header names. | ### Rate limiting — `@wrnexus/core` Fixed-window limiter that returns `429` with `Retry-After` and emits `RateLimit-Limit`/`-Remaining`/`-Reset` headers. | Export | Signature | Notes | | --------------------- | ----------------------------------- | ---------------------------------------------------------------------- | | `rateLimit(options?)` | `(RateLimitOptions?) => Middleware` | Main middleware. | | `peerKey(ctx)` | `(Context) => string` | Non-spoofable key from `ctx.ip` (default). | | `proxyKey(ctx)` | `(Context) => string` | Trusts `x-forwarded-for`/`x-real-ip`. Use only behind a trusted proxy. | | `defaultKey` | — | **Deprecated** alias of `proxyKey`. | `RateLimitOptions`: `windowMs` (default `60_000`), `max` (default `60`), `key`, `trustProxy` (default `false` → keys on `peerKey`; `true` → `proxyKey`), `message`, `headers` (default `true`), `store`. `RateLimitStore` is pluggable — implement `hit(key, windowMs, now) => Bucket | Promise` (a `Bucket` is `{ count, resetAt }`) to back limits with Redis/SQL across instances. The default store is process-local memory. ### Request logging — `@wrnexus/core` | Export | Signature | Notes | | ------------------------- | --------------------------------------- | -------------------------------------------------------------------------------- | | `requestLogger(options?)` | `(RequestLoggerOptions?) => Middleware` | One record per request with a request id (stored on `ctx.locals[requestIdKey]`). | `RequestLoggerOptions`: `format` (`"pretty"` default \| `"json"`), `sink(line, record)` (default `console.log`), `requestIdKey` (default `"requestId"`), `now`. `RequestRecord` = `{ time, id, method, path, status, durationMs }`. ### Caching — `@wrnexus/core` | Export | Kind | Notes | | -------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `TTLCache` | class | In-memory TTL cache: `get`, `set`, `getOrLoad(key, loader, ttlMs?)`, `delete`, `clear`, `size`. Constructor takes a default `ttlMs` (60s). | | `cacheControl(options)` | fn | Build a `Cache-Control` value from `CacheControlOptions`. | | `withCacheControl(res, options)` | fn | Apply `Cache-Control` to a response. | | `etag(body, weak?)` | fn | Stable quoted FNV-1a ETag (weak by default). | | `notModified(req, tag)` | fn | `true` when `If-None-Match` matches — send a `304`. | `CacheControlOptions`: `maxAge`, `sMaxAge`, `private`, `noStore`, `noCache`, `staleWhileRevalidate`, `immutable`. ### File uploads — `@wrnexus/core` Bun parses `multipart/form-data` via `Request.formData()`; these helpers validate and persist the resulting `File`s. | Export | Signature | Notes | | --------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- | | `collectUploads(form)` | `(FormData) => { field, file }[]` | Every non-empty `File` in a parsed form. | | `saveUpload(file, options)` | `(File, SaveUploadOptions) => Promise` | Validates size/type, sanitizes the name, writes via `Bun.write`. Throws `UploadError`. | | `sanitizeFilename(name)` | `(string) => string` | Strips separators, traversal, control/illegal chars; caps at 255. | | `UploadError` | class | Thrown on rejected uploads. | `SaveUploadOptions`: `dir` (required), `maxBytes`, `allowedTypes` (MIME types like `"image/png"` and/or extensions like `".png"`), `filename(file)`. `SavedUpload` = `{ path, filename, size, type }`. ### Streaming & SSE — `@wrnexus/core` | Export | Signature | Notes | | ------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | `streamResponse(source, init?)` | `(Iterable\|AsyncIterable, StreamResponseInit?) => Response` | Streaming `Response` from a chunk source (basis for streaming SSR). | | `sse(source)` | `(Iterable\|AsyncIterable) => Response` | `text/event-stream` response. | `StreamResponseInit`: `status`, `headers`, `contentType` (default `"text/html; charset=utf-8"`). `ServerSentEvent`: `{ data, event?, id?, retry? }`. ### Realtime rooms — `@wrnexus/core` WebSocket rooms. A file in `app/realtime/` exports `default defineRoom({ ... })` and is served at `ws://host/realtime/`. | Export | Signature | Notes | | --------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- | | `defineRoom(handlers)` | `(RoomHandlers) => RoomDefinition` | Define a room. Export the result as `default`. | | `isRoomDefinition(value)` | `(unknown) => boolean` | Type guard for a room definition. | | `createRealtimeRegistry()` | `() => RealtimeRegistry` | Server-side connection manager mapping sockets ↔ rooms. | | `bridgeRealtime(registry, bus, topic?)` | `(RealtimeRegistry, RealtimeBus, string?) => () => void` | Bridge broadcasts/`toUser` sends across processes via a pub/sub bus. | `RoomHandlers`: `authorize(info) => boolean` (gate before accept — return `false` to reject with 403), `onConnect(client)`, `onMessage(client, message)` (JSON auto-parsed), `onLeave(client)`. A handler receives a `RoomClient` with `id`, `user`, `query`, `data`, `room`, and `send` / `broadcast` / `to(id)` / `toUser(user)` / `close`. The `Room` API adds `state`, `clients()`, `count()`, and `broadcast`. `RealtimeBus` is structurally satisfied by `@wrnexus/pubsub`. Legacy `RealtimeHandler`/`RealtimeSocket` raw handlers are still exported. Connection-targeted sends (`send`, `to(id)`) stay local; room broadcasts and `toUser` cross the bridge. ### Error pages — `@wrnexus/core` | Export | Signature | Notes | | ------------------------------ | -------------------------------- | ----------------------------------------------------- | | `renderError(err, mode)` | `(unknown, Mode) => Response` | Dev page (with stack) or generic prod page by `mode`. | | `renderDevError(err, status?)` | `(unknown, number?) => Response` | Readable HTML error page including the stack trace. | | `renderProdError(status?)` | `(number?) => Response` | Generic page that never leaks file paths. | | `renderNotFound()` | `() => Response` | Simple 404 page. | `Mode` = `"development" | "production"`. ### Security headers & CORS — `@wrnexus/core` | Export | Signature | Notes | | -------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `withSecurityHeaders(req, res, mode, security?, nonce?)` | → `Response` | Applies CORS + CSP, HSTS, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, COOP, Trusted Types, and `extraHeaders`. | | `createCorsPreflightResponse(req, security?)` | → `Response \| null` | Builds a `204`/`403` preflight response for CORS `OPTIONS` requests. | | `isWebSocketOriginAllowed(req, security?)` | → `boolean` | Guards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients). | Config types: `SecurityConfig` (top-level), `CorsConfig`/`CorsOrigin`, `ContentSecurityPolicyConfig`/`CspDirectiveValue`, `HstsConfig`, `TrustedTypesConfig`, `PermissionsPolicyConfig`. WrNexus applies sensible defaults (self-only CSP, `frame-ancestors 'none'`, restrictive Permissions-Policy, HSTS in production, Trusted Types in production); each is individually overridable or disable-able via `false`. ### Storage: cookies, sessions, localStorage — `@wrnexus/core` These back the `ctx.cookies`, `ctx.session`, and `ctx.localStorage` fields. | Export | Kind | Notes | | --------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `setSessionBackend(backend)` | fn | Swap the **sync** session persistence backend (`SessionBackend`) — e.g. `bun:sqlite`. Default is process-local memory. Call once at startup. | | `loadSession(backend, options?)` | fn → `Middleware` | Back `ctx.session` with an **async** store (`AsyncSessionBackend`: `load`/`save`/`destroy`) — loads before the request, saves after. `options.ttlMs` default 24h. | | `CookieStore` | type | `get`/`getAll`/`has`/`set(name, value, opts?)`/`delete`/`headers`. | | `SessionStore` | type | `id`/`get`/`getAll`/`set`/`delete`/`regenerate`/`clear`. | | `LocalStorageSnapshot` | type | Read-only view of the browser's localStorage sent via header for CSR bindings. | | `CookieOptions` | type | `path`, `domain`, `maxAge`, `expires`, `httpOnly`, `secure`, `sameSite`. | | `SessionEntry` / `SessionBackend` / `AsyncSessionBackend` | types | Session persistence contracts. | ### Low-level security helpers — `@wrnexus/core` | Export | Signature | Notes | | ----------------------------- | --------------------- | --------------------------------------------------- | | `escapeHtml(value)` | `(string) => string` | Escape for HTML text/attributes. | | `isSafeIslandName(name)` | `(string) => boolean` | Allow only a conservative `[A-Za-z0-9_-]+` charset. | | `isSafeRequestPath(pathname)` | `(string) => boolean` | Reject NULs, `..` traversal, and backslashes. | ### JSX runtime — `@wrnexus/core`, `@wrnexus/core/jsx-runtime`, `@wrnexus/core/jsx-dev-runtime` A server-side JSX runtime that renders to HTML **strings** (no virtual DOM). Point `tsconfig`'s `jsxImportSource` at `@wrnexus/core`. | Export | Kind | Notes | | ------------------------------------------ | ------ | --------------------------------------------------------------------------------------- | | `jsx` / `jsxs` | fn | The runtime factory (TypeScript calls these automatically). Returns an `Html` instance. | | `Fragment` | symbol | JSX fragment marker. | | `Html` | class | Wraps a raw, already-safe HTML string (`toString()` returns it). | | `mustache(expr)` | fn | Emit a `{{expr}}` placeholder (tagged-template or string form) for the client binder. | | `JSXComponent` / `JSXProps` / `Renderable` | types | Component signature and renderable value types. | Values interpolated as children are HTML-escaped unless they are an `Html` instance; use `dangerouslySetInnerHTML={{ __html }}` for trusted markup. Void elements render without a closing tag; `className`→`class`, `htmlFor`→`for`, and `style` objects are serialized to CSS text. The subpath exports map to the runtime TypeScript's JSX transform expects: ```jsonc // tsconfig.json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@wrnexus/core", }, } ``` ## Usage ### A minimal middleware chain ```ts import { createContext, withContextHeaders, sessionAuth, requireAuth, requestLogger, rateLimit, csrfProtection, type Middleware, } from "@wrnexus/core"; const chain: Middleware[] = [ requestLogger({ format: "json" }), rateLimit({ max: 100, windowMs: 60_000 }), csrfProtection(), sessionAuth(), requireAuth({ loginPath: "/login" }), ]; ``` ### Password auth ```ts import { hashPassword, verifyPassword, logIn, getUser } from "@wrnexus/core"; // Registration const passwordHash = await hashPassword(form.password); // Login if (await verifyPassword(form.password, user.passwordHash)) { logIn(ctx, { id: user.id, email: user.email }); } const current = getUser<{ id: string }>(ctx); // or null ``` ### HTTP caching with ETags ```ts import { etag, notModified, withCacheControl } from "@wrnexus/core"; const body = JSON.stringify(data); const tag = etag(body); if (notModified(ctx.req, tag)) { return new Response(null, { status: 304, headers: { ETag: tag } }); } const res = new Response(body, { headers: { ETag: tag, "content-type": "application/json" } }); return withCacheControl(res, { maxAge: 60, staleWhileRevalidate: 300 }); ``` ### Streaming SSE ```ts import { sse } from "@wrnexus/core"; async function* ticks() { for (let n = 0; ; n++) { yield { event: "tick", data: String(n) }; await Bun.sleep(1000); } } export default (ctx) => sse(ticks()); ``` ### A realtime room ```ts // app/realtime/chat.ts import { defineRoom } from "@wrnexus/core"; export default defineRoom({ authorize: (info) => !!info.user, // require auth onConnect(client) { client.user = client.query.user; client.room.broadcast({ type: "join", id: client.id }); }, onMessage(client, msg) { client.broadcast({ type: "say", from: client.id, text: msg.text }); }, }); ``` Scale it across processes: ```ts import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core"; import { createPubSub } from "@wrnexus/pubsub"; import { redisDriver } from "@wrnexus/pubsub/redis"; const registry = createRealtimeRegistry(); bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL))); ``` ### JSX rendering ```tsx import { Html } from "@wrnexus/core"; function Card({ title, body }: { title: string; body: string }) { return (

{title}

{body}

); } const html: Html = ; return new Response(html.toString(), { headers: { "content-type": "text/html" } }); ``` ## Requirements / Notes - **Bun-only.** Uses `Bun.password` (argon2id), `Bun.write`, web-standard `Request`/`Response`/`FormData`/`ReadableStream`, and the global `crypto`. Node is not supported. - Session and rate-limit backends default to **process-local memory**. For multi-instance deployments, swap in a shared backend: `setSessionBackend` (sync, e.g. `bun:sqlite`) or `loadSession` (async, e.g. Redis) for sessions, a custom `RateLimitStore` for limits, and `bridgeRealtime` for realtime. - Works with the rest of the framework: realtime bridging is structurally compatible with [`@wrnexus/pubsub`](../pubsub); the security, auth, and JSX primitives here are consumed by the WrNexus server/router packages. - Subpath exports: `@wrnexus/core/jsx-runtime` and `@wrnexus/core/jsx-dev-runtime` for TypeScript's automatic JSX transform. --- ## Package manual: [`@wrnexus/csr`](https://wrnexusjs.dev/packages/csr) # @wrnexus/csr > 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 ```bash 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. ```ts 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-="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 `` 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/` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes. Programmatic API via `window.wire`: ```ts 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=""` container: | Attribute | On | Purpose | | ------------------------------------ | --------------- | ------------------------------------------------------------------------ | | `data-room=""` | container | Connect to room `` | | `data-room-user=""` | container | Identify the connection (`?user=`) | | `data-room-log` | element | Where incoming messages are appended | | `