Files
WRNexusJS/README.md
2026-07-19 17:45:13 +05:30

1513 lines
54 KiB
Markdown

# WrNexus
An **SSR-first** full-stack web framework MVP with **server-rendered, reactive
components**. Built in TypeScript, **Bun-first**, Node-friendly where possible.
It gives you file-based pages and API routes, middleware, realtime WebSocket
routes, and opt-in client hydration — with a small, readable codebase designed
so a custom `.wrn` language/compiler can be layered on later.
> 📘 **[The Complete Guide](docs/GUIDE.md)** — one document covering every package,
> the whole `.wrn` language, all attributes/config properties, and a step-by-step
> path from `create` to a full app (pages, APIs, DB, auth, realtime, deploy).
---
## Why SSR-first with reactive components?
- **Fast, robust default.** Pages render to HTML on the server, so users get
content immediately and pages work even before (or without) JavaScript.
- **Ship JS only where needed.** A page is plain HTML until it mounts a component
with `data-component="<name>"`. Components render on the server too; only their
reactive state and forms run on the browser — the rest ships zero JavaScript.
- **Clean separation.** Server rendering (`@wrnexus/ssr`) never touches the DOM or
client runtime; client hydration (`@wrnexus/csr`) never runs on the server. They
meet only at one seam: a single runtime, `/__wrnexus/reactive.js`.
---
## Installation
Requires [Bun](https://bun.sh) ≥ 1.1.
```bash
bun install
bun run dev # runs examples/basic-app at http://localhost:3000
```
## CLI commands
```bash
wrnexus dev [app-dir] [--port=3000] # start the dev server (HMR + regenerates typed queries)
wrnexus build [app-dir] # build the production server bundle + assets
wrnexus generate mobile # scaffold a Capacitor iOS/Android shell
wrnexus mobile add @capacitor/camera # install a native plugin + sync projects
wrnexus create <app-name> # scaffold a new app
wrnexus generate <type> <name> # scaffold a page | component | api | schema (alias: g)
wrnexus eject <name...> # copy a Wire UI component into app/components
wrnexus test [app-dir] [--watch] # run the app's tests (bun test, `test` profile)
wrnexus profiles [app-dir] # list config profiles (dev/prod/uat/…) + their env files
# Database
wrnexus db migrate # apply pending migrations
wrnexus db rollback # revert the last migration
wrnexus db status # list applied / pending migrations
wrnexus db new <name> [--from-models] # scaffold a migration (optionally from TS models)
wrnexus db generate # (re)generate typed queries from app/db/queries/*.sql
wrnexus db seed # run app/db/seed.ts (re-runnable dev data)
wrnexus db studio [table] # list tables + row counts, or dump a table's rows
```
### Mobile and PWA
The same `view` HTML renders in browsers and in the Capacitor app. Add device
directives directly to normal elements. Camera directives use the native camera
in Capacitor, phone camera capture in supported mobile browsers, and a file
picker in desktop browsers. The markup and responsive theme remain shared.
Cross-platform capabilities can also be declared directly in `.wrn` markup:
```html
<button
data-native-browser="share"
data-native-mobile="share"
data-native-options='{"title":"WrNexus","url":"https://example.com"}'
@browser-click="browserShares++"
@mobile-click="mobileShares++"
>
Share
</button>
<p data-native-only="browser">Browser help</p>
<nav data-native-only="mobile" hidden>Native navigation</nav>
<button data-native-requires="haptics" data-native-mobile="haptics">Tap</button>
```
Use `wrnexus native list` to see built-in capabilities and
`wrnexus native add camera share` to install the packages required by the configured
Capacitor or Expo mode. Advanced browser code can use `native.run()` and register
custom adapters from `@wrnexus/native`.
The declarative `data-native-browser` and `data-native-mobile` capability actions run
in browser and Capacitor/WebView pages. Fully native Expo compilation supports
`data-native-only` and `@mobile-*` event selection, but capability calls must currently
use the installed Expo module from native screen code; the compiler reports an
actionable error instead of silently dropping a `data-native-mobile` action.
```wrn
page Home {
view {
<main>
<img data-mobile-photo alt="Captured photo" />
<button data-mobile-camera="[data-mobile-photo]">Take photo</button>
<nav data-mobile-only hidden class="bottom-nav">Home · Profile · Settings</nav>
</main>
}
}
```
Configure native and installable-web behavior in `wrnexus.config.ts`:
```ts
const config: AppConfig = {
mobile: {
mode: "webview", // "webview" (Capacitor) or "native" (Expo/React Native)
enabled: true,
appId: "com.example.app",
appName: "Example",
serverUrl: "http://192.168.0.10:3000",
// Native mode uses this backend URL instead of rendering server HTML:
apiUrl: "https://api.example.com",
scheme: "example",
layout: "mobile", // optional app/layouts/mobile.wrn
icon: "resources/icon.png",
// Mode-specific advanced settings:
capacitor: { ios: { contentInset: "automatic" } },
expo: { orientation: "portrait" },
},
pwa: {
enabled: true,
id: "/",
name: "Example",
shortName: "Example",
startUrl: "/",
scope: "/",
orientation: "any",
display: "standalone",
themeColor: "#6366f1",
backgroundColor: "#0f172a",
offlineUrl: "/",
cacheUrls: ["/", "/about"],
cacheName: "wrnexus-pwa-v1", // change to invalidate installed caches
},
};
```
PWA support is on by default: the framework serves `/site.webmanifest`, `/sw.js`,
injects install metadata, precaches configured offline URLs, and registers a
network-first service worker. Set `pwa.serviceWorker: false` to keep only the
manifest, or `pwa: false` to disable PWA support. Generated Capacitor shells include `error.html`, so
an unreachable server shows a retry screen instead of a blank WebView.
Generate the configured mode with `wrnexus generate mobile`. You can override it
once with `--mode=webview` or `--mode=native`. WebView mode shares `.wrn` pages
through Capacitor. Native mode creates an Expo/React Native project with native
controls, file-based screens, and a typed backend helper; it shares WrNexus API,
upload, authentication, and realtime endpoints, but not HTML views.
In native mode, `wrnexus mobile compile` converts `app/pages/**/*.wrn` into
matching Expo Router routes under `mobile/app/`. Portable tags are mapped to
React Native primitives (`div/main``View`, text tags → `Text`, `button`
`Pressable`, `input``TextInput`, and `img``Image`). State, interpolation,
events, `{#if}`, `{#each}`, class styles, and links are compiled to native JSX.
Unsupported DOM elements, inline CSS strings, SSR/data blocks, and browser APIs
produce compile errors instead of silently falling back to a WebView.
From this repo the same commands are wired as root scripts:
```bash
bun run dev # = wrnexus dev examples/basic-app
bun run build # = wrnexus build examples/basic-app
bun run create # = wrnexus create
```
Quality checks are wired at the repo root:
```bash
bun run typecheck # TypeScript check
bun run lint # ESLint
bun run lint:fix # ESLint autofix
bun run format # Prettier write
bun run format:check # Prettier check
bun run check # typecheck + lint + format:check
```
Apps created with `wrnexus create <app-name>` include the same ESLint/Prettier
baseline for app code.
---
## Project structure
```
myframework/
packages/
core/ # Context, Middleware types, security + error helpers
cli/ # `wrnexus` command (dev / build / create)
dev-server/ # Bun.serve HTTP + WebSocket pipeline
router/ # file-based router (scan + safe match)
ssr/ # server-side document rendering
csr/ # browser reactive runtime (hydrates components)
styles/ # global stylesheet pipeline + app config (CSS frameworks)
compiler/ # `.wrn` language compiler (lexer/parser/codegen)
reactive/ # tiny signal() implementation
examples/
basic-app/ # reference app exercising every feature
app/
pages/ # file-based pages -> /, /about
api/ # file-based API routes -> /api/*
middleware/ # global middleware (alphabetical order)
realtime/ # WebSocket routes -> /realtime/*
components/ # reactive components (.wrn, data-component="name")
styles/ # global CSS (global.css -> every page)
public/ # static assets -> /robots.txt, /images/logo.svg
wrnexus.config.ts # optional: SEO, head injection, CSS, security
package.json
tsconfig.json
README.md
```
The app folder is convention-based — drop a file in the right directory and it
becomes a route. Routes are resolved against a table scanned at startup; request
input is never turned into a file path.
---
## Pages
`app/pages/index.tsx``/`
```tsx
export const meta = {
title: "Home",
description: "Welcome to WrNexus",
};
export default function Home() {
return `<h1>Hello from WrNexus</h1>
<div data-component="counter" start="0" label="Count"></div>`;
}
```
`app/pages/about.tsx``/about`. Dynamic segments use brackets:
`app/pages/users/[id].tsx``/users/:id`, with `ctx.params.id` available.
## SSR
The server renders each page to a full HTML document. Page metadata is escaped
and placed in the `<head>`; the body goes inside `#app`:
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Home</title>
<meta name="description" content="Welcome to WrNexus" />
</head>
<body>
<div id="app">
<h1>Hello from WrNexus</h1>
<div data-scope="start: 0, label: 'Count', count: 0">
<button data-on-click="count++">{label}: {count}</button>
</div>
</div>
<script type="module" src="/__wrnexus/reactive.js"></script>
</body>
</html>
```
The `<div data-component="counter">` mount was replaced by the component's
server-rendered HTML, and the reactive runtime is injected only because the page
now contains a `data-scope` (see "Per-page code-splitting" below). Everything in
a page module is **server-only** — it is never bundled to the browser. Errors
are readable in development and generic (no file paths) in production.
## Components (server-rendered, hydrated on the browser)
Components are `.wrn` files under `app/components`. They render on the **server**
(with their props applied) and are hydrated on the browser by the single generic
reactive runtime — they ship no JS of their own. Only forms and reactive state
run in the browser.
`app/components/counter.wrn`:
```
component Counter {
props {
start = 0 // default's type drives coercion (number)
label = "Count" // (string)
}
state count = start
view {
<button @click="count++">{label}: {count}</button>
}
}
```
Mount it in any page and pass props as attributes — pass as many as you like:
```
<div data-component="counter" start="5" label="Clicks"></div>
```
Each attribute becomes a prop, coerced to the type of its declared default (so
`start="5"` arrives as the number `5`). Prop-driven text and attributes are
**baked server-side** (a static component ships zero JS); text that references
`state` stays reactive. Components (and layouts) can take children via `<slot>`
including **named slots**: put `<slot name="header">` in the component and fill it
from the mount with `<div data-slot="header">…</div>` (anything else fills the
default `<slot>`). Component names are validated before rendering.
## Wire UI, theming & layouts
**Wire UI** (`@wrnexus/ui`) ships common components — `button`, `input`, `select`,
`checkbox`, `radio`, `switch`, `textarea`, `badge`, `tag`, `alert`, `card`,
`avatar`, `spinner`, `skeleton`, `progress`, `tooltip`, `table`, `disclosure`,
`theme-toggle` — plus layout primitives `container`, `stack`, `hstack`, `grid`,
`divider`, `spacer`. They are auto-discovered, so you mount them straight away:
```
<div data-component="stack" gap="4">
<div data-component="alert" variant="info" title="Hi" message="Server-rendered."></div>
<div data-component="button" variant="primary" label="Save"></div>
</div>
```
Override styles four ways, least → most control: change a **theme token**,
redefine a **`.wire-*` class** in your CSS, pass a **`class` prop** (appended to
the root), or **`wrnexus eject <name>`** to copy a component into `app/components`
and own it (an app component shadows the library one of the same name).
**Theming.** Design tokens (`--wire-*`) come from `wrnexus.config.ts` `theme` (deep
-merged over built-in light/dark), served at `/__wrnexus/theme.css`. The server
sets `<html data-theme>` from the `wire-theme` cookie (no flash); a `theme-toggle`
component (or any `[data-wire-theme-toggle]` element) switches and persists it.
Choose a complete semantic palette with `theme: { palette: "violet" }`. Available
palettes are `blue`, `indigo`, `violet`, `emerald`, `cyan`, `rose`, `amber`, and
`slate`; a custom palette must provide every primary, secondary, info, success,
warning, danger, hover, and contrast color.
**Page layouts.** Add named layouts under `app/layouts/` — e.g. `public.wrn`,
`dashboard.wrn`, `auth.wrn` — each a `component` with a `<slot></slot>` where the
page body goes. A page picks one by name:
```
page Dashboard {
layout = "dashboard"
view { <h1>Overview</h1> ... }
}
```
Pages with no `layout` fall back to a `default` layout if one exists; `layout = "none"`
opts out. Layouts can mount components (nav, theme-toggle) like any page.
## API routes
`app/api/hello.ts``/api/hello`:
```ts
export const GET = async () => Response.json({ message: "Hello API" });
```
Method-specific handlers live in one file; unsupported methods return **405**
with an `Allow` header:
```ts
export const POST = async (ctx) => {
const body = await ctx.req.json();
return Response.json({ received: body });
};
```
## Validation (one schema, form + API)
Define a schema once in `app/schemas/<name>.ts` with the fluent builder:
```ts
import { v } from "@wrnexus/validation";
export default v.object({
email: v.string().email(),
password: v.string().min(8, "Password must be at least 8 characters"),
});
```
Use it on the **server** in an API route — bad input is rejected even if the
client is bypassed:
```ts
import { parseBody } from "@wrnexus/validation";
import login from "../schemas/login.ts";
export const POST = async (ctx) => {
const r = await parseBody(login, ctx.req);
if (!r.ok) return r.response; // 400 { ok:false, errors }
return Response.json({ ok: true });
};
```
And on the **client** by naming it on a form — the framework injects the schema
descriptor + a tiny, eval-free validator that checks on submit/blur and writes
messages into `[data-error]` spans (no per-form JS, CSP-safe):
```
<form data-schema="login" method="post" action="/api/login">
<input name="email"><span data-error="email"></span>
<input name="password" type="password"><span data-error="password"></span>
<button type="submit">Sign in</button>
</form>
```
Valid forms submit via `fetch` (JSON) and stay on the page: on success they
redirect (`data-redirect`) or reveal a `[data-success]` message; server-returned
field errors map back to the `[data-error]` spans.
## i18n (pages + API)
Put translations in `app/locales/<lang>.json` (nested keys, `{param}` placeholders):
```json
// app/locales/es.json
{ "home": { "title": "¡Hola desde WrNexus!" }, "api": { "greeting": "Hola" } }
```
Translate in **views** with the `{t:key}` sugar (and `t:<attr>="key"` for
attributes), and in **API/handlers** with `ctx.t`:
```
<h1>{t:home.title}</h1>
<input t:placeholder="email_ph">
```
```ts
export const GET = (ctx) => Response.json({ message: ctx.t("api.greeting"), lang: ctx.lang });
```
The active language is resolved per request from the `wire-lang` cookie → the
browser's `Accept-Language` → the config default (`wrnexus.config.ts` `i18n.default`),
and set on `<html lang>` (no flash). A `[data-wire-lang-set="es"]` element (or
`<select data-wire-lang>`) switches language and reloads. Missing keys fall back
to the default language, then to the key itself.
## Database (`@wrnexus/db`)
Define models once in `app/db/schema.ts` — the source of truth for DDL,
migrations, and result typing:
```ts
import { v, table } from "@wrnexus/db";
export const users = table("users", {
id: v.id(),
email: v.string().unique(),
name: v.string(),
active: v.boolean().default(true),
createdAt: v.timestamp().default("now"),
});
```
Configure a connection in `wrnexus.config.ts` (`db: { driver: "sqlite", url: "file:./dev.db" }`)
and run **migrations** with the CLI:
```bash
wrnexus db new init --from-models # generate a migration (CREATE TABLE) from your models
wrnexus db migrate # apply pending migrations (tracked in _wire_migrations)
wrnexus db status # [x] applied / [ ] pending
wrnexus db rollback # revert the last migration
```
Query with the client; results are mapped back through your model (SQLite `0/1`
booleans, timestamps → `Date`, etc.):
```ts
import { createDb } from "@wrnexus/db";
import { sqlite } from "@wrnexus/db/sqlite";
const db = createDb(sqlite("file:./dev.db"));
const list = await db.all("SELECT * FROM users WHERE active = ?", [1], users);
await db.tx(async (t) => {
await t.exec("INSERT INTO users (email,name) VALUES (?,?)", [e, n]);
});
```
**Typed queries (sqlc-style).** Write annotated SQL in `app/db/queries/*.sql` and run
`wrnexus db generate` (also runs during `wrnexus build`) to get typed functions whose
params + results are inferred from your models:
```sql
-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = :email;
-- name: CreateUser :exec
INSERT INTO users (email, name) VALUES (:email, :name);
```
```ts
import { GetUserByEmail, CreateUser } from "./db/queries.gen.ts";
const user = await GetUserByEmail(db, { email }); // Promise<{…} | null>, mapped through the model
await CreateUser(db, { email, name }); // Promise<ExecResult>
```
**From pages & API routes.** The framework opens the connection at startup (and
auto-migrates in dev), so routes just call `getDb()`:
```ts
import { getDb } from "@wrnexus/db";
import { ListUsers } from "../../db/queries.gen.ts";
export const GET = async () => Response.json({ users: await ListUsers(getDb()) });
```
**Adapters.** SQLite (`@wrnexus/db/sqlite`, built on `bun:sqlite`) and — via Bun's
native `Bun.SQL`, no external driver — **PostgreSQL** (`@wrnexus/db/postgres`) and
**MySQL** (`@wrnexus/db/mysql`). Switch by setting `db.driver` and regenerating
migrations (`wrnexus db new --from-models`) for the new dialect. **MongoDB**
(`@wrnexus/db/mongo`) has a document collection API (`db.collection(model).find(…)`)
and lazily loads the `mongodb` driver.
## Middleware
`app/middleware/logger.ts` — runs before pages and API routes, in alphabetical
filename order:
```ts
export default async function logger(ctx, next) {
console.log(ctx.req.method, ctx.url.pathname);
return next();
}
```
Middleware receives a typed `Context`, can read/write `ctx.locals`, call
`next()` to continue, or return a `Response` early:
```ts
export default async function auth(ctx, next) {
if (ctx.url.pathname.startsWith("/dashboard")) {
return new Response("Unauthorized", { status: 401 });
}
return next();
}
```
## Realtime rooms
A file in `app/realtime/` exports `default defineRoom({ … })` and is served at
`ws://host/realtime/<name>`. You write only the room logic — **the framework
owns the entire client side.** Each handler gets a `RoomClient` with everything
you need:
```ts
// app/realtime/chat.ts
import { defineRoom } from "@wrnexus/core";
export default defineRoom({
onConnect(client) {
client.broadcast({ type: "system", text: "A user joined", online: client.room.count() });
},
onMessage(client, msg) {
const text = String(msg.text ?? "")
.slice(0, 500)
.trim();
if (!text) return;
client.room.broadcast({ type: "message", user: msg.user ?? "anon", text });
},
onLeave(client) {
client.broadcast({ type: "system", text: "A user left" });
},
});
```
The `client` / `client.room` API:
| Call | Reaches |
| ---------------------------------- | ---------------------------------------- |
| `client.send(msg)` | this connection |
| `client.broadcast(msg)` | everyone **else** in the room |
| `client.room.broadcast(msg)` | **everyone**, including the sender |
| `client.to(id \| ids).send(msg)` | specific connection(s) |
| `client.toUser(u \| users).send()` | a user / selected users (all their tabs) |
| `client.user = "u1"` | identify a connection for targeting |
| `client.data`, `client.room.state` | per-connection / shared room state |
Messages are objects (auto-serialized to JSON; incoming JSON is parsed). A
connection is identified by the authenticated session user, else `?user=`.
Because the room name is the URL path, a dynamic route `app/realtime/[room].ts`
gives one handler many independent rooms (`/realtime/lobby`, `/realtime/game-7`).
**Zero client JS.** A page just declares a room; the framework's realtime runtime
(`/__wrnexus/realtime.js`, auto-injected) connects, renders incoming messages into
your `<template>`s (fields via `%field%`, HTML-escaped), reflects connection
state, and sends the form:
```html
<div data-room="chat">
<span data-room-status data-room-status-class="wire-badge" class="wire-badge"></span>
<div data-room-log></div>
<template data-room-item="message"
><div><strong>%user%</strong>: %text%</div></template
>
<template data-room-item="system"><div class="muted">%text%</div></template>
<form data-room-send>
<input name="user" placeholder="Name" />
<input name="text" placeholder="Message…" data-room-reset />
<button>Send</button>
</form>
</div>
```
Need imperative control? `const room = wire.room("chat"); room.on("message", fn);
room.send({ … })` — same runtime, no boilerplate. The example ships this exact
chat at `/chat` (`app/pages/chat.wrn` + `app/realtime/chat.ts`, no client file).
The raw `websocket` export (with Bun pub/sub via `RealtimeSocket`) is still
supported for low-level needs.
## Reactive signals
`@wrnexus/reactive` ships a tiny, type-safe signal (no dependencies):
```ts
import { signal } from "@wrnexus/reactive";
const count = signal(0);
count.get(); // 0
count.set(1); // notifies subscribers
const off = count.subscribe((v) => console.log(v));
off(); // unsubscribe
```
This is the seed for richer reactive client state and the `.wrn` `state` block.
## Global styles & CSS frameworks
Styles are **global by default** and work for both SSR and CSR (the stylesheet
is linked in every page's `<head>`, so it styles server-rendered markup and
hydrated components alike).
Put CSS in `app/styles/global.css`. It is bundled by Bun (which resolves
`@import`, **including from `node_modules`**), served at `/__wrnexus/styles.css`,
and linked into every page automatically.
**Use any npm CSS framework** by importing it in `global.css`:
```css
/* app/styles/global.css */
@import "bootstrap/dist/css/bootstrap.min.css"; /* after: bun add bootstrap */
@import "./theme.css";
.card {
padding: 1rem;
& strong {
color: #fff;
}
} /* nesting works */
```
**Use any framework via CDN** (zero build) with `wrnexus.config.ts`:
```ts
import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = {
head: [
`<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />`,
// or Tailwind Play CDN (dev/prototyping):
// `<script src="https://cdn.tailwindcss.com"></script>`,
],
};
export default config;
```
**Use Tailwind / PostCSS / Sass for real** via the `styles.process` hook — it
runs at dev-serve time and at `wrnexus build`, and returns the final CSS:
```ts
const config: AppConfig = {
styles: {
entry: "app/styles/global.css",
process: async ({ entryPath, appRoot }) =>
await Bun.$.cwd(appRoot)`bunx @tailwindcss/cli -i ${entryPath} --minify`.text(),
},
};
```
In production, `wrnexus build` bundles the stylesheet to `dist/styles.css`
(minified) and the server serves it from disk.
---
## Public Assets
Put static files in `public/` next to `app/`. They are served from the site root
and are copied to `dist/public` during `wrnexus build`:
```
public/
favicon.ico -> /favicon.ico
robots.txt -> /robots.txt
site.webmanifest -> /site.webmanifest
images/logo.svg -> /images/logo.svg
```
Static asset resolution is path-safe: no traversal, no hidden files, and no
request path is ever converted into a route module path. API routes keep
priority over public files; public files are checked before page routes. If an
app does not provide `public/favicon.ico`, WrNexus serves a tiny default favicon
so browsers do not log a missing favicon request.
---
## SEO
Global SEO defaults live in `wrnexus.config.ts` and are merged with each page's
`meta` export:
```ts
import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = {
seo: {
title: "WrNexus Basic App",
titleTemplate: "%s | WrNexus",
description: "A Bun-first SSR framework demo.",
canonicalBase: "https://example.com",
robots: "index,follow",
siteName: "WrNexus",
type: "website",
twitterCard: "summary",
themeColor: "#6c8cff",
},
};
export default config;
```
Pages can override these values:
```tsx
export const meta = {
title: "About",
description: "Learn about this WrNexus app.",
canonical: "/about",
};
```
`.wrn` pages use a page-level `seo` block:
```my
page Hello {
seo {
title = "Hello from .wrn"
description = "A WrNexus .wrn page with SSR and CSR data."
canonical = "/hello"
}
view {
<h1>Hello</h1>
}
}
```
WrNexus renders standard description/canonical tags plus Open Graph, Twitter,
robots, keywords, and theme color tags when configured.
---
## Security Headers And CORS
WrNexus applies framework-level security headers to pages, API responses, assets,
errors, and framework endpoints. Defaults include:
- `Content-Security-Policy` with `script-src 'self'`, `object-src 'none'`,
`base-uri 'self'`, and `frame-ancestors 'none'`
- `Strict-Transport-Security` in production with one-year `max-age`,
`includeSubDomains`, and `preload`
- `Cross-Origin-Opener-Policy: same-origin`
- `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy` with risky browser capabilities disabled, including
`unload=()`
- Trusted Types enforcement in production, defaulting to extension-compatible
policy creation. Set `trustedTypes.policyNames` for a stricter allow-list.
CORS is opt-in from `wrnexus.config.ts`:
```ts
import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = {
security: {
cors: {
enabled: true,
origin: ["https://app.example.com", "http://localhost:5173"],
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 600,
},
trustedTypes: {
// Optional stricter production mode:
// policyNames: ["wrnexus", "default"],
},
},
};
export default config;
```
If you inject CDN scripts/styles through `head`, extend the CSP explicitly:
```ts
const config: AppConfig = {
head: [`<script src="https://cdn.tailwindcss.com"></script>`],
security: {
contentSecurityPolicy: {
directives: {
"script-src": ["'self'", "https://cdn.tailwindcss.com"],
"style-src": ["'self'", "'unsafe-inline'"],
},
},
},
};
```
---
## The Context object
```ts
export type Context = {
req: Request;
url: URL;
params: Record<string, string>;
locals: Record<string, unknown>;
cookies: CookieStore;
session: SessionStore;
localStorage: LocalStorageSnapshot;
};
export type Middleware = (
ctx: Context,
next: () => Promise<Response>,
) => Promise<Response> | Response;
```
Storage helpers are available in pages, API routes, middleware, and `.wrn` data
bindings:
```ts
ctx.cookies.get("theme");
ctx.cookies.set("theme", "dark", { sameSite: "Lax" });
ctx.session.set("userId", "42");
ctx.session.get<string>("userId");
ctx.localStorage.get("wrnexus.label"); // read-only snapshot from CSR requests
```
---
## Security notes
- **Safe route resolution.** Routes are matched against a table scanned at
startup; request paths are never concatenated into file paths.
- **No path traversal.** Hidden/underscore files are ignored; only
`.ts`/`.tsx`/`.wrn` route files are loaded; `isSafeRequestPath` rejects `..`
and null bytes as defense-in-depth.
- **HTML-escaped metadata.** Titles/descriptions are escaped before entering the
document head.
- **Validated component names.** `data-component` values and component filenames
must match `[A-Za-z0-9_-]+`; arbitrary imports from request input are impossible.
- **Security headers by default.** CSP, HSTS in production, COOP,
X-Frame-Options, Trusted Types in production, nosniff, referrer policy, and
Permissions-Policy are applied centrally by the runtime.
- **Configurable CORS.** `security.cors` handles preflight requests and applies
`Access-Control-*` headers only for allowed origins.
- **Server-only `.wrn` helpers.** `functions {}`, `ssr { ... }`, and
`client { ... }` data bindings stay in the server module and are never
serialized into HTML. Client data binding elements receive only an opaque
`data-wrnexus-csr` id; the browser calls `/__wrnexus/csr`, and WrNexus resolves the
real API path/render helper on the server. Inline client directives such as
`@click` should contain only behavior that is safe to reveal to the browser.
- **Readable vs. safe errors.** Development shows the stack; production returns a
generic page and never leaks internal file paths.
- **405 with `Allow`.** Unsupported API methods are reported correctly; unknown
routes return **404**.
---
## Future `.wrn` language vision
A single-file language that compiles down to the primitives above
(pages, components, API routes, realtime handlers, signals):
```my
page Home {
state count = 0
ssr {
api users GET /api/users {
return users.map((user) => user.name).join(", ")
}
}
client {
api latestUsers GET /api/users/latest {
return users.map((user) => user.name).join(", ")
}
}
view {
<h1>Hello</h1>
<button @click="count++">Count: {count}</button>
<div api="users">Loading users...</div>
<div api="latestUsers">Loading latest users...</div>
}
realtime chat {
on message(data) {
broadcast(data)
}
}
}
```
`@wrnexus/compiler` now parses a small real subset of `.wrn`; the longer-term vision
is still captured in [`packages/compiler/VISION.md`](packages/compiler/VISION.md).
---
## Roadmap
- [x] File-based pages, API routes, middleware, realtime
- [x] SSR-first rendering with escaped metadata
- [x] Server-rendered reactive components with props (`.wrn`, `data-component`)
- [x] Type-safe signals
- [x] **HMR over WebSocket** (instant CSS swap; soft DOM morph that keeps state — no visible refresh)
- [x] **Reactive directives bound to signals** (`data-scope`, `data-on-*`, `data-text`, `{expr}`)
- [x] **Per-page code-splitting** (the reactive runtime ships only where used)
- [x] **Production build** (`wrnexus build` → static manifest + bundled `dist/server.js`)
- [x] **Real `.wrn` compiler** (lexer → parser → codegen, compiled in dev and prod)
- [x] **Global styles + any CSS framework** (Bun CSS bundler, CDN `head`, Tailwind/PostCSS hook)
- [ ] Scoped / per-component CSS
- [ ] Fine-grained reactivity (per-binding dependency tracking)
- [ ] In-process HMR for page modules too (avoid restart via versioned recompile)
- [ ] Node adapter for `Bun.serve`-free deployment
---
## Newer capabilities
### HMR over WebSocket (dev)
`wrnexus dev` aims for updates that never look like a refresh. The page holds a
WebSocket to `/__wrnexus/hmr`, and the server watches `app/` and picks the cheapest
update per change:
| You edit… | What happens | Reload? |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `app/styles/*.css` | server pushes `{type:"css"}`; the browser **hot-swaps the stylesheet** | none — instant |
| pages / components / api / middleware / realtime | module can't be re-imported in-process, so the child restarts; the browser reconnects and **morphs the new HTML into the live DOM** | none — soft morph |
The soft morph is an index-based DOM diff that preserves scroll, focus, and
**client-owned state**: hydrated subtrees (`data-scope` regions) are left
untouched, so a counter at `5` stays at `5` instead of snapping back to the
server-rendered `0`. CSS edits — the most common tweak — never restart the
process and apply with zero flash.
### Reactive directives
A page subtree can be made reactive declaratively — the same mechanism that
hydrates components. The
generic reactive runtime (`/__wrnexus/reactive.js`, injected only when a page uses
`data-scope`) turns each scope key into a `signal()`:
```html
<div data-scope="count: 0">
Count is <strong data-text="count">0</strong>, doubled is {count * 2}.
<button data-on-click="count++">+1</button>
<button data-on-click="count = 0">reset</button>
</div>
```
`data-on-<event>` runs a statement, `data-text` binds an element's text, and
`{expr}` interpolates inside text nodes. This is exactly what the `.wrn` compiler
emits.
In TSX pages, raw `{count * 2}` is parsed by TypeScript as a server-side
JavaScript expression. Use the helper to emit client-side mustache text:
```tsx
import { mustache } from "@wrnexus/core";
export default function Page() {
return <p>Doubled is {mustache`count * 2`}.</p>;
}
```
The browser receives `{{count * 2}}`, and the reactive runtime evaluates it
against the nearest `data-scope`.
### Per-page code-splitting
Components render on the server, so the only browser script is the reactive
runtime (`/__wrnexus/reactive.js`), injected only when the rendered page contains
a `data-scope`. `/about` ships zero JS; `/` and `/reactive` ship only the shared
`reactive.js`.
### Production build
```bash
wrnexus build examples/basic-app # → examples/basic-app/dist
bun examples/basic-app/dist/server.js # PORT optional
```
`dist/server.js` is a single bundled Bun server with a **static manifest** of
every route and component module (no runtime filesystem scan, no on-the-fly
bundling). It serves production error pages and ships no live-reload client.
Framework JS/CSS/public assets use long production cache headers, and **every
output is minified**: `server.js` (which bundles all page/component/route
modules), the reactive runtime (`dist/reactive.js`), and the stylesheet
(`dist/styles.css`).
### Writing a page in `.wrn`
The `view` block is **plain HTML** — nothing new to learn. Text may contain
`{expr}` interpolation, attributes can be hyphenated (`data-*`, `aria-*`), void
elements (`<br>`, `<img>`) and self-closing tags (`<x/>`) work, and
`@event="..."` declares a client event binding (compiled to `data-on-<event>`).
`app/pages/hello.wrn``/hello`:
```
page Hello {
state count = 0
seo {
title = "Hello from .wrn"
description = "A WrNexus .wrn page showing SSR data, CSR hydration, cookies, sessions, and localStorage."
canonical = "/hello"
}
ssr {
functions {
function userNames(users) {
return users.map((user) => user.name).join(", ")
}
}
api ssrUsers GET /api/users/ssr {
const visits = Number(cookies.get("hello_visits") ?? "0") + 1
cookies.set("hello_visits", String(visits), { sameSite: "Lax" })
session.set("lastHelloVisit", visits)
return `${userNames(users)} - visit ${visits}`
}
}
client {
functions {
function userNames(users) {
return users.map((user) => user.name).join(", ")
}
}
api csrUsers GET /api/users/csr {
const label = localStorage.get("wrnexus.label") ?? "browser"
session.set("lastClientLabel", label)
return `${userNames(users)} - ${label}`
}
}
view {
<h1>Hello from .wrn</h1>
<p>Hello, WrNexus. Count is {count}, doubled is {count * 2}.</p>
<div api="ssrUsers">Loading SSR users...</div>
<div api="csrUsers">Loading CSR users...</div>
<div class="my-actions">
<button @click="count++">Increment</button>
<button @click="count = 0">Reset</button>
</div>
}
style {
.my-actions {
display: flex;
gap: 0.5rem;
}
}
realtime hello {
on message(data) {
console.log(data)
}
}
}
```
The business logic and data definitions live in normal file-based API routes:
```ts
// app/api/users/ssr.ts -> /api/users/ssr
export const GET = async () => {
return Response.json({
users: [
{ id: 1, name: "Ada SSR" },
{ id: 2, name: "Grace SSR" },
],
});
};
```
`ssr { api ... }` runs before the HTML is sent. `client { api ... }` hydrates
after the first paint through `/__wrnexus/csr?route=<page>&id=<binding>`.
Inside `.wrn` data blocks, `cookies`, `session`, and `localStorage` are available
directly. `localStorage` is a read-only snapshot sent by the browser for CSR
data bindings; SSR cannot read browser localStorage before the browser makes a
request.
The compiler (`@wrnexus/compiler`: `tokenizer.ts``parser.ts``codegen.ts`)
lowers `view` to SSR HTML, `style` to a page-local inline stylesheet, `state` to
`data-scope`, `functions` to server-only helpers, `@click` to `data-on-click`,
and keeps `{expr}` as mustache — all hydrated by the reactive runtime above.
Server helper code is never serialized into HTML. SSR bindings replace the
element content before HTML is returned. Client bindings keep the loading text
in the SSR HTML, then fetch after hydration. The target element receives only an
opaque `data-wrnexus-csr` marker; the real API path, response mapping, and helper
functions stay server-side. Legacy `ssrGet`/`csrGet` attributes still work, but
new code should prefer named `ssr`/`client` blocks.
Embedded `realtime` blocks register `/realtime/<name>` WebSocket routes. `.wrn`
files are compiled transparently in both `dev` and `build`.
---
## Authentication, CSRF, sessions
Password hashing (argon2id via `Bun.password`), session login/logout, and a
route guard live in `@wrnexus/core`:
```ts
import {
hashPassword,
verifyPassword,
logIn,
logOut,
getUser,
requireAuth,
sessionAuth,
} from "@wrnexus/core";
// Registration
const passwordHash = await hashPassword(password);
// Login (API route)
const user = await GetUserByEmail(getDb(), { email });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return Response.json({ ok: false, error: "Invalid credentials" }, { status: 401 });
}
logIn(ctx, { id: user.id, email: user.email }); // stores a safe user object in the session
// Read the current user anywhere
const me = getUser(ctx); // null when anonymous
```
Add `sessionAuth()` early in the middleware chain to hydrate `ctx.user` on every
request, and `requireAuth()` to protect routes — it returns **401 JSON** for
`/api/*` (and `Accept: application/json`) requests and a **302 redirect** to
`/login?next=…` for page navigations. `logOut(ctx)` clears the session.
**CSRF** protection uses the double-submit-cookie pattern and is on by default:
page loads set a readable `wire-csrf` cookie, the form runtime echoes it as the
`x-csrf-token` header, and `verifyCsrf(ctx)` (or the `csrfProtection()`
middleware) rejects mismatches with 403. Safe methods (GET/HEAD/OPTIONS) always
pass.
## Client-side navigation
Served on every page as `/__wrnexus/nav.js` — a progressive enhancement that
intercepts same-origin `<a>` clicks, fetches the target, swaps the `#app`
container, and updates history/title/scroll with no full reload. It ensures any
framework runtimes the new page needs are loaded on demand, re-hydrates reactive
scopes, and falls back to a full navigation on cross-origin links, modified
clicks, non-HTML responses, or a missing `#app`. Opt a link out with
`data-no-nav`. Programmatic navigation: `window.__wrnexusNavigate(url)`; forms
with `data-redirect` use it automatically. Server data loads on the server (SSR
`api` bindings), so the fetched HTML already carries fresh data.
## Environment configuration
Validate environment variables at startup with the same schema builder used for
forms — one readable error lists every problem:
```ts
// app/env.ts
import { v, parseEnv } from "@wrnexus/validation";
export const env = parseEnv<{ DATABASE_URL: string; PORT?: number }>(
v.object({ DATABASE_URL: v.string().min(1), PORT: v.number().optional() }),
);
```
Values are read from `Bun.env` / `process.env` and coerced by the schema
(`PORT` → number, flags → boolean).
## Config profiles (dev / prod / uat / test / …)
Run the same project under different named profiles — each bundles config
overrides **and** an `.env` cascade. Select one with `--profile=<name>` (on
`dev`, `build`, `db`) or the `WRNEXUS_PROFILE` env var:
```bash
wrnexus dev --profile=uat # dev server with UAT config + .env.uat
wrnexus build --profile=production # prod build with production overrides
wrnexus db migrate --profile=uat # migrate the UAT database
wrnexus profiles # list profiles + their env files, mark the active one
```
Define profiles in `wrnexus.config.ts` — each block is **deep-merged** over the
base config when active:
```ts
export default {
db: { driver: "sqlite", url: "file:./dev.db" }, // base (development)
profiles: {
production: { db: { driver: "postgres", url: process.env.DATABASE_URL! } },
uat: {
db: { driver: "postgres", url: process.env.DATABASE_URL! },
seo: { robots: "noindex,nofollow" },
},
test: { db: { driver: "sqlite", url: "file:./test.db" } },
},
};
```
Env files load in precedence order (later wins, and **real env vars always win**):
`.env``.env.<profile>``.env.local``.env.<profile>.local`. Keep secrets
in the `.local` files (git-ignored); commit `.env` / `.env.<profile>` for shared
non-secret defaults. In production the built server also loads the deployment's
`.env.<profile>` at startup for runtime secrets.
## Deployment adapters
The production server is split into a portable request handler and a Bun server:
```ts
import { createProductionHandlers, serveNode } from "@wrnexus/dev-server";
const { fetch, websocket } = createProductionHandlers(manifest, options); // WinterCG (req) => Response
Bun.serve({ fetch, websocket }); // Bun (default, includes WebSockets)
await serveNode(fetch, { port: 3000 }); // node:http bridge (HTTP only)
```
`serveNode` / `nodeListener` bridge the fetch handler onto `node:http`
(converting `IncomingMessage``Request`/`Response`, preserving multiple
`Set-Cookie` headers). Note the production handler uses Bun-native APIs for
assets, WebSockets, and the database, so full Node hosting needs Bun-compatible
globals; the bridge is ideal for WinterCG hosts and embedding.
## Rate limiting, logging, caching, uploads, streaming
All in `@wrnexus/core`:
```ts
import {
rateLimit,
requestLogger,
TTLCache,
cacheControl,
withCacheControl,
etag,
notModified,
saveUpload,
collectUploads,
streamResponse,
sse,
} from "@wrnexus/core";
// Rate limit (fixed window, per client IP) — sets RateLimit-* + Retry-After
export default rateLimit({ max: 60, windowMs: 60_000 });
// Structured request logging — pretty in dev, JSON in prod; adds a request id
export default requestLogger({ format: "json" });
// Data cache with TTL (memoise expensive work)
const cache = new TTLCache<User[]>(30_000);
const users = await cache.getOrLoad("active", () => ListActiveUsers(getDb()));
// HTTP caching + conditional requests
const body = renderPage();
const tag = etag(body);
if (notModified(ctx.req, tag)) return new Response(null, { status: 304 });
return withCacheControl(new Response(body, { headers: { etag: tag } }), { maxAge: 60 });
// File uploads (Bun parses multipart natively)
for (const { file } of collectUploads(await ctx.req.formData())) {
await saveUpload(file, {
dir: "./uploads",
maxBytes: 5_000_000,
allowedTypes: ["image/png", ".jpg"],
});
}
// Streaming SSR / SSE
return streamResponse(
(async function* () {
yield "<h1>";
yield await slowPart();
yield "</h1>";
})(),
);
return sse(
(async function* () {
yield { event: "tick", data: String(Date.now()) };
})(),
);
```
## Database: pagination, relations, seeding, studio
```ts
import { paginate, loadRelated } from "@wrnexus/db";
// Offset pagination with metadata (total, totalPages, hasNext/hasPrev)
const page = await paginate(
getDb(),
{ sql: "SELECT * FROM users ORDER BY name", model: users },
{ page: 2, perPage: 20 },
);
// Batched relation loading (no N+1)
const authors = await getDb().all("SELECT * FROM users", [], users);
const withPosts = await loadRelated(getDb(), authors, {
table: "posts",
foreignKey: "userId",
as: "posts",
});
```
- `wrnexus db seed` runs a re-runnable `app/db/seed.ts` (a `default async (db) => {}`).
- `wrnexus db studio` introspects the connected database — list tables with row
counts, or dump a table's first rows.
- `wrnexus dev` regenerates `app/db/queries.gen.ts` from your `.sql` at startup.
## SSR state-text baking
Reactive interpolations in pages and components bake their **initial value** into
a `data-text` span, so no-JS clients see real content (`Count: 0`, not
`Count: {count}`) and the reactive runtime keeps it live after hydration.
Component props also resolve `{t:key}` i18n markers per request, so you can pass
localized text into a component: `<div data-component="badge" label="{t:status.new}">`.
## Production hardening
**Shared, pluggable stores.** Sessions and rate-limit counters default to
process-local memory; swap in a shared backend so they survive restarts and work
across instances. A persistent SQLite backend ships built-in:
```ts
import { setSessionBackend, rateLimit } from "@wrnexus/core";
import { sqliteSessionStore } from "@wrnexus/db/session";
setSessionBackend(sqliteSessionStore("./sessions.db")); // or implement SessionBackend (Redis/SQL)
rateLimit({ max: 100, store: myRateLimitStore }); // store implements RateLimitStore
```
Sessions regenerate their id on login (fixation defense), expire on a 24h idle
TTL, and are GC'd; ids are 256-bit. `rateLimit` keys on the non-spoofable socket
peer IP by default (`trustProxy: true` to honour `x-forwarded-for` behind a proxy).
**ETag / 304.** Rendered pages send a content ETag and answer conditional GETs
with a 304 when unchanged — hashed on page content, so the per-request CSP nonce
doesn't defeat it.
**CSP nonces.** Every request gets a nonce (`ctx.locals.cspNonce`); framework
inline scripts carry it and CSP `script-src` uses `'nonce-…'` instead of
`'unsafe-inline'`. Use it for your own inline scripts under a strict policy.
**gzip.** Text responses are gzipped when the client accepts it (~65% smaller
HTML); streaming/SSE opt out via `no-transform`.
**Live Postgres / MySQL.** `bun run test:db:live` starts both via
`docker-compose.yml`, runs the gated adapter tests, and tears down. The suite
self-skips without `WRNEXUS_PG_URL` / `WRNEXUS_MYSQL_URL`.
## Dynamic lists, typed routes, WS security
- **`data-for`** renders reactive lists — `<li data-for="t in todos">{t.text}</li>`.
The client expression language now supports member access, calls, arrays,
objects, comparison, logical, and ternary — all eval-free (strict-CSP safe).
- **Typed routes.** `wrnexus dev` writes `app/routes.gen.ts` (a `Routes` map +
`href()`), so `href("/users/[id]", { id })` is checked at compile time.
- **WebSocket origin check** rejects cross-site handshakes (CSWSH); rooms gate
with `authorize`; requests over `maxBodyBytes` get a 413; `/healthz` + graceful
shutdown for containers.
## Optional packages
Opt-in helpers and feature packages — import only what you need:
| Package | What it gives you |
| --------------------- | ------------------------------------------------------------------------------------- |
| `@wrnexus/helpers` | Context URL helpers and safe forward-auth `redirectToLogin` responses |
| `@wrnexus/jwt` | HS256 `signJwt`/`verifyJwt` + `jwtAuth` bearer middleware (stateless auth) |
| `@wrnexus/oauth` | OAuth 2.0 sign-in with PKCE — Google/GitHub/Discord presets + `defineProvider` |
| `@wrnexus/authz` | Authorization: RBAC (`defineRbac`), PBAC/ABAC policies (`any`/`all`/`attr`), guards |
| `@wrnexus/encryption` | AES-256-GCM `encrypt`/`decrypt`, `generateKey`, PBKDF2 `deriveKey`, `sha256`, HMAC |
| `@wrnexus/pubsub` | Topic pub/sub with wildcards + a pluggable driver (Redis/NATS) |
| `@wrnexus/queue` | Background job queue — delays, retries + backoff, **recurring** jobs, workers |
| `@wrnexus/tracking` | Error tracking — `capture`, pluggable sinks, and a request-capturing middleware |
| `@wrnexus/test` | App testing — `renderComponent`, `mountHtml`, `callRoute`, `createHarness` + bun:test |
```ts
import { signJwt, jwtAuth } from "@wrnexus/jwt";
import { getOriginalRequestUrl, redirectToLogin } from "@wrnexus/helpers";
import { google, startAuth, completeAuth } from "@wrnexus/oauth";
import { defineRbac, requirePermission, any, attr } from "@wrnexus/authz";
import { encrypt, decrypt, generateKey, sha256, hmacSign } from "@wrnexus/encryption";
import { createPubSub } from "@wrnexus/pubsub";
import { createQueue } from "@wrnexus/queue";
import { createTracker, consoleSink } from "@wrnexus/tracking";
```
OAuth in three lines (Google shown; GitHub/Discord/custom are identical):
```ts
const provider = google({ clientId, clientSecret });
const { url, state, verifier } = await startAuth(provider, { redirectUri }); // store state+verifier, 302 to url
const { profile } = await completeAuth(provider, { code, redirectUri, verifier }); // on the callback → logIn(ctx, profile)
```
**Validation** gained rules — `url()`, `uuid()`, `date()`, `length()`,
`oneOf([...])`, `trim()`, `.default(v)`, and server-only `.refine(fn)` — plus
`v.number().positive()`. All (except `refine`) mirror to the client validator.
**i18n** gained locale formatting: `formatNumber`, `formatCurrency`, `formatDate`,
`formatRelativeTime`, and CLDR `plural(n, forms, lang)`. The reactive runtime
gained **`data-show="expr"`** for conditional visibility (tabs, toggles).
**Containerization.** `wrnexus generate docker` scaffolds a multi-stage
`Dockerfile` (build with Bun → slim runtime, health-checked on `/healthz`), a
`.dockerignore`, and a `docker-compose.yml` (app + Postgres). Then
`docker compose up --build`.
## Testing
Write tests for **your** app with `@wrnexus/test` — one import gives you the
`bun:test` primitives (`test`, `expect`, `describe`, …) plus WrNexus helpers.
Run them with `wrnexus test` (which defaults to the `test` profile, so it loads
`.env.test` and your config's `test` overrides):
```ts
// app/example.test.ts
import { test, expect, renderComponent, callRoute, createHarness } from "@wrnexus/test";
// 1. Render a component to HTML (fast, no server)
test("counter shows its label", async () => {
const html = await renderComponent(COUNTER_SRC, { start: 5, label: "Clicks" });
expect(html).toContain("Clicks");
});
// 2. Call an API handler with a fake Request
test("echo route", async () => {
const { POST } = await import("./api/echo.ts");
const res = await callRoute(
POST,
new Request("http://t/api/echo", { method: "POST", body: "{}" }),
);
expect(res.status).toBe(200);
});
// 3. Boot the whole app on an ephemeral port and fetch real routes
test("home page responds", async () => {
const app = await createHarness(import.meta.dir + "/..");
const res = await app.fetch("/");
expect(res.status).toBe(200);
app.close();
});
```
`@wrnexus/test` also exports `mountHtml(html)` — mounts server-rendered HTML in a
happy-dom window with the reactive runtime hydrated, so you can assert on
`data-for`/`data-show`/`data-text` behaviour.
```bash
wrnexus test # run once (test profile)
wrnexus test --watch # re-run on change
```
The framework itself ships a `bun test` suite across the compiler, reactive
runtime (with dependency tracking), client navigation, validation, i18n,
theming, auth/CSRF/sessions, security headers, middleware
(rate-limit/logging/cache/uploads/streaming), the node adapter, and the database
(CRUD, migrations, query generation, pagination, relations, SQLite sessions).
Run `bun run check` for typecheck + lint + tests + format.
## Editor support (VS Code)
The `editors/vscode` extension gives `.wrn` files first-class editing:
- **Syntax highlighting** with **embedded languages** — HTML inside `view`
(including `{expr}` interpolation, `{t:key}` translations and `@event=` bindings),
CSS inside `style`, and TypeScript inside `functions`/`api`/`ssr`/`client`/`realtime`.
- **WrNexus attributes stand out in their own colors** so you can spot them at a
glance: `@event` bindings and `{t:…}` translations render in one accent, and
runtime directives (`data-component`, `data-for`, `data-show`, `data-text`,
`data-scope`, `data-slot`, `data-on-*`, `data-wire-*`) in another — via a
grammar injection, so they're distinct even inside ordinary HTML. Override the
colors in your settings under `editor.tokenColorCustomizations` → the
`entity.other.attribute-name.wrn.*` scopes.
- **Inline diagnostics** — parse errors from the real `@wrnexus/compiler` appear as
you type, anchored to the exact offset.
- **Snippets** — `page`, `component`, `view`, `state`, `props`, `seo`, `api`,
`ssr`, `client`, `realtime`, `functions`, `style`, plus view helpers `mount`,
`for`, `show`, `t`.
- **Completions** — block keywords at file scope; `data-*` attributes and
`@event` bindings inside a `view`; HTTP methods after `api`.
Load it from `editors/vscode` (press <kbd>F5</kbd> for an Extension Development
Host). Diagnostics use a bundled copy of the compiler — regenerate it with
`bun run build:compiler` after changing `@wrnexus/compiler`.