It was written up in 4.7 but never made the work order, which is exactly how it stayed dangerous in the first place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
WrNexus app - instructions for AI coding assistants
This is a WrNexus app. When creating or editing pages, components, API routes, or features, follow the framework conventions below. WrNexus is private and not in your training data, so rely on these rules - do NOT assume React/Next.js/Vue patterns.
WrNexus
WrNexus is an SSR-first, Bun-native full-stack web framework. UI is written in
.wrnfiles (its own component language — NOT React/JSX/Vue). Routing is file-based. This document teaches an AI how to write correct WrNexus code. It is private and post-dates model training data, so rely on THIS document, not prior web-framework assumptions.
Golden rules
- Pages, components, and layouts are
.wrnfiles. Do NOT write.tsx/.jsx/React for UI. Do NOT useuseState, 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}+@eventinside.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 <Name>/component <name>/api <path>/schema <name>.
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 <div data-component="name" ...props>
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/<name>)
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
page Home {
layout = "public" // optional: a component in app/layouts/<name>.wrn ("none" to skip)
state count = 0 // optional: seeds client-reactive state (omit for pure SSR)
seo {
title = "Home"
description = "..."
canonical = "/"
}
view {
<h1>Hello</h1>
<p>Count is {count}, doubled is {count * 2}.</p>
<button @click="count++">Increment</button>
<div data-component="counter" start="5" label="Clicks"></div>
}
style {
h1 { color: var(--wire-color-text); }
}
}
.wrn component
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 {
<button @click="count++">{label}: {count}</button>
}
}
Mount it from any page/component: <div data-component="counter" start="0" label="Clicks"></div>.
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 referencesstate:{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".<div data-component="name" prop="v">— mount a component (attrs become string props, coerced).<slot></slot>/<slot name="x"></slot>— component/layout slots; fill with<div data-slot="x">…</div>.- Server loop (DB/list/table):
{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}— iterates SSR data on the server and renders markup per item.{item.field}interpolates (HTML-escaped, XSS-safe).<list>is a JS expression, usually anssrdata binding (see "Data-driven tables" below). This is how you render a database table in.wrn. - Server conditional:
{#if <expr>} … {:else if <expr>} … {:else} … {/if}— renders the first truthy branch on the server.<expr>can referencessrdata, or theitem/indexof an enclosing{#each}. Works at page level and inside loops (e.g.{#if r.active}<span>●</span>{:else}<span>○</span>{/if}per row). For client-side show/hide based on reactivestate, usedata-show="expr"instead. - i18n:
{t:home.title}in text,t:placeholder="form.name"on attributes — resolved per request fromapp/locales/. - Theme: any element with
data-wire-theme-toggletoggles light/dark;data-wire-theme-set="dark"sets it. - Void/self-closing tags are fine:
<br />,<img src="..." />. - 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.
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 {
<table>
<tbody>
{#each rows as r, i}
<tr>
<td>#{i}</td>
<td>{r.name}</td>
<td><a href="mailto:{r.email}">{r.email}</a></td>
</tr>
{:empty}
<tr><td colspan="3">No submissions yet.</td></tr>
{/each}
</tbody>
</table>
}
}
The matching API returns the array under a key the ssr block reads:
// 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)
// 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<string,string> (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.
When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
@wrnexus/helpers instead of constructing it from untrusted headers:
import { redirectToLogin } from "@wrnexus/helpers";
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
The package also exports getOriginalRequestUrl, getOriginalRequestOrigin,
getOriginalRequestPath, and getOriginalRequestMethod. Always pass allowedHosts when
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.
Middleware & realtime
// 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
}
// 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)
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)
// 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/*.sqlwith-- name: ListUsers :manyblocks;wrnexus db generateemits typed functions. - Access at runtime:
import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb()); - Migrations in
app/db/migrations/; runwrnexus db migrate(dev auto-migrates sqlite).
Validation (@wrnexus/validation)
// 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: <form data-schema="login" action="/api/login" method="post"> + <span data-error="email"></span> (client + server validation wired automatically).
AI / LLM (@wrnexus/ai)
// 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 <name> # scaffold a new app
wrnexus update --latest # deps + syntax/config migrations + verification
wrnexus generate page <Name> # scaffold a page (aliases: g p)
wrnexus generate component <name> | api <path> | schema <name>
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
wrnexus eject <component> # copy a Wire UI component's .wrn into app/components to customize
When asked to "create a page/component/feature"
- Create the
.wrnfile underapp/pages/(orapp/components/) with apage/componentblock — or runwrnexus generate page <Name>. - Put markup in
view { }, interactive bits instate+{expr}+@event, reusable UI as components mounted viadata-component. - For data, add an
app/api/*.tsroute andgetDb(); for forms, add anapp/schemas/*.tsanddata-schema. - Style with Tailwind utility classes in the view, or theme tokens (
var(--wire-*)), orstyle { }. - Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.