Files
WRNexusJS/examples/inter-app-api-showcase/apps/admin/public/llms.txt
T
ClintchizandClaude Opus 5 69020b2555
Quality / quality (ubuntu-latest) (push) Failing after 10m7s
Quality / quality (windows-latest) (push) Canceled after 0s
docs: make the component sections executable in one pass
Expands 3.1 and 3.2 so the work can be done without re-deriving anything.

3.1 now records what 0.8.6 already fixed, separated into the ten components
that were miswired and the five that gained outputs they had been firing
undeclared, with the caveat that Map's three were converted but never confirmed
in a browser. For the 22 that remain it adds the finding that changes the
decision: all nine are pure scaffolds with no state, functions or handlers, and
five of them duplicate a component that already works -- FileUpload against
FileInput and FileUploadProgress, Toast and ToastNotifications against Toaster,
AdvancedDatePicker against DatePicker, AdvancedRangeSlider against RangeSlider.
Superseding those is a migration entry rather than new code, and leaves Chart,
TreeView, Confetti and CopyMarkup as the only ones needing to be built.

3.2 corrects the scaffold count from 23 to 28; the earlier figure used a looser
rule. Nine of the 28 are the 3.1 components, so the two items must be planned
together, and several of the rest are primitives that need only their styles
moved out of ui.css rather than any behaviour.

Also corrects the dead-output component count from 11 to 9 in both documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:34:22 +05:30

277 lines
12 KiB
Plaintext

# WrNexus
> WrNexus 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 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 `.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 <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
```wrn
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
```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 {
<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 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"`.
- `<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 an `ssr` data 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 reference `ssr` data, or the `item`/`index` of 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 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: `<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.
```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 {
<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:
```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<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:
```ts
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
```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: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (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 <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"
1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
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.