Files
WRNexusJS/examples/inter-app-api-showcase/apps/web/CLAUDE.md
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

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

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

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/*.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)

// 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"

  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.