diff --git a/.publish/ai/README.md b/.publish/ai/README.md deleted file mode 100644 index 94a2cd1c..00000000 --- a/.publish/ai/README.md +++ /dev/null @@ -1,197 +0,0 @@ -# @wrnexus/ai - -Provider-neutral AI orchestration for OpenAI, Anthropic, Google and local OpenAI-compatible models, -with streaming, structured output, tools, embeddings, vector search/RAG, conversation persistence, -templates, guardrails, usage events, fallback, rate limits and evaluation reports. - -> A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code. - -Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. - -## Overview - -`@wrnexus/ai` is a thin, dependency-free wrapper over the Anthropic **Messages API**, -built on `fetch` (Bun-native, no SDK). Use it in API routes, jobs, or middleware to -call Claude. It defaults to the most capable model, **`claude-opus-4-8`**, reads your -key from `ANTHROPIC_API_KEY`, and supports both one-shot generation and streaming. - -## Installation - -```bash -bun add @wrnexus/ai -``` - -> Private package — the machine must be authenticated to the `wrnexus` npm org -> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). - -Set your key in the environment (e.g. `.env`): - -``` -ANTHROPIC_API_KEY=sk-ant-... -``` - -## API - -### `createAI(config?)` - -Creates a client. The key is read at call time, so it's safe to create at import. - -```ts -import { createAI } from "@wrnexus/ai"; -const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version }) -``` - -`AIConfig` fields (all optional): - -| Field | Default | Description | -| ----------- | --------------------------- | -------------------------- | -| `apiKey` | `ANTHROPIC_API_KEY` | Anthropic API key | -| `model` | `"claude-opus-4-8"` | Model id | -| `maxTokens` | `4096` | Default max output tokens | -| `baseURL` | `https://api.anthropic.com` | API base URL | -| `version` | `"2023-06-01"` | `anthropic-version` header | - -### `ai.generate(prompt, opts?): Promise` - -One-shot text generation. `prompt` is a string or a `Message[]` history. - -```ts -const text = await ai.generate("Write a haiku about Bun."); - -const reply = await ai.generate( - [ - { role: "user", content: "My name is Ada." }, - { role: "assistant", content: "Hi Ada!" }, - { role: "user", content: "What's my name?" }, - ], - { system: "You are concise." }, -); -``` - -### `ai.stream(prompt, opts?): AsyncGenerator` - -Yields text deltas as they arrive. - -```ts -for await (const chunk of ai.stream("Tell me a story.")) { - process.stdout.write(chunk); -} -``` - -### `ai.streamResponse(prompt, opts?): Response` - -Returns a streaming `text/plain` `Response` — drop it straight into an API route. - -```ts -// app/api/chat.ts -import { createAI } from "@wrnexus/ai"; -const ai = createAI(); - -export const POST = async (ctx) => { - const { prompt } = await ctx.req.json(); - return ai.streamResponse(prompt); -}; -``` - -### `GenerateOptions` - -| Option | Type | Description | -| ----------- | ------------------------------------------------- | ---------------------------------------------------- | -| `system` | `string` | System prompt | -| `model` | `string` | Override the model for this call | -| `maxTokens` | `number` | Override max output tokens | -| `thinking` | `boolean` | Enable adaptive extended thinking (deeper reasoning) | -| `effort` | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | Reasoning effort / token spend | -| `messages` | `Message[]` | Full history — supersedes `prompt` | -| `signal` | `AbortSignal` | Cancel the request | - -> `temperature` / `top_p` are intentionally **not** exposed — the current Claude -> models reject them (400). Steer output with prompting instead. - -### `AIError` - -Thrown on non-2xx responses or a model refusal. Carries `.status` and `.type` -(e.g. `"authentication_error"`, `"rate_limit_error"`, `"refusal"`). - -```ts -import { AIError } from "@wrnexus/ai"; -try { - await ai.generate("..."); -} catch (e) { - if (e instanceof AIError && e.type === "rate_limit_error") { - /* back off */ - } -} -``` - -### Multi-provider client - -`createAIClient` adds named-provider selection and fallback, capability discovery, -validated JSON output, validated tool execution, abort-aware exponential retries, -and per-provider circuit breakers. Attempt events intentionally contain metadata -only: prompts, credentials, and raw model responses are never passed to telemetry. - -```ts -import { anthropicProvider, createAIClient } from "@wrnexus/ai"; - -const ai = createAIClient({ - providers: [anthropicProvider()], - retry: { attempts: 3, baseDelayMs: 100, maxDelayMs: 2_000 }, - circuitBreaker: { failureThreshold: 5, resetAfterMs: 30_000 }, -}); - -const result = await ai.generateObject<{ title: string }>("Return a JSON title", { - validate: (value): value is { title: string } => - typeof value === "object" && value !== null && "title" in value, -}); -``` - -Providers can return normalized `usage` (`inputTokens`, `outputTokens`, -`totalTokens`, and `costUsd`) and `toolCalls`. Use `executeTools` with a named, -validated tool registry; unknown tools and invalid arguments are rejected before -application code runs. `deterministicAIProvider` supplies ordered or computed -offline responses for tests and examples without API keys or network calls. - -## Usage - -### Return generated JSON from an API route - -```ts -// app/api/summarize.ts — summarize posted text -import { createAI } from "@wrnexus/ai"; -const ai = createAI(); - -export const POST = async (ctx) => { - const { text } = await ctx.req.json().catch(() => ({})); - if (!text) return Response.json({ error: "Provide 'text'." }, { status: 400 }); - const summary = await ai.generate(`Summarize in one sentence:\n\n${text}`, { - system: "You are a precise summarizer.", - }); - return Response.json({ summary }); -}; -``` - -### Stream a chat response to the browser - -```ts -// app/api/chat.ts -import { createAI } from "@wrnexus/ai"; - -const ai = createAI({ model: "claude-sonnet-5" }); - -export const POST = async (ctx) => { - const { messages } = await ctx.req.json(); - return ai.streamResponse(messages, { - system: "Answer using concise Markdown.", - maxTokens: 1_500, - }); -}; -``` - -## Requirements / Notes - -- **Bun-only.** Uses `fetch`, `ReadableStream`, `TextDecoder`/`TextEncoder`, and - reads `ANTHROPIC_API_KEY` from `Bun.env` (falls back to `process.env`). -- **Zero dependencies** — no `@anthropic-ai/sdk`; talks to the Messages API directly. -- Defaults to `claude-opus-4-8`. Pass `{ model }` for a different model (e.g. - `"claude-sonnet-5"` for speed/cost, `"claude-haiku-4-5"` for the fastest). diff --git a/.publish/ai/package.json b/.publish/ai/package.json deleted file mode 100644 index f8c8a749..00000000 --- a/.publish/ai/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@wrnexus/ai", - "version": "0.8.9", - "type": "module", - "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://git.workroot.in/WorkRoot/WRNexusJS.git", - "directory": "packages/ai" - }, - "homepage": "https://wrnexusjs.dev/packages/ai", - "bugs": { - "url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues" - }, - "keywords": [ - "wrnexus", - "bun", - "typescript", - "ai" - ], - "sideEffects": false, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "engines": { - "bun": ">=1.3.0" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "access": "restricted" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./platform": { - "types": "./dist/platform.d.ts", - "import": "./dist/platform.js" - } - }, - "files": [ - "dist", - "README.md" - ] -} diff --git a/.publish/authz/README.md b/.publish/authz/README.md deleted file mode 100644 index 4247ad62..00000000 --- a/.publish/authz/README.md +++ /dev/null @@ -1,304 +0,0 @@ -# @wrnexus/authz - -> Composable authorization for WrNexus — role-based (RBAC), policy-based (PBAC), and attribute-based (ABAC) access control that reduces to a boolean check plus an `authorize()` guard. - -Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. - -## Overview - -`@wrnexus/authz` is a small, server-side authorization toolkit. It gives you three -interchangeable models — RBAC (roles → permissions), PBAC (policy predicates), and -ABAC (attribute matchers) — that all collapse to a `boolean | Promise` decision. -Wrap any decision in a `Middleware` guard (`authorize`, `requireRole`, `requirePermission`) -to protect WrNexus routes. Reach for it whenever a route or action needs to be gated on who -the user is, what roles they hold, or attributes of the user and the resource. It plugs into -`@wrnexus/core` by reading `ctx.user` as the authorization subject. - -## Installation - -```bash -bun add @wrnexus/authz -``` - -> Private package — the machine must be authenticated to the `wrnexus` npm org -> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). - -## API - -The package has a single entry point (`@wrnexus/authz`) exporting the following. - -### Types - -| Symbol | Description | -| ---------------------------------- | --------------------------------------------------------------------------------------------- | -| `Subject` | The authorized principal: `{ id?: string; roles?: string[]; [attribute: string]: unknown }`. | -| `Rbac` | An RBAC checker: `{ can(subject, permission): boolean; permissionsFor(roles): Set }`. | -| `Policy` | A predicate `(subject: S, resource?: R) => boolean \| Promise`. | - -### RBAC - -#### `defineRbac(roles: Record): Rbac` - -Builds an RBAC checker from a role → permissions map. Supported permission forms: - -- `"*"` — grants every permission. -- `"ns:*"` — namespace wildcard (e.g. `"post:*"` grants `"post:write"`). -- `"role:"` — inherits all permissions of another role (resolved recursively, cycle-safe). - -The returned `Rbac` provides: - -- `can(subject, permission)` — `true` if any of `subject.roles` grants `permission` (honouring `*` and namespace wildcards). Returns `false` when the subject has no roles. -- `permissionsFor(roles)` — the resolved `Set` of all permissions granted to a set of roles. - -#### `hasRole(subject: Subject | undefined, ...required: string[]): boolean` - -`true` if the subject holds **all** of the given roles. - -### PBAC / ABAC combinators - -- `any(...policies: Policy[]): Policy` — allow if **any** policy passes (OR); awaits async policies. -- `all(...policies: Policy[]): Policy` — allow only if **all** policies pass (AND); awaits async policies. -- `attr(name: string, match: unknown | ((value: unknown) => boolean)): Policy` — ABAC helper that allows when `subject[name]` equals `match`, or when `match` is a function, when `match(value)` is truthy. - -### Guards (middleware) - -Each guard returns a `@wrnexus/core` `Middleware`. A denied request short-circuits with -`Response.json({ ok: false, error: "Forbidden" }, { status: 403 })`. - -- `authorize(policy: (ctx: Context) => boolean | Promise): Middleware` — runs `policy` against the request `Context`; calls `next()` when it resolves truthy, otherwise returns 403. -- `requireRole(...roles: string[]): Middleware` — allows when `ctx.user` holds **any** of the listed roles. -- `requirePermission(rbac: Rbac, permission: string): Middleware` — allows when `rbac.can(ctx.user, permission)` is `true`. - -## Usage - -### RBAC - -```ts -import { defineRbac, hasRole } from "@wrnexus/authz"; - -const rbac = defineRbac({ - admin: ["*"], - editor: ["post:read", "post:write"], - viewer: ["post:read"], - // role inheritance: lead gets everything an editor has, plus post:publish - lead: ["role:editor", "post:publish"], -}); - -const user = { id: "u1", roles: ["editor"] }; - -rbac.can(user, "post:write"); // true -rbac.can(user, "post:delete"); // false -rbac.permissionsFor(["lead"]); // Set { "post:read", "post:write", "post:publish" } -hasRole(user, "editor"); // true -``` - -### Guarding routes - -```ts -import { authorize, requireRole, requirePermission, defineRbac } from "@wrnexus/authz"; - -const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] }); - -// Only admins or editors -app.get("/dashboard", requireRole("admin", "editor"), handler); - -// Requires a specific permission -app.post("/posts", requirePermission(rbac, "post:write"), handler); - -// Arbitrary policy over the request context -app.delete( - "/posts/:id", - authorize((ctx) => hasRole(ctx.user, "admin")), - handler, -); -``` - -### PBAC / ABAC policies - -```ts -import { any, all, attr, authorize, type Policy } from "@wrnexus/authz"; - -interface User { - id: string; - department?: string; - roles?: string[]; -} -interface Post { - authorId: string; -} - -// Ownership policy (subject + resource) -const ownsPost: Policy = (u, post) => u.id === post?.authorId; - -// ABAC: attribute equality, or a predicate -const inEngineering = attr("department", "engineering"); -const isVerified = attr("verified", (v) => v === true); - -// Compose: allow if the user owns the post OR is in engineering AND verified -const canEdit = any(ownsPost, all(inEngineering, isVerified)); - -app.put( - "/posts/:id", - authorize((ctx) => canEdit(ctx.user as User, loadPost(ctx))), - handler, -); -``` - -## Requirements / Notes - -- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported. -- Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`. -- Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise` (e.g. for a database ownership check). - -## Declaring permissions - -The RBAC/PBAC/ABAC surface above is the low-level toolkit. On top of it sits a -declarative **registry + catalog + store + engine**: permissions, roles, and -policies are declared once in code, merged into a frozen catalog at boot, and -resolved per-request against a pluggable `PermissionStore` that holds who has -what. - -Put declarations in `app/authz/.ts`; they are discovered automatically -and merged (conflicting declarations of the same permission/role/policy across -files fail the boot loudly, naming both source files). - -```ts -import { defineAuthz, owner } from "@wrnexus/authz"; - -export default defineAuthz({ - permissions: { - "post:read": { title: "View posts", public: true }, - "post:delete": { title: "Delete posts", risk: "high" }, - }, - // "post:*" is a namespace wildcard grant, valid inside a role's list — it is - // not itself a registered permission, so it can only ever grant permissions - // that ARE declared above (e.g. "post:read", "post:delete"). - roles: { editor: ["post:*"], admin: ["role:editor"] }, - policies: { ownsPost: owner("id", "authorId") }, - bindings: { "post:delete": ["ownsPost"] }, -}); -``` - -`public: true` means anonymous callers may hold the permission — but any -policy bound to it still runs, and can still veto the anonymous caller (e.g. a -`notBanned` policy on a public `post:preview` permission). - -## Checking permissions - -Register `authzMiddleware` once, in `app/middleware/`, with the merged -catalog and a `PermissionStore`. Like every other `app/middleware/*.ts` file, -the registration is an eager, module-scope call — the same shape as -`authzMiddleware({ catalog, store })` requires — so it must run after the -catalog has been populated. Both the dev server and `wrnexus build`'s -generated production entry guarantee `getAuthzCatalog()` is populated before -any app middleware module evaluates. Name the file so it sorts after whatever -middleware sets `ctx.user` (middleware runs in alphabetical filename order — -`authz.ts` after `auth.ts`, for instance). - -```ts -// app/middleware/authz.ts -import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz"; -import { dbPermissionStore } from "@wrnexus/authz/db"; -import { getDb } from "@wrnexus/db"; - -export default authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) }); -``` - -> **`subject.id` must be a non-empty string.** The engine denies (and logs to -> stderr) whenever `ctx.user.id` is present but not a non-empty string — this -> includes the common case of an integer primary key. Coerce it before it -> reaches `ctx.user`, e.g. `user.id = String(row.id)`, or every request for -> that user denies with "Invalid subject" instead of resolving normally. -> `owner()` (the built-in ownership policy) compares subject and resource ids -> with `Object.is`, so both sides must be the same type too — `owner()` on a -> numeric `resource.authorId` against a stringified `subject.id` never -> matches even when they represent "the same" id. - -There is no per-route `middleware` export — `app/middleware/*.ts` is the only -place middleware is registered. To gate part of the app, branch on the -request the same way any other conditional middleware does (compare -`app/middleware/captcha-login.ts` in the auth showcase, which branches on -method + path the same way): - -```ts -// app/middleware/protect-posts.ts -import type { Context, Next } from "@wrnexus/core"; -import { guardPermission } from "@wrnexus/authz"; - -const guardPostWrite = guardPermission("post:write"); - -export default function protectPosts(ctx: Context, next: Next) { - return ctx.url.pathname.startsWith("/api/posts") && ctx.req.method !== "GET" - ? guardPostWrite(ctx, next) - : next(); -} -``` - -Or check inline inside a route handler with the free function `can()`: - -```ts -// app/api/posts/[id].ts -import type { Context } from "@wrnexus/core"; -import { can } from "@wrnexus/authz"; - -export const DELETE = async (ctx: Context) => { - const post = { id: "1", authorId: "alice" }; // load your own resource here - if (!(await can(ctx, "post:delete", post))) { - return Response.json({ ok: false, error: "Forbidden" }, { status: 403 }); - } - return Response.json({ ok: true }); -}; -``` - -`can()` is a free function taking `ctx`, not `ctx.can` — `@wrnexus/core` must -not depend on `@wrnexus/authz`, so the per-request resolver lives in -`ctx.locals` instead, reached through `can()` / `decideFor()` / -`guardPermission()` / `filterCan()`. Calling any of them before -`authzMiddleware` has run for that request throws a `WRN-AUTHZ-SETUP` error -naming the missing registration, rather than silently denying. - -See `examples/auth-showcase/app/authz/showcase.ts` and -`examples/auth-showcase/app/middleware/authz.ts` for a complete, runnable -version of this wiring. - -## Precedence - -1. An explicit deny wins over everything, including `*` — and honours the - same namespace-wildcard matching as grants (denying `post:*` blocks - `post:comment:delete`, not just `post:*` itself). -2. A bound policy can veto a permission a role grants, and runs even for a - `public: true` permission — including for an anonymous caller. -3. Otherwise the permission must be held via a role or an explicit grant. -4. Default deny. - -Every failure — an unknown permission (outside strict/dev mode), a store -outage, a thrown policy — denies rather than throwing through to the caller. - -`permissionsFor()` (on the resolver returned by `createAuthzResolver`) is a -coarse hint for hiding UI (e.g. a menu section), **never authoritative**. A -`Set` cannot represent "granted `post:*` except `post:delete`", so a -narrow deny beneath a broad grant is invisible to it — the set still contains -`post:*` while `can()` / `decide()` correctly refuse `post:delete`. Gate real -actions with `can()`, `decideFor()`, or `filterCan()`; never by matching -against `permissionsFor()`'s result. - -## CLI - -```bash -wrnexus authz list # every registered permission, role, and policy -wrnexus authz generate # app/authz/permissions.gen.ts type unions -wrnexus authz init # scaffold the assignment-table migration -``` - -`wrnexus authz generate`'s output is a plain `Permission | Role` string-literal -union — `can()`, `guardPermission()`, and `decideFor()` all take a bare -`string` and nothing reads this file automatically, so import it to type your -own helpers/constants against the registered catalog, e.g.: - -```ts -import type { Permission } from "app/authz/permissions.gen.ts"; - -function guard(permission: Permission) { - return guardPermission(permission); -} -``` diff --git a/.publish/authz/package.json b/.publish/authz/package.json deleted file mode 100644 index b6aabcf9..00000000 --- a/.publish/authz/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@wrnexus/authz", - "version": "0.8.9", - "type": "module", - "description": "@wrnexus/authz — part of the WrNexus framework.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://git.workroot.in/WorkRoot/WRNexusJS.git", - "directory": "packages/authz" - }, - "homepage": "https://wrnexusjs.dev/packages/authz", - "bugs": { - "url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues" - }, - "keywords": [ - "wrnexus", - "bun", - "typescript", - "authz" - ], - "sideEffects": false, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "engines": { - "bun": ">=1.3.0" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "access": "restricted" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./db": { - "types": "./dist/db.d.ts", - "import": "./dist/db.js" - } - }, - "dependencies": { - "@wrnexus/core": "^0.8.9", - "@wrnexus/db": "^0.8.9" - }, - "files": [ - "dist", - "README.md" - ] -} diff --git a/.publish/cli/README.md b/.publish/cli/README.md deleted file mode 100644 index 34792727..00000000 --- a/.publish/cli/README.md +++ /dev/null @@ -1,326 +0,0 @@ -# @wrnexus/cli - -Production parity commands: - -```bash -wrnexus build . -wrnexus preview . --port=3000 -wrnexus dev . --production-runtime -``` - -`preview` refuses to start without `dist/server.js` and executes that exact -artifact with the production profile. Production-runtime development rebuilds -the same minified artifact after app, public, or configuration changes and -keeps the last good server running when a rebuild fails. - -> The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps. - -Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. - -## Overview - -`@wrnexus/cli` provides the `wrnexus` executable — the single entry point for developing a WrNexus app. It runs the HMR dev server, produces a self-contained production build, scaffolds apps/pages/components, drives database migrations, regenerates typed routes and queries, runs tests, and manages configuration profiles. It also scaffolds multi-app monorepos and serves them behind a domain-routing gateway. This is a CLI/build-time package (it shells out to the Bun binary for the dev child and tests) and it also exports the workspace config types via a subpath. - -## Installation - -```bash -bun add @wrnexus/cli -``` - -> Private package — the machine must be authenticated to the `wrnexus` npm org -> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). - -Once installed, invoke it from an app directory: - -```bash -bunx wrnexus dev -# or add scripts: "dev": "wrnexus dev .", "build": "wrnexus build ." -``` - -## Commands - -### Local production services - -`wrnexus dev . --services` starts the application and the bounded local database, -cache, mail, SMS, webhook, storage, queue, cron, authentication and metrics simulator. -It generates a localhost/`*.localhost` development certificate under -`.wrnexus/certificates/` and serves both the application and service console over HTTPS. -Trust that certificate locally to remove the browser warning. Use `--services-http` only -when an external development proxy already terminates TLS. - -### Exact production runtime with live updates - -`wrnexus dev . --production-runtime` rebuilds and executes `dist/server.js` with -production resolution, serialization, caching, headers and assets. The supervisor keeps -the last good process when a build fails. On a successful rebuild the opt-in production -HMR socket reconnects, requests the new document and morphs it into the browser; ordinary -`wrnexus preview` and deployed production servers never include that client. - -### API platform - -`wrnexus api generate [app-dir]` (or `api docs`) derives operations from file routes and -emits `generated/api/openapi.json`, safe static documentation, Postman collection, curl examples, -and TypeScript, JavaScript, Java, Go and Python SDKs. Generate one client with -`wrnexus sdk generate [app-dir]`. - -Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=` (see [Profiles](#profiles)). - -| Command | Purpose | -| ------------------------------------- | ---------------------------------------------------------------------- | -| `wrnexus dev [app-dir] [--port=3000]` | Start the development server with live reload / HMR. | -| `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. | -| `wrnexus create ` | Scaffold a new single app from an inline template. | -| `wrnexus workspace ` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | -| `wrnexus workspace add ` | Add and register an app in the current workspace. | -| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. | -| `wrnexus production [workspace-dir]` | Build, migrate, and serve every workspace app in production. | -| `wrnexus generate ` | Scaffold a `page` \| `component` \| `api` \| `schema`. | -| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | -| `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. | -| `wrnexus generate mobile` | Scaffold a Capacitor shell for iOS and Android. | -| `wrnexus mobile add ` | Install Capacitor plugins and sync native projects. | -| `wrnexus eject ` | Copy WrNexus UI component `.wrn` sources into `app/components/`. | -| `wrnexus db ` | Database migrations and tooling (see [db](#wrnexus-db)). | -| `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). | -| `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. | -| `wrnexus compatibility check` | Check whether behavior defaults are explicitly pinned and current. | -| `wrnexus compatibility explain` | Explain configured, effective, and current compatibility behavior. | -| `wrnexus compatibility upgrade` | Back up config and explicitly opt into reviewed current behavior. | -| `wrnexus help` | Print usage. | - -`wrnexus g` is an alias for `wrnexus generate`. - -Compatibility upgrades never happen implicitly. New applications pin -`compatibilityDate` and `frameworkBehaviour`; existing applications use -`wrnexus compatibility explain` before the backed-up, idempotent upgrade command. - -### `wrnexus dev` - -Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`). - -```bash -wrnexus dev . --port=8080 -``` - -### `wrnexus build` - -Emits into `/dist/`: - -- `server.js` — a single, minified, self-contained Bun server with a **static** manifest of every page / api / realtime / middleware / component / layout module (no runtime filesystem scan or on-the-fly bundling). -- `reactive.js`, `theme.css`, `theme.js`, `ui.css`, and (if present) `styles.css` — hashed, minified browser assets. -- `public/` — copied verbatim. - -Before bundling, it regenerates typed queries for the default and every named database. Run the output with: - -```bash -bun dist/server.js # PORT env var optional -# Generated apps also provide: npm start -# Build and start together: npm run production -``` - -### `wrnexus create` - -Scaffolds a complete v0.8 app from an inline template. The generated project includes strict TypeScript, ESLint and Prettier, editor recommendations, environment templates, database migrations, locales, schemas, tests, API/middleware/realtime examples, Tailwind and Iconify, PWA/mobile defaults, and the framework package kits. Its `wrnexus.config.ts` documents the current imports, types, stores, performance, observability, tenancy, build, navigation, theme, i18n, database, storage, realtime, security, and profile configuration. - -Use `bun run dev` during development, `bun run check` for the complete typecheck/lint/test/format gate, `bun run build && bun run start` for production, or `bun run production` to build and start in one command. - -### `wrnexus update` - -`wrnexus update --latest` performs a complete project upgrade. It hands control to the exact target CLI, backs up important project files under `.wrnexus/update-backups/`, updates every `@wrnexus/*` dependency, refreshes framework-owned references, and applies every versioned syntax/config/file migration between the project version and target version. After installation it runs the project's `check` and `build` scripts; the new version is recorded only after verification succeeds. - -Use `--dry-run` to preview an upgrade or `--no-verify` when verification is intentionally handled elsewhere. Migrations never overwrite user-owned configuration wholesale: each release must provide a focused, idempotent transformation for any changed syntax or config contract. - -```bash -wrnexus create my-app -``` - -### `wrnexus generate` - -Scaffolds a single file from a template, refusing to overwrite an existing file. Types (with aliases): `page`/`p`, `component`/`c`, `api`/`a`, `schema`/`s`. Nested names create nested paths. - -```bash -wrnexus generate page about # app/pages/about.wrn -wrnexus generate component user-card # app/components/user-card.wrn -wrnexus generate api users/list # app/api/users/list.ts -wrnexus generate schema signup # app/schemas/signup.ts -wrnexus generate routes # regenerate app/routes.gen.ts -wrnexus generate docker # Dockerfile + compose + .dockerignore -wrnexus generate mobile --mode=webview --app-id=com.example.app --app-name="Example" --url=https://app.example.com -wrnexus generate mobile --mode=native -``` - -The mobile generator creates a separate `mobile/` package and reads -`config.mobile.mode`. `webview` creates a Capacitor shell that renders the hosted -WrNexus application. `native` creates a WebView-free Expo/React Native app whose -screens call the shared backend through `mobile/src/wrnexus.ts`. Native screens -do not render `.wrn` HTML. In either mode, run `bun install` in `mobile/`; iOS -device builds require macOS and Xcode. - -Install official or community Capacitor plugins through the root CLI: - -```bash -wrnexus mobile add @capacitor/camera @capacitor/haptics -wrnexus mobile sync -wrnexus mobile assets # generate native icons from config.mobile.icon -``` - -In native mode, `mobile add` runs `expo install` and `mobile sync` runs Expo -prebuild. In WebView mode they retain the Capacitor install/sync behavior. -`wrnexus mobile compile` maps portable `app/pages/**/*.wrn` pages to Expo Router -TSX routes. Native `bun run start` invokes this compilation automatically. - -Browser code can access installed plugins through the SSR-safe -`@wrnexus/mobile` bridge. The command adds each plugin to both the WrNexus app -(JavaScript proxy) and `mobile/` (native synchronization). - -`wrnexus mobile sync` also configures Android so only true network failures use -the local connection-error screen. HTTP errors such as 404 and 500 keep their -WrNexus response pages. - -### `wrnexus eject` - -Copies a WrNexus UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app. - -```bash -wrnexus eject button card modal -``` - -### `wrnexus db` - -Database migrations and tooling. Without a flag, commands target the **default** database (`db` in `wrnexus.config.ts`, files under `app/db/`). Pass `--db=` to target a named database (`databases.`, files under `app/db//`). - -| Subcommand | Purpose | -| ------------------------------- | ----------------------------------------------------------------------------------- | -| `db new [--from-models]` | Scaffold a migration; `--from-models` derives it from the TS models in `schema.ts`. | -| `db migrate` | Apply all pending migrations. | -| `db rollback` | Revert the last applied migration. | -| `db status` | List applied / pending migrations. | -| `db generate` | Regenerate typed queries (`queries/*.sql` → `queries.gen.ts`). | -| `db seed` | Run the database's `seed.ts` (default export / `seed` function). | -| `db studio [table]` | Inspect tables — list row counts, or dump the first 50 rows of one table. | - -```bash -wrnexus db new create_users --from-models -wrnexus db migrate -wrnexus db studio users -wrnexus db status --db=analytics -``` - -### `wrnexus workspace` and `wrnexus gateway` - -`workspace ` scaffolds a monorepo: complete v0.8 apps under `apps/*`, shared libraries under `packages/*`, root TypeScript/lint/format/editor/environment tooling, and a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). Newly added workspace apps use the same current scaffold. - -```bash -wrnexus workspace acme -wrnexus gateway --port=3000 -``` - -For a complete production start, use the first-class workspace orchestrator: - -```bash -wrnexus production --host=0.0.0.0 --port=3000 -``` - -It builds every registered app, applies default and named-database SQL migrations -when present, and starts the production gateway only after preparation succeeds. -Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are -managed separately; `--environment=` selects another workspace environment. - -From a workspace root, add and register another app in one command: - -```bash -wrnexus workspace add reports --domain=reports.localhost -bun install -``` - -Development gateways bind to `127.0.0.1` by default for reliable access on Windows, -macOS, and Linux. Open the configured app domain on the gateway port (for example -`http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports -printed while apps start. Pass `--host=0.0.0.0` to accept connections from other devices. - -### `wrnexus test` - -Runs the app's tests with `bun test`. Defaults to the `test` profile (config + `.env.test`). Pass `--watch` to re-run on change; extra flags pass straight through to `bun test`. - -```bash -wrnexus test . --watch -``` - -## Usage - -### Create and run a single application - -```bash -bunx @wrnexus/cli create customer-portal -cd customer-portal -bun install -bun run dev -``` - -### Add routes and shared UI to an existing app - -```bash -wrnexus generate page reports/monthly -wrnexus generate api reports/export -wrnexus generate component report-filter -wrnexus generate routes -``` - -### Create a multi-app workspace and add another app - -```bash -wrnexus workspace company-suite -cd company-suite -wrnexus workspace add reports --domain=reports.localhost -bun install -wrnexus gateway --port=3000 -``` - -Open `http://reports.localhost:3000`; the gateway selects `apps/reports` from the -request host. - -### Upgrade with migrations and verification - -```bash -wrnexus update --latest --dry-run -wrnexus update --latest -wrnexus doctor -``` - -Use `wrnexus doctor --fix [app-dir]` to apply conservative repairs before the -health check: create missing `app/pages` and a default config, align skewed -`@wrnexus/*` dependency ranges, record the current migration marker, and format -only syntax-valid `.wrn` files. Invalid JSON or WRN sources are reported/skipped -instead of overwritten; repeat runs are idempotent. - -## Profiles - -Pass `--profile=` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.`, `.env..local`) into `process.env`. - -```bash -wrnexus dev --profile=uat -wrnexus profiles # ● development (config, .env.development) - # ○ production - # ○ uat (config, .env.uat) -``` - -## Subpath exports - -`@wrnexus/cli/workspace` exposes the workspace configuration types used by `wrnexus.workspace.ts`: - -```ts -import type { WorkspaceConfig, WorkspaceApp } from "@wrnexus/cli/workspace"; - -const config: WorkspaceConfig = { - security: { trustedHostsOnly: true, headers: true, accessLog: true }, - apps: [{ name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] }], -}; - -export default config; -``` - -## Requirements / Notes - -- **Bun-only.** The CLI runs on Bun, spawns the Bun binary for the dev child and `bun test`, and the production build uses `Bun.build`. Node is not supported. -- Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn` → `.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable WrNexus UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`. -- Reads `wrnexus.config.ts` for `db` / `databases`, `theme`, `styles`, `seo`, `security`, `i18n`, and `profiles`, and `wrnexus.workspace.ts` for the gateway. diff --git a/.publish/cli/package.json b/.publish/cli/package.json deleted file mode 100644 index d9893b8e..00000000 --- a/.publish/cli/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "@wrnexus/cli", - "version": "0.8.17", - "type": "module", - "description": "@wrnexus/cli — part of the WrNexus framework.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://git.workroot.in/WorkRoot/WRNexusJS.git", - "directory": "packages/cli" - }, - "homepage": "https://wrnexusjs.dev/packages/cli", - "bugs": { - "url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues" - }, - "keywords": [ - "wrnexus", - "bun", - "typescript", - "cli" - ], - "sideEffects": false, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "engines": { - "bun": ">=1.3.0" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "access": "restricted" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./workspace": { - "types": "./dist/workspace.d.ts", - "import": "./dist/workspace.js" - } - }, - "bin": { - "wrnexus": "./dist/index.js" - }, - "dependencies": { - "@wrnexus/core": "^0.8.9", - "@wrnexus/router": "^0.8.9", - "@wrnexus/csr": "^0.8.16", - "@wrnexus/compiler": "^0.8.9", - "@wrnexus/styles": "^0.8.10", - "@wrnexus/dev-server": "^0.8.16", - "@wrnexus/ui": "^0.8.12", - "@wrnexus/validation": "^0.8.9", - "@wrnexus/i18n": "^0.8.9", - "@wrnexus/mcp": "^0.8.8", - "@wrnexus/playground": "^0.8.8", - "@wrnexus/db": "^0.8.9", - "@wrnexus/authz": "^0.8.9", - "@wrnexus/plugin": "^0.8.8", - "@wrnexus/syntax": "^0.8.8", - "@wrnexus/typecheck": "^0.8.8", - "@wrnexus/security": "^0.8.8", - "selfsigned": "^5.5.0" - }, - "files": [ - "dist", - "README.md" - ] -} diff --git a/.publish/compiler/README.md b/.publish/compiler/README.md deleted file mode 100644 index 70854969..00000000 --- a/.publish/compiler/README.md +++ /dev/null @@ -1,217 +0,0 @@ -# @wrnexus/compiler - -## Partial-static rendering - -Pages can select `render = "partial-static"` and divide their view with `` and -`` boundaries. The compiler emits a build-only shell renderer that never evaluates -dynamic-boundary children. `wrnexus build` expands static component mounts into -`dist/partial-shells.json`, records byte/region evidence in `build-report.json`, and embeds -the shell in the production route manifest. At request time the production runtime retains -request-aware layouts, locale/theme metadata and security nonces while streaming dynamic -regions into stable placeholders. - -> Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript. - -Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. - -Production adapters use `analyzeRuntimeImports` before bundling. Edge, worker, -service-worker, and browser targets reject Node filesystem, TCP, and process -modules with `WRN-RUNTIME-CAPABILITY`. Package manifests can declare supported -`wrnexus.runtimes` and required `wrnexus.requires` capabilities; discovery fails -when the selected deployment cannot satisfy them. - -## Server actions - -```wrn -action createUser using CreateUserSchema { - const user = await users.create(input) - invalidate("users") - return user -} - -view { -
...
-} -``` - -The compiler produces a schema-aware server registry, a fully inferred action -client, and progressively enhanced form metadata. The shared runtime performs -validation, authentication/permission checks, CSRF verification, serialization, -invalidation reporting, and browser lifecycle events. - -## Overview - -`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page. - -Static ES module imports may appear before the root declaration. Imported values are -available to server-rendered expressions, including component props: - -```wrn -import { appUrl } from "@wrnexus/helpers"; - -layout PublicLayout { - view { - - } -} -``` - -## Installation - -```bash -bun add @wrnexus/compiler -``` - -> Private package — the machine must be authenticated to the `wrnexus` npm org -> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). - -## API - -All exports come from the package root (`@wrnexus/compiler`). - -### `compileWrnFile(source: string): string` - -Compile `.wrn` source to a TypeScript module string. Throws `ParseError` on invalid input. The output is prefixed with a `// compiled from .wrn` comment. - -### `compile(source: string): CompileResult` - -Richer entry point that returns the generated code, the AST, and any diagnostics. - -```ts -interface CompileResult { - code: string; - ast: PageAst; - diagnostics: string[]; -} -``` - -On a `ParseError` it pushes the message into `diagnostics` and re-throws. - -### `parse(source: string): PageAst` - -Run the lexer + recursive-descent parser and return the AST. Throws `ParseError` (lexer `LexError`s are caught and rethrown as `ParseError`). - -### `generate(ast: PageAst): string` - -Lower a `PageAst` to TypeScript. `page` ASTs become a default-export page component (plus `meta`, optional `layout`, `__wrnexusApi`/method handlers, `websocket`, and SSR/CSR data bindings); `component` ASTs become a module exporting `render(props)` and `__wrnexusComponent`. - -### `Lexer` - -On-demand lexer for `.wrn`. Yields structural tokens and exposes raw-span readers for the parser. - -```ts -class Lexer { - pos: number; - constructor(src: string); - next(): Token; // consume next structural token - peek(): Token; // look ahead without consuming - readPath(): string; // route path, e.g. /users/[id] - readToLineEnd(): string; // rest of line (state/prop initializers) - readBalancedBraces(): string; // inner text of a { ... } block, string-aware -} -``` - -`Token` is `{ type: TokenType; value: string; pos: number }`, where `TokenType` is one of `ident`, `string`, `lbrace`, `rbrace`, `lparen`, `rparen`, `at`, `eq`, `comma`, `eof`. - -### Errors - -| Class | Thrown by | Meaning | -| ------------ | ------------------------------------------------ | --------------------------------------------------------------- | -| `ParseError` | `parse`, `compile`, `compileWrnFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. | -| `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. | - -### AST types - -Exported type-only symbols describing the parsed tree: - -| Type | Description | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PageAst` | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. | -| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. | -| `Attr` | `{ name; value; event; boolean? }` — `event` marks `@event` bindings. | -| `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = ` declaration. | -| `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. | -| `SeoBlock` | `Record` from the `seo { ... }` block. | -| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. | -| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. | -| `DataMode` | `"ssr" \| "client"`. | -| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. | -| `RealtimeBlock` | `{ name; handlers }` — a `realtime { on evt(args) { ... } }` block. | - -## Usage - -Compile a page: - -```ts -import { compileWrnFile } from "@wrnexus/compiler"; - -const ts = compileWrnFile(` -page Home { - state count = 0 - seo { title = "Home" description = "Welcome" } - view { - - } -} -`); -// ts is a TypeScript module: exports `meta`, and a default page component -// returning an HTML string, wrapped in a data-scope for the reactive runtime. -``` - -Inspect the AST and diagnostics: - -```ts -import { compile, ParseError } from "@wrnexus/compiler"; - -try { - const { code, ast, diagnostics } = compile(source); - console.log(ast.kind, ast.name, ast.states.length); -} catch (err) { - if (err instanceof ParseError) console.error(err.message); -} -``` - -Drive the parse/codegen stages directly: - -```ts -import { parse, generate } from "@wrnexus/compiler"; - -const ast = parse(componentSource); // ast.kind === "component" -const module = generate(ast); // exports render(props) + __wrnexusComponent -``` - -Use the lexer standalone: - -```ts -import { Lexer } from "@wrnexus/compiler"; - -const lx = new Lexer("page Home {"); -lx.next(); // { type: "ident", value: "page", pos: 0 } -lx.next(); // { type: "ident", value: "Home", pos: 5 } -lx.next(); // { type: "lbrace", value: "{", pos: 10 } -``` - -## The `.wrn` language (as parsed) - -A file opens with `page ` or `component ` followed by a `{ ... }` body containing zero or more members: - -- `layout = ""` — selects `app/layouts/.wrn` (pages only). -- `types { }` — reusable interfaces and aliases for the current file. -- `props { name: Type = ... }` — typed component props. Omit `= ` to make a prop required. Legacy inferred props remain supported. -- `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with ``. -- `state : Type = ` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility. -- `view { }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and ``. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser. -- Client functions automatically commit state changed by `setTimeout` callbacks. For other deferred callbacks (observers, third-party APIs, or detached promise callbacks), call the injected `commit()` function after changing local state; returning/awaiting a promise also commits through the normal function boundary. -- `seo { key = "value" ... }` — metadata merged into the generated `meta`. -- `style { }` — inlined page/component stylesheet (repeatable). -- `functions { }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code. -- `api { }` — route handler, lowered to a `METHOD` export (repeatable). -- `ssr { ... }` / `client { ... }` — data blocks holding `api { ... }` bindings and their own `functions { ... }`. -- `realtime { on () { } ... }` — websocket handlers, lowered to a `websocket` export. - -`view` markup is parsed by a lenient dedicated HTML parser (`parseHtmlView`); HTML void elements (`
`, ``, …) take no closing tag. Line comments (`//`) are skipped by the lexer. - -## Requirements / Notes - -- Pure TypeScript with no runtime dependencies; runs under **Bun** as part of the WrNexus toolchain (Node is not supported). -- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wrn*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader. diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json deleted file mode 100644 index 80c5cc4a..00000000 --- a/.publish/compiler/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@wrnexus/compiler", - "version": "0.8.9", - "type": "module", - "description": "@wrnexus/compiler — part of the WrNexus framework.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://git.workroot.in/WorkRoot/WRNexusJS.git", - "directory": "packages/compiler" - }, - "homepage": "https://wrnexusjs.dev/packages/compiler", - "bugs": { - "url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues" - }, - "keywords": [ - "wrnexus", - "bun", - "typescript", - "compiler" - ], - "sideEffects": false, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "engines": { - "bun": ">=1.3.0" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "access": "restricted" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "dependencies": { - "@wrnexus/csr": "^0.8.16", - "@wrnexus/syntax": "^0.8.8", - "@wrnexus/store": "^0.8.8", - "@wrnexus/validation": "^0.8.9" - }, - "files": [ - "dist", - "README.md" - ] -} diff --git a/.publish/core/README.md b/.publish/core/README.md deleted file mode 100644 index 4fd267da..00000000 --- a/.publish/core/README.md +++ /dev/null @@ -1,412 +0,0 @@ -# @wrnexus/core - -> The framework core: the request `Context`, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WrNexus package builds on. - -Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. - -## Overview - -`@wrnexus/core` is the shared foundation of WrNexus. It defines the `Context` -object that flows through every middleware, page, and API route, plus the -`Middleware`/`Next` contract they implement. On top of that it ships the -building blocks a real app needs: cookie-backed sessions, password auth, CSRF -protection, rate limiting, request logging, HTTP + in-memory caching, file -uploads, streaming/SSE responses, WebSocket "rooms", security headers/CORS, and -a server-side JSX runtime that renders to HTML strings. Everything here is -**server-side** and Bun-native (it uses `Bun.password`, `Bun.write`, the -web-standard `Request`/`Response`, and `crypto`). You depend on it directly and -transitively through the rest of the framework. - -## Installation - -```bash -bun add @wrnexus/core -``` - -> Private package — the machine must be authenticated to the `wrnexus` npm org -> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). - -## API - -### Context & middleware — `@wrnexus/core` - -The `Context` (`ctx`) is the single value passed to middleware and handlers. - -| Export | Kind | Description | -| ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------ | -| `Context` | type | Per-request object: `req`, `url`, `lang`, `t`, `params`, `locals`, `user?`, `ip?`, `cookies`, `session`, `localStorage`. | -| `Next` | type | `() => Promise \| Response` — invokes the next middleware/handler. | -| `Middleware` | type | `(ctx, next) => Promise \| Response`. Return `next()` to continue, or a `Response` to short-circuit. | -| `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (connects up cookies, session, localStorage snapshot). | -| `withContextHeaders(ctx, res)` | fn | Apply accumulated headers (e.g. `Set-Cookie`) from the context onto a response. | -| `PageComponent` | type | `(ctx) => string \| Promise` — a page module's default export. | -| `PageMeta` / `SeoConfig` | type | `` metadata: `title`, `description`, `canonical`, `robots`, `image`, `twitterCard`, `themeColor`, … | -| `TFunction` | type | `(key, params?) => string` — translate a key for `ctx.lang`, interpolating `{param}` placeholders. | - -Key `Context` fields: - -- `ctx.locals` — per-request scratch space for passing values between middleware. -- `ctx.user` — the authenticated user (populated by `sessionAuth`/`logIn`), or `null`. -- `ctx.ip` — the direct socket peer IP (not spoofable via headers). -- `ctx.cookies` / `ctx.session` / `ctx.localStorage` — see **Storage** below. - -### Authentication — `@wrnexus/core` - -Passwords are hashed with argon2id via `Bun.password`; sessions ride the -cookie-backed `SessionStore`. - -| Export | Signature | Notes | -| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `hashPassword(password)` | `(string) => Promise` | argon2id hash to store. | -| `verifyPassword(password, hash)` | `(string, string) => Promise` | Constant-safe; returns `false` on bad/empty hash. | -| `logIn(ctx, user)` | `(Context, U) => void` | Regenerates the session id (fixation defense), stores the user, sets `ctx.user`. | -| `logOut(ctx)` | `(Context) => void` | Clears the session and `ctx.user`. | -| `getUser(ctx)` | `(Context) => U \| null` | Current user from `ctx.user`, falling back to the session. | -| `sessionAuth()` | `() => Middleware` | Hydrates `ctx.user` from the session each request. Register early. | -| `requireAuth(options?)` | `(RequireAuthOptions?) => Middleware` | Guard: API/fetch requests get `401 JSON`, page navigations get `302` to `loginPath` (default `/login`) with `?next=`. | -| `SESSION_USER_KEY` | `"user"` | Session key holding the user. | - -`RequireAuthOptions`: `{ loginPath?: string }`. - -### CSRF — `@wrnexus/core` - -Double-submit cookie pattern: a readable `wrn-csrf` cookie is echoed in an -`x-csrf-token` header on unsafe requests. - -| Export | Signature | Notes | -| ----------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| `csrfToken(ctx)` | `(Context) => string` | Ensures the CSRF cookie exists and returns its token. | -| `verifyCsrf(ctx)` | `(Context) => boolean` | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). | -| `csrfProtection()` | `() => Middleware` | 403s unsafe requests with a missing/mismatched token. | -| `CSRF_COOKIE` / `CSRF_HEADER` | `"wrn-csrf"` / `"x-csrf-token"` | Cookie & header names. | - -### Rate limiting — `@wrnexus/core` - -Fixed-window limiter that returns `429` with `Retry-After` and emits -`RateLimit-Limit`/`-Remaining`/`-Reset` headers. - -| Export | Signature | Notes | -| --------------------- | ----------------------------------- | ---------------------------------------------------------------------- | -| `rateLimit(options?)` | `(RateLimitOptions?) => Middleware` | Main middleware. | -| `peerKey(ctx)` | `(Context) => string` | Non-spoofable key from `ctx.ip` (default). | -| `proxyKey(ctx)` | `(Context) => string` | Trusts `x-forwarded-for`/`x-real-ip`. Use only behind a trusted proxy. | -| `defaultKey` | — | **Deprecated** alias of `proxyKey`. | - -`RateLimitOptions`: `windowMs` (default `60_000`), `max` (default `60`), -`key`, `trustProxy` (default `false` → keys on `peerKey`; `true` → `proxyKey`), -`message`, `headers` (default `true`), `store`. - -`RateLimitStore` is pluggable — implement `hit(key, windowMs, now) => Bucket | Promise` -(a `Bucket` is `{ count, resetAt }`) to back limits with Redis/SQL across -instances. The default store is process-local memory. - -### Request logging — `@wrnexus/core` - -| Export | Signature | Notes | -| ------------------------- | --------------------------------------- | -------------------------------------------------------------------------------- | -| `requestLogger(options?)` | `(RequestLoggerOptions?) => Middleware` | One record per request with a request id (stored on `ctx.locals[requestIdKey]`). | - -`RequestLoggerOptions`: `format` (`"pretty"` default \| `"json"`), `sink(line, record)` -(default `console.log`), `requestIdKey` (default `"requestId"`), `now`. -`RequestRecord` = `{ time, id, method, path, status, durationMs }`. - -### Resilience — `@wrnexus/core` - -`resilientCall` standardizes cancellation-aware timeouts, controlled retries, -fixed or exponential backoff, fallback responses, circuit breaking, and bounded -concurrency. Reuse a declarative circuit/bulkhead options object, or an explicit -`CircuitBreaker`/`Bulkhead` instance, wherever calls must share health and -capacity state. - -```ts -import { resilientCall } from "@wrnexus/core"; - -const paymentCircuit = { failures: 5, resetAfter: "30s" } as const; - -const status = await resilientCall({ - timeout: "5s", - retries: 3, - retryDelay: "100ms", - backoff: "exponential", - circuitBreaker: paymentCircuit, - bulkhead: { concurrency: 20, queue: 100 }, - run: (signal) => paymentProvider.checkStatus({ signal }), - fallback: () => ({ state: "unavailable" }), -}); -``` - -`CircuitBreaker.snapshot()` reports `closed`, `open`, or `half-open`, failure -and success counts, and the remaining retry delay for health endpoints and -development tooling. Fail-fast conditions use stable `WRN-RESILIENCE-*` codes. -Core's existing `HealthRegistry`, `withIdempotency`, and pluggable stores/locks -cover health reporting, idempotent requests, and distributed coordination. - -### Caching — `@wrnexus/core` - -| Export | Kind | Notes | -| -------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `TTLCache` | class | In-memory TTL cache: `get`, `set`, `getOrLoad(key, loader, ttlMs?)`, `delete`, `clear`, `size`. Constructor takes a default `ttlMs` (60s). | -| `cacheControl(options)` | fn | Build a `Cache-Control` value from `CacheControlOptions`. | -| `withCacheControl(res, options)` | fn | Apply `Cache-Control` to a response. | -| `etag(body, weak?)` | fn | Stable quoted FNV-1a ETag (weak by default). | -| `notModified(req, tag)` | fn | `true` when `If-None-Match` matches — send a `304`. | - -`CacheControlOptions`: `maxAge`, `sMaxAge`, `private`, `noStore`, `noCache`, -`staleWhileRevalidate`, `immutable`. - -### File uploads — `@wrnexus/core` - -Bun parses `multipart/form-data` via `Request.formData()`; these helpers -validate and persist the resulting `File`s. - -| Export | Signature | Notes | -| --------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- | -| `collectUploads(form)` | `(FormData) => { field, file }[]` | Every non-empty `File` in a parsed form. | -| `saveUpload(file, options)` | `(File, SaveUploadOptions) => Promise` | Validates size/type, sanitizes the name, writes via `Bun.write`. Throws `UploadError`. | -| `sanitizeFilename(name)` | `(string) => string` | Strips separators, traversal, control/illegal chars; caps at 255. | -| `UploadError` | class | Thrown on rejected uploads. | - -`SaveUploadOptions`: `dir` (required), `maxBytes`, `allowedTypes` (MIME types -like `"image/png"` and/or extensions like `".png"`), `filename(file)`. -`SavedUpload` = `{ path, filename, size, type }`. - -### Streaming & SSE — `@wrnexus/core` - -| Export | Signature | Notes | -| ------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `streamResponse(source, init?)` | `(Iterable\|AsyncIterable, StreamResponseInit?) => Response` | Streaming `Response` from a chunk source (basis for streaming SSR). | -| `sse(source)` | `(Iterable\|AsyncIterable) => Response` | `text/event-stream` response. | - -`StreamResponseInit`: `status`, `headers`, `contentType` (default -`"text/html; charset=utf-8"`). `ServerSentEvent`: `{ data, event?, id?, retry? }`. - -### Realtime rooms — `@wrnexus/core` - -WebSocket rooms. A file in `app/realtime/` exports -`default defineRoom({ ... })` and is served at `ws://host/realtime/`. - -| Export | Signature | Notes | -| --------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- | -| `defineRoom(handlers)` | `(RoomHandlers) => RoomDefinition` | Define a room. Export the result as `default`. | -| `isRoomDefinition(value)` | `(unknown) => boolean` | Type guard for a room definition. | -| `createRealtimeRegistry()` | `() => RealtimeRegistry` | Server-side connection manager mapping sockets ↔ rooms. | -| `bridgeRealtime(registry, bus, topic?)` | `(RealtimeRegistry, RealtimeBus, string?) => () => void` | Bridge broadcasts/`toUser` sends across processes via a pub/sub bus. | - -`RoomHandlers`: `authorize(info) => boolean` (gate before accept — return -`false` to reject with 403), `onConnect(client)`, `onMessage(client, message)` -(JSON auto-parsed), `onLeave(client)`. A handler receives a `RoomClient` with -`id`, `user`, `query`, `data`, `room`, and `send` / `broadcast` / -`to(id)` / `toUser(user)` / `close`. The `Room` API adds `state`, `clients()`, -`count()`, and `broadcast`. `RealtimeBus` is structurally satisfied by -`@wrnexus/pubsub`. Legacy `RealtimeHandler`/`RealtimeSocket` raw handlers are -still exported. Connection-targeted sends (`send`, `to(id)`) stay local; room -broadcasts and `toUser` cross the bridge. - -### Error pages — `@wrnexus/core` - -| Export | Signature | Notes | -| ------------------------------ | -------------------------------- | ----------------------------------------------------- | -| `renderError(err, mode)` | `(unknown, Mode) => Response` | Dev page (with stack) or generic prod page by `mode`. | -| `renderDevError(err, status?)` | `(unknown, number?) => Response` | Readable HTML error page including the stack trace. | -| `renderProdError(status?)` | `(number?) => Response` | Generic page that never leaks file paths. | -| `renderNotFound()` | `() => Response` | Simple 404 page. | - -`Mode` = `"development" | "production"`. - -### Security headers & CORS — `@wrnexus/core` - -| Export | Signature | Notes | -| -------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `withSecurityHeaders(req, res, mode, security?, nonce?)` | → `Response` | Applies CORS + CSP, HSTS, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, COOP, Trusted Types, and `extraHeaders`. | -| `createCorsPreflightResponse(req, security?)` | → `Response \| null` | Builds a `204`/`403` preflight response for CORS `OPTIONS` requests. | -| `isWebSocketOriginAllowed(req, security?)` | → `boolean` | Guards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients). | - -Config types: `SecurityConfig` (top-level), `CorsConfig`/`CorsOrigin`, -`ContentSecurityPolicyConfig`/`CspDirectiveValue`, `HstsConfig`, -`TrustedTypesConfig`, `PermissionsPolicyConfig`. WrNexus applies sensible -defaults (self-only CSP, `frame-ancestors 'none'`, restrictive Permissions-Policy, -HSTS in production, Trusted Types in production); each is individually -overridable or disable-able via `false`. - -### Storage: cookies, sessions, localStorage — `@wrnexus/core` - -These back the `ctx.cookies`, `ctx.session`, and `ctx.localStorage` fields. - -| Export | Kind | Notes | -| --------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `setSessionBackend(backend)` | fn | Swap the **sync** session persistence backend (`SessionBackend`) — e.g. `bun:sqlite`. Default is process-local memory. Call once at startup. | -| `loadSession(backend, options?)` | fn → `Middleware` | Back `ctx.session` with an **async** store (`AsyncSessionBackend`: `load`/`save`/`destroy`) — loads before the request, saves after. `options.ttlMs` default 24h. | -| `CookieStore` | type | `get`/`getAll`/`has`/`set(name, value, opts?)`/`delete`/`headers`. | -| `SessionStore` | type | `id`/`get`/`getAll`/`set`/`delete`/`regenerate`/`clear`. | -| `LocalStorageSnapshot` | type | Read-only view of the browser's localStorage sent via header for CSR bindings. | -| `CookieOptions` | type | `path`, `domain`, `maxAge`, `expires`, `httpOnly`, `secure`, `sameSite`. | -| `SessionEntry` / `SessionBackend` / `AsyncSessionBackend` | types | Session persistence contracts. | - -### Low-level security helpers — `@wrnexus/core` - -| Export | Signature | Notes | -| ----------------------------- | --------------------- | --------------------------------------------------- | -| `escapeHtml(value)` | `(string) => string` | Escape for HTML text/attributes. | -| `isSafeIslandName(name)` | `(string) => boolean` | Allow only a conservative `[A-Za-z0-9_-]+` charset. | -| `isSafeRequestPath(pathname)` | `(string) => boolean` | Reject NULs, `..` traversal, and backslashes. | - -### JSX runtime — `@wrnexus/core`, `@wrnexus/core/jsx-runtime`, `@wrnexus/core/jsx-dev-runtime` - -A server-side JSX runtime that renders to HTML **strings** (no virtual DOM). -Point `tsconfig`'s `jsxImportSource` at `@wrnexus/core`. - -| Export | Kind | Notes | -| ------------------------------------------ | ------ | --------------------------------------------------------------------------------------- | -| `jsx` / `jsxs` | fn | The runtime factory (TypeScript calls these automatically). Returns an `Html` instance. | -| `Fragment` | symbol | JSX fragment marker. | -| `Html` | class | Wraps a raw, already-safe HTML string (`toString()` returns it). | -| `mustache(expr)` | fn | Emit a `{{expr}}` placeholder (tagged-template or string form) for the client binder. | -| `JSXComponent` / `JSXProps` / `Renderable` | types | Component signature and renderable value types. | - -Values interpolated as children are HTML-escaped unless they are an `Html` -instance; use `dangerouslySetInnerHTML={{ __html }}` for trusted markup. Void -elements render without a closing tag; `className`→`class`, `htmlFor`→`for`, and -`style` objects are serialized to CSS text. - -The subpath exports map to the runtime TypeScript's JSX transform expects: - -```jsonc -// tsconfig.json -{ - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "@wrnexus/core", - }, -} -``` - -## Usage - -### A minimal middleware chain - -```ts -import { - createContext, - withContextHeaders, - sessionAuth, - requireAuth, - requestLogger, - rateLimit, - csrfProtection, - type Middleware, -} from "@wrnexus/core"; - -const chain: Middleware[] = [ - requestLogger({ format: "json" }), - rateLimit({ max: 100, windowMs: 60_000 }), - csrfProtection(), - sessionAuth(), - requireAuth({ loginPath: "/login" }), -]; -``` - -### Password auth - -```ts -import { hashPassword, verifyPassword, logIn, getUser } from "@wrnexus/core"; - -// Registration -const passwordHash = await hashPassword(form.password); - -// Login -if (await verifyPassword(form.password, user.passwordHash)) { - logIn(ctx, { id: user.id, email: user.email }); -} - -const current = getUser<{ id: string }>(ctx); // or null -``` - -### HTTP caching with ETags - -```ts -import { etag, notModified, withCacheControl } from "@wrnexus/core"; - -const body = JSON.stringify(data); -const tag = etag(body); -if (notModified(ctx.req, tag)) { - return new Response(null, { status: 304, headers: { ETag: tag } }); -} -const res = new Response(body, { headers: { ETag: tag, "content-type": "application/json" } }); -return withCacheControl(res, { maxAge: 60, staleWhileRevalidate: 300 }); -``` - -### Streaming SSE - -```ts -import { sse } from "@wrnexus/core"; - -async function* ticks() { - for (let n = 0; ; n++) { - yield { event: "tick", data: String(n) }; - await Bun.sleep(1000); - } -} -export default (ctx) => sse(ticks()); -``` - -### A realtime room - -```ts -// app/realtime/chat.ts -import { defineRoom } from "@wrnexus/core"; - -export default defineRoom({ - authorize: (info) => !!info.user, // require auth - onConnect(client) { - client.user = client.query.user; - client.room.broadcast({ type: "join", id: client.id }); - }, - onMessage(client, msg) { - client.broadcast({ type: "say", from: client.id, text: msg.text }); - }, -}); -``` - -Scale it across processes: - -```ts -import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core"; -import { createPubSub } from "@wrnexus/pubsub"; -import { redisDriver } from "@wrnexus/pubsub/redis"; - -const registry = createRealtimeRegistry(); -bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL))); -``` - -### JSX rendering - -```tsx -import { Html } from "@wrnexus/core"; - -function Card({ title, body }: { title: string; body: string }) { - return ( -
-

{title}

-

{body}

-
- ); -} - -const html: Html = ; -return new Response(html.toString(), { headers: { "content-type": "text/html" } }); -``` - -## Requirements / Notes - -- **Bun-only.** Uses `Bun.password` (argon2id), `Bun.write`, web-standard - `Request`/`Response`/`FormData`/`ReadableStream`, and the global `crypto`. - Node is not supported. -- Session and rate-limit backends default to **process-local memory**. For - multi-instance deployments, swap in a shared backend: `setSessionBackend` (sync, - e.g. `bun:sqlite`) or `loadSession` (async, e.g. Redis) for sessions, a custom - `RateLimitStore` for limits, and `bridgeRealtime` for realtime. -- Works with the rest of the framework: realtime bridging is structurally - compatible with [`@wrnexus/pubsub`](../pubsub); the security, auth, and JSX - primitives here are consumed by the WrNexus server/router packages. -- Subpath exports: `@wrnexus/core/jsx-runtime` and `@wrnexus/core/jsx-dev-runtime` - for TypeScript's automatic JSX transform. diff --git a/.publish/core/package.json b/.publish/core/package.json deleted file mode 100644 index 0b912234..00000000 --- a/.publish/core/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@wrnexus/core", - "version": "0.8.9", - "type": "module", - "description": "@wrnexus/core — part of the WrNexus framework.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://git.workroot.in/WorkRoot/WRNexusJS.git", - "directory": "packages/core" - }, - "homepage": "https://wrnexusjs.dev/packages/core", - "bugs": { - "url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues" - }, - "keywords": [ - "wrnexus", - "bun", - "typescript", - "core" - ], - "sideEffects": false, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "engines": { - "bun": ">=1.3.0" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "access": "restricted" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./jsx-runtime": { - "types": "./dist/jsx-runtime.d.ts", - "import": "./dist/jsx-runtime.js" - }, - "./jsx-dev-runtime": { - "types": "./dist/jsx-dev-runtime.d.ts", - "import": "./dist/jsx-dev-runtime.js" - } - }, - "files": [ - "dist", - "README.md" - ] -} diff --git a/.publish/csr/README.md b/.publish/csr/README.md deleted file mode 100644 index 66c295f1..00000000 --- a/.publish/csr/README.md +++ /dev/null @@ -1,224 +0,0 @@ -# @wrnexus/csr - -## Navigation state preservation - -Pages can opt into restoration across client navigation: - -```wrn -page Users { - navigation { - preserve = ["filters", "pagination", "scroll", "tabs", "expanded"] - } -} -``` - -Form-like categories restore named inputs, selects, and textareas. Password, -file, hidden, CSRF/token/secret/credential fields, and elements marked -`data-no-preserve` are never saved. For tab, expanded, or component UI state, -mark stable elements with `data-wrn-preserve="key"`; their value and ARIA -selected/expanded state are restored. State is scoped to pathname plus query. - -## Typed server actions - -`createActionClient(route, name)` supports programmatic calls. -Schema-backed WRN actions also export `__wrnexusActionClients`, whose input and -output are inferred automatically. Enhanced forms expose -`data-wrn-action-state="pending|success|error"` and dispatch bubbling -`wrnexus:action:optimistic`, `:pending`, `:success`, and `:error` events. -Success details contain returned data and invalidated cache tags; error details -contain field errors. Without JavaScript, the same form posts to its page and -receives a 303 redirect or accessible validation response. - -> The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms. - -Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. - -## Overview - -`@wrnexus/csr` holds the three client runtimes that WrNexus serves to the browser. Components are authored as `.wrn` files and rendered on the **server**; this package provides the single, generic runtime that **hydrates** that HTML in the browser — there are no per-component browser bundles. Each runtime is exported as a plain-JS string (no build step, no imports) intended to be served verbatim from a well-known URL: - -- **reactive** at `/__wrnexus/reactive.js` — reactive directives (`data-scope`, `data-text`, `data-for`, …) -- **nav** at `/__wrnexus/nav.js` — SPA-style client navigation with graceful fallback -- **realtime** at `/__wrnexus/realtime.js` — WebSocket "rooms", declarative or programmatic - -The package itself runs on the server (it just returns strings); the strings it returns run in the browser. A dev/prod server (see `@wrnexus/core`) is responsible for actually serving them. - -## Installation - -```bash -bun add @wrnexus/csr -``` - -> Private package — the machine must be authenticated to the `wrnexus` npm org -> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). - -## API - -All exports come from the package root (`@wrnexus/csr`). The runtime source is delivered as strings, so the "API" on the server side is small; the real surface is the browser directives/globals each string installs. - -### Runtime strings - -| Export | Type | Served at | Contents | -| ------------------ | -------- | ------------------------ | ------------------------------ | -| `REACTIVE_RUNTIME` | `string` | `/__wrnexus/reactive.js` | Reactive directive runtime | -| `NAV_RUNTIME` | `string` | `/__wrnexus/nav.js` | Client-side navigation runtime | -| `REALTIME_RUNTIME` | `string` | `/__wrnexus/realtime.js` | Realtime rooms runtime | - -### Accessor functions - -Convenience getters that return the same strings. - -```ts -getReactiveRuntime(): string // → REACTIVE_RUNTIME -getNavRuntime(): string // → NAV_RUNTIME -getRealtimeRuntime(): string // → REALTIME_RUNTIME -``` - -### Browser: reactive directives - -Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works. - -| Directive | Purpose | -| ---------------------------------- | ---------------------------------------------------- | -| `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree | -| `data-on-="count++"` | Run a statement in scope on a DOM event | -| `data-text="expr"` | Bind an element's `textContent` to an expression | -| `data-show="expr"` | Toggle visibility while preserving interactive state | - -Compiled conditional rendering and dynamic component cases omit inactive elements from the live -DOM. `data-show` is a visibility directive for stateful controls and keeps its element mounted. -Neither mechanism is authorization: never place secrets in client-rendered branches. Authorize on -the server and return only data the current request may access. -| `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder | -| `data-key="item.id"` | Alternative key declaration for `data-for` templates | -| `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values | -| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) | - -Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it. - -Browser globals installed: `window.__wrnexusHydrateScopes(root)` and `window.__wrnexusHydrateCsrFetches(root)` — both idempotent, so re-running after a DOM swap or HMR morph is safe. Both run automatically on `DOMContentLoaded`. - -### Browser: navigation - -Intercepts same-origin `` clicks, fetches the target page, and swaps the `#app` container in place (via `importNode` — not `innerHTML` — so it works under a Trusted-Types CSP), updating history, title, and scroll, then re-hydrates. Cross-origin links, modified clicks, `download`/`data-no-nav`/`rel="external"`/`target` links, non-HTML responses, or a missing `#app` fall back to a full browser navigation. - -- Programmatic navigation: `window.__wrnexusNavigate(url)` -- Emits a `wrnexus:navigated` `CustomEvent` (`detail.url`) after each swap -- Sends `x-wrnexus-nav: 1` on fetches so the server can return the page fragment -- Appends any `/__wrnexus/*` runtime scripts the incoming page needs but the current document lacks - -### Browser: realtime rooms - -Connects to `/realtime/` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes. - -Programmatic API via `window.wrn`: - -```ts -wrn.room(name): Room // open (or reuse) a room connection -wrn.bindRooms(root?) // (re)bind declarative [data-room] containers - -interface Room { - name: string; - send(obj: object | string): Room; // JSON-stringifies objects; queues until open - on(type: string, cb): Room; // filter by msg.type; "*" or a fn = all messages - on(cb): Room; - close(): Room; -} -``` - -Internal lifecycle messages are emitted to listeners as `{ type }`: `__open`, `__close`, `__error`, and `__raw` (non-JSON frames, with `data`). Reconnect uses exponential backoff capped at 5s; queued sends flush on reconnect. - -Declarative binding (zero JS) on a `data-room=""` container: - -| Attribute | On | Purpose | -| ------------------------------------ | --------------- | ------------------------------------------------------------------------ | -| `data-room=""` | container | Connect to room `` | -| `data-room-user=""` | container | Identify the connection (`?user=`) | -| `data-room-log` | element | Where incoming messages are appended | -| `