Files
Clintchiz 2c960fc1dc
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
refactor: migrate legacy wire namespace to wrn
2026-08-12 18:51:15 +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(--wrn-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-wrn-theme-toggle toggles light/dark; data-wrn-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 connected 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 WrNexus 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(--wrn-*)), or style { }.
  5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.