diff --git a/docs/framework-remediation-plan.md b/docs/framework-remediation-plan.md index 18b9fd69..5eee289c 100644 --- a/docs/framework-remediation-plan.md +++ b/docs/framework-remediation-plan.md @@ -581,7 +581,7 @@ confusing ways. --- -## 4.6 Runtime and client-module size — measured +### 4.6 Runtime and client-module size — measured A per-subsystem measurement of `reactive.js` and of the generated client modules, made by minifying the runtime repeatedly with one subsystem removed @@ -593,7 +593,7 @@ the real number. "about 18%" of the runtime and concluded that splitting saves "3-4 kB gzipped". Both figures were wrong, and the conclusion that followed from them was wrong. -### The runtime today +#### The runtime today `reactive.js` is **70,101 bytes minified, 21,736 gzipped**. Removing each subsystem and re-minifying gives its true cost: @@ -619,7 +619,7 @@ fetch, which are framework features) total **23,722 minified / 6,660 gzipped — 15,076 gzipped: the expression engine, the scope and reactivity core, and loop diffing. -### How much of it a page actually uses +#### How much of it a page actually uses Measured against the example app by checking which controller markers appear in the served HTML: @@ -650,7 +650,7 @@ bun run scripts/lib/measure-runtime-size.ts # core must stay under budget Plus a browser check on `/`: zero controller chunks requested. -### The bigger problem: generated client modules +#### The bigger problem: generated client modules The runtime is not where the weight is. On `/navigation`: @@ -723,6 +723,80 @@ is duplicating itself and should fail the check. Existing behaviour is covered by the current suite, so correctness is the 1,427 tests; this is purely a size assertion on top. +### 4.7 `generate-ui-complete-catalog.mjs` is broken and destructive + +**Issue.** The script fails partway through with +`TypeError: factories[entry.category] is not a function` +(`scripts/generate-ui-complete-catalog.mjs:156`) — but not before it has already +started writing. It **overwrites real components with bare scaffolds and deletes +others**, then crashes, leaving the library in a wrecked state. + +**Evidence.** Running it on 2026-08-09 rewrote Accordion, alert, Badge, +AvatarGroup, ToggleCount and LayoutSplitter down to ~15-line stubs, deleted 24 +component files, and renamed `Card`, `Container`, `Divider` and `Grid` to +lowercase — 113 files changed in total. Nothing in `package.json` references it, +so no gate runs it and no gate would have caught the damage. + +**Why it is worse than it looks.** The rename is the dangerous part. Windows is +case-insensitive, so after `git checkout -- .` the tree reported **clean** while +four components were still misnamed on disk. Only a test failure exposed it. On +a case-sensitive filesystem the same script produces duplicate files instead. + +**Change.** Pick one: + +- **Delete it.** `generate-ui-component-reference.mjs` is the maintained + generator, it is wired into `release:prepare`, and it works. If this script is + redundant, it is a loaded gun in the repo for no benefit. +- **Or fix and gate it**: make it write to a temp directory and swap atomically + only on success, so a mid-run crash cannot leave a partial library. Then add + it to a check so it cannot rot again. + +Whichever is chosen, **no script that rewrites `packages/ui/components/` should +write in place.** Generate to a staging directory, validate, then move. + +**Related, and worth doing regardless:** several other ungated scripts mutate +the repository or start servers when run — `install-captcha.mjs`, the +`validate-*.mjs` and `verify-*.mjs` families, and `benchmark-framework.mjs`. All +of them fail today. They should either be repaired and gated, or removed. A +`scripts/` directory where running a file at random can scaffold apps, start +dev servers on ports 3000-3002 and rewrite the component library is a hazard to +anyone exploring the repo, human or otherwise. + +**How to test.** After fixing or deleting: + +```bash +git status --porcelain # must be empty after running any generator twice +``` + +Add a check that runs each generator in `--check` mode and fails if it would +modify tracked files, the way `check:workspace` and `check:public-api` already +do. + +### 4.8 Filename casing is not consistent with the git index + +**Issue.** Four components were tracked in git under lowercase names +(`card.wrn`, `container.wrn`, `divider.wrn`, `grid.wrn`) while existing on disk +under capitalised ones. Windows hid the discrepancy; `git status` reported clean. + +**Why it matters.** `ui-redesign-contract.test.ts` reads the real directory and +expects `Card.wrn`. **On a fresh clone on Linux or in CI the files arrive +lowercase and that test fails** — a latent break that could not reproduce on a +Windows workstation. + +**Change.** Done — the index now tracks the capitalised names, matching the +component each file declares (`component Card`, `component Container`, and so +on) and matching every other component in the library. + +**How to test.** + +```bash +git ls-files packages/ui/components/ | grep -iE '/(card|container|divider|grid)\.wrn' +``` + +must return the capitalised names. Better, set `git config core.ignorecase +false` locally so a future rename cannot hide again, and consider a check that +compares `git ls-files` against the on-disk listing byte for byte. + ## 5. Order of work Ranked by return, not by size. The first item changes the cost of every item diff --git a/examples/inter-app-api-showcase/.editorconfig b/examples/inter-app-api-showcase/.editorconfig deleted file mode 100644 index 86a63dc0..00000000 --- a/examples/inter-app-api-showcase/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/examples/inter-app-api-showcase/.env.example b/examples/inter-app-api-showcase/.env.example deleted file mode 100644 index a5c8eed9..00000000 --- a/examples/inter-app-api-showcase/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -REDIS_URL=redis://localhost:6379 -AUTH_SECRET=replace-with-at-least-32-random-characters diff --git a/examples/inter-app-api-showcase/.gitignore b/examples/inter-app-api-showcase/.gitignore deleted file mode 100644 index 82122e42..00000000 --- a/examples/inter-app-api-showcase/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -node_modules/ -dist/ -.wrnexus/ -**/.wrnexus/ -coverage/ -.env -.env.* -!.env.example -!.env.*.example -*.log -*.db -*.db-shm -*.db-wal -*.sqlite -*.sqlite3 -uploads/ -mobile/android/ -mobile/ios/ -mobile/.expo/ -.idea/ -.vscode/* -!.vscode/settings.json -!.vscode/extensions.json -*.tsbuildinfo -.eslintcache diff --git a/examples/inter-app-api-showcase/.prettierignore b/examples/inter-app-api-showcase/.prettierignore deleted file mode 100644 index b62d22e1..00000000 --- a/examples/inter-app-api-showcase/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -dist/ -.wrnexus/ -**/.wrnexus/ -*.log -**/CLAUDE.md diff --git a/examples/inter-app-api-showcase/.prettierrc.json b/examples/inter-app-api-showcase/.prettierrc.json deleted file mode 100644 index 32474fc7..00000000 --- a/examples/inter-app-api-showcase/.prettierrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": false, - "trailingComma": "all", - "endOfLine": "lf" -} diff --git a/examples/inter-app-api-showcase/.vscode/extensions.json b/examples/inter-app-api-showcase/.vscode/extensions.json deleted file mode 100644 index 59ff820b..00000000 --- a/examples/inter-app-api-showcase/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] -} diff --git a/examples/inter-app-api-showcase/.vscode/settings.json b/examples/inter-app-api-showcase/.vscode/settings.json deleted file mode 100644 index 90ef9bee..00000000 --- a/examples/inter-app-api-showcase/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "prettier.requireConfig": true, - "[wrn]": { "editor.defaultFormatter": "wrnexus.wrnexus", "editor.formatOnSave": true } -} diff --git a/examples/inter-app-api-showcase/README.md b/examples/inter-app-api-showcase/README.md index e75be186..d9ceaf5a 100644 --- a/examples/inter-app-api-showcase/README.md +++ b/examples/inter-app-api-showcase/README.md @@ -1,44 +1,19 @@ -# inter-app-api-showcase +# Inter-app + external API showcase -A WrNexus **workspace** — multiple apps, one gateway, interconnected. +`GET /api/product-summary?sku=starter` demonstrates one request handler making: -``` -inter-app-api-showcase/ - wrnexus.workspace.ts # apps ↔ domains map (used by `wrnexus gateway`) - apps/ - web/ # a WrNexus app → localhost, web.localhost - admin/ # a WrNexus app → admin.localhost - packages/ - shared/ # @app/shared — shared code + cross-app pubsub bus +1. an RPC request to the `catalog` app (`getProduct`); +2. an external HTTPS request to GitHub's public REST API; and +3. an RPC request to the `audit` app (`recordLookup`). + +The handler deliberately forwards `as: ctx` only to WRNexus peer apps. The RPC package turns that into a short-lived subject/tenant token; it is never forwarded to GitHub. Each peer app must implement the same contract from `app/lib/contracts.ts` (normally a shared workspace package) under `app/services/`, and must authorize its own procedures. + +Before running this app, configure all three apps with the same private internal-origin map and a distinct, 32+ character RPC secret: + +```sh +WRNEXUS_RPC_SECRET=replace-with-a-private-32-character-minimum-secret +WRNEXUS_APP_NAME=product-summary +WRNEXUS_INTERNAL_ORIGINS={"catalog":"http://127.0.0.1:4101","audit":"http://127.0.0.1:4102"} ``` -## Run everything (one port, routed by domain) - -```bash -bun install -bun run dev # = wrnexus gateway → http://127.0.0.1:3000 -``` - -Add the hosts to your machine (e.g. /etc/hosts): - -``` -127.0.0.1 web.localhost admin.localhost -``` - -Open `http://localhost:3000` for the web app or -`http://admin.localhost:3000` for the admin app. The ports printed for individual -apps are internal gateway targets, not public workspace URLs. - -## Interconnect - -- **Shared code:** import `@app/shared` in any app. -- **Runtime messaging:** `import { bus } from "@app/shared"` then - `bus.publish("tenant:created", {...})` in one app and - `bus.subscribe("tenant:*", fn)` in another (needs Redis). -- **Databases:** point apps at the same `db`/`databases` in their config. - -## Add another app - -```bash -wrnexus workspace add reports --domain=reports.localhost -``` +The peer app processes must remain private; the public gateway blocks the RPC route by design. Run with `bun run --cwd examples/inter-app-api-showcase dev`. diff --git a/examples/inter-app-api-showcase/app/api/product-summary.ts b/examples/inter-app-api-showcase/app/api/product-summary.ts new file mode 100644 index 00000000..6567754a --- /dev/null +++ b/examples/inter-app-api-showcase/app/api/product-summary.ts @@ -0,0 +1,60 @@ +import type { Context } from "@wrnexus/core"; +import { httpTransport, serviceClient } from "@wrnexus/rpc"; +import { auditService, catalogService } from "../lib/contracts.ts"; + +interface GitHubRepository { + full_name?: unknown; + stargazers_count?: unknown; +} + +function requestedSku(ctx: Context): string { + return new URL(ctx.req.url).searchParams.get("sku")?.trim() || "starter"; +} + +/** GET /api/product-summary?sku=starter */ +export async function GET(ctx: Context): Promise { + const sku = requestedSku(ctx); + const transport = httpTransport(); + + // Inter-app call #1: query the catalog app. `{ as: ctx }` forwards the + // signed subject/tenant context; catalog still authorizes independently. + const catalog = serviceClient(catalogService, { app: "catalog", as: ctx, transport }); + const product = await catalog.getProduct({ sku }); + + // External API call: GitHub's public repository endpoint. Do not send the + // user's RPC identity token or internal headers to external services. + let githubResponse: Response; + try { + githubResponse = await fetch("https://api.github.com/repos/octocat/Hello-World", { + headers: { accept: "application/vnd.github+json", "user-agent": "wrnexus-example" }, + signal: ctx.req.signal, + }); + } catch { + return Response.json({ error: "External repository lookup failed." }, { status: 502 }); + } + if (!githubResponse.ok) { + return Response.json({ error: "External repository lookup failed." }, { status: 502 }); + } + const github = (await githubResponse.json()) as GitHubRepository; + if (typeof github.full_name !== "string" || typeof github.stargazers_count !== "number") { + return Response.json( + { error: "External repository returned an unexpected response." }, + { status: 502 }, + ); + } + + // Inter-app call #2: record the completed lookup in the audit app. This is + // intentionally awaited: callers learn whether the audit record was saved. + const audit = serviceClient(auditService, { app: "audit", as: ctx, transport }); + const receipt = await audit.recordLookup({ + sku: product.sku, + repository: github.full_name, + stars: github.stargazers_count, + }); + + return Response.json({ + product, + external: { repository: github.full_name, stars: github.stargazers_count }, + audit: receipt, + }); +} diff --git a/examples/inter-app-api-showcase/app/example.test.ts b/examples/inter-app-api-showcase/app/example.test.ts new file mode 100644 index 00000000..971e7524 --- /dev/null +++ b/examples/inter-app-api-showcase/app/example.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +test("product summary composes one external request with two peer-app calls", () => { + const source = readFileSync(join(import.meta.dir, "api", "product-summary.ts"), "utf8"); + expect(source).toContain('serviceClient(catalogService, { app: "catalog", as: ctx, transport })'); + expect(source).toContain('serviceClient(auditService, { app: "audit", as: ctx, transport })'); + expect(source).toContain('fetch("https://api.github.com/repos/octocat/Hello-World"'); + expect(source).toContain("ctx.req.signal"); +}); diff --git a/examples/inter-app-api-showcase/app/lib/contracts.ts b/examples/inter-app-api-showcase/app/lib/contracts.ts new file mode 100644 index 00000000..9c608188 --- /dev/null +++ b/examples/inter-app-api-showcase/app/lib/contracts.ts @@ -0,0 +1,34 @@ +import { defineService, procedure } from "@wrnexus/rpc"; +import { v } from "@wrnexus/validation"; + +/** + * In a real multi-app workspace, put these contracts in a shared package and + * import that package from this app and each peer. They live together here so + * the example is self-contained. + */ +export const catalogService = defineService({ + name: "catalog", + procedures: { + getProduct: procedure + .input(v.object({ sku: v.string() })) + .output<{ sku: string; displayName: string; enabled: boolean }>() + .idempotent() + .build(), + }, +}); + +export const auditService = defineService({ + name: "audit", + procedures: { + recordLookup: procedure + .input( + v.object({ + sku: v.string(), + repository: v.string(), + stars: v.number(), + }), + ) + .output<{ eventId: string }>() + .build(), + }, +}); diff --git a/examples/inter-app-api-showcase/apps/admin/.editorconfig b/examples/inter-app-api-showcase/apps/admin/.editorconfig deleted file mode 100644 index 86a63dc0..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/examples/inter-app-api-showcase/apps/admin/.env.example b/examples/inter-app-api-showcase/apps/admin/.env.example deleted file mode 100644 index c214ea2b..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# Copy to .env for local development. Never commit real secrets. -WRNEXUS_PUBLIC_ORIGIN=http://localhost:3000 -DATABASE_URL=file:./dev.db -REDIS_URL=redis://localhost:6379 -AUTH_SECRET=replace-with-at-least-32-random-characters -ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key -ANTHROPIC_API_KEY= -OTEL_EXPORTER_OTLP_ENDPOINT= diff --git a/examples/inter-app-api-showcase/apps/admin/.env.test.example b/examples/inter-app-api-showcase/apps/admin/.env.test.example deleted file mode 100644 index 5a1efcc3..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.env.test.example +++ /dev/null @@ -1,3 +0,0 @@ -WRNEXUS_PUBLIC_ORIGIN=http://localhost:3000 -DATABASE_URL=file:./test.db -AUTH_SECRET=test-only-secret-replace-outside-tests diff --git a/examples/inter-app-api-showcase/apps/admin/.gitignore b/examples/inter-app-api-showcase/apps/admin/.gitignore deleted file mode 100644 index ce512f8f..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.gitignore +++ /dev/null @@ -1,48 +0,0 @@ -# Dependencies -node_modules/ - -# WRNexusJS and production builds -dist/ -.wrnexus/ -**/.wrnexus/ -coverage/ - -# Environment files and local secrets -.env -.env.* -!.env.example -!.env.*.example - -# Logs and runtime files -*.log -logs/ -*.pid -*.pid.lock - -# Local databases -*.db -*.db-shm -*.db-wal -*.sqlite -*.sqlite3 -uploads/ - -# Generated native projects -mobile/android/ -mobile/ios/ -mobile/.expo/ - -# Editors and operating systems -.idea/ -.vscode/* -!.vscode/settings.json -!.vscode/extensions.json -*.swp -*.swo -.DS_Store -Thumbs.db - -# TypeScript and test caches -*.tsbuildinfo -.eslintcache -.nyc_output/ diff --git a/examples/inter-app-api-showcase/apps/admin/.prettierignore b/examples/inter-app-api-showcase/apps/admin/.prettierignore deleted file mode 100644 index 3fd6c5c7..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -dist/ -.wrnexus/ -**/.wrnexus/ -*.log -CLAUDE.md diff --git a/examples/inter-app-api-showcase/apps/admin/.prettierrc.json b/examples/inter-app-api-showcase/apps/admin/.prettierrc.json deleted file mode 100644 index 32474fc7..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.prettierrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": false, - "trailingComma": "all", - "endOfLine": "lf" -} diff --git a/examples/inter-app-api-showcase/apps/admin/.vscode/extensions.json b/examples/inter-app-api-showcase/apps/admin/.vscode/extensions.json deleted file mode 100644 index 59ff820b..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] -} diff --git a/examples/inter-app-api-showcase/apps/admin/.vscode/settings.json b/examples/inter-app-api-showcase/apps/admin/.vscode/settings.json deleted file mode 100644 index 6cad09f0..00000000 --- a/examples/inter-app-api-showcase/apps/admin/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "prettier.requireConfig": true, - "[wrn]": { - "editor.defaultFormatter": "wrnexus.wrnexus", - "editor.formatOnSave": true - } -} diff --git a/examples/inter-app-api-showcase/apps/admin/CLAUDE.md b/examples/inter-app-api-showcase/apps/admin/CLAUDE.md deleted file mode 100644 index 7555f023..00000000 --- a/examples/inter-app-api-showcase/apps/admin/CLAUDE.md +++ /dev/null @@ -1,295 +0,0 @@ -# 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 ` / `component ` / `api ` / `schema `. - -## 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
- 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/) - 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/.wrn ("none" to skip) - - state count = 0 // optional: seeds client-reactive state (omit for pure SSR) - - seo { - title = "Home" - description = "..." - canonical = "/" - } - - view { -

Hello

-

Count is {count}, doubled is {count * 2}.

- -
- } - - 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 { - - } -} -``` - -Mount it from any page/component: `
`. -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"`. -- `
` — mount a component (attrs become string props, coerced). -- `` / `` — component/layout slots; fill with `
`. -- **Server loop (DB/list/table):** `{#each as [, ]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `` 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 } … {:else if } … {:else} … {/if}` — renders the first truthy branch on the server. `` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}{:else}{/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: `
`, ``. -- 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 { - - - {#each rows as r, i} - - - - - - {:empty} - - {/each} - -
#{i}{r.name}{r.email}
No submissions yet.
- } -} -``` - -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` (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: `
` + `` (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 # scaffold a new app -wrnexus update --latest # deps + syntax/config migrations + verification -wrnexus generate page # scaffold a page (aliases: g p) -wrnexus generate component | api | schema -wrnexus db migrate | rollback | status | new [--from-models] | generate | seed -wrnexus eject # 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 `. -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. diff --git a/examples/inter-app-api-showcase/apps/admin/app/api/ai.ts b/examples/inter-app-api-showcase/apps/admin/app/api/ai.ts deleted file mode 100644 index 3abef18c..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/api/ai.ts +++ /dev/null @@ -1,17 +0,0 @@ -// POST /api/ai { "prompt": "..." } → Claude's reply. -// Set ANTHROPIC_API_KEY in your environment (e.g. a .env file) to enable this. -import { createAI } from "@wrnexus/ai"; -import type { Context } from "@wrnexus/core"; - -const ai = createAI(); // reads ANTHROPIC_API_KEY; defaults to claude-opus-4-8 - -export const POST = async (ctx: Context) => { - if (!process.env.ANTHROPIC_API_KEY) { - return Response.json({ error: "Set ANTHROPIC_API_KEY to use AI." }, { status: 501 }); - } - const { prompt } = await ctx.req.json().catch(() => ({})); - if (!prompt) return Response.json({ error: "Provide a 'prompt'." }, { status: 400 }); - - // Stream the reply back as plain text. Use `ai.generate(prompt)` for a one-shot string. - return ai.streamResponse(prompt); -}; diff --git a/examples/inter-app-api-showcase/apps/admin/app/api/hello.ts b/examples/inter-app-api-showcase/apps/admin/app/api/hello.ts deleted file mode 100644 index d359e795..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/api/hello.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const GET = async () => { - return Response.json({ message: "Hello API" }); -}; diff --git a/examples/inter-app-api-showcase/apps/admin/app/components/counter.wrn b/examples/inter-app-api-showcase/apps/admin/app/components/counter.wrn deleted file mode 100644 index 58c305b8..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/components/counter.wrn +++ /dev/null @@ -1,20 +0,0 @@ -// A reusable component. Route: none — mounted inside a page with -//
. -// -// Components render on the SERVER (with their props applied) and are hydrated in -// the browser by the generic reactive runtime — they ship no JS of their own. -component Counter { - // Props arrive as mount attributes, each coerced to the type of its default - // (so start="5" arrives as the number 5). - props { - start = 0 - label = "Count" - } - - // State can reference props. `count` seeds the reactive scope. - state count = start - - view { - - } -} diff --git a/examples/inter-app-api-showcase/apps/admin/app/db/migrations/0001_init.sql b/examples/inter-app-api-showcase/apps/admin/app/db/migrations/0001_init.sql deleted file mode 100644 index 39a33e74..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/db/migrations/0001_init.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Create application tables here. --- Run with: bunx wrnexus db migrate diff --git a/examples/inter-app-api-showcase/apps/admin/app/db/seed.ts b/examples/inter-app-api-showcase/apps/admin/app/db/seed.ts deleted file mode 100644 index 642d429f..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/db/seed.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Add deterministic development seed data here. -export async function seed(): Promise {} diff --git a/examples/inter-app-api-showcase/apps/admin/app/layouts/document.wrn b/examples/inter-app-api-showcase/apps/admin/app/layouts/document.wrn deleted file mode 100644 index 1aaaf5dd..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/layouts/document.wrn +++ /dev/null @@ -1,22 +0,0 @@ -// Global document layout. The framework renders this once around the selected -// page layout and merges SEO metadata, styles, and scripts into /. -// Request cookies, resolved theme, language, URL, and pathname are available -// as SSR props, so document attributes do not need a client-side correction. -layout Document { - props { - cookies = {} - theme = "light" - language = "en" - url = "" - pathname = "/" - } - - view { - - - -
- - - } -} diff --git a/examples/inter-app-api-showcase/apps/admin/app/locales/en.json b/examples/inter-app-api-showcase/apps/admin/app/locales/en.json deleted file mode 100644 index d1e54ea3..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/locales/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "common": { - "appName": "admin", - "welcome": "Welcome to admin" - } -} diff --git a/examples/inter-app-api-showcase/apps/admin/app/middleware/logger.ts b/examples/inter-app-api-showcase/apps/admin/app/middleware/logger.ts deleted file mode 100644 index a582fddf..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/middleware/logger.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Middleware } from "@wrnexus/core"; - -const logger: Middleware = async (ctx, next) => { - console.log(ctx.req.method, ctx.url.pathname); - return next(); -}; - -export default logger; diff --git a/examples/inter-app-api-showcase/apps/admin/app/pages/about.wrn b/examples/inter-app-api-showcase/apps/admin/app/pages/about.wrn deleted file mode 100644 index 267eeb37..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/pages/about.wrn +++ /dev/null @@ -1,20 +0,0 @@ -page About { - seo { - title = "About" - description = "Learn how admin is built with WrNexus." - } - - view { -
-
- ← Home -

WrNexus application

-

About admin

-

- This page is server-rendered from app/pages/about.wrn. Add state, - events, components, APIs, and data without switching to another UI framework. -

-
-
- } -} diff --git a/examples/inter-app-api-showcase/apps/admin/app/pages/index.wrn b/examples/inter-app-api-showcase/apps/admin/app/pages/index.wrn deleted file mode 100644 index ff66ec05..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/pages/index.wrn +++ /dev/null @@ -1,63 +0,0 @@ -// Home page (route: /). SSR-first: the view is server-rendered, then components -// (.wrn files under app/components) hydrate in the browser. Styled with Tailwind. -page Home { - seo { - title = "Home" - description = "admin — built with WrNexus, an SSR-first Bun framework." - } - - view { -
- - -
-
- - W - admin - - -
- -
-

SSR-first · Bun-native

- -

- Server-rendered.
- Instantly interactive. -

- -

- admin runs on WrNexus — write .wrn components, ship no client boilerplate, and let the server do the work. -

- - - -
-
- - live · hydrated on the server -
-
-
- This button works. You wrote zero client JavaScript. -
-
- -

- edit app/pages/index.wrn to make it yours -

-
- - -
-
- } -} diff --git a/examples/inter-app-api-showcase/apps/admin/app/realtime/chat.ts b/examples/inter-app-api-showcase/apps/admin/app/realtime/chat.ts deleted file mode 100644 index d89a0680..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/realtime/chat.ts +++ /dev/null @@ -1,20 +0,0 @@ -// ws:///realtime/chat — a simple broadcast room. -// -// The client side is the framework's realtime runtime; a page opts in with -// `data-room="chat"`. Here we only handle room events. -// -// client.send(msg) → just this connection -// client.broadcast(msg) → everyone else in the room -// client.room.broadcast(msg) → everyone, including the sender -import { defineRoom } from "@wrnexus/core"; - -export default defineRoom({ - onConnect(client) { - client.send({ type: "system", text: "connected" }); - }, - - onMessage(client, msg) { - // Echo each message to the whole room so every tab stays in sync. - client.room.broadcast({ type: "message", data: msg }); - }, -}); diff --git a/examples/inter-app-api-showcase/apps/admin/app/schemas/contact.ts b/examples/inter-app-api-showcase/apps/admin/app/schemas/contact.ts deleted file mode 100644 index bcd3c02e..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/schemas/contact.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { v } from "@wrnexus/validation"; - -export const contactSchema = v.object({ - email: v.string().email(), - message: v.string().min(10).max(2_000), -}); diff --git a/examples/inter-app-api-showcase/apps/admin/app/services/catalog.ts b/examples/inter-app-api-showcase/apps/admin/app/services/catalog.ts deleted file mode 100644 index 673cb263..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/services/catalog.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { implement } from "../../../../../../packages/rpc/src/index.ts"; -import { catalogService } from "../../../../packages/shared/src/index.ts"; - -/** Private service consumed by the workspace's web app. */ -export default implement( - catalogService, - { - getProduct: ({ sku }) => ({ sku, name: "WRNexus Sta \ No newline at end of file diff --git a/examples/inter-app-api-showcase/apps/admin/app/styles/global.css b/examples/inter-app-api-showcase/apps/admin/app/styles/global.css deleted file mode 100644 index afb7b3cf..00000000 --- a/examples/inter-app-api-showcase/apps/admin/app/styles/global.css +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Global stylesheet. Tailwind v4 is compiled by the styles.process hook in - * wrnexus.config.ts and served at /__wrnexus/styles.css on every page. - * - * @source tells Tailwind which files to scan for class names. - */ -@import "tailwindcss"; -@plugin "@iconify/tailwind4"; -@source "../**/*.wrn"; -@source "../**/*.tsx"; - -/* Make Tailwind's `dark:` variant follow the framework's data-theme attribute - * (set on by the theme system), not the OS setting. Any element with - * data-wire-theme-toggle flips it. */ -@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); - -body { - font-family: var(--wire-font-sans, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif); -} diff --git a/examples/inter-app-api-showcase/apps/admin/eslint.config.js b/examples/inter-app-api-showcase/apps/admin/eslint.config.js deleted file mode 100644 index 5c86a656..00000000 --- a/examples/inter-app-api-showcase/apps/admin/eslint.config.js +++ /dev/null @@ -1,44 +0,0 @@ -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import tseslint from "typescript-eslint"; - -const tsconfigRootDir = dirname(fileURLToPath(import.meta.url)); - -export default tseslint.config( - { - ignores: [ - "node_modules/**", - "dist/**", - ".wrnexus/**", - "**/.wrnexus/**", - "mobile/android/**", - "mobile/ios/**", - ], - }, - { - languageOptions: { - parserOptions: { - tsconfigRootDir, - }, - }, - }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ["**/*.{ts,tsx}"], - rules: { - "no-undef": "off", - "no-console": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - }, - }, -); diff --git a/examples/inter-app-api-showcase/apps/admin/package.json b/examples/inter-app-api-showcase/apps/admin/package.json deleted file mode 100644 index ed5de0f7..00000000 --- a/examples/inter-app-api-showcase/apps/admin/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "admin", - "version": "0.1.0", - "private": true, - "type": "module", - "wrnexus": { - "version": "0.8.6" - }, - "scripts": { - "dev": "wrnexus dev .", - "build": "wrnexus build .", - "start": "bun dist/server.js", - "production": "bun run build && bun run start", - "typecheck": "tsc --noEmit", - "test": "wrnexus test .", - "test:watch": "wrnexus test . --watch", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier . --write", - "format:check": "prettier . --check", - "doctor": "wrnexus doctor .", - "analyze": "wrnexus analyze .", - "inspect": "wrnexus inspect packages .", - "check": "bun run typecheck && bun run lint && bun run test && bun run format:check" - }, - "dependencies": { - "@wrnexus/ai": "0.8.6", - "@wrnexus/auth": "0.8.6", - "@wrnexus/captcha": "0.8.6", - "@wrnexus/core": "0.8.6", - "@wrnexus/csr": "0.8.6", - "@wrnexus/db": "0.8.6", - "@wrnexus/dev-server": "0.8.6", - "@wrnexus/encryption": "0.8.6", - "@wrnexus/helpers": "0.8.6", - "@wrnexus/i18n": "0.8.6", - "@wrnexus/image": "0.8.6", - "@wrnexus/jwt": "0.8.6", - "@wrnexus/observability": "0.8.6", - "@wrnexus/realtime": "0.8.6", - "@wrnexus/security": "0.8.6", - "@wrnexus/store": "0.8.6", - "@wrnexus/styles": "0.8.6", - "@wrnexus/tracking": "0.8.6", - "@wrnexus/ui": "0.8.6", - "@wrnexus/uploader": "0.8.6", - "@wrnexus/validation": "0.8.6", - "@wrnexus/authz": "0.8.6", - "@wrnexus/rpc": "file:../../../../packages/rpc", - "@app/shared": "workspace:*" - }, - "devDependencies": { - "@wrnexus/cli": "0.8.6", - "@eslint/js": "^9.0.0", - "@iconify-json/lucide": "^1.2.118", - "@iconify/tailwind4": "^1.2.3", - "@tailwindcss/cli": "^4.0.0", - "@types/bun": "latest", - "eslint": "^9.0.0", - "prettier": "latest", - "tailwindcss": "^4.0.0", - "typescript": "^5.5.0", - "typescript-eslint": "latest" - } -} diff --git a/examples/inter-app-api-showcase/apps/admin/public/llms.txt b/examples/inter-app-api-showcase/apps/admin/public/llms.txt deleted file mode 100644 index 34305ccc..00000000 --- a/examples/inter-app-api-showcase/apps/admin/public/llms.txt +++ /dev/null @@ -1,276 +0,0 @@ -# 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 ` / `component ` / `api ` / `schema `. - -## 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
- 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/) - 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/.wrn ("none" to skip) - - state count = 0 // optional: seeds client-reactive state (omit for pure SSR) - - seo { - title = "Home" - description = "..." - canonical = "/" - } - - view { -

Hello

-

Count is {count}, doubled is {count * 2}.

- -
- } - - 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 { - - } -} -``` - -Mount it from any page/component: `
`. -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"`. -- `
` — mount a component (attrs become string props, coerced). -- `` / `` — component/layout slots; fill with `
`. -- **Server loop (DB/list/table):** `{#each as [, ]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `` 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 } … {:else if } … {:else} … {/if}` — renders the first truthy branch on the server. `` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}{:else}{/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: `
`, ``. -- 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 { - - - {#each rows as r, i} - - - - - - {:empty} - - {/each} - -
#{i}{r.name}{r.email}
No submissions yet.
- } -} -``` - -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` (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: `` + `` (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 # scaffold a new app -wrnexus update --latest # deps + syntax/config migrations + verification -wrnexus generate page # scaffold a page (aliases: g p) -wrnexus generate component | api | schema -wrnexus db migrate | rollback | status | new [--from-models] | generate | seed -wrnexus eject # 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 `. -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. diff --git a/examples/inter-app-api-showcase/apps/admin/public/robots.txt b/examples/inter-app-api-showcase/apps/admin/public/robots.txt deleted file mode 100644 index c2a49f4f..00000000 --- a/examples/inter-app-api-showcase/apps/admin/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Allow: / diff --git a/examples/inter-app-api-showcase/apps/admin/test/smoke.test.ts b/examples/inter-app-api-showcase/apps/admin/test/smoke.test.ts deleted file mode 100644 index 4de7c068..00000000 --- a/examples/inter-app-api-showcase/apps/admin/test/smoke.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, test } from "bun:test"; -import { parseOrThrow } from "@wrnexus/validation"; -import { contactSchema } from "../app/schemas/contact.ts"; - -test("starter validation schema accepts a contact request", () => { - expect( - parseOrThrow(contactSchema, { - email: "hello@example.com", - message: "Hello from the generated application.", - }), - ).toEqual({ - email: "hello@example.com", - message: "Hello from the generated application.", - }); -}); diff --git a/examples/inter-app-api-showcase/apps/admin/tsconfig.json b/examples/inter-app-api-showcase/apps/admin/tsconfig.json deleted file mode 100644 index 4ab5d990..00000000 --- a/examples/inter-app-api-showcase/apps/admin/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "types": ["bun"], - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": false, - "esModuleInterop": true, - "resolveJsonModule": true, - "jsx": "react-jsx", - "jsxImportSource": "@wrnexus/core" - }, - "include": ["app", "test", "wrnexus.config.ts"], - "exclude": ["node_modules", "dist", "**/dist", "**/.wrnexus"] -} diff --git a/examples/inter-app-api-showcase/apps/admin/wrnexus.config.ts b/examples/inter-app-api-showcase/apps/admin/wrnexus.config.ts deleted file mode 100644 index 3508cb54..00000000 --- a/examples/inter-app-api-showcase/apps/admin/wrnexus.config.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { AppConfig } from "@wrnexus/styles"; - -const config: AppConfig = { - compatibilityDate: "2026-08-02", - frameworkBehaviour: 1, - // v0.8 defaults: explicit imports, strict template types, safe stores, and - // automatic progressive navigation. Package plugins are discovered from the - // installed packages above; add custom plugins to this array when needed. - plugins: [], - imports: { mode: "explicit", autoImport: true, aliases: { "@": "./app" } }, - types: { - strict: true, - noImplicitAny: true, - strictNullChecks: true, - checkTemplates: true, - checkComponentProps: true, - generateDeclarations: true, - }, - functions: { legacyDefaultRuntime: "current" }, - stores: { strictMutations: true, persistence: true }, - compatibility: { - legacyEmit: false, - legacyEventProps: false, - legacyComponentDiscovery: false, - stringLayouts: false, - }, - experimental: {}, - - performance: { - enforcement: "warn", - analyze: true, - budgets: { - routeJsBytes: 50 * 1024, - routeCssBytes: 25 * 1024, - lcpMs: 2_500, - inpMs: 200, - cls: 0.1, - }, - }, - observability: { - enabled: true, - serviceName: "admin", - serverTiming: true, - sampleRate: 1, - exporter: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ? "otlp" : "none", - endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, - webVitals: true, - }, - tenancy: { mode: "domain", required: false, rootDomains: ["localhost"] }, - build: { cache: true, sourceMaps: true, report: true, adapter: "bun" }, - navigation: { mode: "auto" }, - devToolbar: { enabled: true, position: "bottom-center", openEditor: true }, - - mobile: { - enabled: true, - appId: "com.example.admin", - appName: "admin", - userAgent: "WrNexusMobile", - backgroundColor: "#0f172a", - // layout: "mobile", // app/layouts/mobile.wrn - // icon: "resources/icon.png", - }, - - // PWA support is enabled automatically. Override any install metadata here. - pwa: { - name: "admin", - shortName: "admin", - display: "standalone", - themeColor: "#6366f1", - backgroundColor: "#0f172a", - }, - - seo: { - title: "admin", - titleTemplate: "%s | admin", - // Set WRNEXUS_PUBLIC_ORIGIN in production when TLS terminates at a proxy. - canonicalBase: process.env.WRNEXUS_PUBLIC_ORIGIN, - description: "An SSR-first WrNexus app.", - robots: "index,follow", - themeColor: "#6366f1", - }, - - styles: { - entry: "app/styles/global.css", - - // Tailwind v4 build. Runs once at dev-serve time (cached; re-run on restart) - // and at `wrnexus build`. `@tailwindcss/cli` writes to stdout, so we capture - // and return the final CSS. Delete this hook to drop Tailwind — global.css is - // still bundled and served as-is. - process: async ({ entryPath, appRoot, mode }) => { - const args = ["@tailwindcss/cli", "-i", entryPath!]; - if (mode === "production") args.push("--minify"); - return await Bun.$.cwd(appRoot)`bunx ${args}`.text(); - }, - }, - - // Fonts — optimized preconnect, subsetted weights, font-display, and CSP. - fonts: { - sans: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif', - google: [{ family: "Plus Jakarta Sans", weights: [400, 500, 600, 700] }], - }, - // - // // Or self-host (fastest, no third party) — drop files in public/fonts/: - // // local: [{ family: "Inter", src: "/fonts/inter.woff2", weight: "100 900", preload: true }], - - theme: { palette: "violet", default: "light" }, - i18n: { default: "en", locales: ["en"] }, - db: { driver: "sqlite", url: process.env.DATABASE_URL ?? "file:./dev.db" }, - databases: {}, - storage: { - default: "public", - stores: { - public: { - driver: "local", - access: "public", - dir: "uploads/public", - maxBytes: 10_000_000, - accept: ["image/*", "application/pdf"], - }, - private: { - driver: "local", - access: "private", - dir: "uploads/private", - maxBytes: 10_000_000, - }, - }, - }, - realtime: { scale: Boolean(process.env.REDIS_URL), redisUrl: process.env.REDIS_URL }, - port: Number(process.env.PORT ?? 3000), - security: { - cors: { enabled: false }, - }, - profiles: { - development: {}, - test: { - db: { driver: "sqlite", url: "file:./test.db" }, - observability: { exporter: "none", sampleRate: 0 }, - }, - staging: { - seo: { robots: "noindex,nofollow" }, - performance: { enforcement: "error" }, - build: { sourceMaps: true, report: true }, - }, - production: { - seo: { canonicalBase: process.env.WRNEXUS_PUBLIC_ORIGIN }, - performance: { enforcement: "error" }, - build: { sourceMaps: false, report: true }, - devToolbar: false, - }, - }, -}; - -export default config; diff --git a/examples/inter-app-api-showcase/apps/web/.editorconfig b/examples/inter-app-api-showcase/apps/web/.editorconfig deleted file mode 100644 index 86a63dc0..00000000 --- a/examples/inter-app-api-showcase/apps/web/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/examples/inter-app-api-showcase/apps/web/.env.example b/examples/inter-app-api-showcase/apps/web/.env.example deleted file mode 100644 index c214ea2b..00000000 --- a/examples/inter-app-api-showcase/apps/web/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# Copy to .env for local development. Never commit real secrets. -WRNEXUS_PUBLIC_ORIGIN=http://localhost:3000 -DATABASE_URL=file:./dev.db -REDIS_URL=redis://localhost:6379 -AUTH_SECRET=replace-with-at-least-32-random-characters -ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key -ANTHROPIC_API_KEY= -OTEL_EXPORTER_OTLP_ENDPOINT= diff --git a/examples/inter-app-api-showcase/apps/web/.env.test.example b/examples/inter-app-api-showcase/apps/web/.env.test.example deleted file mode 100644 index 5a1efcc3..00000000 --- a/examples/inter-app-api-showcase/apps/web/.env.test.example +++ /dev/null @@ -1,3 +0,0 @@ -WRNEXUS_PUBLIC_ORIGIN=http://localhost:3000 -DATABASE_URL=file:./test.db -AUTH_SECRET=test-only-secret-replace-outside-tests diff --git a/examples/inter-app-api-showcase/apps/web/.gitignore b/examples/inter-app-api-showcase/apps/web/.gitignore deleted file mode 100644 index ce512f8f..00000000 --- a/examples/inter-app-api-showcase/apps/web/.gitignore +++ /dev/null @@ -1,48 +0,0 @@ -# Dependencies -node_modules/ - -# WRNexusJS and production builds -dist/ -.wrnexus/ -**/.wrnexus/ -coverage/ - -# Environment files and local secrets -.env -.env.* -!.env.example -!.env.*.example - -# Logs and runtime files -*.log -logs/ -*.pid -*.pid.lock - -# Local databases -*.db -*.db-shm -*.db-wal -*.sqlite -*.sqlite3 -uploads/ - -# Generated native projects -mobile/android/ -mobile/ios/ -mobile/.expo/ - -# Editors and operating systems -.idea/ -.vscode/* -!.vscode/settings.json -!.vscode/extensions.json -*.swp -*.swo -.DS_Store -Thumbs.db - -# TypeScript and test caches -*.tsbuildinfo -.eslintcache -.nyc_output/ diff --git a/examples/inter-app-api-showcase/apps/web/.prettierignore b/examples/inter-app-api-showcase/apps/web/.prettierignore deleted file mode 100644 index 3fd6c5c7..00000000 --- a/examples/inter-app-api-showcase/apps/web/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -dist/ -.wrnexus/ -**/.wrnexus/ -*.log -CLAUDE.md diff --git a/examples/inter-app-api-showcase/apps/web/.prettierrc.json b/examples/inter-app-api-showcase/apps/web/.prettierrc.json deleted file mode 100644 index 32474fc7..00000000 --- a/examples/inter-app-api-showcase/apps/web/.prettierrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": false, - "trailingComma": "all", - "endOfLine": "lf" -} diff --git a/examples/inter-app-api-showcase/apps/web/.vscode/extensions.json b/examples/inter-app-api-showcase/apps/web/.vscode/extensions.json deleted file mode 100644 index 59ff820b..00000000 --- a/examples/inter-app-api-showcase/apps/web/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] -} diff --git a/examples/inter-app-api-showcase/apps/web/.vscode/settings.json b/examples/inter-app-api-showcase/apps/web/.vscode/settings.json deleted file mode 100644 index 6cad09f0..00000000 --- a/examples/inter-app-api-showcase/apps/web/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "prettier.requireConfig": true, - "[wrn]": { - "editor.defaultFormatter": "wrnexus.wrnexus", - "editor.formatOnSave": true - } -} diff --git a/examples/inter-app-api-showcase/apps/web/CLAUDE.md b/examples/inter-app-api-showcase/apps/web/CLAUDE.md deleted file mode 100644 index 7555f023..00000000 --- a/examples/inter-app-api-showcase/apps/web/CLAUDE.md +++ /dev/null @@ -1,295 +0,0 @@ -# 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 ` / `component ` / `api ` / `schema `. - -## 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
- 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/) - 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/.wrn ("none" to skip) - - state count = 0 // optional: seeds client-reactive state (omit for pure SSR) - - seo { - title = "Home" - description = "..." - canonical = "/" - } - - view { -

Hello

-

Count is {count}, doubled is {count * 2}.

- -
- } - - 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 { - - } -} -``` - -Mount it from any page/component: `
`. -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"`. -- `
` — mount a component (attrs become string props, coerced). -- `` / `` — component/layout slots; fill with `
`. -- **Server loop (DB/list/table):** `{#each as [, ]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `` 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 } … {:else if } … {:else} … {/if}` — renders the first truthy branch on the server. `` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}{:else}{/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: `
`, ``. -- 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 { - - - {#each rows as r, i} - - - - - - {:empty} - - {/each} - -
#{i}{r.name}{r.email}
No submissions yet.
- } -} -``` - -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` (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: `` + `` (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 # scaffold a new app -wrnexus update --latest # deps + syntax/config migrations + verification -wrnexus generate page # scaffold a page (aliases: g p) -wrnexus generate component | api | schema -wrnexus db migrate | rollback | status | new [--from-models] | generate | seed -wrnexus eject # 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 `. -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. diff --git a/examples/inter-app-api-showcase/apps/web/app/api/ai.ts b/examples/inter-app-api-showcase/apps/web/app/api/ai.ts deleted file mode 100644 index 3abef18c..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/api/ai.ts +++ /dev/null @@ -1,17 +0,0 @@ -// POST /api/ai { "prompt": "..." } → Claude's reply. -// Set ANTHROPIC_API_KEY in your environment (e.g. a .env file) to enable this. -import { createAI } from "@wrnexus/ai"; -import type { Context } from "@wrnexus/core"; - -const ai = createAI(); // reads ANTHROPIC_API_KEY; defaults to claude-opus-4-8 - -export const POST = async (ctx: Context) => { - if (!process.env.ANTHROPIC_API_KEY) { - return Response.json({ error: "Set ANTHROPIC_API_KEY to use AI." }, { status: 501 }); - } - const { prompt } = await ctx.req.json().catch(() => ({})); - if (!prompt) return Response.json({ error: "Provide a 'prompt'." }, { status: 400 }); - - // Stream the reply back as plain text. Use `ai.generate(prompt)` for a one-shot string. - return ai.streamResponse(prompt); -}; diff --git a/examples/inter-app-api-showcase/apps/web/app/api/hello.ts b/examples/inter-app-api-showcase/apps/web/app/api/hello.ts deleted file mode 100644 index d359e795..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/api/hello.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const GET = async () => { - return Response.json({ message: "Hello API" }); -}; diff --git a/examples/inter-app-api-showcase/apps/web/app/api/product.ts b/examples/inter-app-api-showcase/apps/web/app/api/product.ts deleted file mode 100644 index 92b5345a..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/api/product.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Context } from "../../../../../../packages/core/src/index.ts"; -import { - httpTransport, - retryingTransport, - serviceClient, -} from "../../../../../../packages/rpc/src/index.ts"; -import { catalogService } from "../../../../packages/shared/src/index.ts"; - -/** GET /api/product?sku=starter — obtains product data from the admin app. */ -export async function GET(ctx: Context): Promise { - const sku = new URL(ctx.req.url).searchParams.get("sku")?.trim() || "starter"; - const catalog = serviceClient(catalogService, { - app: "admin", - as: ctx, - transport: retryingTransport( \ No newline at end of file diff --git a/examples/inter-app-api-showcase/apps/web/app/components/counter.wrn b/examples/inter-app-api-showcase/apps/web/app/components/counter.wrn deleted file mode 100644 index 58c305b8..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/components/counter.wrn +++ /dev/null @@ -1,20 +0,0 @@ -// A reusable component. Route: none — mounted inside a page with -//
. -// -// Components render on the SERVER (with their props applied) and are hydrated in -// the browser by the generic reactive runtime — they ship no JS of their own. -component Counter { - // Props arrive as mount attributes, each coerced to the type of its default - // (so start="5" arrives as the number 5). - props { - start = 0 - label = "Count" - } - - // State can reference props. `count` seeds the reactive scope. - state count = start - - view { - - } -} diff --git a/examples/inter-app-api-showcase/apps/web/app/db/migrations/0001_init.sql b/examples/inter-app-api-showcase/apps/web/app/db/migrations/0001_init.sql deleted file mode 100644 index 39a33e74..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/db/migrations/0001_init.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Create application tables here. --- Run with: bunx wrnexus db migrate diff --git a/examples/inter-app-api-showcase/apps/web/app/db/seed.ts b/examples/inter-app-api-showcase/apps/web/app/db/seed.ts deleted file mode 100644 index 642d429f..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/db/seed.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Add deterministic development seed data here. -export async function seed(): Promise {} diff --git a/examples/inter-app-api-showcase/apps/web/app/layouts/document.wrn b/examples/inter-app-api-showcase/apps/web/app/layouts/document.wrn deleted file mode 100644 index 1aaaf5dd..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/layouts/document.wrn +++ /dev/null @@ -1,22 +0,0 @@ -// Global document layout. The framework renders this once around the selected -// page layout and merges SEO metadata, styles, and scripts into /. -// Request cookies, resolved theme, language, URL, and pathname are available -// as SSR props, so document attributes do not need a client-side correction. -layout Document { - props { - cookies = {} - theme = "light" - language = "en" - url = "" - pathname = "/" - } - - view { - - - -
- - - } -} diff --git a/examples/inter-app-api-showcase/apps/web/app/locales/en.json b/examples/inter-app-api-showcase/apps/web/app/locales/en.json deleted file mode 100644 index 60ce8b04..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/locales/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "common": { - "appName": "web", - "welcome": "Welcome to web" - } -} diff --git a/examples/inter-app-api-showcase/apps/web/app/middleware/logger.ts b/examples/inter-app-api-showcase/apps/web/app/middleware/logger.ts deleted file mode 100644 index a582fddf..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/middleware/logger.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Middleware } from "@wrnexus/core"; - -const logger: Middleware = async (ctx, next) => { - console.log(ctx.req.method, ctx.url.pathname); - return next(); -}; - -export default logger; diff --git a/examples/inter-app-api-showcase/apps/web/app/pages/about.wrn b/examples/inter-app-api-showcase/apps/web/app/pages/about.wrn deleted file mode 100644 index ea5496ad..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/pages/about.wrn +++ /dev/null @@ -1,20 +0,0 @@ -page About { - seo { - title = "About" - description = "Learn how web is built with WrNexus." - } - - view { -
-
- ← Home -

WrNexus application

-

About web

-

- This page is server-rendered from app/pages/about.wrn. Add state, - events, components, APIs, and data without switching to another UI framework. -

-
-
- } -} diff --git a/examples/inter-app-api-showcase/apps/web/app/pages/index.wrn b/examples/inter-app-api-showcase/apps/web/app/pages/index.wrn deleted file mode 100644 index 25c1fc43..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/pages/index.wrn +++ /dev/null @@ -1,63 +0,0 @@ -// Home page (route: /). SSR-first: the view is server-rendered, then components -// (.wrn files under app/components) hydrate in the browser. Styled with Tailwind. -page Home { - seo { - title = "Home" - description = "web — built with WrNexus, an SSR-first Bun framework." - } - - view { -
- - -
-
- - W - web - - -
- -
-

SSR-first · Bun-native

- -

- Server-rendered.
- Instantly interactive. -

- -

- web runs on WrNexus — write .wrn components, ship no client boilerplate, and let the server do the work. -

- - - -
-
- - live · hydrated on the server -
-
-
- This button works. You wrote zero client JavaScript. -
-
- -

- edit app/pages/index.wrn to make it yours -

-
- - -
-
- } -} diff --git a/examples/inter-app-api-showcase/apps/web/app/realtime/chat.ts b/examples/inter-app-api-showcase/apps/web/app/realtime/chat.ts deleted file mode 100644 index d89a0680..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/realtime/chat.ts +++ /dev/null @@ -1,20 +0,0 @@ -// ws:///realtime/chat — a simple broadcast room. -// -// The client side is the framework's realtime runtime; a page opts in with -// `data-room="chat"`. Here we only handle room events. -// -// client.send(msg) → just this connection -// client.broadcast(msg) → everyone else in the room -// client.room.broadcast(msg) → everyone, including the sender -import { defineRoom } from "@wrnexus/core"; - -export default defineRoom({ - onConnect(client) { - client.send({ type: "system", text: "connected" }); - }, - - onMessage(client, msg) { - // Echo each message to the whole room so every tab stays in sync. - client.room.broadcast({ type: "message", data: msg }); - }, -}); diff --git a/examples/inter-app-api-showcase/apps/web/app/schemas/contact.ts b/examples/inter-app-api-showcase/apps/web/app/schemas/contact.ts deleted file mode 100644 index bcd3c02e..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/schemas/contact.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { v } from "@wrnexus/validation"; - -export const contactSchema = v.object({ - email: v.string().email(), - message: v.string().min(10).max(2_000), -}); diff --git a/examples/inter-app-api-showcase/apps/web/app/styles/global.css b/examples/inter-app-api-showcase/apps/web/app/styles/global.css deleted file mode 100644 index afb7b3cf..00000000 --- a/examples/inter-app-api-showcase/apps/web/app/styles/global.css +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Global stylesheet. Tailwind v4 is compiled by the styles.process hook in - * wrnexus.config.ts and served at /__wrnexus/styles.css on every page. - * - * @source tells Tailwind which files to scan for class names. - */ -@import "tailwindcss"; -@plugin "@iconify/tailwind4"; -@source "../**/*.wrn"; -@source "../**/*.tsx"; - -/* Make Tailwind's `dark:` variant follow the framework's data-theme attribute - * (set on by the theme system), not the OS setting. Any element with - * data-wire-theme-toggle flips it. */ -@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); - -body { - font-family: var(--wire-font-sans, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif); -} diff --git a/examples/inter-app-api-showcase/apps/web/eslint.config.js b/examples/inter-app-api-showcase/apps/web/eslint.config.js deleted file mode 100644 index 5c86a656..00000000 --- a/examples/inter-app-api-showcase/apps/web/eslint.config.js +++ /dev/null @@ -1,44 +0,0 @@ -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import tseslint from "typescript-eslint"; - -const tsconfigRootDir = dirname(fileURLToPath(import.meta.url)); - -export default tseslint.config( - { - ignores: [ - "node_modules/**", - "dist/**", - ".wrnexus/**", - "**/.wrnexus/**", - "mobile/android/**", - "mobile/ios/**", - ], - }, - { - languageOptions: { - parserOptions: { - tsconfigRootDir, - }, - }, - }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ["**/*.{ts,tsx}"], - rules: { - "no-undef": "off", - "no-console": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - }, - }, -); diff --git a/examples/inter-app-api-showcase/apps/web/package.json b/examples/inter-app-api-showcase/apps/web/package.json deleted file mode 100644 index 498385b5..00000000 --- a/examples/inter-app-api-showcase/apps/web/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "web", - "version": "0.1.0", - "private": true, - "type": "module", - "wrnexus": { - "version": "0.8.6" - }, - "scripts": { - "dev": "wrnexus dev .", - "build": "wrnexus build .", - "start": "bun dist/server.js", - "production": "bun run build && bun run start", - "typecheck": "tsc --noEmit", - "test": "wrnexus test .", - "test:watch": "wrnexus test . --watch", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier . --write", - "format:check": "prettier . --check", - "doctor": "wrnexus doctor .", - "analyze": "wrnexus analyze .", - "inspect": "wrnexus inspect packages .", - "check": "bun run typecheck && bun run lint && bun run test && bun run format:check" - }, - "dependencies": { - "@wrnexus/ai": "0.8.6", - "@wrnexus/auth": "0.8.6", - "@wrnexus/captcha": "0.8.6", - "@wrnexus/core": "file:../../../../packages/core", - "@wrnexus/csr": "0.8.6", - "@wrnexus/db": "0.8.6", - "@wrnexus/dev-server": "0.8.6", - "@wrnexus/encryption": "0.8.6", - "@wrnexus/helpers": "0.8.6", - "@wrnexus/i18n": "0.8.6", - "@wrnexus/image": "0.8.6", - "@wrnexus/jwt": "0.8.6", - "@wrnexus/observability": "0.8.6", - "@wrnexus/realtime": "0.8.6", - "@wrnexus/security": "0.8.6", - "@wrnexus/store": "0.8.6", - "@wrnexus/styles": "0.8.6", - "@wrnexus/tracking": "0.8.6", - "@wrnexus/ui": "0.8.6", - "@wrnexus/uploader": "0.8.6", - "@wrnexus/validation": "file:../../../../packages/validation", - "@wrnexus/authz": "0.8.6", - "@wrnexus/rpc": "file:../../../../packages/rpc", - "@app/shared": "workspace:*" - }, - "devDependencies": { - "@wrnexus/cli": "0.8.6", - "@eslint/js": "^9.0.0", - "@iconify-json/lucide": "^1.2.118", - "@iconify/tailwind4": "^1.2.3", - "@tailwindcss/cli": "^4.0.0", - "@types/bun": "latest", - "eslint": "^9.0.0", - "prettier": "latest", - "tailwindcss": "^4.0.0", - "typescript": "^5.5.0", - "typescript-eslint": "latest" - } -} diff --git a/examples/inter-app-api-showcase/apps/web/public/llms.txt b/examples/inter-app-api-showcase/apps/web/public/llms.txt deleted file mode 100644 index 34305ccc..00000000 --- a/examples/inter-app-api-showcase/apps/web/public/llms.txt +++ /dev/null @@ -1,276 +0,0 @@ -# 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 ` / `component ` / `api ` / `schema `. - -## 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
- 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/) - 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/.wrn ("none" to skip) - - state count = 0 // optional: seeds client-reactive state (omit for pure SSR) - - seo { - title = "Home" - description = "..." - canonical = "/" - } - - view { -

Hello

-

Count is {count}, doubled is {count * 2}.

- -
- } - - 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 { - - } -} -``` - -Mount it from any page/component: `
`. -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"`. -- `
` — mount a component (attrs become string props, coerced). -- `` / `` — component/layout slots; fill with `
`. -- **Server loop (DB/list/table):** `{#each as [, ]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `` 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 } … {:else if } … {:else} … {/if}` — renders the first truthy branch on the server. `` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}{:else}{/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: `
`, ``. -- 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 { - - - {#each rows as r, i} - - - - - - {:empty} - - {/each} - -
#{i}{r.name}{r.email}
No submissions yet.
- } -} -``` - -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` (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: `` + `` (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 # scaffold a new app -wrnexus update --latest # deps + syntax/config migrations + verification -wrnexus generate page # scaffold a page (aliases: g p) -wrnexus generate component | api | schema -wrnexus db migrate | rollback | status | new [--from-models] | generate | seed -wrnexus eject # 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 `. -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. diff --git a/examples/inter-app-api-showcase/apps/web/public/robots.txt b/examples/inter-app-api-showcase/apps/web/public/robots.txt deleted file mode 100644 index c2a49f4f..00000000 --- a/examples/inter-app-api-showcase/apps/web/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Allow: / diff --git a/examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts b/examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts deleted file mode 100644 index a7045c22..00000000 --- a/examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { afterEach, expect, test } from "bun:test"; -import { implement } from "../../../../../packages/rpc/src/index.ts"; -import { handleRpcRequest } from "../../../../../packages/dev-server/src/rpc-dispatch.ts"; -import { catalogService } from "../../../packages/shared/src/index.ts"; -import { GET } from "../app/api/product.ts"; - -const secret = process.env.WRNEXUS_RPC_SECRET; -const app = process.env.WRNEXUS_APP_NAME; -const origins = process.env.WRNEXUS_INTERNAL_ORIGINS; -afterEach(() => { - if (secret === undefined) delete process.env.WRNEXUS_RPC_SECRET; - else process.env.WRNEXUS_RPC_SECRET = secret; - if (app === undefined) delete process.env.WRNEXUS_APP_NAME; - else process.env.WRNEXUS_APP_NAME = app; - if (origins === undefined) delete process.env.WRNEXUS_INTERNAL_ORIGINS; - else process.env.WRNEXUS_INTERNAL_ORIGINS = origins; -}); - -test("web calls the generated admin app over private RPC", async () => { - process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long"; - process.env.WRNEXUS_APP_NAME = "web"; - const service = implement( - catalogService, - { getProduct: ({ sku }) => ({ sku, name: "WRNexus Starter", priceCents: 4900 }) }, - { selfApp: "admin" }, - ); - const server = Bun.serve({ - port: 0, - hostname: "127.0.0.1", - async fetch(request) { - return ( - (await handleRpcRequest(request, new URL(request.url), new Map([["catalog", service]]))) ?? - new Response("Not found", { status: 404 }) - ); - }, - }); - process.env.WRNEXUS_INTERNAL_ORIGINS = JSON.stringify({ - admin: `http://127.0.0.1:${server.port}`, - }); - try { - const response = await GET({ - req: new Request("http://web.test/api/product?sku=starter"), - user: { id: "u1" }, - locals: {}, - } as never); - expect(await response.json()).toEqual({ - product: { sku: "starter", name: "WRNexus Starter", priceCents: 4900 }, - }); - } finally { - \ No newline at end of file diff --git a/examples/inter-app-api-showcase/apps/web/test/smoke.test.ts b/examples/inter-app-api-showcase/apps/web/test/smoke.test.ts deleted file mode 100644 index 4de7c068..00000000 --- a/examples/inter-app-api-showcase/apps/web/test/smoke.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, test } from "bun:test"; -import { parseOrThrow } from "@wrnexus/validation"; -import { contactSchema } from "../app/schemas/contact.ts"; - -test("starter validation schema accepts a contact request", () => { - expect( - parseOrThrow(contactSchema, { - email: "hello@example.com", - message: "Hello from the generated application.", - }), - ).toEqual({ - email: "hello@example.com", - message: "Hello from the generated application.", - }); -}); diff --git a/examples/inter-app-api-showcase/apps/web/tsconfig.json b/examples/inter-app-api-showcase/apps/web/tsconfig.json deleted file mode 100644 index 4ab5d990..00000000 --- a/examples/inter-app-api-showcase/apps/web/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "types": ["bun"], - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": false, - "esModuleInterop": true, - "resolveJsonModule": true, - "jsx": "react-jsx", - "jsxImportSource": "@wrnexus/core" - }, - "include": ["app", "test", "wrnexus.config.ts"], - "exclude": ["node_modules", "dist", "**/dist", "**/.wrnexus"] -} diff --git a/examples/inter-app-api-showcase/apps/web/wrnexus.config.ts b/examples/inter-app-api-showcase/apps/web/wrnexus.config.ts deleted file mode 100644 index 3172c1d1..00000000 --- a/examples/inter-app-api-showcase/apps/web/wrnexus.config.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { AppConfig } from "@wrnexus/styles"; - -const config: AppConfig = { - compatibilityDate: "2026-08-02", - frameworkBehaviour: 1, - // v0.8 defaults: explicit imports, strict template types, safe stores, and - // automatic progressive navigation. Package plugins are discovered from the - // installed packages above; add custom plugins to this array when needed. - plugins: [], - imports: { mode: "explicit", autoImport: true, aliases: { "@": "./app" } }, - types: { - strict: true, - noImplicitAny: true, - strictNullChecks: true, - checkTemplates: true, - checkComponentProps: true, - generateDeclarations: true, - }, - functions: { legacyDefaultRuntime: "current" }, - stores: { strictMutations: true, persistence: true }, - compatibility: { - legacyEmit: false, - legacyEventProps: false, - legacyComponentDiscovery: false, - stringLayouts: false, - }, - experimental: {}, - - performance: { - enforcement: "warn", - analyze: true, - budgets: { - routeJsBytes: 50 * 1024, - routeCssBytes: 25 * 1024, - lcpMs: 2_500, - inpMs: 200, - cls: 0.1, - }, - }, - observability: { - enabled: true, - serviceName: "web", - serverTiming: true, - sampleRate: 1, - exporter: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ? "otlp" : "none", - endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, - webVitals: true, - }, - tenancy: { mode: "domain", required: false, rootDomains: ["localhost"] }, - build: { cache: true, sourceMaps: true, report: true, adapter: "bun" }, - navigation: { mode: "auto" }, - devToolbar: { enabled: true, position: "bottom-center", openEditor: true }, - - mobile: { - enabled: true, - appId: "com.example.web", - appName: "web", - userAgent: "WrNexusMobile", - backgroundColor: "#0f172a", - // layout: "mobile", // app/layouts/mobile.wrn - // icon: "resources/icon.png", - }, - - // PWA support is enabled automatically. Override any install metadata here. - pwa: { - name: "web", - shortName: "web", - display: "standalone", - themeColor: "#6366f1", - backgroundColor: "#0f172a", - }, - - seo: { - title: "web", - titleTemplate: "%s | web", - // Set WRNEXUS_PUBLIC_ORIGIN in production when TLS terminates at a proxy. - canonicalBase: process.env.WRNEXUS_PUBLIC_ORIGIN, - description: "An SSR-first WrNexus app.", - robots: "index,follow", - themeColor: "#6366f1", - }, - - styles: { - entry: "app/styles/global.css", - - // Tailwind v4 build. Runs once at dev-serve time (cached; re-run on restart) - // and at `wrnexus build`. `@tailwindcss/cli` writes to stdout, so we capture - // and return the final CSS. Delete this hook to drop Tailwind — global.css is - // still bundled and served as-is. - process: async ({ entryPath, appRoot, mode }) => { - const args = ["@tailwindcss/cli", "-i", entryPath!]; - if (mode === "production") args.push("--minify"); - return await Bun.$.cwd(appRoot)`bunx ${args}`.text(); - }, - }, - - // Fonts — optimized preconnect, subsetted weights, font-display, and CSP. - fonts: { - sans: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif', - google: [{ family: "Plus Jakarta Sans", weights: [400, 500, 600, 700] }], - }, - // - // // Or self-host (fastest, no third party) — drop files in public/fonts/: - // // local: [{ family: "Inter", src: "/fonts/inter.woff2", weight: "100 900", preload: true }], - - theme: { palette: "violet", default: "light" }, - i18n: { default: "en", locales: ["en"] }, - db: { driver: "sqlite", url: process.env.DATABASE_URL ?? "file:./dev.db" }, - databases: {}, - storage: { - default: "public", - stores: { - public: { - driver: "local", - access: "public", - dir: "uploads/public", - maxBytes: 10_000_000, - accept: ["image/*", "application/pdf"], - }, - private: { - driver: "local", - access: "private", - dir: "uploads/private", - maxBytes: 10_000_000, - }, - }, - }, - realtime: { scale: Boolean(process.env.REDIS_URL), redisUrl: process.env.REDIS_URL }, - port: Number(process.env.PORT ?? 3000), - security: { - cors: { enabled: false }, - }, - profiles: { - development: {}, - test: { - db: { driver: "sqlite", url: "file:./test.db" }, - observability: { exporter: "none", sampleRate: 0 }, - }, - staging: { - seo: { robots: "noindex,nofollow" }, - performance: { enforcement: "error" }, - build: { sourceMaps: true, report: true }, - }, - production: { - seo: { canonicalBase: process.env.WRNEXUS_PUBLIC_ORIGIN }, - performance: { enforcement: "error" }, - build: { sourceMaps: false, report: true }, - devToolbar: false, - }, - }, -}; - -export default config; diff --git a/examples/inter-app-api-showcase/eslint.config.js b/examples/inter-app-api-showcase/eslint.config.js deleted file mode 100644 index 177993d1..00000000 --- a/examples/inter-app-api-showcase/eslint.config.js +++ /dev/null @@ -1,25 +0,0 @@ -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import tseslint from "typescript-eslint"; - -const tsconfigRootDir = dirname(fileURLToPath(import.meta.url)); - -export default tseslint.config( - { ignores: ["node_modules/**", "dist/**", "**/dist/**", ".wrnexus/**", "**/.wrnexus/**"] }, - { languageOptions: { parserOptions: { tsconfigRootDir } } }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ["**/*.{ts,tsx}"], - rules: { - "no-undef": "off", - "no-console": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": [ - "warn", - { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }, - ], - }, - }, -); diff --git a/examples/inter-app-api-showcase/package.json b/examples/inter-app-api-showcase/package.json index 1733ed14..4b29d644 100644 --- a/examples/inter-app-api-showcase/package.json +++ b/examples/inter-app-api-showcase/package.json @@ -1,32 +1,22 @@ { "name": "inter-app-api-showcase", + "version": "0.8.6", "private": true, "type": "module", - "workspaces": [ - "apps/*", - "packages/*" - ], "scripts": { - "dev": "wrnexus gateway", - "gateway": "wrnexus gateway", - "staging": "wrnexus staging", - "production": "wrnexus production", - "typecheck": "tsc --noEmit && bun run --filter './apps/*' typecheck", - "test": "bun run --filter './apps/*' test", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier . --write", - "format:check": "prettier . --check", - "doctor": "bun run --filter './apps/*' doctor", - "check": "bun run typecheck && bun run lint && bun run test && bun run format:check" + "dev": "bun run ../../packages/cli/src/index.ts dev .", + "build": "bun run ../../packages/cli/src/index.ts build .", + "test": "bun test", + "typecheck": "tsc --noEmit -p tsconfig.json", + "check": "bun run typecheck && bun run test && bun run build" + }, + "dependencies": { + "@wrnexus/core": "workspace:*", + "@wrnexus/rpc": "workspace:*", + "@wrnexus/validation": "workspace:*" }, "devDependencies": { - "@wrnexus/cli": "0.8.6", - "@eslint/js": "^9.0.0", - "@types/bun": "latest", - "eslint": "^9.0.0", - "prettier": "latest", - "typescript": "^5.5.0", - "typescript-eslint": "latest" + "@types/bun": "^1.3.14", + "typescript": "^5.9.2" } } diff --git a/examples/inter-app-api-showcase/packages/shared/package.json b/examples/inter-app-api-showcase/packages/shared/package.json deleted file mode 100644 index 1ef4b3d3..00000000 --- a/examples/inter-app-api-showcase/packages/shared/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@app/shared", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "@wrnexus/pubsub": "file:../../../../packages/pubsub", - "@wrnexus/rpc": "file:../../../../packages/rpc", - "@wrnexus/validation": "file:../../../../packages/validation" - } -} diff --git a/examples/inter-app-api-showcase/packages/shared/src/index.ts b/examples/inter-app-api-showcase/packages/shared/src/index.ts deleted file mode 100644 index 4f197aac..00000000 --- a/examples/inter-app-api-showcase/packages/shared/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Shared code for every app in this workspace. Import it anywhere: `@app/shared`. - * The cross-app event bus uses Redis so messages reach every app process/domain. - */ -import { createPubSub } from "../../../../../packages/pubsub/src/index.ts"; -import { redisDriver } from "../../../../../packages/pubsub/src/redis.ts"; -import { defineService, procedure } from "../../../../../packages/rpc/src/index.ts"; -import { v } from "../../../../../packages/validation/src/index.ts"; - -// One bus per process, backed by Redis (set REDIS_URL, defaults to localhost:6379). -export const bus = createPubSub(redisDriver(process.env.REDIS_URL)); - -// Shared domain types can live here and be imported by every app. -export interface Tenant { - id: string; - name: string; -} - -/** Typed contract imported by both generated workspace apps. */ -export const catalogService = defineService({ - name: "catalog", - procedures: { - getProduct: procedure - .input(v.object({ sku: v.string() })) - \ No newline at end of file diff --git a/examples/inter-app-api-showcase/tsconfig.json b/examples/inter-app-api-showcase/tsconfig.json index d4437925..b077260d 100644 --- a/examples/inter-app-api-showcase/tsconfig.json +++ b/examples/inter-app-api-showcase/tsconfig.json @@ -1,15 +1,5 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ESNext", "DOM"], - "types": ["bun"], - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "allowImportingTsExtensions": true - }, - "include": ["wrnexus.workspace.ts", "packages/**/*.ts"], - "exclude": ["node_modules", "dist", "apps"] + "extends": "../../tsconfig.json", + "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable"] }, + "include": ["app"] } diff --git a/examples/inter-app-api-showcase/wrnexus.workspace.ts b/examples/inter-app-api-showcase/wrnexus.workspace.ts deleted file mode 100644 index 3a1dd77e..00000000 --- a/examples/inter-app-api-showcase/wrnexus.workspace.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { WorkspaceConfig } from "@wrnexus/cli/workspace"; - -// Map each app to the domains it serves. `wrnexus gateway` runs them all behind -// one port and routes by Host header (add these hosts to your /etc/hosts). -const config: WorkspaceConfig = { - defaultEnvironment: "development", - environments: { - development: { - protocol: "http", - rootDomain: "localhost", - port: 3000, - runtime: "development", - hmr: true, - build: false, - migrate: false, - }, - staging: { - protocol: "https", - rootDomain: "staging.example.com", - port: 443, - runtime: "production", - hmr: false, - build: true, - migrate: true, - }, - production: { - protocol: "https", - rootDomain: "example.com", - port: 443, - runtime: "production", - hmr: false, - build: true, - migrate: true, - }, - }, - // Gateway-wide security (all optional): - security: { - trustedHostsOnly: true, // reject requests for unknown domains - rateLimit: { max: 300, windowMs: 60_000 }, // per client IP - headers: true, // baseline security headers at the edge - accessLog: true, // log host → app, method, path, status - }, - apps: [ - { name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] }, - { - name: "admin", - dir: "apps/admin", - domains: ["admin.localhost"], - // Lock the admin app down at the edge (pick one): - auth: { basic: { user: "admin", pass: "change-me" } }, - // auth: { allowIps: ["127.0.0.1", "::1"] }, - // auth: { forward: { url: "http://localhost:4001/api/verify" } }, // SSO - }, - ], -}; - -export default config; diff --git a/packages/ui/components/card.wrn b/packages/ui/components/Card.wrn similarity index 100% rename from packages/ui/components/card.wrn rename to packages/ui/components/Card.wrn diff --git a/packages/ui/components/container.wrn b/packages/ui/components/Container.wrn similarity index 100% rename from packages/ui/components/container.wrn rename to packages/ui/components/Container.wrn diff --git a/packages/ui/components/divider.wrn b/packages/ui/components/Divider.wrn similarity index 100% rename from packages/ui/components/divider.wrn rename to packages/ui/components/Divider.wrn diff --git a/packages/ui/components/grid.wrn b/packages/ui/components/Grid.wrn similarity index 100% rename from packages/ui/components/grid.wrn rename to packages/ui/components/Grid.wrn