From 790b81330a1b838118a6017b7955d66b436daa6c Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sun, 9 Aug 2026 11:11:57 +0530 Subject: [PATCH] fix: repair main after an unreviewed commit, and record the cause Three separate problems, all traceable to `git add -A` sweeping up a working tree I had not inspected. Commit 69020b25 ("docs: make the component sections executable") committed far more than docs: 79 files of a half-scaffolded inter-app example, and four of those files were truncated mid-statement. That broke `bun run typecheck` on main. The example is reverted to its last green six-file form. The truncated fragments and the fuller working copy are NOT in this commit -- if any of that workspace was wanted, it needs to be reconstructed deliberately and committed on its own, not as a side effect of a docs change. Separately, `scripts/generate-ui-complete-catalog.mjs` was run while checking which helper scripts still work. It rewrites components in place, so it flattened six of them to stubs, deleted 24 more and lower-cased four filenames before crashing. Contents were restored from HEAD, but the renames survived that restore: Windows is case-insensitive, so `git status` reported clean while Card, Container, Divider and Grid sat on disk under the wrong names. The index now tracks the capitalised names, which is what the components declare and what ui-redesign-contract.test.ts reads -- that test would have failed on any case-sensitive checkout. Documented both as 4.7 and 4.8 in the remediation plan, with the general rule: no script that rewrites packages/ui/components/ may write in place. Also fixes the heading level on 4.6, which was rendering outside section 4. bun run check is green: 1,433 pass, 0 fail. Co-Authored-By: Claude Opus 5 --- docs/framework-remediation-plan.md | 82 ++++- examples/inter-app-api-showcase/.editorconfig | 9 - examples/inter-app-api-showcase/.env.example | 2 - examples/inter-app-api-showcase/.gitignore | 25 -- .../inter-app-api-showcase/.prettierignore | 6 - .../inter-app-api-showcase/.prettierrc.json | 9 - .../.vscode/extensions.json | 3 - .../.vscode/settings.json | 7 - examples/inter-app-api-showcase/README.md | 55 +--- .../app/api/product-summary.ts | 60 ++++ .../app/example.test.ts | 11 + .../app/lib/contracts.ts | 34 ++ .../apps/admin/.editorconfig | 9 - .../apps/admin/.env.example | 8 - .../apps/admin/.env.test.example | 3 - .../apps/admin/.gitignore | 48 --- .../apps/admin/.prettierignore | 6 - .../apps/admin/.prettierrc.json | 9 - .../apps/admin/.vscode/extensions.json | 3 - .../apps/admin/.vscode/settings.json | 12 - .../apps/admin/CLAUDE.md | 295 ------------------ .../apps/admin/app/api/ai.ts | 17 - .../apps/admin/app/api/hello.ts | 3 - .../apps/admin/app/components/counter.wrn | 20 -- .../admin/app/db/migrations/0001_init.sql | 2 - .../apps/admin/app/db/seed.ts | 2 - .../apps/admin/app/layouts/document.wrn | 22 -- .../apps/admin/app/locales/en.json | 6 - .../apps/admin/app/middleware/logger.ts | 8 - .../apps/admin/app/pages/about.wrn | 20 -- .../apps/admin/app/pages/index.wrn | 63 ---- .../apps/admin/app/realtime/chat.ts | 20 -- .../apps/admin/app/schemas/contact.ts | 6 - .../apps/admin/app/services/catalog.ts | 8 - .../apps/admin/app/styles/global.css | 19 -- .../apps/admin/eslint.config.js | 44 --- .../apps/admin/package.json | 65 ---- .../apps/admin/public/llms.txt | 276 ---------------- .../apps/admin/public/robots.txt | 2 - .../apps/admin/test/smoke.test.ts | 15 - .../apps/admin/tsconfig.json | 20 -- .../apps/admin/wrnexus.config.ts | 153 --------- .../apps/web/.editorconfig | 9 - .../apps/web/.env.example | 8 - .../apps/web/.env.test.example | 3 - .../apps/web/.gitignore | 48 --- .../apps/web/.prettierignore | 6 - .../apps/web/.prettierrc.json | 9 - .../apps/web/.vscode/extensions.json | 3 - .../apps/web/.vscode/settings.json | 12 - .../inter-app-api-showcase/apps/web/CLAUDE.md | 295 ------------------ .../apps/web/app/api/ai.ts | 17 - .../apps/web/app/api/hello.ts | 3 - .../apps/web/app/api/product.ts | 15 - .../apps/web/app/components/counter.wrn | 20 -- .../apps/web/app/db/migrations/0001_init.sql | 2 - .../apps/web/app/db/seed.ts | 2 - .../apps/web/app/layouts/document.wrn | 22 -- .../apps/web/app/locales/en.json | 6 - .../apps/web/app/middleware/logger.ts | 8 - .../apps/web/app/pages/about.wrn | 20 -- .../apps/web/app/pages/index.wrn | 63 ---- .../apps/web/app/realtime/chat.ts | 20 -- .../apps/web/app/schemas/contact.ts | 6 - .../apps/web/app/styles/global.css | 19 -- .../apps/web/eslint.config.js | 44 --- .../apps/web/package.json | 65 ---- .../apps/web/public/llms.txt | 276 ---------------- .../apps/web/public/robots.txt | 2 - .../apps/web/test/inter-app.test.ts | 50 --- .../apps/web/test/smoke.test.ts | 15 - .../apps/web/tsconfig.json | 20 -- .../apps/web/wrnexus.config.ts | 153 --------- .../inter-app-api-showcase/eslint.config.js | 25 -- examples/inter-app-api-showcase/package.json | 36 +-- .../packages/shared/package.json | 19 -- .../packages/shared/src/index.ts | 25 -- examples/inter-app-api-showcase/tsconfig.json | 16 +- .../wrnexus.workspace.ts | 57 ---- packages/ui/components/{card.wrn => Card.wrn} | 0 .../{container.wrn => Container.wrn} | 0 .../components/{divider.wrn => Divider.wrn} | 0 packages/ui/components/{grid.wrn => Grid.wrn} | 0 83 files changed, 214 insertions(+), 2692 deletions(-) delete mode 100644 examples/inter-app-api-showcase/.editorconfig delete mode 100644 examples/inter-app-api-showcase/.env.example delete mode 100644 examples/inter-app-api-showcase/.gitignore delete mode 100644 examples/inter-app-api-showcase/.prettierignore delete mode 100644 examples/inter-app-api-showcase/.prettierrc.json delete mode 100644 examples/inter-app-api-showcase/.vscode/extensions.json delete mode 100644 examples/inter-app-api-showcase/.vscode/settings.json create mode 100644 examples/inter-app-api-showcase/app/api/product-summary.ts create mode 100644 examples/inter-app-api-showcase/app/example.test.ts create mode 100644 examples/inter-app-api-showcase/app/lib/contracts.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/.editorconfig delete mode 100644 examples/inter-app-api-showcase/apps/admin/.env.example delete mode 100644 examples/inter-app-api-showcase/apps/admin/.env.test.example delete mode 100644 examples/inter-app-api-showcase/apps/admin/.gitignore delete mode 100644 examples/inter-app-api-showcase/apps/admin/.prettierignore delete mode 100644 examples/inter-app-api-showcase/apps/admin/.prettierrc.json delete mode 100644 examples/inter-app-api-showcase/apps/admin/.vscode/extensions.json delete mode 100644 examples/inter-app-api-showcase/apps/admin/.vscode/settings.json delete mode 100644 examples/inter-app-api-showcase/apps/admin/CLAUDE.md delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/api/ai.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/api/hello.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/components/counter.wrn delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/db/migrations/0001_init.sql delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/db/seed.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/layouts/document.wrn delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/locales/en.json delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/middleware/logger.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/pages/about.wrn delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/pages/index.wrn delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/realtime/chat.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/schemas/contact.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/services/catalog.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/app/styles/global.css delete mode 100644 examples/inter-app-api-showcase/apps/admin/eslint.config.js delete mode 100644 examples/inter-app-api-showcase/apps/admin/package.json delete mode 100644 examples/inter-app-api-showcase/apps/admin/public/llms.txt delete mode 100644 examples/inter-app-api-showcase/apps/admin/public/robots.txt delete mode 100644 examples/inter-app-api-showcase/apps/admin/test/smoke.test.ts delete mode 100644 examples/inter-app-api-showcase/apps/admin/tsconfig.json delete mode 100644 examples/inter-app-api-showcase/apps/admin/wrnexus.config.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/.editorconfig delete mode 100644 examples/inter-app-api-showcase/apps/web/.env.example delete mode 100644 examples/inter-app-api-showcase/apps/web/.env.test.example delete mode 100644 examples/inter-app-api-showcase/apps/web/.gitignore delete mode 100644 examples/inter-app-api-showcase/apps/web/.prettierignore delete mode 100644 examples/inter-app-api-showcase/apps/web/.prettierrc.json delete mode 100644 examples/inter-app-api-showcase/apps/web/.vscode/extensions.json delete mode 100644 examples/inter-app-api-showcase/apps/web/.vscode/settings.json delete mode 100644 examples/inter-app-api-showcase/apps/web/CLAUDE.md delete mode 100644 examples/inter-app-api-showcase/apps/web/app/api/ai.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/app/api/hello.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/app/api/product.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/app/components/counter.wrn delete mode 100644 examples/inter-app-api-showcase/apps/web/app/db/migrations/0001_init.sql delete mode 100644 examples/inter-app-api-showcase/apps/web/app/db/seed.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/app/layouts/document.wrn delete mode 100644 examples/inter-app-api-showcase/apps/web/app/locales/en.json delete mode 100644 examples/inter-app-api-showcase/apps/web/app/middleware/logger.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/app/pages/about.wrn delete mode 100644 examples/inter-app-api-showcase/apps/web/app/pages/index.wrn delete mode 100644 examples/inter-app-api-showcase/apps/web/app/realtime/chat.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/app/schemas/contact.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/app/styles/global.css delete mode 100644 examples/inter-app-api-showcase/apps/web/eslint.config.js delete mode 100644 examples/inter-app-api-showcase/apps/web/package.json delete mode 100644 examples/inter-app-api-showcase/apps/web/public/llms.txt delete mode 100644 examples/inter-app-api-showcase/apps/web/public/robots.txt delete mode 100644 examples/inter-app-api-showcase/apps/web/test/inter-app.test.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/test/smoke.test.ts delete mode 100644 examples/inter-app-api-showcase/apps/web/tsconfig.json delete mode 100644 examples/inter-app-api-showcase/apps/web/wrnexus.config.ts delete mode 100644 examples/inter-app-api-showcase/eslint.config.js delete mode 100644 examples/inter-app-api-showcase/packages/shared/package.json delete mode 100644 examples/inter-app-api-showcase/packages/shared/src/index.ts delete mode 100644 examples/inter-app-api-showcase/wrnexus.workspace.ts rename packages/ui/components/{card.wrn => Card.wrn} (100%) rename packages/ui/components/{container.wrn => Container.wrn} (100%) rename packages/ui/components/{divider.wrn => Divider.wrn} (100%) rename packages/ui/components/{grid.wrn => Grid.wrn} (100%) 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