first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.wrnexus/
|
||||
dist/
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.wrnexus/
|
||||
**/.wrnexus/
|
||||
*.log
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
# WrNexus app - instructions for AI coding assistants
|
||||
|
||||
This is a **WrNexus** app. When creating or editing pages, components, API routes,
|
||||
or features, follow the framework conventions below. WrNexus is private and not in
|
||||
your training data, so rely on these rules - do NOT assume React/Next.js/Vue patterns.
|
||||
|
||||
# WrNexus
|
||||
|
||||
> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
|
||||
> `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based.
|
||||
> This document teaches an AI how to write correct WrNexus code. It is private and
|
||||
> post-dates model training data, so rely on THIS document, not prior web-framework
|
||||
> assumptions.
|
||||
|
||||
## Golden rules
|
||||
|
||||
- **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React
|
||||
for UI. Do NOT use `useState`, hooks, JSX, or a client bundler.
|
||||
- **Routing is file-based** under `app/`. The filename is the route. No router config.
|
||||
- **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render
|
||||
on the server and hydrate automatically — you never write client-side JS islands.
|
||||
- **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported.
|
||||
- To add files, prefer the CLI: `wrnexus generate page <Name>` / `component <name>` / `api <path>` / `schema <name>`.
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
app/
|
||||
pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug"
|
||||
components/ *.wrn → reusable UI, mounted in a page/component via <div data-component="name" ...props>
|
||||
layouts/ *.wrn → named layouts; a page opts in with layout = "name"
|
||||
api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response
|
||||
middleware/ *.ts → export default async (ctx, next) => next()
|
||||
realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/<name>)
|
||||
schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody
|
||||
locales/ *.json → i18n messages per language
|
||||
db/ schema.ts, queries/*.sql, migrations/*.sql
|
||||
styles/ global.css → Tailwind (default) or plain CSS
|
||||
wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles")
|
||||
public/ → static assets served at /
|
||||
```
|
||||
|
||||
## `.wrn` page
|
||||
|
||||
```wrn
|
||||
page Home {
|
||||
layout = "public" // optional: a component in app/layouts/<name>.wrn ("none" to skip)
|
||||
|
||||
state count = 0 // optional: seeds client-reactive state (omit for pure SSR)
|
||||
|
||||
seo {
|
||||
title = "Home"
|
||||
description = "..."
|
||||
canonical = "/"
|
||||
}
|
||||
|
||||
view {
|
||||
<h1>Hello</h1>
|
||||
<p>Count is {count}, doubled is {count * 2}.</p>
|
||||
<button @click="count++">Increment</button>
|
||||
<div data-component="counter" start="5" label="Clicks"></div>
|
||||
}
|
||||
|
||||
style {
|
||||
h1 { color: var(--wire-color-text); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `.wrn` component
|
||||
|
||||
```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 {
|
||||
<button @click="count++">{label}: {count}</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Mount it from any page/component: `<div data-component="counter" start="0" label="Clicks"></div>`.
|
||||
Components render on the server with their props, then hydrate — no per-component JS.
|
||||
|
||||
## The `view { }` block (plain HTML + a few directives)
|
||||
|
||||
- `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`.
|
||||
- `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`.
|
||||
- `<div data-component="name" prop="v">` — mount a component (attrs become string props, coerced).
|
||||
- `<slot></slot>` / `<slot name="x"></slot>` — component/layout slots; fill with `<div data-slot="x">…</div>`.
|
||||
- **Server loop (DB/list/table):** `{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `<list>` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`.
|
||||
- **Server conditional:** `{#if <expr>} … {:else if <expr>} … {:else} … {/if}` — renders the first truthy branch on the server. `<expr>` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}<span>●</span>{:else}<span>○</span>{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead.
|
||||
- i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`.
|
||||
- Theme: any element with `data-wire-theme-toggle` toggles light/dark; `data-wire-theme-set="dark"` sets it.
|
||||
- Void/self-closing tags are fine: `<br />`, `<img src="..." />`.
|
||||
- Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text.
|
||||
|
||||
## Data-driven tables / lists (server-rendered `.wrn`)
|
||||
|
||||
Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them.
|
||||
This renders on the **server** (SSR-first) and is HTML-escaped by default.
|
||||
|
||||
```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 {
|
||||
<table>
|
||||
<tbody>
|
||||
{#each rows as r, i}
|
||||
<tr>
|
||||
<td>#{i}</td>
|
||||
<td>{r.name}</td>
|
||||
<td><a href="mailto:{r.email}">{r.email}</a></td>
|
||||
</tr>
|
||||
{:empty}
|
||||
<tr><td colspan="3">No submissions yet.</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The matching API returns the array under a key the `ssr` block reads:
|
||||
|
||||
```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<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`),
|
||||
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.
|
||||
|
||||
## 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: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (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
|
||||
wrnexus create <name> # scaffold a new app
|
||||
wrnexus generate page <Name> # scaffold a page (aliases: g p)
|
||||
wrnexus generate component <name> | api <path> | schema <name>
|
||||
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
|
||||
wrnexus eject <component> # copy a Wire UI component's .wrn into app/components to customize
|
||||
```
|
||||
|
||||
## When asked to "create a page/component/feature"
|
||||
|
||||
1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
|
||||
2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`.
|
||||
3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`.
|
||||
4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wire-*)`), or `style { }`.
|
||||
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.
|
||||
@@ -0,0 +1,23 @@
|
||||
# WRNexusJS Documentation
|
||||
|
||||
Standalone documentation site for all 25 private `@wrnexus/*` packages at version `0.2.12`.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run docs:generate
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Package pages are generated from the installed package README and TypeScript declarations, so the documentation stays aligned with the published release.
|
||||
|
||||
## Verification and production
|
||||
|
||||
```bash
|
||||
bun run check
|
||||
bun run build
|
||||
bun dist/server.js
|
||||
```
|
||||
|
||||
The site includes searchable package discovery, complete API declarations, installation commands, usage guides, responsive styling, dark mode, SEO metadata, PWA configuration, and WRNexusJS production security headers.
|
||||
@@ -0,0 +1,122 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { compileWireFile } from "@wrnexus/compiler";
|
||||
import { mountHtml } from "@wrnexus/test";
|
||||
|
||||
const root = join(import.meta.dir, "..");
|
||||
const packagePages = join(root, "app", "pages", "packages");
|
||||
const expected = [
|
||||
"ai",
|
||||
"authz",
|
||||
"cli",
|
||||
"compiler",
|
||||
"core",
|
||||
"csr",
|
||||
"db",
|
||||
"dev-server",
|
||||
"encryption",
|
||||
"i18n",
|
||||
"jwt",
|
||||
"mobile",
|
||||
"native",
|
||||
"oauth",
|
||||
"pubsub",
|
||||
"queue",
|
||||
"reactive",
|
||||
"router",
|
||||
"ssr",
|
||||
"styles",
|
||||
"test",
|
||||
"tracking",
|
||||
"ui",
|
||||
"uploader",
|
||||
"validation",
|
||||
];
|
||||
|
||||
test("generates one detailed page for every published package", () => {
|
||||
const generated = readdirSync(packagePages)
|
||||
.filter((file) => file.endsWith(".wrn"))
|
||||
.map((file) => file.replace(/\.wrn$/, ""))
|
||||
.sort();
|
||||
expect(generated).toEqual([...expected].sort());
|
||||
});
|
||||
|
||||
test("every package page contains installation, guide, and complete API sections", () => {
|
||||
for (const name of expected) {
|
||||
const source = readFileSync(join(packagePages, `${name}.wrn`), "utf8");
|
||||
expect(source).toContain(`bun add @wrnexus/${name}@0.2.12`);
|
||||
expect(source).toContain('id="guide"');
|
||||
expect(source).toContain('id="api"');
|
||||
expect(source).toContain("Complete TypeScript API");
|
||||
expect(source).toContain('class="on-this-page"');
|
||||
expect(source).toContain('id="examples"');
|
||||
expect(source).toContain('class="example-card"');
|
||||
}
|
||||
});
|
||||
|
||||
test("site includes core guides and production configuration", () => {
|
||||
for (const route of [
|
||||
"index.wrn",
|
||||
"packages.wrn",
|
||||
"getting-started.wrn",
|
||||
"language.wrn",
|
||||
"architecture.wrn",
|
||||
]) {
|
||||
expect(existsSync(join(root, "app", "pages", route))).toBe(true);
|
||||
}
|
||||
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toContain(
|
||||
"WRNexusJS Documentation",
|
||||
);
|
||||
});
|
||||
|
||||
test("language reference covers directives, events, loops, and conditionals", () => {
|
||||
const source = readFileSync(join(root, "app", "pages", "language.wrn"), "utf8");
|
||||
for (const section of [
|
||||
"events",
|
||||
"directives",
|
||||
"conditionals",
|
||||
"loops",
|
||||
"components",
|
||||
"forms",
|
||||
"realtime",
|
||||
"native",
|
||||
]) {
|
||||
expect(source).toContain(`id="${section}"`);
|
||||
}
|
||||
expect(source).toContain("data-show");
|
||||
expect(source).toContain("@mobile-click");
|
||||
});
|
||||
|
||||
test("package index includes searchable category filters", () => {
|
||||
const source = readFileSync(join(root, "app", "pages", "packages.wrn"), "utf8");
|
||||
expect(source).toContain('state category = "All"');
|
||||
expect(source).toContain("category = 'Security'");
|
||||
expect(source).toContain("category = 'Native'");
|
||||
expect(source).toContain("category === 'All'");
|
||||
expect(source).not.toContain("category === 'All' ||");
|
||||
expect(source).not.toContain(") && (query");
|
||||
expect(source.match(/class="package-card"/g)).toHaveLength(25);
|
||||
});
|
||||
|
||||
test("clicking a category hides packages from other categories", async () => {
|
||||
const source = readFileSync(join(root, "app", "pages", "packages.wrn"), "utf8");
|
||||
const directory = join(tmpdir(), "wrnexus-doc-tests");
|
||||
mkdirSync(directory, { recursive: true });
|
||||
const modulePath = join(directory, `packages-${Date.now()}.ts`);
|
||||
writeFileSync(modulePath, compileWireFile(source));
|
||||
const page = (await import(pathToFileURL(modulePath).href)).default as () => string;
|
||||
const mounted = mountHtml(page());
|
||||
const dataButton = mounted
|
||||
.querySelectorAll(".category-row button")
|
||||
.find((button) => button.textContent?.startsWith("Data"));
|
||||
expect(dataButton).toBeDefined();
|
||||
(dataButton as HTMLElement).click();
|
||||
const visible = mounted
|
||||
.querySelectorAll(".package-card")
|
||||
.filter((card) => (card as HTMLElement).style.display !== "none")
|
||||
.map((card) => card.getAttribute("href"));
|
||||
expect(visible).toEqual(["/packages/db", "/packages/queue", "/packages/uploader"]);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
page Architecture {
|
||||
seo {
|
||||
title = "Architecture"
|
||||
description = "Understand the WRNexusJS SSR, compiler, runtime, and package architecture."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page"><article class="documentation prose standalone"><span class="eyebrow">Concepts</span><h1>Architecture</h1><p>WRNexusJS separates server work, generated markup, and browser behavior so applications stay understandable and efficient.</p><h2>Request path</h2><pre><code>Request → Router → Middleware → Page/API → SSR document → Browser runtime</code></pre><h2>Compiler</h2><p>The compiler parses .wrn files and lowers state, events, interpolation, loops, conditionals, data bindings, components, and styles into server modules and small declarative browser directives.</p><h2>Runtime</h2><p>The server owns routing, data, secrets, sessions, validation, uploads, and rendering. The browser owns reactive scopes, navigation, forms, realtime clients, and native capability dispatch.</p><h2>Package boundaries</h2><p>Each package is independently installable. Start with the CLI and core, then add database, security, realtime, native, UI, and operational packages as required.</p><div class="actions"><a class="primary" href="/packages/core">Read core API</a><a href="/packages/compiler">Read compiler API</a></div></article></main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
page Gettingstarted {
|
||||
seo {
|
||||
title = "Getting started"
|
||||
description = "Getting started with WRNexusJS"
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page"><article class="documentation prose standalone"><span class="eyebrow">Guide</span><h1>Getting started</h1><p>Create a production-ready WRNexusJS application with Bun.</p><h2>1. Create the project</h2><pre><code>bunx @wrnexus/cli create my-app
|
||||
cd my-app
|
||||
bun install
|
||||
bun run dev</code></pre><h2>2. Add a page</h2><pre><code>page Dashboard {
|
||||
state count = 0
|
||||
view {
|
||||
<main>
|
||||
<h1>Dashboard</h1>
|
||||
<button @click="count++">{count}</button>
|
||||
</main>
|
||||
}
|
||||
}</code></pre><h2>3. Verify and build</h2><pre><code>wrnexus doctor
|
||||
wrnexus test
|
||||
wrnexus build</code></pre><h2>Where things live</h2><ul><li><code>app/pages</code> contains routes.</li><li><code>app/components</code> contains reusable .wrn components.</li><li><code>app/api</code> contains server API handlers.</li><li><code>app/layouts</code> contains shared shells.</li><li><code>app/middleware</code> contains request middleware.</li><li><code>wrnexus.config.ts</code> configures security, styles, data, mobile, and deployment.</li></ul></article></main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
page Home {
|
||||
seo {
|
||||
title = "Home"
|
||||
description = "WRNexusJS documentation: build secure, server-rendered, reactive applications with Bun."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page"><section class="hero"><span class="eyebrow">WRNexusJS 0.2.12</span><h1>Build from the server.<br><em>Ship only what matters.</em></h1><p>An SSR-first, Bun-native framework with reactive .wrn components, typed data, realtime rooms, mobile capabilities, and production security built in.</p><div class="actions"><a class="primary" href="/getting-started">Start building</a><a href="/packages">Explore 25 packages</a></div><div class="code-window"><span>app/pages/counter.wrn</span><pre><code>page Counter {
|
||||
state count = 0
|
||||
view {
|
||||
<button @click="count++">
|
||||
Count {count}
|
||||
</button>
|
||||
}
|
||||
}</code></pre></div></section><section class="feature-grid"><article><h2>SSR by default</h2><p>Useful HTML reaches the browser immediately. Interactive pages hydrate only the runtime they use.</p></article><article><h2>Secure foundations</h2><p>CSP, Trusted Types, CSRF, sessions, validation, authorization, encryption, and safe rendering are integrated.</p></article><article><h2>Web to native</h2><p>Share markup through Capacitor or compile portable pages into Expo and React Native routes.</p></article></section></main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
page Languageanddirectives {
|
||||
seo {
|
||||
title = "Language and directives"
|
||||
description = "Complete WRNexusJS language reference for events, directives, loops, conditionals, data, components, forms, realtime, and native behavior."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page"><article class="documentation prose standalone language-reference"><span class="eyebrow">Complete reference</span><h1>Language and directives</h1><p>This page documents the .wrn language and declarative browser features that span multiple packages.</p>
|
||||
<h2 id="file-anatomy">File anatomy</h2><p>A file declares a <code>page</code> or <code>component</code> and can contain metadata, props, state, data, view, style, server functions, APIs, and realtime handlers.</p><pre><code>page Dashboard {
|
||||
layout = "default"
|
||||
seo { title = "Dashboard" }
|
||||
state count = 0
|
||||
view { <button @click="count++">{count}</button> }
|
||||
style { button { padding: 12px; } }
|
||||
}</code></pre>
|
||||
<h2 id="state">State and interpolation</h2><p>State is scoped to the nearest generated <code>data-scope</code>. Text expressions update reactively after hydration.</p><pre><code>state count = 0
|
||||
state user = { name: "Ada" }
|
||||
|
||||
view {
|
||||
<p>Count: {count}</p>
|
||||
<p>{user.name}</p>
|
||||
}</code></pre>
|
||||
<h2 id="events">Events</h2><p>Any DOM event can use <code>@event="statement"</code>. The compiler emits <code>data-on-event</code>. The expression receives <code>event</code> and can mutate state.</p><div class="table-wrap"><table><thead><tr><th>Syntax</th><th>Purpose</th></tr></thead><tbody><tr><td><code>@click</code></td><td>Pointer or keyboard activation.</td></tr><tr><td><code>@input</code></td><td>Read live field values.</td></tr><tr><td><code>@change</code></td><td>React to committed field changes.</td></tr><tr><td><code>@submit</code></td><td>Handle form submission behavior.</td></tr><tr><td><code>@browser-click</code></td><td>Run only in a browser target.</td></tr><tr><td><code>@mobile-click</code></td><td>Run only in a native/mobile target.</td></tr></tbody></table></div><pre><code><input @input="name = event.target.value">
|
||||
<button @click="count++">Add</button>
|
||||
<form @submit="submitted = true">...</form></code></pre>
|
||||
<h2 id="directives">Reactive data attributes</h2><div class="table-wrap"><table><thead><tr><th>Directive</th><th>Behavior</th></tr></thead><tbody><tr><td><code>data-scope</code></td><td>Declares reactive state for a subtree.</td></tr><tr><td><code>data-text</code></td><td>Synchronizes textContent with an expression.</td></tr><tr><td><code>data-show</code></td><td>Shows or hides an element by truthiness.</td></tr><tr><td><code>data-for</code></td><td>Repeats an element for a client-side list.</td></tr><tr><td><code>data-on-<event></code></td><td>Compiled form of an event binding.</td></tr><tr><td><code>data-component</code></td><td>Mounts a server-rendered component.</td></tr><tr><td><code>data-slot</code></td><td>Fills a named component or layout slot.</td></tr><tr><td><code>data-wrnexus-csr</code></td><td>Connects generated client data fetching.</td></tr></tbody></table></div>
|
||||
<h2 id="conditionals">Conditional rendering</h2><h3>Server conditionals</h3><p>Server blocks render only the selected branch into the response.</p><pre><code>{#if user.isAdmin}
|
||||
<a href="/admin">Admin</a>
|
||||
{:else if user}
|
||||
<p>Welcome {user.name}</p>
|
||||
{:else}
|
||||
<a href="/login">Sign in</a>
|
||||
{/if}</code></pre><h3>Client visibility</h3><pre><code><section data-show="open">Visible while open is true</section></code></pre>
|
||||
<h2 id="loops">Loops and lists</h2><h3>Server each block</h3><pre><code>{#each users as user, i}
|
||||
<p>{i + 1}. {user.name}</p>
|
||||
{:empty}
|
||||
<p>No users</p>
|
||||
{/each}</code></pre><h3>Reactive client loop</h3><pre><code><li data-for="item, i in items">
|
||||
<span data-text="item.name"></span>
|
||||
<button data-on-click="items = items.filter(x => x !== item)">Remove</button>
|
||||
</li></code></pre>
|
||||
<h2 id="components">Components, props, and slots</h2><pre><code>component Card {
|
||||
props { title = "Card" }
|
||||
view {
|
||||
<article><h2>{title}</h2><slot></slot></article>
|
||||
}
|
||||
}
|
||||
|
||||
<div data-component="card" title="Profile">
|
||||
<p>Card content</p>
|
||||
</div></code></pre>
|
||||
<h2 id="data">Server and client data</h2><p>Use named data bindings for SSR data or client hydration. Secrets and database work stay on the server.</p><pre><code>data users {
|
||||
ssr GET "/api/users"
|
||||
}
|
||||
|
||||
view {
|
||||
{#each users as user}<p>{user.name}</p>{/each}
|
||||
}</code></pre>
|
||||
<h2 id="forms">Forms and validation</h2><p>Schema-backed forms validate in the browser and on the server with the same descriptor.</p><pre><code><form data-schema="login" method="post" action="/api/login" data-redirect="/dashboard">
|
||||
<input name="email" type="email">
|
||||
<span data-error="email"></span>
|
||||
<button>Sign in</button>
|
||||
<p data-success="Signed in" hidden></p>
|
||||
</form></code></pre>
|
||||
<h2 id="i18n">Internationalization and themes</h2><pre><code><h1>{t:home.title}</h1>
|
||||
<button data-wire-lang-set="fr">Français</button>
|
||||
<button data-wire-theme-toggle>Toggle theme</button>
|
||||
<button data-wire-theme-set="dark">Dark</button></code></pre>
|
||||
<h2 id="realtime">Realtime rooms</h2><pre><code><div data-room="chat" data-room-user="Ada">
|
||||
<span data-room-status></span>
|
||||
<div data-room-log></div>
|
||||
<template data-room-item="message"><p>%user%: %text%</p></template>
|
||||
<form data-room-send><input name="text" data-room-reset></form>
|
||||
</div></code></pre>
|
||||
<h2 id="native">Browser and native directives</h2><pre><code><button data-native-browser="share" data-native-mobile="share"
|
||||
data-native-options='{"title":"WRNexusJS"}'>Share</button>
|
||||
<nav data-native-only="mobile">Mobile navigation</nav>
|
||||
<p data-native-only="browser">Browser instructions</p>
|
||||
<button data-native-requires="haptics">Haptic action</button></code></pre>
|
||||
<h2 id="other">Other framework attributes</h2><div class="table-wrap"><table><thead><tr><th>Attribute</th><th>Purpose</th></tr></thead><tbody><tr><td><code>data-error</code></td><td>Field validation error destination.</td></tr><tr><td><code>data-success</code></td><td>Successful form message.</td></tr><tr><td><code>data-redirect</code></td><td>Navigation after form success.</td></tr><tr><td><code>data-room-*</code></td><td>Realtime status, templates, sending, and reset behavior.</td></tr><tr><td><code>data-uploader</code></td><td>Config-driven upload widget.</td></tr><tr><td><code>data-wire-theme-*</code></td><td>Theme selection and toggling.</td></tr><tr><td><code>data-wire-lang*</code></td><td>Language selection.</td></tr><tr><td><code>data-native-*</code></td><td>Cross-platform capability and visibility behavior.</td></tr></tbody></table></div></article></main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
page Packages {
|
||||
seo {
|
||||
title = "Packages"
|
||||
description = "Explore every WRNexusJS package, API, function, and copy-ready usage example."
|
||||
}
|
||||
state query = ""
|
||||
state category = "All"
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page"><section class="hero compact"><span class="eyebrow">25 focused packages</span><h1>Package reference</h1><p>Everything in the framework, organized by responsibility and documented from the published 0.2.12 APIs.</p><input class="search" type="search" placeholder="Search packages, features, or categories…" @input="query = event.target.value" /></section><section class="category-filter" aria-label="Filter packages by category"><div class="category-row"><button type="button" @click="category = 'All'">All<span>25</span></button><button type="button" @click="category = 'AI'">AI<span>1</span></button><button type="button" @click="category = 'Security'">Security<span>5</span></button><button type="button" @click="category = 'Tooling'">Tooling<span>2</span></button><button type="button" @click="category = 'Core'">Core<span>3</span></button><button type="button" @click="category = 'Frontend'">Frontend<span>5</span></button><button type="button" @click="category = 'Data'">Data<span>3</span></button><button type="button" @click="category = 'Runtime'">Runtime<span>3</span></button><button type="button" @click="category = 'Native'">Native<span>2</span></button><button type="button" @click="category = 'Realtime'">Realtime<span>1</span></button></div><p>Showing <strong>{category}</strong> packages</p></section><section class="package-grid"><a class="package-card" href="/packages/ai" data-show="(category === 'All' ? true : category === 'AI') ? (query === '' ? true : 'ai ai server-side anthropic client with generation and streaming. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">AI</span><h2>@wrnexus/ai</h2><p>Server-side Anthropic client with generation and streaming.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/authz" data-show="(category === 'All' ? true : category === 'Security') ? (query === '' ? true : 'authz security role, permission, policy, and authorization guards. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Security</span><h2>@wrnexus/authz</h2><p>Role, permission, policy, and authorization guards.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/cli" data-show="(category === 'All' ? true : category === 'Tooling') ? (query === '' ? true : 'cli tooling create, develop, build, generate, test, and maintain wrnexus apps. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Tooling</span><h2>@wrnexus/cli</h2><p>Create, develop, build, generate, test, and maintain WRNexusJS apps.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/compiler" data-show="(category === 'All' ? true : category === 'Core') ? (query === '' ? true : 'compiler core parser and code generators for the .wrn language. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Core</span><h2>@wrnexus/compiler</h2><p>Parser and code generators for the .wrn language.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/core" data-show="(category === 'All' ? true : category === 'Core') ? (query === '' ? true : 'core core contexts, middleware, security, sessions, caching, jsx, and realtime. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Core</span><h2>@wrnexus/core</h2><p>Contexts, middleware, security, sessions, caching, JSX, and realtime.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/csr" data-show="(category === 'All' ? true : category === 'Frontend') ? (query === '' ? true : 'csr frontend reactive, navigation, and realtime browser runtimes. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Frontend</span><h2>@wrnexus/csr</h2><p>Reactive, navigation, and realtime browser runtimes.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/db" data-show="(category === 'All' ? true : category === 'Data') ? (query === '' ? true : 'db data database adapters, typed queries, models, migrations, and sessions. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Data</span><h2>@wrnexus/db</h2><p>Database adapters, typed queries, models, migrations, and sessions.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/dev-server" data-show="(category === 'All' ? true : category === 'Runtime') ? (query === '' ? true : 'dev-server runtime development and production servers, hmr, assets, and gateways. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Runtime</span><h2>@wrnexus/dev-server</h2><p>Development and production servers, HMR, assets, and gateways.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/encryption" data-show="(category === 'All' ? true : category === 'Security') ? (query === '' ? true : 'encryption security hashing, hmac, authenticated encryption, and key derivation. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Security</span><h2>@wrnexus/encryption</h2><p>Hashing, HMAC, authenticated encryption, and key derivation.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/i18n" data-show="(category === 'All' ? true : category === 'Frontend') ? (query === '' ? true : 'i18n frontend translation loading, locale resolution, and intl formatting. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Frontend</span><h2>@wrnexus/i18n</h2><p>Translation loading, locale resolution, and Intl formatting.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/jwt" data-show="(category === 'All' ? true : category === 'Security') ? (query === '' ? true : 'jwt security hs256 jwt signing, verification, and bearer authentication. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Security</span><h2>@wrnexus/jwt</h2><p>HS256 JWT signing, verification, and bearer authentication.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/mobile" data-show="(category === 'All' ? true : category === 'Native') ? (query === '' ? true : 'mobile native ssr-safe compatibility access to capacitor plugins. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Native</span><h2>@wrnexus/mobile</h2><p>SSR-safe compatibility access to Capacitor plugins.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/native" data-show="(category === 'All' ? true : category === 'Native') ? (query === '' ? true : 'native native cross-platform browser and capacitor capability registry. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Native</span><h2>@wrnexus/native</h2><p>Cross-platform browser and Capacitor capability registry.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/oauth" data-show="(category === 'All' ? true : category === 'Security') ? (query === '' ? true : 'oauth security oauth 2.0, pkce, provider presets, and profile mapping. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Security</span><h2>@wrnexus/oauth</h2><p>OAuth 2.0, PKCE, provider presets, and profile mapping.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/pubsub" data-show="(category === 'All' ? true : category === 'Realtime') ? (query === '' ? true : 'pubsub realtime in-process and redis-backed publish/subscribe. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Realtime</span><h2>@wrnexus/pubsub</h2><p>In-process and Redis-backed publish/subscribe.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/queue" data-show="(category === 'All' ? true : category === 'Data') ? (query === '' ? true : 'queue data background jobs with delay, concurrency, retry, and repetition. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Data</span><h2>@wrnexus/queue</h2><p>Background jobs with delay, concurrency, retry, and repetition.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/reactive" data-show="(category === 'All' ? true : category === 'Frontend') ? (query === '' ? true : 'reactive frontend small type-safe reactive signal primitives. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Frontend</span><h2>@wrnexus/reactive</h2><p>Small type-safe reactive signal primitives.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/router" data-show="(category === 'All' ? true : category === 'Core') ? (query === '' ? true : 'router core filesystem discovery, route matching, and typed route generation. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Core</span><h2>@wrnexus/router</h2><p>Filesystem discovery, route matching, and typed route generation.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/ssr" data-show="(category === 'All' ? true : category === 'Runtime') ? (query === '' ? true : 'ssr runtime secure html document rendering and seo metadata. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Runtime</span><h2>@wrnexus/ssr</h2><p>Secure HTML document rendering and SEO metadata.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/styles" data-show="(category === 'All' ? true : category === 'Frontend') ? (query === '' ? true : 'styles frontend css pipeline, themes, fonts, profiles, and application config. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Frontend</span><h2>@wrnexus/styles</h2><p>CSS pipeline, themes, fonts, profiles, and application config.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/test" data-show="(category === 'All' ? true : category === 'Tooling') ? (query === '' ? true : 'test tooling wrnexus-aware component, route, and browser testing utilities. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Tooling</span><h2>@wrnexus/test</h2><p>WRNexusJS-aware component, route, and browser testing utilities.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/tracking" data-show="(category === 'All' ? true : category === 'Runtime') ? (query === '' ? true : 'tracking runtime error/event capture, middleware, filtering, and sinks. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Runtime</span><h2>@wrnexus/tracking</h2><p>Error/event capture, middleware, filtering, and sinks.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/ui" data-show="(category === 'All' ? true : category === 'Frontend') ? (query === '' ? true : 'ui frontend themeable server-rendered ui components and css. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Frontend</span><h2>@wrnexus/ui</h2><p>Themeable server-rendered UI components and CSS.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/uploader" data-show="(category === 'All' ? true : category === 'Data') ? (query === '' ? true : 'uploader data validated local/s3 uploads and secure file serving. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Data</span><h2>@wrnexus/uploader</h2><p>Validated local/S3 uploads and secure file serving.</p><span class="card-link">Open documentation →</span>
|
||||
</a>
|
||||
<a class="package-card" href="/packages/validation" data-show="(category === 'All' ? true : category === 'Security') ? (query === '' ? true : 'validation security typed schemas, coercion, validation, and browser descriptors. '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">Security</span><h2>@wrnexus/validation</h2><p>Typed schemas, coercion, validation, and browser descriptors.</p><span class="card-link">Open documentation →</span>
|
||||
</a></section></main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
page wrnexusai {
|
||||
seo {
|
||||
title = "@wrnexus/ai"
|
||||
description = "Server-side Anthropic client with generation and streaming."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">AI</span><h1>@wrnexus/ai</h1><p>Server-side Anthropic client with generation and streaming.</p><code>bun add @wrnexus/ai@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">AI</span><h1>@wrnexus/ai</h1><p>Server-side Anthropic client with generation and streaming.</p><pre><code>bun add @wrnexus/ai@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>A tiny, zero-dependency Claude (Anthropic) client for WRNexusJS apps — generate and stream text with Claude from any server-side code.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/ai</code> is a thin, dependency-free wrapper over the Anthropic <strong>Messages API</strong>, built on <code>fetch</code> (Bun-native, no SDK). Use it in API routes, jobs, or middleware to call Claude. It defaults to the most capable model, <strong><code>claude-opus-4-8</code></strong>, reads your key from <code>ANTHROPIC_API_KEY</code>, and supports both one-shot generation and streaming.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/ai</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<p>Set your key in the environment (e.g. <code>.env</code>):</p>
|
||||
<pre data-language=""><code>ANTHROPIC_API_KEY=sk-ant-...</code></pre>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="createai-config"><code>createAI(config?)</code></h4>
|
||||
<p>Creates a client. The key is read at call time, so it's safe to create at import.</p>
|
||||
<pre data-language="ts"><code>import { createAI } from "@wrnexus/ai";
|
||||
const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version })</code></pre>
|
||||
<p><code>AIConfig</code> fields (all optional):</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Field</th><th>Default</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>apiKey</code></td><td><code>ANTHROPIC_API_KEY</code></td><td>Anthropic API key</td></tr><tr><td><code>model</code></td><td><code>"claude-opus-4-8"</code></td><td>Model id</td></tr><tr><td><code>maxTokens</code></td><td><code>4096</code></td><td>Default max output tokens</td></tr><tr><td><code>baseURL</code></td><td><code>https://api.anthropic.com</code></td><td>API base URL</td></tr><tr><td><code>version</code></td><td><code>"2023-06-01"</code></td><td><code>anthropic-version</code> header</td></tr></tbody></table></div>
|
||||
<h4 id="ai-generate-prompt-opts-promise-string"><code>ai.generate(prompt, opts?): Promise<string></code></h4>
|
||||
<p>One-shot text generation. <code>prompt</code> is a string or a <code>Message[]</code> history.</p>
|
||||
<pre data-language="ts"><code>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." },
|
||||
);</code></pre>
|
||||
<h4 id="ai-stream-prompt-opts-asyncgenerator-string"><code>ai.stream(prompt, opts?): AsyncGenerator<string></code></h4>
|
||||
<p>Yields text deltas as they arrive.</p>
|
||||
<pre data-language="ts"><code>for await (const chunk of ai.stream("Tell me a story.")) {
|
||||
process.stdout.write(chunk);
|
||||
}</code></pre>
|
||||
<h4 id="ai-streamresponse-prompt-opts-response"><code>ai.streamResponse(prompt, opts?): Response</code></h4>
|
||||
<p>Returns a streaming <code>text/plain</code> <code>Response</code> — drop it straight into an API route.</p>
|
||||
<pre data-language="ts"><code>// 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);
|
||||
};</code></pre>
|
||||
<h4 id="generateoptions"><code>GenerateOptions</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>system</code></td><td><code>string</code></td><td>System prompt</td></tr><tr><td><code>model</code></td><td><code>string</code></td><td>Override the model for this call</td></tr><tr><td><code>maxTokens</code></td><td><code>number</code></td><td>Override max output tokens</td></tr><tr><td><code>thinking</code></td><td><code>boolean</code></td><td>Enable adaptive extended thinking (deeper reasoning)</td></tr><tr><td><code>effort</code></td><td>`"low" \</td><td>"medium" \</td><td>"high" \</td><td>"xhigh" \</td><td>"max"`</td><td>Reasoning effort / token spend</td></tr><tr><td><code>messages</code></td><td><code>Message[]</code></td><td>Full history — supersedes <code>prompt</code></td></tr><tr><td><code>signal</code></td><td><code>AbortSignal</code></td><td>Cancel the request</td></tr></tbody></table></div>
|
||||
<blockquote><code>temperature</code> / <code>top_p</code> are intentionally <strong>not</strong> exposed — the current Claude</blockquote>
|
||||
<blockquote>models reject them (400). Steer output with prompting instead.</blockquote>
|
||||
<h4 id="aierror"><code>AIError</code></h4>
|
||||
<p>Thrown on non-2xx responses or a model refusal. Carries <code>.status</code> and <code>.type</code> (e.g. <code>"authentication_error"</code>, <code>"rate_limit_error"</code>, <code>"refusal"</code>).</p>
|
||||
<pre data-language="ts"><code>import { AIError } from "@wrnexus/ai";
|
||||
try {
|
||||
await ai.generate("...");
|
||||
} catch (e) {
|
||||
if (e instanceof AIError && e.type === "rate_limit_error") {
|
||||
/* back off */
|
||||
}
|
||||
}</code></pre>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre data-language="ts"><code>// 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 });
|
||||
};</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Uses <code>fetch</code>, <code>ReadableStream</code>, <code>TextDecoder</code>/<code>TextEncoder</code>, and</li>
|
||||
<p>reads <code>ANTHROPIC_API_KEY</code> from <code>Bun.env</code> (falls back to <code>process.env</code>).</p>
|
||||
<li><strong>Zero dependencies</strong> — no <code>@anthropic-ai/sdk</code>; talks to the Messages API directly.</li>
|
||||
<li>Defaults to <code>claude-opus-4-8</code>. Pass <code>{ model }</code> for a different model (e.g.</li>
|
||||
<p><code>"claude-sonnet-5"</code> for speed/cost, <code>"claude-haiku-4-5"</code> for the fastest).</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* @wrnexus/ai — a tiny, zero-dependency Claude (Anthropic) client for WRNexusJS apps.
|
||||
*
|
||||
* Use it in API routes, jobs, or anywhere server-side to generate text with Claude.
|
||||
* It talks to the Anthropic Messages API over `fetch` (no SDK dependency, Bun-native),
|
||||
* and defaults to the most capable model, `claude-opus-4-8`.
|
||||
*
|
||||
* import { createAI } from "@wrnexus/ai";
|
||||
* const ai = createAI(); // reads ANTHROPIC_API_KEY
|
||||
* const text = await ai.generate("Write a haiku about Bun.");
|
||||
*
|
||||
* Streaming (great for API routes):
|
||||
* export const POST = async (ctx) => ai.streamResponse(await ctx.req.text());
|
||||
*/
|
||||
type Role = "user" | "assistant";
|
||||
interface Message {
|
||||
role: Role;
|
||||
content: string;
|
||||
}
|
||||
/** Reasoning effort — higher means deeper thinking + more tokens. */
|
||||
type Effort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
interface AIConfig {
|
||||
/** Anthropic API key. Default: `ANTHROPIC_API_KEY` from the environment. */
|
||||
apiKey?: string;
|
||||
/** Model id. Default: `claude-opus-4-8` (the most capable Claude model). */
|
||||
model?: string;
|
||||
/** Default max output tokens. Default: 4096. */
|
||||
maxTokens?: number;
|
||||
/** API base URL. Default: `https://api.anthropic.com`. */
|
||||
baseURL?: string;
|
||||
/** `anthropic-version` header. Default: `2023-06-01`. */
|
||||
version?: string;
|
||||
}
|
||||
interface GenerateOptions {
|
||||
/** System prompt — sets the assistant's role/behavior. */
|
||||
system?: string;
|
||||
/** Override the model for this call. */
|
||||
model?: string;
|
||||
/** Override max output tokens for this call. */
|
||||
maxTokens?: number;
|
||||
/** Enable adaptive extended thinking (slower, deeper reasoning). */
|
||||
thinking?: boolean;
|
||||
/** Reasoning effort / token spend (`output_config.effort`). */
|
||||
effort?: Effort;
|
||||
/** Full message history — supersedes the `prompt` argument when provided. */
|
||||
messages?: Message[];
|
||||
/** Abort the request. */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Thrown when the API returns a non-2xx response or refuses the request. */
|
||||
declare class AIError extends Error {
|
||||
readonly status: number;
|
||||
readonly type: string;
|
||||
constructor(message: string, status?: number, type?: string);
|
||||
}
|
||||
interface AI {
|
||||
/** Generate a full text response (non-streaming). */
|
||||
generate(prompt: string | Message[], opts?: GenerateOptions): Promise<string>;
|
||||
/** Stream the response as text deltas, as they arrive. */
|
||||
stream(prompt: string | Message[], opts?: GenerateOptions): AsyncGenerator<string, void, unknown>;
|
||||
/** Stream straight to a `Response` (text/plain) — drop-in for an API route return. */
|
||||
streamResponse(prompt: string | Message[], opts?: GenerateOptions): Response;
|
||||
}
|
||||
/** Create a Claude client. Reads `ANTHROPIC_API_KEY` from the environment by default. */
|
||||
declare function createAI(config?: AIConfig): AI;
|
||||
|
||||
export { type AI, type AIConfig, AIError, type Effort, type GenerateOptions, type Message, type Role, createAI };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/ai</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="text"><code>ANTHROPIC_API_KEY=sk-ant-...</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>import { createAI } from "@wrnexus/ai";
|
||||
const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version })</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>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." },
|
||||
);</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#createai-config">createAI(config?)</a><a class="toc-level-4" href="#ai-generate-prompt-opts-promise-string">ai.generate(prompt, opts?): Promise<string></a><a class="toc-level-4" href="#ai-stream-prompt-opts-asyncgenerator-string">ai.stream(prompt, opts?): AsyncGenerator<string></a><a class="toc-level-4" href="#ai-streamresponse-prompt-opts-response">ai.streamResponse(prompt, opts?): Response</a><a class="toc-level-4" href="#generateoptions">GenerateOptions</a><a class="toc-level-4" href="#aierror">AIError</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
page wrnexusauthz {
|
||||
seo {
|
||||
title = "@wrnexus/authz"
|
||||
description = "Role, permission, policy, and authorization guards."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Security</span><h1>@wrnexus/authz</h1><p>Role, permission, policy, and authorization guards.</p><code>bun add @wrnexus/authz@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Security</span><h1>@wrnexus/authz</h1><p>Role, permission, policy, and authorization guards.</p><pre><code>bun add @wrnexus/authz@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Composable authorization for WRNexusJS — role-based (RBAC), policy-based (PBAC), and attribute-based (ABAC) access control that reduces to a boolean check plus an <code>authorize()</code> guard.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/authz</code> 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 <code>boolean | Promise<boolean></code> decision. Wrap any decision in a <code>Middleware</code> guard (<code>authorize</code>, <code>requireRole</code>, <code>requirePermission</code>) to protect WRNexusJS 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 <code>@wrnexus/core</code> by reading <code>ctx.user</code> as the authorization subject.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/authz</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>The package has a single entry point (<code>@wrnexus/authz</code>) exporting the following.</p>
|
||||
<h4 id="types">Types</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Symbol</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>Subject</code></td><td>The authorized principal: <code>{ id?: string; roles?: string[]; [attribute: string]: unknown }</code>.</td></tr><tr><td><code>Rbac</code></td><td>An RBAC checker: <code>{ can(subject, permission): boolean; permissionsFor(roles): Set<string> }</code>.</td></tr><tr><td><code>Policy<S = Subject, R = unknown></code></td><td>A predicate `(subject: S, resource?: R) => boolean \</td><td>Promise<boolean>`.</td></tr></tbody></table></div>
|
||||
<h4 id="rbac">RBAC</h4>
|
||||
<h4 id="definerbac-roles-record-string-string-rbac"><code>defineRbac(roles: Record<string, string[]>): Rbac</code></h4>
|
||||
<p>Builds an RBAC checker from a role → permissions map. Supported permission forms:</p>
|
||||
<ul>
|
||||
<li><code>"*"</code> — grants every permission.</li>
|
||||
<li><code>"ns:*"</code> — namespace wildcard (e.g. <code>"post:*"</code> grants <code>"post:write"</code>).</li>
|
||||
<li><code>"role:<name>"</code> — inherits all permissions of another role (resolved recursively, cycle-safe).</li>
|
||||
</ul>
|
||||
<p>The returned <code>Rbac</code> provides:</p>
|
||||
<ul>
|
||||
<li><code>can(subject, permission)</code> — <code>true</code> if any of <code>subject.roles</code> grants <code>permission</code> (honouring <code>*</code> and namespace wildcards). Returns <code>false</code> when the subject has no roles.</li>
|
||||
<li><code>permissionsFor(roles)</code> — the resolved <code>Set<string></code> of all permissions granted to a set of roles.</li>
|
||||
</ul>
|
||||
<h4 id="hasrole-subject-subject-undefined-required-string-boolean"><code>hasRole(subject: Subject | undefined, ...required: string[]): boolean</code></h4>
|
||||
<p><code>true</code> if the subject holds <strong>all</strong> of the given roles.</p>
|
||||
<h4 id="pbac-abac-combinators">PBAC / ABAC combinators</h4>
|
||||
<ul>
|
||||
<li><code>any<S, R>(...policies: Policy<S, R>[]): Policy<S, R></code> — allow if <strong>any</strong> policy passes (OR); awaits async policies.</li>
|
||||
<li><code>all<S, R>(...policies: Policy<S, R>[]): Policy<S, R></code> — allow only if <strong>all</strong> policies pass (AND); awaits async policies.</li>
|
||||
<li><code>attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S></code> — ABAC helper that allows when <code>subject[name]</code> equals <code>match</code>, or when <code>match</code> is a function, when <code>match(value)</code> is truthy.</li>
|
||||
</ul>
|
||||
<h4 id="guards-middleware">Guards (middleware)</h4>
|
||||
<p>Each guard returns a <code>@wrnexus/core</code> <code>Middleware</code>. A denied request short-circuits with <code>Response.json({ ok: false, error: "Forbidden" }, { status: 403 })</code>.</p>
|
||||
<ul>
|
||||
<li><code>authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware</code> — runs <code>policy</code> against the request <code>Context</code>; calls <code>next()</code> when it resolves truthy, otherwise returns 403.</li>
|
||||
<li><code>requireRole(...roles: string[]): Middleware</code> — allows when <code>ctx.user</code> holds <strong>any</strong> of the listed roles.</li>
|
||||
<li><code>requirePermission(rbac: Rbac, permission: string): Middleware</code> — allows when <code>rbac.can(ctx.user, permission)</code> is <code>true</code>.</li>
|
||||
</ul>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<h4 id="rbac-2">RBAC</h4>
|
||||
<pre data-language="ts"><code>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</code></pre>
|
||||
<h4 id="guarding-routes">Guarding routes</h4>
|
||||
<pre data-language="ts"><code>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,
|
||||
);</code></pre>
|
||||
<h4 id="pbac-abac-policies">PBAC / ABAC policies</h4>
|
||||
<pre data-language="ts"><code>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<User, Post> = (u, post) => u.id === post?.authorId;
|
||||
|
||||
// ABAC: attribute equality, or a predicate
|
||||
const inEngineering = attr<User>("department", "engineering");
|
||||
const isVerified = attr<User>("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,
|
||||
);</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only</strong> — like the rest of WRNexusJS, this package targets the Bun runtime; Node is not supported.</li>
|
||||
<li>Works with [<code>@wrnexus/core</code>](../core) — the guards return <code>Middleware</code> and read the subject from <code>ctx.user</code> on the request <code>Context</code>. Both types are imported from <code>@wrnexus/core</code>.</li>
|
||||
<li>Policy combinators (<code>any</code>, <code>all</code>) and <code>authorize</code> are async-aware, so policies may return a <code>Promise<boolean></code> (e.g. for a database ownership check).</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { Context, Middleware } from '@wrnexus/core';
|
||||
|
||||
/**
|
||||
* @wrnexus/authz — authorization: role-based (RBAC), policy-based (PBAC), and
|
||||
* attribute-based (ABAC). Compose freely; all three reduce to a boolean check
|
||||
* plus an `authorize()` guard middleware.
|
||||
*
|
||||
* const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });
|
||||
* rbac.can(user, "post:write");
|
||||
*
|
||||
* // PBAC/ABAC: a policy is a predicate over subject + resource + attributes
|
||||
* const ownsPost: Policy<User, Post> = (u, post) => u.id === post.authorId;
|
||||
* authorize((ctx) => ownsPost(ctx.user, resource)) // middleware
|
||||
*/
|
||||
|
||||
interface Subject {
|
||||
id?: string;
|
||||
roles?: string[];
|
||||
[attribute: string]: unknown;
|
||||
}
|
||||
interface Rbac {
|
||||
/** True if any of the subject's roles grants `permission` (supports "*" and "ns:*"). */
|
||||
can(subject: Subject | undefined, permission: string): boolean;
|
||||
/** All permissions granted to a set of roles. */
|
||||
permissionsFor(roles: string[]): Set<string>;
|
||||
}
|
||||
/** Build an RBAC checker from a role → permissions map. */
|
||||
declare function defineRbac(roles: Record<string, string[]>): Rbac;
|
||||
/** True if the subject has ALL of the given roles. */
|
||||
declare function hasRole(subject: Subject | undefined, ...required: string[]): boolean;
|
||||
/** A policy predicate: subject (+ optional resource/attributes) → allowed. */
|
||||
type Policy<S = Subject, R = unknown> = (subject: S, resource?: R) => boolean | Promise<boolean>;
|
||||
/** Combine policies: allow if ANY passes (OR). */
|
||||
declare function any<S, R>(...policies: Policy<S, R>[]): Policy<S, R>;
|
||||
/** Combine policies: allow only if ALL pass (AND). */
|
||||
declare function all<S, R>(...policies: Policy<S, R>[]): Policy<S, R>;
|
||||
/** ABAC helper: allow when an attribute matches (equality or predicate). */
|
||||
declare function attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S>;
|
||||
/** Guard a route with a policy over `ctx` (reads `ctx.user` as the subject). */
|
||||
declare function authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware;
|
||||
/** Guard requiring one of the given roles. */
|
||||
declare function requireRole(...roles: string[]): Middleware;
|
||||
/** Guard requiring an RBAC permission. */
|
||||
declare function requirePermission(rbac: Rbac, permission: string): Middleware;
|
||||
|
||||
export { type Policy, type Rbac, type Subject, all, any, attr, authorize, defineRbac, hasRole, requirePermission, requireRole };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/authz</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>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</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>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,
|
||||
);</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>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<User, Post> = (u, post) => u.id === post?.authorId;
|
||||
|
||||
// ABAC: attribute equality, or a predicate
|
||||
const inEngineering = attr<User>("department", "engineering");
|
||||
const isVerified = attr<User>("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,
|
||||
);</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#types">Types</a><a class="toc-level-4" href="#rbac">RBAC</a><a class="toc-level-4" href="#definerbac-roles-record-string-string-rbac">defineRbac(roles: Record<string, string[]>): Rbac</a><a class="toc-level-4" href="#hasrole-subject-subject-undefined-required-string-boolean">hasRole(subject: Subject | undefined, ...required: string[]): boolean</a><a class="toc-level-4" href="#pbac-abac-combinators">PBAC / ABAC combinators</a><a class="toc-level-4" href="#guards-middleware">Guards (middleware)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#rbac-2">RBAC</a><a class="toc-level-4" href="#guarding-routes">Guarding routes</a><a class="toc-level-4" href="#pbac-abac-policies">PBAC / ABAC policies</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
page wrnexuscli {
|
||||
seo {
|
||||
title = "@wrnexus/cli"
|
||||
description = "Create, develop, build, generate, test, and maintain WRNexusJS apps."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Tooling</span><h1>@wrnexus/cli</h1><p>Create, develop, build, generate, test, and maintain WRNexusJS apps.</p><code>bun add @wrnexus/cli@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Tooling</span><h1>@wrnexus/cli</h1><p>Create, develop, build, generate, test, and maintain WRNexusJS apps.</p><pre><code>bun add @wrnexus/cli@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>The <code>wrnexus</code> command-line tool that scaffolds, runs, builds, tests, and manages WRNexusJS apps.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/cli</code> provides the <code>wrnexus</code> executable — the single entry point for developing a WRNexusJS 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.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/cli</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<p>Once installed, invoke it from an app directory:</p>
|
||||
<pre data-language="bash"><code>bunx wrnexus dev
|
||||
# or add scripts: "dev": "wrnexus dev .", "build": "wrnexus build ."</code></pre>
|
||||
<h3 id="commands">Commands</h3>
|
||||
<p>Every command accepts an optional <code>[app-dir]</code> (defaults to <code>.</code>). Commands that read config or <code>.env</code> also accept <code>--profile=<name></code> (see [Profiles](#profiles)).</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Command</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>wrnexus dev [app-dir] [--port=3000]</code></td><td>Start the development server with live reload / HMR.</td></tr><tr><td><code>wrnexus build [app-dir]</code></td><td>Build a self-contained production server bundle + assets into <code>dist/</code>.</td></tr><tr><td><code>wrnexus create <app-name></code></td><td>Scaffold a new single app from an inline template.</td></tr><tr><td><code>wrnexus workspace <name></code></td><td>Scaffold a monorepo (<code>apps/*</code> + shared <code>packages/*</code>).</td></tr><tr><td><code>wrnexus gateway [--port=3000]</code></td><td>Serve every workspace app behind one port, routed by domain.</td></tr><tr><td><code>wrnexus generate <type> <name></code></td><td>Scaffold a <code>page</code> \</td><td><code>component</code> \</td><td><code>api</code> \</td><td><code>schema</code>.</td></tr><tr><td><code>wrnexus generate routes</code></td><td>Regenerate the typed routes file (<code>app/routes.gen.ts</code>).</td></tr><tr><td><code>wrnexus generate docker</code></td><td>Scaffold <code>Dockerfile</code>, <code>.dockerignore</code>, and <code>docker-compose.yml</code>.</td></tr><tr><td><code>wrnexus generate mobile</code></td><td>Scaffold a Capacitor shell for iOS and Android.</td></tr><tr><td><code>wrnexus mobile add <package...></code></td><td>Install Capacitor plugins and sync native projects.</td></tr><tr><td><code>wrnexus eject <name...></code></td><td>Copy Wire UI component <code>.wrn</code> sources into <code>app/components/</code>.</td></tr><tr><td><code>wrnexus db <cmd></code></td><td>Database migrations and tooling (see [db](#wrnexus-db)).</td></tr><tr><td><code>wrnexus test [app-dir] [--watch]</code></td><td>Run the app's tests via <code>bun test</code> (defaults to the <code>test</code> profile).</td></tr><tr><td><code>wrnexus profiles [app-dir]</code></td><td>List config profiles and their <code>.env</code> files, marking the active one.</td></tr><tr><td><code>wrnexus help</code></td><td>Print usage.</td></tr></tbody></table></div>
|
||||
<p><code>wrnexus g</code> is an alias for <code>wrnexus generate</code>.</p>
|
||||
<h4 id="wrnexus-dev"><code>wrnexus dev</code></h4>
|
||||
<p>Supervises a child dev-server process (from <code>@wrnexus/dev-server</code>). 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 <code>--port=</code> to change the port (default <code>3000</code>).</p>
|
||||
<pre data-language="bash"><code>wrnexus dev . --port=8080</code></pre>
|
||||
<h4 id="wrnexus-build"><code>wrnexus build</code></h4>
|
||||
<p>Emits into <code><app-dir>/dist/</code>:</p>
|
||||
<ul>
|
||||
<li><code>server.js</code> — a single, minified, self-contained Bun server with a <strong>static</strong> manifest of every page / api / realtime / middleware / component / layout module (no runtime filesystem scan or on-the-fly bundling).</li>
|
||||
<li><code>reactive.js</code>, <code>theme.css</code>, <code>theme.js</code>, <code>ui.css</code>, and (if present) <code>styles.css</code> — hashed, minified browser assets.</li>
|
||||
<li><code>public/</code> — copied verbatim.</li>
|
||||
</ul>
|
||||
<p>Before bundling, it regenerates typed queries for the default and every named database. Run the output with:</p>
|
||||
<pre data-language="bash"><code>bun dist/server.js # PORT env var optional</code></pre>
|
||||
<h4 id="wrnexus-create"><code>wrnexus create</code></h4>
|
||||
<p>Scaffolds a new app from an inline (dependency-free) template — <code>package.json</code>, config, and starter <code>app/</code> files. Run <code>wrnexus dev</code> in the new directory to start.</p>
|
||||
<pre data-language="bash"><code>wrnexus create my-app</code></pre>
|
||||
<h4 id="wrnexus-generate"><code>wrnexus generate</code></h4>
|
||||
<p>Scaffolds a single file from a template, refusing to overwrite an existing file. Types (with aliases): <code>page</code>/<code>p</code>, <code>component</code>/<code>c</code>, <code>api</code>/<code>a</code>, <code>schema</code>/<code>s</code>. Nested names create nested paths.</p>
|
||||
<pre data-language="bash"><code>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</code></pre>
|
||||
<p>The mobile generator creates a separate <code>mobile/</code> package and reads <code>config.mobile.mode</code>. <code>webview</code> creates a Capacitor shell that renders the hosted WRNexusJS application. <code>native</code> creates a WebView-free Expo/React Native app whose screens call the shared backend through <code>mobile/src/wrnexus.ts</code>. Native screens do not render <code>.wrn</code> HTML. In either mode, run <code>bun install</code> in <code>mobile/</code>; iOS device builds require macOS and Xcode.</p>
|
||||
<p>Install official or community Capacitor plugins through the root CLI:</p>
|
||||
<pre data-language="bash"><code>wrnexus mobile add @capacitor/camera @capacitor/haptics
|
||||
wrnexus mobile sync
|
||||
wrnexus mobile assets # generate native icons from config.mobile.icon</code></pre>
|
||||
<p>In native mode, <code>mobile add</code> runs <code>expo install</code> and <code>mobile sync</code> runs Expo prebuild. In WebView mode they retain the Capacitor install/sync behavior. <code>wrnexus mobile compile</code> maps portable <code>app/pages/**/*.wrn</code> pages to Expo Router TSX routes. Native <code>bun run start</code> invokes this compilation automatically.</p>
|
||||
<p>Browser code can access installed plugins through the SSR-safe <code>@wrnexus/mobile</code> bridge. The command adds each plugin to both the WRNexusJS app (JavaScript proxy) and <code>mobile/</code> (native synchronization).</p>
|
||||
<p><code>wrnexus mobile sync</code> also configures Android so only true network failures use the local connection-error screen. HTTP errors such as 404 and 500 keep their WRNexusJS response pages.</p>
|
||||
<h4 id="wrnexus-eject"><code>wrnexus eject</code></h4>
|
||||
<p>Copies a Wire UI component's <code>.wrn</code> source out of <code>@wrnexus/ui</code> into <code>app/components/</code>, 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.</p>
|
||||
<pre data-language="bash"><code>wrnexus eject button card modal</code></pre>
|
||||
<h4 id="wrnexus-db"><code>wrnexus db</code></h4>
|
||||
<p>Database migrations and tooling. Without a flag, commands target the <strong>default</strong> database (<code>db</code> in <code>wrnexus.config.ts</code>, files under <code>app/db/</code>). Pass <code>--db=<name></code> to target a named database (<code>databases.<name></code>, files under <code>app/db/<name>/</code>).</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Subcommand</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>db new <name> [--from-models]</code></td><td>Scaffold a migration; <code>--from-models</code> derives it from the TS models in <code>schema.ts</code>.</td></tr><tr><td><code>db migrate</code></td><td>Apply all pending migrations.</td></tr><tr><td><code>db rollback</code></td><td>Revert the last applied migration.</td></tr><tr><td><code>db status</code></td><td>List applied / pending migrations.</td></tr><tr><td><code>db generate</code></td><td>Regenerate typed queries (<code>queries/*.sql</code> → <code>queries.gen.ts</code>).</td></tr><tr><td><code>db seed</code></td><td>Run the database's <code>seed.ts</code> (default export / <code>seed</code> function).</td></tr><tr><td><code>db studio [table]</code></td><td>Inspect tables — list row counts, or dump the first 50 rows of one table.</td></tr></tbody></table></div>
|
||||
<pre data-language="bash"><code>wrnexus db new create_users --from-models
|
||||
wrnexus db migrate
|
||||
wrnexus db studio users
|
||||
wrnexus db status --db=analytics</code></pre>
|
||||
<h4 id="wrnexus-workspace-and-wrnexus-gateway"><code>wrnexus workspace</code> and <code>wrnexus gateway</code></h4>
|
||||
<p><code>workspace <name></code> scaffolds a monorepo: several WRNexusJS apps under <code>apps/*</code> and shared libraries under <code>packages/*</code>, plus a <code>wrnexus.workspace.ts</code> that maps each app to the domains it serves. <code>gateway</code> runs every app behind one port and routes by <code>Host</code> header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log).</p>
|
||||
<pre data-language="bash"><code>wrnexus workspace acme
|
||||
wrnexus gateway --port=3000</code></pre>
|
||||
<h4 id="wrnexus-test"><code>wrnexus test</code></h4>
|
||||
<p>Runs the app's tests with <code>bun test</code>. Defaults to the <code>test</code> profile (config + <code>.env.test</code>). Pass <code>--watch</code> to re-run on change; extra flags pass straight through to <code>bun test</code>.</p>
|
||||
<pre data-language="bash"><code>wrnexus test . --watch</code></pre>
|
||||
<h3 id="profiles">Profiles</h3>
|
||||
<p>Pass <code>--profile=<name></code> to <code>dev</code>, <code>build</code>, <code>db</code> (or set <code>WRNEXUS_PROFILE</code>) to select a config profile. The CLI publishes <code>WRNEXUS_PROFILE</code> so config loaders and the dev child pick it up, and loads that profile's <code>.env</code> cascade (<code>.env</code>, <code>.env.local</code>, <code>.env.<profile></code>, <code>.env.<profile>.local</code>) into <code>process.env</code>.</p>
|
||||
<pre data-language="bash"><code>wrnexus dev --profile=uat
|
||||
wrnexus profiles # ● development (config, .env.development)
|
||||
# ○ production
|
||||
# ○ uat (config, .env.uat)</code></pre>
|
||||
<h3 id="subpath-exports">Subpath exports</h3>
|
||||
<p><code>@wrnexus/cli/workspace</code> exposes the workspace configuration types used by <code>wrnexus.workspace.ts</code>:</p>
|
||||
<pre data-language="ts"><code>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;</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> The CLI runs on Bun, spawns the Bun binary for the dev child and <code>bun test</code>, and the production build uses <code>Bun.build</code>. Node is not supported.</li>
|
||||
<li>Orchestrates the rest of the framework: <code>@wrnexus/dev-server</code> (dev/prod server + gateway), <code>@wrnexus/router</code> (route + typed-routes codegen), <code>@wrnexus/compiler</code> (<code>.wrn</code> → <code>.ts</code>), <code>@wrnexus/db</code> (migrations, typed queries), <code>@wrnexus/styles</code> (config, profiles, <code>.env</code>, themes, styles), <code>@wrnexus/ui</code> (ejectable Wire UI components), <code>@wrnexus/validation</code>, <code>@wrnexus/csr</code>, and <code>@wrnexus/i18n</code>.</li>
|
||||
<li>Reads <code>wrnexus.config.ts</code> for <code>db</code> / <code>databases</code>, <code>theme</code>, <code>styles</code>, <code>seo</code>, <code>security</code>, <code>i18n</code>, and <code>profiles</code>, and <code>wrnexus.workspace.ts</code> for the gateway.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>#!/usr/bin/env bun
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/cli</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="bash"><code>bunx wrnexus dev
|
||||
# or add scripts: "dev": "wrnexus dev .", "build": "wrnexus build ."</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="bash"><code>wrnexus dev . --port=8080</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="bash"><code>bun dist/server.js # PORT env var optional</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#commands">Commands</a><a class="toc-level-4" href="#wrnexus-dev">wrnexus dev</a><a class="toc-level-4" href="#wrnexus-build">wrnexus build</a><a class="toc-level-4" href="#wrnexus-create">wrnexus create</a><a class="toc-level-4" href="#wrnexus-generate">wrnexus generate</a><a class="toc-level-4" href="#wrnexus-eject">wrnexus eject</a><a class="toc-level-4" href="#wrnexus-db">wrnexus db</a><a class="toc-level-4" href="#wrnexus-workspace-and-wrnexus-gateway">wrnexus workspace and wrnexus gateway</a><a class="toc-level-4" href="#wrnexus-test">wrnexus test</a><a class="toc-level-3" href="#profiles">Profiles</a><a class="toc-level-3" href="#subpath-exports">Subpath exports</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
page wrnexuscompiler {
|
||||
seo {
|
||||
title = "@wrnexus/compiler"
|
||||
description = "Parser and code generators for the .wrn language."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Core</span><h1>@wrnexus/compiler</h1><p>Parser and code generators for the .wrn language.</p><code>bun add @wrnexus/compiler@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Core</span><h1>@wrnexus/compiler</h1><p>Parser and code generators for the .wrn language.</p><pre><code>bun add @wrnexus/compiler@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Compiler for the <code>.wrn</code> language — tokenizes, parses, and lowers <code>.wrn</code> page and component files to TypeScript.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/compiler</code> turns <code>.wrn</code> source into TypeScript that targets the framework's runtime primitives. A <code>.wrn</code> file declares either a <code>page</code> (a route) or a <code>component</code> (a reusable, prop-driven fragment) with blocks for <code>state</code>, <code>view</code> (plain HTML), <code>seo</code>, <code>style</code>, <code>functions</code>, <code>api</code>, <code>ssr</code>/<code>client</code> data bindings, and <code>realtime</code> websocket handlers. The pipeline is <code>source → Lexer → parse() → PageAst → generate() → TypeScript</code>. It is a build/server-side library — the WRNexusJS dev loader calls it to compile <code>.wrn</code> files on the fly, surfacing <code>ParseError</code> as a readable error page.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/compiler</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>All exports come from the package root (<code>@wrnexus/compiler</code>).</p>
|
||||
<h4 id="compilewirefile-source-string-string"><code>compileWireFile(source: string): string</code></h4>
|
||||
<p>Compile <code>.wrn</code> source to a TypeScript module string. Throws <code>ParseError</code> on invalid input. The output is prefixed with a <code>// compiled from .wrn</code> comment.</p>
|
||||
<h4 id="compile-source-string-compileresult"><code>compile(source: string): CompileResult</code></h4>
|
||||
<p>Richer entry point that returns the generated code, the AST, and any diagnostics.</p>
|
||||
<pre data-language="ts"><code>interface CompileResult {
|
||||
code: string;
|
||||
ast: PageAst;
|
||||
diagnostics: string[];
|
||||
}</code></pre>
|
||||
<p>On a <code>ParseError</code> it pushes the message into <code>diagnostics</code> and re-throws.</p>
|
||||
<h4 id="parse-source-string-pageast"><code>parse(source: string): PageAst</code></h4>
|
||||
<p>Run the lexer + recursive-descent parser and return the AST. Throws <code>ParseError</code> (lexer <code>LexError</code>s are caught and rethrown as <code>ParseError</code>).</p>
|
||||
<h4 id="generate-ast-pageast-string"><code>generate(ast: PageAst): string</code></h4>
|
||||
<p>Lower a <code>PageAst</code> to TypeScript. <code>page</code> ASTs become a default-export page component (plus <code>meta</code>, optional <code>layout</code>, <code>__wrnexusApi</code>/method handlers, <code>websocket</code>, and SSR/CSR data bindings); <code>component</code> ASTs become a module exporting <code>render(props)</code> and <code>__wrnexusComponent</code>.</p>
|
||||
<h4 id="lexer"><code>Lexer</code></h4>
|
||||
<p>On-demand lexer for <code>.wrn</code>. Yields structural tokens and exposes raw-span readers for the parser.</p>
|
||||
<pre data-language="ts"><code>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
|
||||
}</code></pre>
|
||||
<p><code>Token</code> is <code>{ type: TokenType; value: string; pos: number }</code>, where <code>TokenType</code> is one of <code>ident</code>, <code>string</code>, <code>lbrace</code>, <code>rbrace</code>, <code>lparen</code>, <code>rparen</code>, <code>at</code>, <code>eq</code>, <code>comma</code>, <code>eof</code>.</p>
|
||||
<h4 id="errors">Errors</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Class</th><th>Thrown by</th><th>Meaning</th></tr></thead>
|
||||
<tbody><tr><td><code>ParseError</code></td><td><code>parse</code>, <code>compile</code>, <code>compileWireFile</code>, <code>generate</code></td><td>Invalid <code>.wrn</code> grammar or (rewrapped) lex failure.</td></tr><tr><td><code>LexError</code></td><td><code>Lexer</code></td><td>Unexpected character / unterminated string / unbalanced braces.</td></tr></tbody></table></div>
|
||||
<h4 id="ast-types">AST types</h4>
|
||||
<p>Exported type-only symbols describing the parsed tree:</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>PageAst</code></td><td>Root node: <code>kind</code> (`"page" \</td><td>"component"<code>), </code>name<code>, optional </code>layout<code>, </code>props<code>, </code>states<code>, </code>seo<code>, </code>view<code>, </code>styles<code>, </code>functions<code>, </code>dataApis<code>, </code>modeFunctions<code>, </code>apis<code>, </code>realtimes`.</td></tr><tr><td><code>ViewNode</code></td><td><code>{ type: "text"; value }</code> or <code>{ type: "element"; tag; attrs; children }</code>.</td></tr><tr><td><code>Attr</code></td><td><code>{ name; value; event; boolean? }</code> — <code>event</code> marks <code>@event</code> bindings.</td></tr><tr><td><code>StateDecl</code></td><td><code>{ name; expr }</code> — a <code>state x = <expr></code> declaration.</td></tr><tr><td><code>SeoBlock</code></td><td><code>Record<string, string></code> from the <code>seo { ... }</code> block.</td></tr><tr><td><code>ApiBlock</code></td><td><code>{ method; path; body }</code> — a top-level <code>api METHOD /path { ... }</code>.</td></tr><tr><td><code>DataApiBlock</code></td><td><code>{ mode; name; method; path; body }</code> — an <code>api</code> inside an <code>ssr</code>/<code>client</code> block.</td></tr><tr><td><code>DataMode</code></td><td>`"ssr" \</td><td>"client"`.</td></tr><tr><td><code>ModeFunctionsBlock</code></td><td><code>{ mode; body }</code> — a <code>functions { ... }</code> inside an <code>ssr</code>/<code>client</code> block.</td></tr><tr><td><code>RealtimeBlock</code></td><td><code>{ name; handlers }</code> — a <code>realtime <name> { on evt(args) { ... } }</code> block.</td></tr></tbody></table></div>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>Compile a page:</p>
|
||||
<pre data-language="ts"><code>import { compileWireFile } from "@wrnexus/compiler";
|
||||
|
||||
const ts = compileWireFile(`
|
||||
page Home {
|
||||
state count = 0
|
||||
seo { title = "Home" description = "Welcome" }
|
||||
view {
|
||||
<button @click="count++">Clicked {count} times</button>
|
||||
}
|
||||
}
|
||||
`);
|
||||
// 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.</code></pre>
|
||||
<p>Inspect the AST and diagnostics:</p>
|
||||
<pre data-language="ts"><code>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);
|
||||
}</code></pre>
|
||||
<p>Drive the parse/codegen stages directly:</p>
|
||||
<pre data-language="ts"><code>import { parse, generate } from "@wrnexus/compiler";
|
||||
|
||||
const ast = parse(componentSource); // ast.kind === "component"
|
||||
const module = generate(ast); // exports render(props) + __wrnexusComponent</code></pre>
|
||||
<p>Use the lexer standalone:</p>
|
||||
<pre data-language="ts"><code>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 }</code></pre>
|
||||
<h3 id="the-wrn-language-as-parsed">The <code>.wrn</code> language (as parsed)</h3>
|
||||
<p>A file opens with <code>page <Name></code> or <code>component <Name></code> followed by a <code>{ ... }</code> body containing zero or more members:</p>
|
||||
<ul>
|
||||
<li><code>layout = "<name>"</code> — selects <code>app/layouts/<name>.wrn</code> (pages only).</li>
|
||||
<li><code>props { name = <default> ... }</code> — component props; each default's type drives coercion.</li>
|
||||
<li><code>state <ident> = <expr></code> — reactive state seeded from a raw JS expression.</li>
|
||||
<li><code>view { <html> }</code> — plain HTML with <code>{expr}</code> interpolation, hyphenated attributes, boolean attributes, <code>@event="..."</code> client bindings, and <code><!-- comments --></code>.</li>
|
||||
<li><code>seo { key = "value" ... }</code> — metadata merged into the generated <code>meta</code>.</li>
|
||||
<li><code>style { <raw css> }</code> — inlined page/component stylesheet (repeatable).</li>
|
||||
<li><code>functions { <raw js> }</code> — shared server-side helpers (repeatable).</li>
|
||||
<li><code>api <METHOD> <path> { <raw js> }</code> — route handler, lowered to a <code>METHOD</code> export (repeatable).</li>
|
||||
<li><code>ssr { ... }</code> / <code>client { ... }</code> — data blocks holding <code>api <name> <METHOD> <path> { ... }</code> bindings and their own <code>functions { ... }</code>.</li>
|
||||
<li><code>realtime <name> { on <evt>(<args>) { <raw js> } ... }</code> — websocket handlers, lowered to a <code>websocket</code> export.</li>
|
||||
</ul>
|
||||
<p><code>view</code> markup is parsed by a lenient dedicated HTML parser (<code>parseHtmlView</code>); HTML void elements (<code><br></code>, <code><img></code>, …) take no closing tag. Line comments (<code>//</code>) are skipped by the lexer.</p>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li>Pure TypeScript with no runtime dependencies; runs under <strong>Bun</strong> as part of the WRNexusJS toolchain (Node is not supported).</li>
|
||||
<li>Generated modules target WRNexusJS runtime primitives (<code>data-scope</code>, <code>data-text</code>, <code>data-on-*</code>, <code>data-for</code>, <code>data-component</code>, <code>__wrnexus*</code>/<code>__wire*</code> helpers) — consume the output within a WRNexusJS app, e.g. via <code>@wrnexus/core</code>'s dev loader.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* Recursive-descent parser for `.wrn`, producing a small AST.
|
||||
*
|
||||
* Grammar (subset of the vision, but real):
|
||||
*
|
||||
* page <Name> {
|
||||
* state <ident> = <expr> // zero or more
|
||||
* view { <html> } // plain HTML (see parseHtmlView)
|
||||
* seo { title = "Home" description = "..." }
|
||||
* ssr { api <name> <METHOD> <path> { <render js> } functions { <raw js> } }
|
||||
* client { api <name> <METHOD> <path> { <render js> } functions { <raw js> } }
|
||||
* style { <raw css> } // zero or more, inlined with the page
|
||||
* functions { <raw js> } // zero or more, shared helpers
|
||||
* api <METHOD> <path> { <raw js> } // zero or more
|
||||
* realtime <name> { on <evt>(<args>) { <raw js> } * } // zero or more
|
||||
* }
|
||||
*
|
||||
* The `view` block is written as ordinary HTML — nothing new to learn. Text may
|
||||
* contain `{expr}` interpolation, attributes may be hyphenated (`data-*`), and
|
||||
* `@event="..."` declares a client event binding. See `parseHtmlView`.
|
||||
*/
|
||||
interface StateDecl {
|
||||
name: string;
|
||||
/** Raw JS initializer expression, e.g. `0` or `'x'`. */
|
||||
expr: string;
|
||||
}
|
||||
interface Attr {
|
||||
name: string;
|
||||
value: string;
|
||||
/** True for `@event` bindings (vs. plain HTML attributes). */
|
||||
event: boolean;
|
||||
/** True for a valueless boolean attribute, e.g. `<button disabled>`. */
|
||||
boolean?: boolean;
|
||||
}
|
||||
type ViewNode = {
|
||||
type: "text";
|
||||
value: string;
|
||||
} | {
|
||||
type: "element";
|
||||
tag: string;
|
||||
attrs: Attr[];
|
||||
children: ViewNode[];
|
||||
}
|
||||
/**
|
||||
* A server-side loop: `{#each <list> as <item>[, <index>]} …body… {:empty} …empty… {/each}`.
|
||||
* `list` is a JS expression (evaluated on the server, may reference an `ssr` data
|
||||
* binding). The `body` is rendered once per item with `{item.field}` interpolation;
|
||||
* `empty` renders when the list is empty. See codegen `compileEach`.
|
||||
*/
|
||||
| {
|
||||
type: "each";
|
||||
list: string;
|
||||
item: string;
|
||||
index?: string;
|
||||
body: ViewNode[];
|
||||
empty: ViewNode[];
|
||||
}
|
||||
/**
|
||||
* A server-side conditional: `{#if <expr>} … {:else if <expr>} … {:else} … {/if}`.
|
||||
* Rendered branches are chosen on the server. Each branch's `cond` is a JS expression
|
||||
* (`null` for the final `{:else}`); the first truthy branch renders. See `compileIfExpr`.
|
||||
*/
|
||||
| {
|
||||
type: "if";
|
||||
branches: {
|
||||
cond: string | null;
|
||||
body: ViewNode[];
|
||||
}[];
|
||||
};
|
||||
interface ApiBlock {
|
||||
method: string;
|
||||
path: string;
|
||||
body: string;
|
||||
}
|
||||
type SeoBlock = Record<string, string>;
|
||||
type DataMode = "ssr" | "client";
|
||||
interface DataApiBlock {
|
||||
mode: DataMode;
|
||||
name: string;
|
||||
method: string;
|
||||
path: string;
|
||||
body: string;
|
||||
}
|
||||
interface ModeFunctionsBlock {
|
||||
mode: DataMode;
|
||||
body: string;
|
||||
}
|
||||
interface RealtimeHandler {
|
||||
event: string;
|
||||
args: string[];
|
||||
body: string;
|
||||
}
|
||||
interface RealtimeBlock {
|
||||
name: string;
|
||||
handlers: RealtimeHandler[];
|
||||
}
|
||||
interface PropDecl {
|
||||
name: string;
|
||||
/** Raw JS default expression, e.g. `0` or `'Count'`. Its type drives coercion. */
|
||||
default: string;
|
||||
}
|
||||
interface PageAst {
|
||||
type: "page";
|
||||
/** `page` (a route) or `component` (a reusable, prop-driven fragment). */
|
||||
kind: "page" | "component";
|
||||
name: string;
|
||||
/** Name of the page layout (`app/layouts/<layout>.wrn`), if the page sets one. */
|
||||
layout?: string;
|
||||
/** Declared component props (empty for pages). */
|
||||
props: PropDecl[];
|
||||
states: StateDecl[];
|
||||
seo: SeoBlock;
|
||||
view: ViewNode[];
|
||||
styles: string[];
|
||||
functions: string[];
|
||||
dataApis: DataApiBlock[];
|
||||
modeFunctions: ModeFunctionsBlock[];
|
||||
apis: ApiBlock[];
|
||||
realtimes: RealtimeBlock[];
|
||||
}
|
||||
declare class ParseError extends Error {
|
||||
}
|
||||
declare function parse(source: string): PageAst;
|
||||
|
||||
/**
|
||||
* Code generation: lower a `.wrn` AST to TypeScript that targets the framework's
|
||||
* existing primitives.
|
||||
*
|
||||
* state -> a `data-scope` declaration consumed by the runtime
|
||||
* view -> an HTML string returned by a page component
|
||||
* @event="..." -> data-on-<event>="..."
|
||||
* "...{expr}..." -> text kept verbatim ({expr} is mustache for runtime)
|
||||
* api="<name>" -> SSR/client data binding declared in a mode block
|
||||
* ssrGet/ssrText -> legacy server-side API fetch + render
|
||||
* csrGet/csrText -> legacy browser-side API fetch + render
|
||||
* style -> an inline page stylesheet
|
||||
* functions -> server-only helpers for API/realtime code
|
||||
* api M /p {b} -> export const M = async (ctx) => { b }
|
||||
* realtime {..} -> export const websocket = { evt(ws, ...args) { b } }
|
||||
*/
|
||||
|
||||
declare function generate(ast: PageAst): string;
|
||||
|
||||
declare class NativeCompileError extends Error {
|
||||
constructor(message: string);
|
||||
}
|
||||
/** Compile a parsed `.wrn` page to an Expo Router React Native screen. */
|
||||
declare function generateNative(ast: PageAst): string;
|
||||
|
||||
/**
|
||||
* Lexer for the `.wrn` language.
|
||||
*
|
||||
* `.wrn` mixes a small structural grammar (page/state/view/api/realtime) with
|
||||
* raw JavaScript bodies. A pure token stream can't represent the raw JS, so the
|
||||
* lexer is driven on demand by the parser: it yields structural tokens via
|
||||
* `next()`/`peek()`, and exposes `readBalancedBraces()`, `readPath()` and
|
||||
* `readToLineEnd()` for the parser to grab raw spans when grammar demands it.
|
||||
*/
|
||||
type TokenType = "ident" | "string" | "lbrace" | "rbrace" | "lparen" | "rparen" | "at" | "eq" | "comma" | "eof";
|
||||
interface Token {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
pos: number;
|
||||
}
|
||||
declare class LexError extends Error {
|
||||
}
|
||||
declare class Lexer {
|
||||
readonly src: string;
|
||||
pos: number;
|
||||
constructor(src: string);
|
||||
/** Skip whitespace and `// line comments`. */
|
||||
private skipTrivia;
|
||||
/** Read and consume the next structural token. */
|
||||
next(): Token;
|
||||
/** Look at the next token without consuming it. */
|
||||
peek(): Token;
|
||||
private readString;
|
||||
/** Read a route path like `/users/[id]` up to whitespace or `{`. */
|
||||
readPath(): string;
|
||||
/** Read the rest of the current line (used for `state x = <expr>`). */
|
||||
readToLineEnd(): string;
|
||||
/**
|
||||
* Read a `{ ... }` block and return its INNER text (no outer braces), with
|
||||
* brace counting that respects string and template literals so a `}` inside a
|
||||
* string doesn't end the block early.
|
||||
*/
|
||||
readBalancedBraces(): string;
|
||||
private lineAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @wrnexus/compiler — the `.wrn` language compiler.
|
||||
*
|
||||
* Pipeline: source ──▶ Lexer ──▶ parse() ──▶ AST ──▶ generate() ──▶ TypeScript
|
||||
*
|
||||
* See VISION.md for the language design. The MVP supports `page` with `state`,
|
||||
* `view`, `api`, and `realtime` blocks, lowering to the framework's primitives.
|
||||
*/
|
||||
|
||||
interface CompileResult {
|
||||
code: string;
|
||||
ast: PageAst;
|
||||
diagnostics: string[];
|
||||
}
|
||||
/** Compile `.wrn` source into an Expo Router React Native screen. */
|
||||
declare function compileNativeWireFile(source: string): string;
|
||||
/**
|
||||
* Compile `.wrn` source into TypeScript source. Throws `ParseError` on invalid
|
||||
* input (the dev loader surfaces this as a readable error page).
|
||||
*/
|
||||
declare function compileWireFile(source: string): string;
|
||||
/** Richer entry point returning the AST and diagnostics alongside the code. */
|
||||
declare function compile(source: string): CompileResult;
|
||||
|
||||
export { type ApiBlock, type Attr, type CompileResult, type DataApiBlock, type DataMode, LexError, Lexer, type ModeFunctionsBlock, NativeCompileError, type PageAst, ParseError, type RealtimeBlock, type SeoBlock, type StateDecl, type ViewNode, compile, compileNativeWireFile, compileWireFile, generate, generateNative, parse };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/compiler</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>interface CompileResult {
|
||||
code: string;
|
||||
ast: PageAst;
|
||||
diagnostics: string[];
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>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
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import { compileWireFile } from "@wrnexus/compiler";
|
||||
|
||||
const ts = compileWireFile(`
|
||||
page Home {
|
||||
state count = 0
|
||||
seo { title = "Home" description = "Welcome" }
|
||||
view {
|
||||
<button @click="count++">Clicked {count} times</button>
|
||||
}
|
||||
}
|
||||
`);
|
||||
// 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.</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#compilewirefile-source-string-string">compileWireFile(source: string): string</a><a class="toc-level-4" href="#compile-source-string-compileresult">compile(source: string): CompileResult</a><a class="toc-level-4" href="#parse-source-string-pageast">parse(source: string): PageAst</a><a class="toc-level-4" href="#generate-ast-pageast-string">generate(ast: PageAst): string</a><a class="toc-level-4" href="#lexer">Lexer</a><a class="toc-level-4" href="#errors">Errors</a><a class="toc-level-4" href="#ast-types">AST types</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#the-wrn-language-as-parsed">The .wrn language (as parsed)</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,966 @@
|
||||
page wrnexuscore {
|
||||
seo {
|
||||
title = "@wrnexus/core"
|
||||
description = "Contexts, middleware, security, sessions, caching, JSX, and realtime."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Core</span><h1>@wrnexus/core</h1><p>Contexts, middleware, security, sessions, caching, JSX, and realtime.</p><code>bun add @wrnexus/core@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Core</span><h1>@wrnexus/core</h1><p>Contexts, middleware, security, sessions, caching, JSX, and realtime.</p><pre><code>bun add @wrnexus/core@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>The framework core: the request <code>Context</code>, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WRNexusJS package builds on.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/core</code> is the shared foundation of WRNexusJS. It defines the <code>Context</code> object that flows through every middleware, page, and API route, plus the <code>Middleware</code>/<code>Next</code> 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 <strong>server-side</strong> and Bun-native (it uses <code>Bun.password</code>, <code>Bun.write</code>, the web-standard <code>Request</code>/<code>Response</code>, and <code>crypto</code>). You depend on it directly and transitively through the rest of the framework.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/core</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="context-middleware-wrnexus-core">Context & middleware — <code>@wrnexus/core</code></h4>
|
||||
<p>The <code>Context</code> (<code>ctx</code>) is the single value passed to middleware and handlers.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Kind</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>Context</code></td><td>type</td><td>Per-request object: <code>req</code>, <code>url</code>, <code>lang</code>, <code>t</code>, <code>params</code>, <code>locals</code>, <code>user?</code>, <code>ip?</code>, <code>cookies</code>, <code>session</code>, <code>localStorage</code>.</td></tr><tr><td><code>Next</code></td><td>type</td><td>`() => Promise<Response> \</td><td>Response` — invokes the next middleware/handler.</td></tr><tr><td><code>Middleware</code></td><td>type</td><td>`(ctx, next) => Promise<Response> \</td><td>Response<code>. Return </code>next()<code> to continue, or a </code>Response` to short-circuit.</td></tr><tr><td><code>createContext(req, url)</code></td><td>fn</td><td>Build a fresh <code>Context</code> for an incoming request (wires up cookies, session, localStorage snapshot).</td></tr><tr><td><code>withContextHeaders(ctx, res)</code></td><td>fn</td><td>Apply accumulated headers (e.g. <code>Set-Cookie</code>) from the context onto a response.</td></tr><tr><td><code>PageComponent</code></td><td>type</td><td>`(ctx) => string \</td><td>Promise<string>` — a page module's default export.</td></tr><tr><td><code>PageMeta</code> / <code>SeoConfig</code></td><td>type</td><td><code><head></code> metadata: <code>title</code>, <code>description</code>, <code>canonical</code>, <code>robots</code>, <code>image</code>, <code>twitterCard</code>, <code>themeColor</code>, …</td></tr><tr><td><code>TFunction</code></td><td>type</td><td><code>(key, params?) => string</code> — translate a key for <code>ctx.lang</code>, interpolating <code>{param}</code> placeholders.</td></tr></tbody></table></div>
|
||||
<p>Key <code>Context</code> fields:</p>
|
||||
<ul>
|
||||
<li><code>ctx.locals</code> — per-request scratch space for passing values between middleware.</li>
|
||||
<li><code>ctx.user</code> — the authenticated user (populated by <code>sessionAuth</code>/<code>logIn</code>), or <code>null</code>.</li>
|
||||
<li><code>ctx.ip</code> — the direct socket peer IP (not spoofable via headers).</li>
|
||||
<li><code>ctx.cookies</code> / <code>ctx.session</code> / <code>ctx.localStorage</code> — see <strong>Storage</strong> below.</li>
|
||||
</ul>
|
||||
<h4 id="authentication-wrnexus-core">Authentication — <code>@wrnexus/core</code></h4>
|
||||
<p>Passwords are hashed with argon2id via <code>Bun.password</code>; sessions ride the cookie-backed <code>SessionStore</code>.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>hashPassword(password)</code></td><td><code>(string) => Promise<string></code></td><td>argon2id hash to store.</td></tr><tr><td><code>verifyPassword(password, hash)</code></td><td><code>(string, string) => Promise<boolean></code></td><td>Constant-safe; returns <code>false</code> on bad/empty hash.</td></tr><tr><td><code>logIn(ctx, user)</code></td><td><code>(Context, U) => void</code></td><td>Regenerates the session id (fixation defense), stores the user, sets <code>ctx.user</code>.</td></tr><tr><td><code>logOut(ctx)</code></td><td><code>(Context) => void</code></td><td>Clears the session and <code>ctx.user</code>.</td></tr><tr><td><code>getUser(ctx)</code></td><td>`(Context) => U \</td><td>null`</td><td>Current user from <code>ctx.user</code>, falling back to the session.</td></tr><tr><td><code>sessionAuth()</code></td><td><code>() => Middleware</code></td><td>Hydrates <code>ctx.user</code> from the session each request. Register early.</td></tr><tr><td><code>requireAuth(options?)</code></td><td><code>(RequireAuthOptions?) => Middleware</code></td><td>Guard: API/fetch requests get <code>401 JSON</code>, page navigations get <code>302</code> to <code>loginPath</code> (default <code>/login</code>) with <code>?next=</code>.</td></tr><tr><td><code>SESSION_USER_KEY</code></td><td><code>"user"</code></td><td>Session key holding the user.</td></tr></tbody></table></div>
|
||||
<p><code>RequireAuthOptions</code>: <code>{ loginPath?: string }</code>.</p>
|
||||
<h4 id="csrf-wrnexus-core">CSRF — <code>@wrnexus/core</code></h4>
|
||||
<p>Double-submit cookie pattern: a readable <code>wire-csrf</code> cookie is echoed in an <code>x-csrf-token</code> header on unsafe requests.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>csrfToken(ctx)</code></td><td><code>(Context) => string</code></td><td>Ensures the CSRF cookie exists and returns its token.</td></tr><tr><td><code>verifyCsrf(ctx)</code></td><td><code>(Context) => boolean</code></td><td>Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/<code>ctx.locals._csrf</code> must match the cookie (constant-time).</td></tr><tr><td><code>csrfProtection()</code></td><td><code>() => Middleware</code></td><td>403s unsafe requests with a missing/mismatched token.</td></tr><tr><td><code>CSRF_COOKIE</code> / <code>CSRF_HEADER</code></td><td><code>"wire-csrf"</code> / <code>"x-csrf-token"</code></td><td>Cookie & header names.</td></tr></tbody></table></div>
|
||||
<h4 id="rate-limiting-wrnexus-core">Rate limiting — <code>@wrnexus/core</code></h4>
|
||||
<p>Fixed-window limiter that returns <code>429</code> with <code>Retry-After</code> and emits <code>RateLimit-Limit</code>/<code>-Remaining</code>/<code>-Reset</code> headers.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>rateLimit(options?)</code></td><td><code>(RateLimitOptions?) => Middleware</code></td><td>Main middleware.</td></tr><tr><td><code>peerKey(ctx)</code></td><td><code>(Context) => string</code></td><td>Non-spoofable key from <code>ctx.ip</code> (default).</td></tr><tr><td><code>proxyKey(ctx)</code></td><td><code>(Context) => string</code></td><td>Trusts <code>x-forwarded-for</code>/<code>x-real-ip</code>. Use only behind a trusted proxy.</td></tr><tr><td><code>defaultKey</code></td><td>—</td><td><strong>Deprecated</strong> alias of <code>proxyKey</code>.</td></tr></tbody></table></div>
|
||||
<p><code>RateLimitOptions</code>: <code>windowMs</code> (default <code>60_000</code>), <code>max</code> (default <code>60</code>), <code>key</code>, <code>trustProxy</code> (default <code>false</code> → keys on <code>peerKey</code>; <code>true</code> → <code>proxyKey</code>), <code>message</code>, <code>headers</code> (default <code>true</code>), <code>store</code>.</p>
|
||||
<p><code>RateLimitStore</code> is pluggable — implement <code>hit(key, windowMs, now) => Bucket | Promise<Bucket></code> (a <code>Bucket</code> is <code>{ count, resetAt }</code>) to back limits with Redis/SQL across instances. The default store is process-local memory.</p>
|
||||
<h4 id="request-logging-wrnexus-core">Request logging — <code>@wrnexus/core</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>requestLogger(options?)</code></td><td><code>(RequestLoggerOptions?) => Middleware</code></td><td>One record per request with a request id (stored on <code>ctx.locals[requestIdKey]</code>).</td></tr></tbody></table></div>
|
||||
<p><code>RequestLoggerOptions</code>: <code>format</code> (<code>"pretty"</code> default \| <code>"json"</code>), <code>sink(line, record)</code> (default <code>console.log</code>), <code>requestIdKey</code> (default <code>"requestId"</code>), <code>now</code>. <code>RequestRecord</code> = <code>{ time, id, method, path, status, durationMs }</code>.</p>
|
||||
<h4 id="caching-wrnexus-core">Caching — <code>@wrnexus/core</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>TTLCache<V></code></td><td>class</td><td>In-memory TTL cache: <code>get</code>, <code>set</code>, <code>getOrLoad(key, loader, ttlMs?)</code>, <code>delete</code>, <code>clear</code>, <code>size</code>. Constructor takes a default <code>ttlMs</code> (60s).</td></tr><tr><td><code>cacheControl(options)</code></td><td>fn</td><td>Build a <code>Cache-Control</code> value from <code>CacheControlOptions</code>.</td></tr><tr><td><code>withCacheControl(res, options)</code></td><td>fn</td><td>Apply <code>Cache-Control</code> to a response.</td></tr><tr><td><code>etag(body, weak?)</code></td><td>fn</td><td>Stable quoted FNV-1a ETag (weak by default).</td></tr><tr><td><code>notModified(req, tag)</code></td><td>fn</td><td><code>true</code> when <code>If-None-Match</code> matches — send a <code>304</code>.</td></tr></tbody></table></div>
|
||||
<p><code>CacheControlOptions</code>: <code>maxAge</code>, <code>sMaxAge</code>, <code>private</code>, <code>noStore</code>, <code>noCache</code>, <code>staleWhileRevalidate</code>, <code>immutable</code>.</p>
|
||||
<h4 id="file-uploads-wrnexus-core">File uploads — <code>@wrnexus/core</code></h4>
|
||||
<p>Bun parses <code>multipart/form-data</code> via <code>Request.formData()</code>; these helpers validate and persist the resulting <code>File</code>s.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>collectUploads(form)</code></td><td><code>(FormData) => { field, file }[]</code></td><td>Every non-empty <code>File</code> in a parsed form.</td></tr><tr><td><code>saveUpload(file, options)</code></td><td><code>(File, SaveUploadOptions) => Promise<SavedUpload></code></td><td>Validates size/type, sanitizes the name, writes via <code>Bun.write</code>. Throws <code>UploadError</code>.</td></tr><tr><td><code>sanitizeFilename(name)</code></td><td><code>(string) => string</code></td><td>Strips separators, traversal, control/illegal chars; caps at 255.</td></tr><tr><td><code>UploadError</code></td><td>class</td><td>Thrown on rejected uploads.</td></tr></tbody></table></div>
|
||||
<p><code>SaveUploadOptions</code>: <code>dir</code> (required), <code>maxBytes</code>, <code>allowedTypes</code> (MIME types like <code>"image/png"</code> and/or extensions like <code>".png"</code>), <code>filename(file)</code>. <code>SavedUpload</code> = <code>{ path, filename, size, type }</code>.</p>
|
||||
<h4 id="streaming-sse-wrnexus-core">Streaming & SSE — <code>@wrnexus/core</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>streamResponse(source, init?)</code></td><td>`(Iterable\</td><td>AsyncIterable<string\</td><td>Uint8Array>, StreamResponseInit?) => Response`</td><td>Streaming <code>Response</code> from a chunk source (basis for streaming SSR).</td></tr><tr><td><code>sse(source)</code></td><td>`(Iterable\</td><td>AsyncIterable<ServerSentEvent>) => Response`</td><td><code>text/event-stream</code> response.</td></tr></tbody></table></div>
|
||||
<p><code>StreamResponseInit</code>: <code>status</code>, <code>headers</code>, <code>contentType</code> (default <code>"text/html; charset=utf-8"</code>). <code>ServerSentEvent</code>: <code>{ data, event?, id?, retry? }</code>.</p>
|
||||
<h4 id="realtime-rooms-wrnexus-core">Realtime rooms — <code>@wrnexus/core</code></h4>
|
||||
<p>WebSocket rooms. A file in <code>app/realtime/</code> exports <code>default defineRoom({ ... })</code> and is served at <code>ws://host/realtime/<name></code>.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>defineRoom(handlers)</code></td><td><code>(RoomHandlers) => RoomDefinition</code></td><td>Define a room. Export the result as <code>default</code>.</td></tr><tr><td><code>isRoomDefinition(value)</code></td><td><code>(unknown) => boolean</code></td><td>Type guard for a room definition.</td></tr><tr><td><code>createRealtimeRegistry()</code></td><td><code>() => RealtimeRegistry</code></td><td>Server-side connection manager mapping sockets ↔ rooms.</td></tr><tr><td><code>bridgeRealtime(registry, bus, topic?)</code></td><td><code>(RealtimeRegistry, RealtimeBus, string?) => () => void</code></td><td>Bridge broadcasts/<code>toUser</code> sends across processes via a pub/sub bus.</td></tr></tbody></table></div>
|
||||
<p><code>RoomHandlers</code>: <code>authorize(info) => boolean</code> (gate before accept — return <code>false</code> to reject with 403), <code>onConnect(client)</code>, <code>onMessage(client, message)</code> (JSON auto-parsed), <code>onLeave(client)</code>. A handler receives a <code>RoomClient</code> with <code>id</code>, <code>user</code>, <code>query</code>, <code>data</code>, <code>room</code>, and <code>send</code> / <code>broadcast</code> / <code>to(id)</code> / <code>toUser(user)</code> / <code>close</code>. The <code>Room</code> API adds <code>state</code>, <code>clients()</code>, <code>count()</code>, and <code>broadcast</code>. <code>RealtimeBus</code> is structurally satisfied by <code>@wrnexus/pubsub</code>. Legacy <code>RealtimeHandler</code>/<code>RealtimeSocket</code> raw handlers are still exported. Connection-targeted sends (<code>send</code>, <code>to(id)</code>) stay local; room broadcasts and <code>toUser</code> cross the bridge.</p>
|
||||
<h4 id="error-pages-wrnexus-core">Error pages — <code>@wrnexus/core</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>renderError(err, mode)</code></td><td><code>(unknown, Mode) => Response</code></td><td>Dev page (with stack) or generic prod page by <code>mode</code>.</td></tr><tr><td><code>renderDevError(err, status?)</code></td><td><code>(unknown, number?) => Response</code></td><td>Readable HTML error page including the stack trace.</td></tr><tr><td><code>renderProdError(status?)</code></td><td><code>(number?) => Response</code></td><td>Generic page that never leaks file paths.</td></tr><tr><td><code>renderNotFound()</code></td><td><code>() => Response</code></td><td>Simple 404 page.</td></tr></tbody></table></div>
|
||||
<p><code>Mode</code> = <code>"development" | "production"</code>.</p>
|
||||
<h4 id="security-headers-cors-wrnexus-core">Security headers & CORS — <code>@wrnexus/core</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>withSecurityHeaders(req, res, mode, security?, nonce?)</code></td><td>→ <code>Response</code></td><td>Applies CORS + CSP, HSTS, <code>X-Frame-Options</code>, <code>X-Content-Type-Options</code>, <code>Referrer-Policy</code>, <code>Permissions-Policy</code>, COOP, Trusted Types, and <code>extraHeaders</code>.</td></tr><tr><td><code>createCorsPreflightResponse(req, security?)</code></td><td>→ `Response \</td><td>null`</td><td>Builds a <code>204</code>/<code>403</code> preflight response for CORS <code>OPTIONS</code> requests.</td></tr><tr><td><code>isWebSocketOriginAllowed(req, security?)</code></td><td>→ <code>boolean</code></td><td>Guards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients).</td></tr></tbody></table></div>
|
||||
<p>Config types: <code>SecurityConfig</code> (top-level), <code>CorsConfig</code>/<code>CorsOrigin</code>, <code>ContentSecurityPolicyConfig</code>/<code>CspDirectiveValue</code>, <code>HstsConfig</code>, <code>TrustedTypesConfig</code>, <code>PermissionsPolicyConfig</code>. WRNexusJS applies sensible defaults (self-only CSP, <code>frame-ancestors 'none'</code>, restrictive Permissions-Policy, HSTS in production, Trusted Types in production); each is individually overridable or disable-able via <code>false</code>.</p>
|
||||
<h4 id="storage-cookies-sessions-localstorage-wrnexus-core">Storage: cookies, sessions, localStorage — <code>@wrnexus/core</code></h4>
|
||||
<p>These back the <code>ctx.cookies</code>, <code>ctx.session</code>, and <code>ctx.localStorage</code> fields.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>setSessionBackend(backend)</code></td><td>fn</td><td>Swap the <strong>sync</strong> session persistence backend (<code>SessionBackend</code>) — e.g. <code>bun:sqlite</code>. Default is process-local memory. Call once at startup.</td></tr><tr><td><code>loadSession(backend, options?)</code></td><td>fn → <code>Middleware</code></td><td>Back <code>ctx.session</code> with an <strong>async</strong> store (<code>AsyncSessionBackend</code>: <code>load</code>/<code>save</code>/<code>destroy</code>) — loads before the request, saves after. <code>options.ttlMs</code> default 24h.</td></tr><tr><td><code>CookieStore</code></td><td>type</td><td><code>get</code>/<code>getAll</code>/<code>has</code>/<code>set(name, value, opts?)</code>/<code>delete</code>/<code>headers</code>.</td></tr><tr><td><code>SessionStore</code></td><td>type</td><td><code>id</code>/<code>get</code>/<code>getAll</code>/<code>set</code>/<code>delete</code>/<code>regenerate</code>/<code>clear</code>.</td></tr><tr><td><code>LocalStorageSnapshot</code></td><td>type</td><td>Read-only view of the browser's localStorage sent via header for CSR bindings.</td></tr><tr><td><code>CookieOptions</code></td><td>type</td><td><code>path</code>, <code>domain</code>, <code>maxAge</code>, <code>expires</code>, <code>httpOnly</code>, <code>secure</code>, <code>sameSite</code>.</td></tr><tr><td><code>SessionEntry</code> / <code>SessionBackend</code> / <code>AsyncSessionBackend</code></td><td>types</td><td>Session persistence contracts.</td></tr></tbody></table></div>
|
||||
<h4 id="low-level-security-helpers-wrnexus-core">Low-level security helpers — <code>@wrnexus/core</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>escapeHtml(value)</code></td><td><code>(string) => string</code></td><td>Escape for HTML text/attributes.</td></tr><tr><td><code>isSafeIslandName(name)</code></td><td><code>(string) => boolean</code></td><td>Allow only a conservative <code>[A-Za-z0-9_-]+</code> charset.</td></tr><tr><td><code>isSafeRequestPath(pathname)</code></td><td><code>(string) => boolean</code></td><td>Reject NULs, <code>..</code> traversal, and backslashes.</td></tr></tbody></table></div>
|
||||
<h4 id="jsx-runtime-wrnexus-core-wrnexus-core-jsx-runtime-wrnexus-core-jsx-dev-runtime">JSX runtime — <code>@wrnexus/core</code>, <code>@wrnexus/core/jsx-runtime</code>, <code>@wrnexus/core/jsx-dev-runtime</code></h4>
|
||||
<p>A server-side JSX runtime that renders to HTML <strong>strings</strong> (no virtual DOM). Point <code>tsconfig</code>'s <code>jsxImportSource</code> at <code>@wrnexus/core</code>.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>jsx</code> / <code>jsxs</code></td><td>fn</td><td>The runtime factory (TypeScript calls these automatically). Returns an <code>Html</code> instance.</td></tr><tr><td><code>Fragment</code></td><td>symbol</td><td>JSX fragment marker.</td></tr><tr><td><code>Html</code></td><td>class</td><td>Wraps a raw, already-safe HTML string (<code>toString()</code> returns it).</td></tr><tr><td><code>mustache(expr)</code></td><td>fn</td><td>Emit a <code>{{expr}}</code> placeholder (tagged-template or string form) for the client binder.</td></tr><tr><td><code>JSXComponent</code> / <code>JSXProps</code> / <code>Renderable</code></td><td>types</td><td>Component signature and renderable value types.</td></tr></tbody></table></div>
|
||||
<p>Values interpolated as children are HTML-escaped unless they are an <code>Html</code> instance; use <code>dangerouslySetInnerHTML={{ __html }}</code> for trusted markup. Void elements render without a closing tag; <code>className</code>→<code>class</code>, <code>htmlFor</code>→<code>for</code>, and <code>style</code> objects are serialized to CSS text.</p>
|
||||
<p>The subpath exports map to the runtime TypeScript's JSX transform expects:</p>
|
||||
<pre data-language="jsonc"><code>// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@wrnexus/core",
|
||||
},
|
||||
}</code></pre>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<h4 id="a-minimal-middleware-chain">A minimal middleware chain</h4>
|
||||
<pre data-language="ts"><code>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" }),
|
||||
];</code></pre>
|
||||
<h4 id="password-auth">Password auth</h4>
|
||||
<pre data-language="ts"><code>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</code></pre>
|
||||
<h4 id="http-caching-with-etags">HTTP caching with ETags</h4>
|
||||
<pre data-language="ts"><code>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 });</code></pre>
|
||||
<h4 id="streaming-sse">Streaming SSE</h4>
|
||||
<pre data-language="ts"><code>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());</code></pre>
|
||||
<h4 id="a-realtime-room">A realtime room</h4>
|
||||
<pre data-language="ts"><code>// 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 });
|
||||
},
|
||||
});</code></pre>
|
||||
<p>Scale it across processes:</p>
|
||||
<pre data-language="ts"><code>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)));</code></pre>
|
||||
<h4 id="jsx-rendering">JSX rendering</h4>
|
||||
<pre data-language="tsx"><code>import { Html } from "@wrnexus/core";
|
||||
|
||||
function Card({ title, body }: { title: string; body: string }) {
|
||||
return (
|
||||
<article class="card">
|
||||
<h2>{title}</h2>
|
||||
<p>{body}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
const html: Html = <Card title="Hi" body="<b>escaped</b> automatically" />;
|
||||
return new Response(html.toString(), { headers: { "content-type": "text/html" } });</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Uses <code>Bun.password</code> (argon2id), <code>Bun.write</code>, web-standard</li>
|
||||
<p><code>Request</code>/<code>Response</code>/<code>FormData</code>/<code>ReadableStream</code>, and the global <code>crypto</code>. Node is not supported.</p>
|
||||
<li>Session and rate-limit backends default to <strong>process-local memory</strong>. For</li>
|
||||
<p>multi-instance deployments, swap in a shared backend: <code>setSessionBackend</code> (sync, e.g. <code>bun:sqlite</code>) or <code>loadSession</code> (async, e.g. Redis) for sessions, a custom <code>RateLimitStore</code> for limits, and <code>bridgeRealtime</code> for realtime.</p>
|
||||
<li>Works with the rest of the framework: realtime bridging is structurally</li>
|
||||
<p>compatible with [<code>@wrnexus/pubsub</code>](../pubsub); the security, auth, and JSX primitives here are consumed by the WRNexusJS server/router packages.</p>
|
||||
<li>Subpath exports: <code>@wrnexus/core/jsx-runtime</code> and <code>@wrnexus/core/jsx-dev-runtime</code></li>
|
||||
<p>for TypeScript's automatic JSX transform.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>export { Fragment, Html, Component as JSXComponent, Props as JSXProps, Renderable, jsx, jsxs, mustache } from './jsx-runtime.js';
|
||||
|
||||
interface CookieOptions {
|
||||
path?: string;
|
||||
domain?: string;
|
||||
maxAge?: number;
|
||||
expires?: Date | string;
|
||||
httpOnly?: boolean;
|
||||
secure?: boolean;
|
||||
sameSite?: "Strict" | "Lax" | "None" | "strict" | "lax" | "none";
|
||||
}
|
||||
interface CookieStore {
|
||||
get(name: string): string | undefined;
|
||||
getAll(): Record<string, string>;
|
||||
has(name: string): boolean;
|
||||
set(name: string, value: string, options?: CookieOptions): void;
|
||||
delete(name: string, options?: CookieOptions): void;
|
||||
headers(): string[];
|
||||
}
|
||||
interface SessionStore {
|
||||
id(): string;
|
||||
get<T = unknown>(key: string): T | undefined;
|
||||
getAll(): Record<string, unknown>;
|
||||
set(key: string, value: unknown): void;
|
||||
delete(key: string): void;
|
||||
/** Issue a fresh session id, keeping the data — defends against fixation. */
|
||||
regenerate(): void;
|
||||
clear(): void;
|
||||
}
|
||||
interface LocalStorageSnapshot {
|
||||
get(key: string): string | undefined;
|
||||
getAll(): Record<string, string>;
|
||||
has(key: string): boolean;
|
||||
}
|
||||
/** A stored session: its data plus an absolute expiry timestamp (ms). */
|
||||
interface SessionEntry {
|
||||
data: Record<string, unknown>;
|
||||
expiresAt: number;
|
||||
}
|
||||
/**
|
||||
* Pluggable session persistence. The default is process-local memory; swap in a
|
||||
* shared backend (Redis, SQL, etc.) via `setSessionBackend` so sessions survive
|
||||
* restarts and work across multiple instances. Methods are synchronous, so a
|
||||
* backend must be sync (e.g. `bun:sqlite`); async stores need a load/save
|
||||
* wrapper around the request (future work).
|
||||
*/
|
||||
interface SessionBackend {
|
||||
get(id: string): SessionEntry | undefined;
|
||||
set(id: string, entry: SessionEntry): void;
|
||||
delete(id: string): void;
|
||||
/** Optional: drop expired entries. Called periodically by the store. */
|
||||
gc?(now: number): void;
|
||||
}
|
||||
/** Replace the session persistence backend (call once at startup). */
|
||||
declare function setSessionBackend(backend: SessionBackend): void;
|
||||
/**
|
||||
* An ASYNC session store (Redis, a remote DB). Use it via the `loadSession`
|
||||
* middleware, which loads the session before the request and saves it after —
|
||||
* keeping the `ctx.session` API synchronous while persistence is shared across
|
||||
* instances.
|
||||
*/
|
||||
interface AsyncSessionBackend {
|
||||
load(id: string): Promise<SessionEntry | undefined>;
|
||||
save(id: string, entry: SessionEntry): Promise<void>;
|
||||
destroy(id: string): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* Back `ctx.session` with an async store. Register early (before anything reads
|
||||
* `ctx.session`). Loads once at the start of the request and saves once at the
|
||||
* end; regenerate/clear destroy the old id.
|
||||
*/
|
||||
declare function loadSession(backend: AsyncSessionBackend, options?: {
|
||||
ttlMs?: number;
|
||||
}): Middleware;
|
||||
|
||||
/**
|
||||
* Core request context and middleware contracts.
|
||||
*
|
||||
* The `Context` object is the single value that flows through middleware,
|
||||
* pages and API routes. It is intentionally small and framework-agnostic so
|
||||
* it can later be reused by the `.wrn` compiler output.
|
||||
*/
|
||||
|
||||
/** Translate a key for the active language, interpolating `{param}` placeholders. */
|
||||
type TFunction = (key: string, params?: Record<string, string | number>) => string;
|
||||
type Context = {
|
||||
/** The raw incoming web-standard Request. */
|
||||
req: Request;
|
||||
/** Parsed URL of the request (pathname, query, etc.). */
|
||||
url: URL;
|
||||
/** Active language for this request (resolved by the runtime); "" if i18n is unused. */
|
||||
lang: string;
|
||||
/** Translate a key for the active language (identity until the runtime sets it). */
|
||||
t: TFunction;
|
||||
/** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */
|
||||
params: Record<string, string>;
|
||||
/**
|
||||
* Per-request scratch space. Middleware can attach values here
|
||||
* (e.g. the authenticated user) and downstream handlers can read them.
|
||||
*/
|
||||
locals: Record<string, unknown>;
|
||||
/**
|
||||
* The authenticated user for this request, or null when anonymous. Populated
|
||||
* by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`.
|
||||
*/
|
||||
user?: unknown;
|
||||
/**
|
||||
* The direct socket peer IP, set by the server from `server.requestIP`. This
|
||||
* is NOT spoofable by request headers — prefer it over `x-forwarded-for` for
|
||||
* rate limiting unless you run behind a trusted proxy.
|
||||
*/
|
||||
ip?: string;
|
||||
/** Read/write HTTP cookies for the current response. */
|
||||
cookies: CookieStore;
|
||||
/** In-memory cookie-backed session store. */
|
||||
session: SessionStore;
|
||||
/** Read-only localStorage snapshot sent by the browser for CSR data bindings. */
|
||||
localStorage: LocalStorageSnapshot;
|
||||
};
|
||||
/** Calls the next middleware in the chain (or the final route handler). */
|
||||
type Next = () => Promise<Response> | Response;
|
||||
/**
|
||||
* Middleware runs before pages and API routes. It can:
|
||||
* - inspect/modify `ctx`
|
||||
* - short-circuit by returning a `Response` without calling `next()`
|
||||
* - continue by returning `await next()`
|
||||
*/
|
||||
type Middleware = (ctx: Context, next: Next) => Promise<Response> | Response;
|
||||
/** SEO metadata rendered into the document `<head>`. */
|
||||
type SeoConfig = {
|
||||
title?: string;
|
||||
titleTemplate?: string;
|
||||
description?: string;
|
||||
canonical?: string;
|
||||
canonicalBase?: string;
|
||||
robots?: string;
|
||||
keywords?: string | string[];
|
||||
image?: string;
|
||||
siteName?: string;
|
||||
type?: string;
|
||||
locale?: string;
|
||||
twitterCard?: string;
|
||||
twitterSite?: string;
|
||||
themeColor?: string;
|
||||
};
|
||||
/** Page metadata rendered into the document `<head>`. */
|
||||
type PageMeta = SeoConfig;
|
||||
/** A page module's default export. Returns an HTML string for the body. */
|
||||
type PageComponent = (ctx: Context) => string | Promise<string>;
|
||||
/** Create a fresh context for an incoming request. */
|
||||
declare function createContext(req: Request, url: URL): Context;
|
||||
/** Apply headers accumulated on the context, such as Set-Cookie. */
|
||||
declare function withContextHeaders(ctx: Context, res: Response): Response;
|
||||
|
||||
/**
|
||||
* Small, dependency-free security helpers shared across packages.
|
||||
*/
|
||||
/**
|
||||
* Escape a string for safe interpolation into HTML text or attributes.
|
||||
* Used for page metadata (title/description) so untrusted values can't
|
||||
* break out of an attribute or inject markup.
|
||||
*/
|
||||
declare function escapeHtml(value: string): string;
|
||||
declare function isSafeIslandName(name: string): boolean;
|
||||
/**
|
||||
* Reject obvious path-traversal in a request path before it is ever used to
|
||||
* resolve a file. The router never builds file paths from request input
|
||||
* (routes are resolved against a pre-scanned table), but this is a cheap
|
||||
* defense-in-depth guard.
|
||||
*/
|
||||
declare function isSafeRequestPath(pathname: string): boolean;
|
||||
|
||||
/**
|
||||
* CSRF protection via the double-submit cookie pattern.
|
||||
*
|
||||
* The framework sets a readable `wire-csrf` cookie on page loads; the client
|
||||
* echoes it in an `x-csrf-token` header on unsafe requests (the Wire UI form
|
||||
* runtime does this automatically). The server checks header === cookie. A
|
||||
* cross-site attacker can't read the cookie to forge the header, so the request
|
||||
* is rejected — while same-origin requests pass.
|
||||
*/
|
||||
|
||||
declare const CSRF_COOKIE = "wire-csrf";
|
||||
declare const CSRF_HEADER = "x-csrf-token";
|
||||
/** Ensure the CSRF cookie exists (readable by JS) and return its token. */
|
||||
declare function csrfToken(ctx: Context): string;
|
||||
/**
|
||||
* Verify an unsafe request's CSRF token against the cookie. Safe methods
|
||||
* (GET/HEAD/OPTIONS) always pass. The token may arrive in the `x-csrf-token`
|
||||
* header or a `_csrf` field already parsed onto `ctx.locals`.
|
||||
*/
|
||||
declare function verifyCsrf(ctx: Context): boolean;
|
||||
/** Middleware that 403s unsafe requests with a missing/mismatched CSRF token. */
|
||||
declare function csrfProtection(): Middleware;
|
||||
|
||||
/**
|
||||
* Authentication primitives.
|
||||
*
|
||||
* Passwords are hashed with argon2id via `Bun.password`. Sessions ride on the
|
||||
* existing cookie-backed `SessionStore`: logging a user in stores a serializable
|
||||
* user object under the "user" key, and `sessionAuth` hydrates `ctx.user` from
|
||||
* it on every request. `requireAuth` is a guard middleware for protected routes.
|
||||
*/
|
||||
|
||||
/** Session key under which the authenticated user is stored. */
|
||||
declare const SESSION_USER_KEY = "user";
|
||||
/** Hash a plaintext password (argon2id). Store the returned string. */
|
||||
declare function hashPassword(password: string): Promise<string>;
|
||||
/** Verify a plaintext password against a stored hash. Safe against bad hashes. */
|
||||
declare function verifyPassword(password: string, hash: string): Promise<boolean>;
|
||||
/** Persist the authenticated user in the session and on the context. */
|
||||
declare function logIn<U = unknown>(ctx: Context, user: U): void;
|
||||
/** Clear the session and forget the current user. */
|
||||
declare function logOut(ctx: Context): void;
|
||||
/**
|
||||
* The currently-authenticated user, or null. Reads `ctx.user` first (set by
|
||||
* `sessionAuth`/`logIn`), falling back to the session store.
|
||||
*/
|
||||
declare function getUser<U = unknown>(ctx: Context): U | null;
|
||||
/**
|
||||
* Hydrate `ctx.user` from the session for every request. Register this early in
|
||||
* the middleware chain so downstream pages and API routes can read `ctx.user`.
|
||||
*/
|
||||
declare function sessionAuth(): Middleware;
|
||||
interface RequireAuthOptions {
|
||||
/** Where to redirect unauthenticated page requests. Default "/login". */
|
||||
loginPath?: string;
|
||||
}
|
||||
/**
|
||||
* Guard that requires an authenticated user. Unauthenticated requests that look
|
||||
* like an API/fetch call get a 401 JSON response; page navigations get a 302
|
||||
* redirect to the login page with the original target preserved as `?next=`.
|
||||
*/
|
||||
declare function requireAuth(options?: RequireAuthOptions): Middleware;
|
||||
|
||||
/**
|
||||
* Fixed-window rate limiting middleware. Keeps an in-memory counter per key
|
||||
* (client IP by default, read from `x-forwarded-for` / `x-real-ip`) and rejects
|
||||
* requests over the limit with a 429 and a `Retry-After` header. Sets the
|
||||
* `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` headers.
|
||||
*
|
||||
* The store is process-local; behind multiple instances use a shared store
|
||||
* (out of scope here). Suitable as-is for single-process apps and dev.
|
||||
*/
|
||||
|
||||
interface RateLimitOptions {
|
||||
/** Window length in milliseconds. Default 60_000 (1 minute). */
|
||||
windowMs?: number;
|
||||
/** Max requests allowed per key per window. Default 60. */
|
||||
max?: number;
|
||||
/** Derive the bucket key from the request. Default: client IP. */
|
||||
key?: (ctx: Context) => string;
|
||||
/**
|
||||
* Trust `x-forwarded-for` / `x-real-ip` for the client IP. Default false —
|
||||
* those headers are attacker-spoofable, so by default we key on the direct
|
||||
* socket peer (`ctx.ip`). Enable ONLY when behind a proxy that overwrites
|
||||
* these headers (nginx, a load balancer, Cloudflare).
|
||||
*/
|
||||
trustProxy?: boolean;
|
||||
/** Body returned on 429. Default "Too Many Requests". */
|
||||
message?: string;
|
||||
/** Emit RateLimit-* headers. Default true. */
|
||||
headers?: boolean;
|
||||
/** Persistence for the counters. Default: process-local memory. */
|
||||
store?: RateLimitStore;
|
||||
/** Maximum in-memory keys before oldest buckets are evicted. Ignored for custom stores. */
|
||||
maxKeys?: number;
|
||||
}
|
||||
interface Bucket {
|
||||
count: number;
|
||||
resetAt: number;
|
||||
}
|
||||
/**
|
||||
* Pluggable rate-limit counter store. The default is process-local memory; swap
|
||||
* in a shared store (Redis/SQL) so limits hold across instances. `hit` records
|
||||
* one request for `key` in the current window and returns the running bucket.
|
||||
* It may be async (e.g. a Redis INCR + PEXPIRE) — the middleware awaits it.
|
||||
*/
|
||||
interface RateLimitStore {
|
||||
hit(key: string, windowMs: number, now: number): Bucket | Promise<Bucket>;
|
||||
}
|
||||
declare function rateLimit(options?: RateLimitOptions): Middleware;
|
||||
/** Non-spoofable key: the direct socket peer IP (set by the server). */
|
||||
declare function peerKey(ctx: Context): string;
|
||||
/** Proxy-aware key: trusts `x-forwarded-for` / `x-real-ip`, else the peer IP. */
|
||||
declare function proxyKey(ctx: Context): string;
|
||||
/** @deprecated Use `peerKey` (default) or `proxyKey`. Kept for compatibility. */
|
||||
declare const defaultKey: typeof proxyKey;
|
||||
|
||||
/**
|
||||
* Structured request logging middleware. Emits one record per request with a
|
||||
* request id, method, path, status, and duration — as pretty text (dev) or JSON
|
||||
* (production/log aggregation). The request id is stored on `ctx.locals` so
|
||||
* downstream handlers can correlate their own logs.
|
||||
*/
|
||||
|
||||
interface RequestRecord {
|
||||
time: string;
|
||||
id: string;
|
||||
method: string;
|
||||
path: string;
|
||||
status: number;
|
||||
durationMs: number;
|
||||
}
|
||||
interface RequestLoggerOptions {
|
||||
/** "pretty" (default) for humans, "json" for machines. */
|
||||
format?: "pretty" | "json";
|
||||
/** Where each finished record goes. Default console.log. */
|
||||
sink?: (line: string, record: RequestRecord) => void;
|
||||
/** ctx.locals key for the request id. Default "requestId". */
|
||||
requestIdKey?: string;
|
||||
/** Clock injection for tests. Default Date.now. */
|
||||
now?: () => number;
|
||||
}
|
||||
declare function requestLogger(options?: RequestLoggerOptions): Middleware;
|
||||
|
||||
/**
|
||||
* Caching primitives:
|
||||
* - `TTLCache` — a small in-memory time-to-live cache with `getOrLoad`, for
|
||||
* memoising expensive data (query results, computed pages).
|
||||
* - HTTP helpers — `cacheControl` to build a directive, `withCacheControl` to
|
||||
* apply it, and `etag` / `notModified` for conditional requests (304s).
|
||||
*/
|
||||
declare class TTLCache<V = unknown> {
|
||||
private readonly ttlMs;
|
||||
private store;
|
||||
private loading;
|
||||
private revisions;
|
||||
private generation;
|
||||
constructor(ttlMs?: number);
|
||||
get(key: string): V | undefined;
|
||||
set(key: string, value: V, ttlMs?: number): void;
|
||||
/** Return the cached value or compute, cache, and return it. */
|
||||
getOrLoad(key: string, loader: () => Promise<V> | V, ttlMs?: number): Promise<V>;
|
||||
delete(key: string): void;
|
||||
clear(): void;
|
||||
get size(): number;
|
||||
}
|
||||
interface CacheControlOptions {
|
||||
/** max-age in seconds. */
|
||||
maxAge?: number;
|
||||
/** s-maxage (shared/CDN cache) in seconds. */
|
||||
sMaxAge?: number;
|
||||
/** Mark private (per-user) rather than public. */
|
||||
private?: boolean;
|
||||
/** no-store: never cache. Overrides other directives. */
|
||||
noStore?: boolean;
|
||||
/** no-cache: revalidate before use. */
|
||||
noCache?: boolean;
|
||||
/** stale-while-revalidate window in seconds. */
|
||||
staleWhileRevalidate?: number;
|
||||
immutable?: boolean;
|
||||
}
|
||||
/** Build a Cache-Control header value from options. */
|
||||
declare function cacheControl(options: CacheControlOptions): string;
|
||||
/** Apply a Cache-Control header to a response (returns the same response). */
|
||||
declare function withCacheControl(res: Response, options: CacheControlOptions): Response;
|
||||
/** A stable, quoted ETag for a string/bytes body (FNV-1a, weak by default). */
|
||||
declare function etag(body: string | ArrayBuffer | Uint8Array, weak?: boolean): string;
|
||||
/** True when the request's If-None-Match matches the given ETag (send a 304). */
|
||||
declare function notModified(req: Request, tag: string): boolean;
|
||||
|
||||
/**
|
||||
* File upload helpers. Bun parses `multipart/form-data` natively via
|
||||
* `Request.formData()`, yielding web `File` objects; these helpers validate and
|
||||
* persist them safely (size/type limits, filename sanitisation to prevent path
|
||||
* traversal).
|
||||
*/
|
||||
declare class UploadError extends Error {
|
||||
constructor(message: string);
|
||||
}
|
||||
interface SaveUploadOptions {
|
||||
/** Destination directory. */
|
||||
dir: string;
|
||||
/** Reject files larger than this many bytes. */
|
||||
maxBytes?: number;
|
||||
/** Allowed MIME types (e.g. "image/png") and/or extensions (e.g. ".png"). */
|
||||
allowedTypes?: string[];
|
||||
/** Choose the stored filename. Default: the sanitised original name. */
|
||||
filename?: (file: File) => string;
|
||||
}
|
||||
interface SavedUpload {
|
||||
path: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
type: string;
|
||||
}
|
||||
/** All `File` values in a parsed form, with their field names. */
|
||||
declare function collectUploads(form: FormData): {
|
||||
field: string;
|
||||
file: File;
|
||||
}[];
|
||||
/** Validate and write one uploaded file to disk. Throws `UploadError` on reject. */
|
||||
declare function saveUpload(file: File, options: SaveUploadOptions): Promise<SavedUpload>;
|
||||
/** Strip directory separators, traversal, and control chars from a filename. */
|
||||
declare function sanitizeFilename(name: string): string;
|
||||
|
||||
/**
|
||||
* Streaming response primitives.
|
||||
*
|
||||
* `streamResponse` turns a (sync or async) iterable of strings/bytes into a
|
||||
* streaming `Response` — the basis for streaming SSR (send the shell, then flush
|
||||
* page chunks as they render) and any progressively-generated output. `sse`
|
||||
* builds a Server-Sent Events stream from an async iterable of events.
|
||||
*
|
||||
* API routes and pages can already return a `Response` with a `ReadableStream`
|
||||
* body and the framework streams it unbuffered; these helpers just make the
|
||||
* common cases ergonomic.
|
||||
*/
|
||||
interface StreamResponseInit {
|
||||
status?: number;
|
||||
headers?: HeadersInit;
|
||||
/** Content-Type; default "text/html; charset=utf-8". */
|
||||
contentType?: string;
|
||||
}
|
||||
type Chunk = string | Uint8Array;
|
||||
type ChunkSource = Iterable<Chunk> | AsyncIterable<Chunk>;
|
||||
/** Build a streaming Response from an (async) iterable of chunks. */
|
||||
declare function streamResponse(source: ChunkSource, init?: StreamResponseInit): Response;
|
||||
interface ServerSentEvent {
|
||||
data: string;
|
||||
event?: string;
|
||||
id?: string;
|
||||
/** Client reconnection hint in milliseconds. */
|
||||
retry?: number;
|
||||
}
|
||||
/** Build a Server-Sent Events (text/event-stream) Response from events. */
|
||||
declare function sse(source: Iterable<ServerSentEvent> | AsyncIterable<ServerSentEvent>): Response;
|
||||
|
||||
/**
|
||||
* Realtime rooms.
|
||||
*
|
||||
* A file in `app/realtime/` exports `default defineRoom({ onConnect, onMessage,
|
||||
* onLeave })` and is served at `ws://host/realtime/<name>`. The framework's
|
||||
* client runtime (`/__wrnexus/realtime.js`) handles the browser side, so pages
|
||||
* ship NO hand-written WebSocket code.
|
||||
*
|
||||
* Handlers get a `RoomClient` with everything you need:
|
||||
* client.send(msg) → this connection
|
||||
* client.broadcast(msg) → everyone else in the room
|
||||
* client.room.broadcast(msg) → everyone (incl. sender)
|
||||
* client.to(id | ids).send(msg) → specific connection(s)
|
||||
* client.toUser(u | users).send() → a user / selected users (all their tabs)
|
||||
* client.user = "u1" → identify a connection for targeting
|
||||
* client.data / client.room.state → per-connection / shared room state
|
||||
*
|
||||
* The dynamic route `app/realtime/[room].ts` gives one handler many independent
|
||||
* rooms — `/realtime/lobby` and `/realtime/game-7` are separate room instances.
|
||||
*/
|
||||
interface RawSocket {
|
||||
send(data: string): unknown;
|
||||
close(code?: number, reason?: string): void;
|
||||
}
|
||||
interface RealtimeSocket<Data = unknown> {
|
||||
readonly data: Data;
|
||||
send(data: string | Uint8Array): number;
|
||||
subscribe(topic: string): void;
|
||||
unsubscribe(topic: string): void;
|
||||
publish(topic: string, data: string | Uint8Array): number;
|
||||
isSubscribed(topic: string): boolean;
|
||||
close(code?: number, reason?: string): void;
|
||||
}
|
||||
interface RealtimeHandler<Data = unknown> {
|
||||
open?(ws: RealtimeSocket<Data>): void | Promise<void>;
|
||||
message?(ws: RealtimeSocket<Data>, message: string | Uint8Array): void | Promise<void>;
|
||||
close?(ws: RealtimeSocket<Data>, code?: number, reason?: string): void | Promise<void>;
|
||||
drain?(ws: RealtimeSocket<Data>): void | Promise<void>;
|
||||
}
|
||||
interface Target {
|
||||
/** Send a message (objects are JSON-serialized). */
|
||||
send(message: unknown): void;
|
||||
}
|
||||
interface Room<TData = Record<string, unknown>> {
|
||||
readonly name: string;
|
||||
/** Shared, in-memory room state (lives while ≥1 client is connected). */
|
||||
readonly state: Record<string, unknown>;
|
||||
/** All connected clients. */
|
||||
clients(): RoomClient<TData>[];
|
||||
/** Number of connected clients. */
|
||||
count(): number;
|
||||
/** Send to everyone in the room, including the sender. */
|
||||
broadcast(message: unknown): void;
|
||||
/** Target specific connection id(s). */
|
||||
to(id: string | string[]): Target;
|
||||
/** Target a user / users by identity (reaches all their connections). */
|
||||
toUser(user: string | string[]): Target;
|
||||
}
|
||||
interface RoomClient<TData = Record<string, unknown>> {
|
||||
/** Unique per connection (a tab). */
|
||||
readonly id: string;
|
||||
/** App identity for targeting; assign it in `onConnect`. */
|
||||
user: string | undefined;
|
||||
/** Query params from the connection URL. */
|
||||
readonly query: Record<string, string>;
|
||||
/** Per-connection scratch state. */
|
||||
readonly data: TData;
|
||||
readonly room: Room<TData>;
|
||||
/** Send to THIS connection. */
|
||||
send(message: unknown): void;
|
||||
/** Send to everyone else in the room. */
|
||||
broadcast(message: unknown): void;
|
||||
/** Target specific connection id(s). */
|
||||
to(id: string | string[]): Target;
|
||||
/** Target a user / users by identity. */
|
||||
toUser(user: string | string[]): Target;
|
||||
/** Close this connection. */
|
||||
close(code?: number, reason?: string): void;
|
||||
}
|
||||
/** Info available when authorizing a connection, before it is accepted. */
|
||||
interface RoomAuthInfo {
|
||||
/** Authenticated session user id, or `?user=` — undefined when anonymous. */
|
||||
user?: string;
|
||||
/** Connection URL query params. */
|
||||
query: Record<string, string>;
|
||||
/** The upgrade request's headers (cookies, etc.). */
|
||||
headers: Headers;
|
||||
}
|
||||
interface RoomHandlers<TData = Record<string, unknown>> {
|
||||
/**
|
||||
* Gate the connection BEFORE it is accepted. Return false to reject the
|
||||
* upgrade with 403 (e.g. `authorize: (info) => !!info.user` to require auth).
|
||||
*/
|
||||
authorize?(info: RoomAuthInfo): boolean | Promise<boolean>;
|
||||
/** A client connected (a new tab joined the room). */
|
||||
onConnect?(client: RoomClient<TData>): void | Promise<void>;
|
||||
/** A message arrived (JSON is parsed; non-JSON arrives as a string). */
|
||||
onMessage?(client: RoomClient<TData>, message: any): void | Promise<void>;
|
||||
/** A client disconnected. */
|
||||
onLeave?(client: RoomClient<TData>): void | Promise<void>;
|
||||
}
|
||||
interface RoomDefinition<TData = Record<string, unknown>> {
|
||||
readonly __wrnexusRoom: true;
|
||||
readonly handlers: RoomHandlers<TData>;
|
||||
}
|
||||
/** Define a realtime room. Export the result as the `default` of a realtime file. */
|
||||
declare function defineRoom<TData = Record<string, unknown>>(handlers: RoomHandlers<TData>): RoomDefinition<TData>;
|
||||
declare function isRoomDefinition(value: unknown): value is RoomDefinition;
|
||||
interface RealtimeConnectMeta {
|
||||
room: string;
|
||||
def: RoomDefinition;
|
||||
query?: Record<string, string>;
|
||||
user?: string;
|
||||
}
|
||||
/** One cross-instance message: a room broadcast, or a targeted user send. */
|
||||
interface RealtimeEnvelope {
|
||||
room: string;
|
||||
/** If set, deliver only to these user identities; otherwise the whole room. */
|
||||
users?: string[];
|
||||
message: unknown;
|
||||
}
|
||||
/**
|
||||
* A pub/sub bridge for horizontal scaling. Wire the registry to a shared bus
|
||||
* (Redis pub/sub, NATS, …): local broadcasts/`toUser` sends are published to
|
||||
* peers, and messages received from peers are delivered via `registry.deliver`.
|
||||
* Connection-targeted sends (`send`, `to(id)`) stay local (ids are per-process).
|
||||
*/
|
||||
interface RealtimeBridge {
|
||||
publish(envelope: RealtimeEnvelope): void;
|
||||
}
|
||||
interface RealtimeRegistry {
|
||||
open(socket: RawSocket, meta: RealtimeConnectMeta): void | Promise<void>;
|
||||
message(socket: RawSocket, raw: string | Uint8Array): void | Promise<void>;
|
||||
close(socket: RawSocket): void | Promise<void>;
|
||||
/** Attach a cross-instance bridge (call once at startup). */
|
||||
setBridge(bridge: RealtimeBridge): void;
|
||||
/** Deliver an envelope received from a peer to LOCAL connections only. */
|
||||
deliver(envelope: RealtimeEnvelope): void;
|
||||
/** Number of live connections (across all rooms) — for tests/metrics. */
|
||||
size(): number;
|
||||
}
|
||||
/** Create the registry that maps sockets ↔ rooms and drives room handlers. */
|
||||
declare function createRealtimeRegistry(): RealtimeRegistry;
|
||||
/**
|
||||
* A minimal pub/sub bus (structurally satisfied by `@wrnexus/pubsub`). Used to
|
||||
* bridge realtime broadcasts across processes without a hard dependency.
|
||||
*/
|
||||
interface RealtimeBus {
|
||||
publish(topic: string, message: unknown): void | Promise<void>;
|
||||
subscribe(topic: string, handler: (message: unknown, topic: string) => void): () => void;
|
||||
}
|
||||
/**
|
||||
* Bridge a realtime registry across processes/instances via a pub/sub bus (use
|
||||
* the Redis driver so it crosses machines). After this, `client.room.broadcast`
|
||||
* and `client.toUser(...)` reach connected clients on **every** app process/
|
||||
* instance subscribed to the same bus — the foundation for realtime that works
|
||||
* with multiple running apps behind the gateway. Connection-targeted sends
|
||||
* (`send`, `to(id)`) stay local. Returns an unsubscribe function.
|
||||
*
|
||||
* import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core";
|
||||
* import { createPubSub } from "@wrnexus/pubsub";
|
||||
* import { redisDriver } from "@wrnexus/pubsub/redis";
|
||||
* bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL)));
|
||||
*/
|
||||
declare function bridgeRealtime(registry: RealtimeRegistry, bus: RealtimeBus, topic?: string): () => void;
|
||||
|
||||
/**
|
||||
* Error + status pages. Every page here is a self-contained HTML document —
|
||||
* inline CSS only, no external stylesheet, no JavaScript (so it renders under the
|
||||
* strict CSP, even when the app's assets are what failed). Theme-aware via
|
||||
* `prefers-color-scheme`, styled in the WRNexusJS design language (ink-navy,
|
||||
* azure, a faint blueprint grid + glow). Development shows the stack trace;
|
||||
* production never leaks internal paths.
|
||||
*/
|
||||
type Mode = "development" | "production";
|
||||
/** A beautiful, self-contained HTML page for any 4xx/5xx status. */
|
||||
declare function renderStatusPage(status: number): Response;
|
||||
/** Readable, styled development error page — includes the stack trace. */
|
||||
declare function renderDevError(err: unknown, status?: number): Response;
|
||||
/** Generic production error page — no stack, no file paths. */
|
||||
declare function renderProdError(status?: number): Response;
|
||||
/** Pick the right error page for the current mode. */
|
||||
declare function renderError(err: unknown, mode: Mode): Response;
|
||||
/** Beautiful 404 page. */
|
||||
declare function renderNotFound(): Response;
|
||||
|
||||
type CorsOrigin = "*" | string | string[];
|
||||
interface CorsConfig {
|
||||
/** Enable CORS headers and preflight handling. Defaults to false. */
|
||||
enabled?: boolean;
|
||||
/** Allowed origins. Use "*" for public APIs. Defaults to "*". */
|
||||
origin?: CorsOrigin;
|
||||
/** Allowed methods for preflight responses. */
|
||||
methods?: string[];
|
||||
/** Allowed request headers. Defaults to the browser's requested headers. */
|
||||
allowedHeaders?: string[];
|
||||
/** Response headers exposed to browser JavaScript. */
|
||||
exposedHeaders?: string[];
|
||||
/** Whether to send Access-Control-Allow-Credentials. */
|
||||
credentials?: boolean;
|
||||
/** Access-Control-Max-Age, in seconds. */
|
||||
maxAge?: number;
|
||||
}
|
||||
type CspDirectiveValue = string | string[] | false | null | undefined;
|
||||
interface ContentSecurityPolicyConfig {
|
||||
/** Defaults to true. */
|
||||
enabled?: boolean;
|
||||
/** Use Content-Security-Policy-Report-Only instead of enforcing. */
|
||||
reportOnly?: boolean;
|
||||
/** Merge or remove directives. Set a directive to false/null to remove it. */
|
||||
directives?: Record<string, CspDirectiveValue>;
|
||||
/** Set false to start from an empty policy instead of WRNexusJS defaults. */
|
||||
useDefaults?: boolean;
|
||||
}
|
||||
interface HstsConfig {
|
||||
/** Defaults to true in production, false in development. */
|
||||
enabled?: boolean;
|
||||
/** Defaults to 31536000 seconds (1 year). */
|
||||
maxAge?: number;
|
||||
/** Defaults to true. */
|
||||
includeSubDomains?: boolean;
|
||||
/** Defaults to true. */
|
||||
preload?: boolean;
|
||||
}
|
||||
interface TrustedTypesConfig {
|
||||
/** Defaults to true in production, false in development. */
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Defaults to ["*"] in production so browser extensions and dev tooling can
|
||||
* create their own policies without noisy console errors. Set this to a
|
||||
* concrete list, e.g. ["wrnexus", "default"], for stricter deployments.
|
||||
*/
|
||||
policyNames?: string[];
|
||||
/** Defaults to true. */
|
||||
requireForScript?: boolean;
|
||||
/** Adds "allow-duplicates" to the trusted-types directive. */
|
||||
allowDuplicates?: boolean;
|
||||
}
|
||||
type PermissionsPolicyConfig = Record<string, string | string[] | false | null | undefined>;
|
||||
interface SecurityConfig {
|
||||
/** Set false to skip all framework security headers except explicitly enabled CORS. */
|
||||
headers?: boolean;
|
||||
/**
|
||||
* Trust `X-Forwarded-Proto` / `X-Forwarded-Host` when building `ctx.url` — set
|
||||
* this when the app runs behind a TLS-terminating reverse proxy (nginx, the
|
||||
* WRNexusJS gateway, a load balancer). Without it, a proxied app sees the internal
|
||||
* `http://` request and marks cookies (e.g. CSRF/session) non-`Secure`. Default
|
||||
* false; enable ONLY when a trusted proxy actually sets these headers.
|
||||
*/
|
||||
trustProxy?: boolean;
|
||||
cors?: boolean | CorsConfig;
|
||||
contentSecurityPolicy?: false | ContentSecurityPolicyConfig;
|
||||
hsts?: false | HstsConfig;
|
||||
trustedTypes?: false | TrustedTypesConfig;
|
||||
/** Defaults to "same-origin". */
|
||||
crossOriginOpenerPolicy?: false | "same-origin" | "same-origin-allow-popups" | "unsafe-none";
|
||||
/** Defaults to "DENY". */
|
||||
frameOptions?: false | "DENY" | "SAMEORIGIN";
|
||||
/** Defaults to "strict-origin-when-cross-origin". */
|
||||
referrerPolicy?: false | string;
|
||||
/** Defaults to a restrictive browser capability policy. */
|
||||
permissionsPolicy?: false | PermissionsPolicyConfig;
|
||||
/** Extra static headers applied last. */
|
||||
extraHeaders?: Record<string, string>;
|
||||
}
|
||||
/**
|
||||
* Guard a WebSocket upgrade against Cross-Site WebSocket Hijacking: browsers
|
||||
* always send an `Origin` header on a WS handshake, and — unlike fetch — WS is
|
||||
* NOT subject to CORS, so cookies would otherwise flow cross-site. We allow
|
||||
* same-origin (Origin host === Host header), configured CORS origins, and
|
||||
* non-browser clients (no Origin, which also carry no ambient cookies).
|
||||
*/
|
||||
declare function isWebSocketOriginAllowed(req: Request, security?: SecurityConfig): boolean;
|
||||
declare function createCorsPreflightResponse(req: Request, security?: SecurityConfig): Response | null;
|
||||
/**
|
||||
* Build the request URL, honoring `X-Forwarded-Proto` / `X-Forwarded-Host` when
|
||||
* `trustProxy` is set (app behind a TLS-terminating reverse proxy). This makes
|
||||
* `ctx.url.protocol` reflect the EXTERNAL scheme, so protocol-dependent logic —
|
||||
* `Secure` cookies, canonical URLs — is correct behind nginx / the gateway.
|
||||
* Security checks that compare the raw `Host`/`Origin` headers don't use this URL,
|
||||
* so they are unaffected. An invalid forwarded value is ignored by the URL setter.
|
||||
*/
|
||||
declare function resolveRequestUrl(req: Request, trustProxy?: boolean): URL;
|
||||
declare function withSecurityHeaders(req: Request, res: Response, mode: Mode, security?: SecurityConfig, nonce?: string): Response;
|
||||
|
||||
export { type AsyncSessionBackend, type Bucket, CSRF_COOKIE, CSRF_HEADER, type CacheControlOptions, type ContentSecurityPolicyConfig, type Context, type CookieOptions, type CookieStore, type CorsConfig, type CorsOrigin, type CspDirectiveValue, type HstsConfig, type LocalStorageSnapshot, type Middleware, type Mode, type Next, type PageComponent, type PageMeta, type PermissionsPolicyConfig, type RateLimitOptions, type RateLimitStore, type RawSocket, type RealtimeBridge, type RealtimeBus, type RealtimeConnectMeta, type RealtimeEnvelope, type RealtimeHandler, type RealtimeRegistry, type RealtimeSocket, type RequestLoggerOptions, type RequestRecord, type RequireAuthOptions, type Room, type RoomAuthInfo, type RoomClient, type RoomDefinition, type RoomHandlers, SESSION_USER_KEY, type SaveUploadOptions, type SavedUpload, type SecurityConfig, type SeoConfig, type ServerSentEvent, type SessionBackend, type SessionEntry, type SessionStore, type StreamResponseInit, type TFunction, TTLCache, type Target, type TrustedTypesConfig, UploadError, bridgeRealtime, cacheControl, collectUploads, createContext, createCorsPreflightResponse, createRealtimeRegistry, csrfProtection, csrfToken, defaultKey, defineRoom, escapeHtml, etag, getUser, hashPassword, isRoomDefinition, isSafeIslandName, isSafeRequestPath, isWebSocketOriginAllowed, loadSession, logIn, logOut, notModified, peerKey, proxyKey, rateLimit, renderDevError, renderError, renderNotFound, renderProdError, renderStatusPage, requestLogger, requireAuth, resolveRequestUrl, sanitizeFilename, saveUpload, sessionAuth, setSessionBackend, sse, streamResponse, verifyCsrf, verifyPassword, withCacheControl, withContextHeaders, withSecurityHeaders };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/core</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="jsonc"><code>// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@wrnexus/core",
|
||||
},
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>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" }),
|
||||
];</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>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</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#context-middleware-wrnexus-core">Context & middleware — @wrnexus/core</a><a class="toc-level-4" href="#authentication-wrnexus-core">Authentication — @wrnexus/core</a><a class="toc-level-4" href="#csrf-wrnexus-core">CSRF — @wrnexus/core</a><a class="toc-level-4" href="#rate-limiting-wrnexus-core">Rate limiting — @wrnexus/core</a><a class="toc-level-4" href="#request-logging-wrnexus-core">Request logging — @wrnexus/core</a><a class="toc-level-4" href="#caching-wrnexus-core">Caching — @wrnexus/core</a><a class="toc-level-4" href="#file-uploads-wrnexus-core">File uploads — @wrnexus/core</a><a class="toc-level-4" href="#streaming-sse-wrnexus-core">Streaming & SSE — @wrnexus/core</a><a class="toc-level-4" href="#realtime-rooms-wrnexus-core">Realtime rooms — @wrnexus/core</a><a class="toc-level-4" href="#error-pages-wrnexus-core">Error pages — @wrnexus/core</a><a class="toc-level-4" href="#security-headers-cors-wrnexus-core">Security headers & CORS — @wrnexus/core</a><a class="toc-level-4" href="#storage-cookies-sessions-localstorage-wrnexus-core">Storage: cookies, sessions, localStorage — @wrnexus/core</a><a class="toc-level-4" href="#low-level-security-helpers-wrnexus-core">Low-level security helpers — @wrnexus/core</a><a class="toc-level-4" href="#jsx-runtime-wrnexus-core-wrnexus-core-jsx-runtime-wrnexus-core-jsx-dev-runtime">JSX runtime — @wrnexus/core, @wrnexus/core/jsx-runtime, @wrnexus/core/jsx-dev-runtime</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#a-minimal-middleware-chain">A minimal middleware chain</a><a class="toc-level-4" href="#password-auth">Password auth</a><a class="toc-level-4" href="#http-caching-with-etags">HTTP caching with ETags</a><a class="toc-level-4" href="#streaming-sse">Streaming SSE</a><a class="toc-level-4" href="#a-realtime-room">A realtime room</a><a class="toc-level-4" href="#jsx-rendering">JSX rendering</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
page wrnexuscsr {
|
||||
seo {
|
||||
title = "@wrnexus/csr"
|
||||
description = "Reactive, navigation, and realtime browser runtimes."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h1>@wrnexus/csr</h1><p>Reactive, navigation, and realtime browser runtimes.</p><code>bun add @wrnexus/csr@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend</span><h1>@wrnexus/csr</h1><p>Reactive, navigation, and realtime browser runtimes.</p><pre><code>bun add @wrnexus/csr@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>The browser-side client runtime for WRNexusJS — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/csr</code> holds the three client runtimes that WRNexusJS serves to the browser. Components are authored as <code>.wrn</code> files and rendered on the <strong>server</strong>; this package provides the single, generic runtime that <strong>hydrates</strong> 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:</p>
|
||||
<ul>
|
||||
<li><strong>reactive</strong> at <code>/__wrnexus/reactive.js</code> — reactive directives (<code>data-scope</code>, <code>data-text</code>, <code>data-for</code>, …)</li>
|
||||
<li><strong>nav</strong> at <code>/__wrnexus/nav.js</code> — SPA-style client navigation with graceful fallback</li>
|
||||
<li><strong>realtime</strong> at <code>/__wrnexus/realtime.js</code> — WebSocket "rooms", declarative or programmatic</li>
|
||||
</ul>
|
||||
<p>The package itself runs on the server (it just returns strings); the strings it returns run in the browser. A dev/prod server (see <code>@wrnexus/core</code>) is responsible for actually serving them.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/csr</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>All exports come from the package root (<code>@wrnexus/csr</code>). 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.</p>
|
||||
<h4 id="runtime-strings">Runtime strings</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Type</th><th>Served at</th><th>Contents</th></tr></thead>
|
||||
<tbody><tr><td><code>REACTIVE_RUNTIME</code></td><td><code>string</code></td><td><code>/__wrnexus/reactive.js</code></td><td>Reactive directive runtime</td></tr><tr><td><code>NAV_RUNTIME</code></td><td><code>string</code></td><td><code>/__wrnexus/nav.js</code></td><td>Client-side navigation runtime</td></tr><tr><td><code>REALTIME_RUNTIME</code></td><td><code>string</code></td><td><code>/__wrnexus/realtime.js</code></td><td>Realtime rooms runtime</td></tr></tbody></table></div>
|
||||
<h4 id="accessor-functions">Accessor functions</h4>
|
||||
<p>Convenience getters that return the same strings.</p>
|
||||
<pre data-language="ts"><code>getReactiveRuntime(): string // → REACTIVE_RUNTIME
|
||||
getNavRuntime(): string // → NAV_RUNTIME
|
||||
getRealtimeRuntime(): string // → REALTIME_RUNTIME</code></pre>
|
||||
<h4 id="browser-reactive-directives">Browser: reactive directives</h4>
|
||||
<p>Applied to any subtree containing <code>data-scope</code>. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no <code>unsafe-eval</code> works.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Directive</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>data-scope="count: 0, name: 'x'"</code></td><td>Declare reactive state on a subtree</td></tr><tr><td><code>data-on-<event>="count++"</code></td><td>Run a statement in scope on a DOM event</td></tr><tr><td><code>data-text="expr"</code></td><td>Bind an element's <code>textContent</code> to an expression</td></tr><tr><td><code>data-show="expr"</code></td><td>Toggle visibility (<code>display</code>) on truthiness</td></tr><tr><td><code>data-for="item in list"</code> (opt. <code>item, i in list</code>)</td><td>Per-item list rendering template</td></tr><tr><td><code>{{expr}}</code> or <code>{expr}</code></td><td>Interpolation inside text nodes and attribute values</td></tr><tr><td><code>data-wrnexus-csr="id"</code></td><td>Target for a generated CSR fetch binding (fetches <code>/__wrnexus/csr?...</code>)</td></tr></tbody></table></div>
|
||||
<p>Supported expression features: literals, identifiers, member access (<code>a.b</code>, <code>a[b]</code>), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (<code>&& ||</code>), unary (<code>! - +</code>), and ternary. Statements support <code>++</code>/<code>--</code>, assignment operators (<code>= += -= *= /= %=</code>), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it.</p>
|
||||
<p>Browser globals installed: <code>window.__wrnexusHydrateScopes(root)</code> and <code>window.__wrnexusHydrateCsrFetches(root)</code> — both idempotent, so re-running after a DOM swap or HMR morph is safe. Both run automatically on <code>DOMContentLoaded</code>.</p>
|
||||
<h4 id="browser-navigation">Browser: navigation</h4>
|
||||
<p>Intercepts same-origin <code><a></code> clicks, fetches the target page, and swaps the <code>#app</code> container in place (via <code>importNode</code> — not <code>innerHTML</code> — so it works under a Trusted-Types CSP), updating history, title, and scroll, then re-hydrates. Cross-origin links, modified clicks, <code>download</code>/<code>data-no-nav</code>/<code>rel="external"</code>/<code>target</code> links, non-HTML responses, or a missing <code>#app</code> fall back to a full browser navigation.</p>
|
||||
<ul>
|
||||
<li>Programmatic navigation: <code>window.__wrnexusNavigate(url)</code></li>
|
||||
<li>Emits a <code>wrnexus:navigated</code> <code>CustomEvent</code> (<code>detail.url</code>) after each swap</li>
|
||||
<li>Sends <code>x-wrnexus-nav: 1</code> on fetches so the server can return the page fragment</li>
|
||||
<li>Appends any <code>/__wrnexus/*</code> runtime scripts the incoming page needs but the current document lacks</li>
|
||||
</ul>
|
||||
<h4 id="browser-realtime-rooms">Browser: realtime rooms</h4>
|
||||
<p>Connects to <code>/realtime/<name></code> over WebSocket (<code>ws</code>/<code>wss</code> chosen from <code>location.protocol</code>). Two usage modes.</p>
|
||||
<p>Programmatic API via <code>window.wire</code>:</p>
|
||||
<pre data-language="ts"><code>wire.room(name): Room // open (or reuse) a room connection
|
||||
wire.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;
|
||||
}</code></pre>
|
||||
<p>Internal lifecycle messages are emitted to listeners as <code>{ type }</code>: <code>__open</code>, <code>__close</code>, <code>__error</code>, and <code>__raw</code> (non-JSON frames, with <code>data</code>). Reconnect uses exponential backoff capped at 5s; queued sends flush on reconnect.</p>
|
||||
<p>Declarative binding (zero JS) on a <code>data-room="<name>"</code> container:</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Attribute</th><th>On</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>data-room="<name>"</code></td><td>container</td><td>Connect to room <code><name></code></td></tr><tr><td><code>data-room-user="<id>"</code></td><td>container</td><td>Identify the connection (<code>?user=<id></code>)</td></tr><tr><td><code>data-room-log</code></td><td>element</td><td>Where incoming messages are appended</td></tr><tr><td><code><template data-room-item="<type>"></code></td><td>template</td><td>Row template for messages of that <code>type</code> (empty = fallback)</td></tr><tr><td><code>%field%</code></td><td>inside template</td><td>Placeholder filled from the message field (text/attr only, HTML-escaped)</td></tr><tr><td><code>data-room-status</code></td><td>element</td><td>Reflects connection state text (<code>connected</code>/<code>disconnected</code>/<code>error</code>)</td></tr><tr><td><code>data-room-status-class</code></td><td>status element</td><td>Base class; a state variant (<code>is-connected</code>, …) is appended</td></tr><tr><td><code><form data-room-send></code></td><td>form</td><td>Submits named fields as a JSON message</td></tr><tr><td><code>data-room-reset</code></td><td>form field</td><td>Clears that field after send</td></tr></tbody></table></div>
|
||||
<p>Rebinds on <code>wrnexus:navigated</code> and closes rooms whose container has left the page.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>Server side — serve the runtime strings from your router (example with <code>Bun.serve</code>):</p>
|
||||
<pre data-language="ts"><code>import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";
|
||||
|
||||
const routes: Record<string, string> = {
|
||||
"/__wrnexus/reactive.js": getReactiveRuntime(),
|
||||
"/__wrnexus/nav.js": getNavRuntime(),
|
||||
"/__wrnexus/realtime.js": getRealtimeRuntime(),
|
||||
};
|
||||
|
||||
Bun.serve({
|
||||
fetch(req) {
|
||||
const body = routes[new URL(req.url).pathname];
|
||||
if (body) {
|
||||
return new Response(body, {
|
||||
headers: { "content-type": "text/javascript; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});</code></pre>
|
||||
<p>Browser side — server-rendered HTML that the reactive runtime hydrates:</p>
|
||||
<pre data-language="html"><code><div data-scope="count: 0">
|
||||
<button data-on-click="count++">+1</button>
|
||||
<span data-text="count"></span>
|
||||
<p>Total: {{count}}</p>
|
||||
</div>
|
||||
<script src="/__wrnexus/reactive.js"></script></code></pre>
|
||||
<p>A realtime chat, fully declarative:</p>
|
||||
<pre data-language="html"><code><div data-room="lobby" data-room-user="ada">
|
||||
<div data-room-status></div>
|
||||
<ul data-room-log></ul>
|
||||
<template data-room-item="chat"><li>%user%: %text%</li></template>
|
||||
<form data-room-send>
|
||||
<input name="text" data-room-reset />
|
||||
<input type="hidden" name="type" value="chat" />
|
||||
<button>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
<script src="/__wrnexus/realtime.js"></script></code></pre>
|
||||
<p>Or drive a room from code:</p>
|
||||
<pre data-language="ts"><code>const room = wire.room("lobby");
|
||||
room.on("chat", (msg) => console.log(msg.user, msg.text));
|
||||
room.send({ type: "chat", user: "ada", text: "hi" });</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only</strong> on the server (the package integrates with Bun-based WRNexusJS servers); the emitted strings are plain browser JS with no dependencies.</li>
|
||||
<li>Browser runtimes are <strong>self-contained</strong> (no imports, no build step) and <strong>idempotent</strong>, so re-hydration after navigation or HMR is safe.</li>
|
||||
<li>Designed for a <strong>strict CSP</strong>: the reactive expression evaluator avoids <code>eval</code>/<code>new Function</code> (no <code>unsafe-eval</code>), and DOM swaps use <code>importNode</code>/attribute writes rather than <code>innerHTML</code> (Trusted-Types friendly).</li>
|
||||
<li>Peer packages: rendered <code>.wrn</code> components and the serving layer come from <code>@wrnexus/core</code> (the sole dependency); pages are rendered by the WRNexusJS dev/prod server.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* Browser reactive runtime (Point 2: reactive directives).
|
||||
*
|
||||
* Served verbatim at `/__wrnexus/reactive.js` for any page that contains a
|
||||
* `data-scope`. It is plain browser JS (no build step) and self-contained: it
|
||||
* inlines a tiny `signal()` so it has no imports to resolve.
|
||||
*
|
||||
* Supported directives (this is exactly what the `.wrn` compiler emits):
|
||||
* data-scope="count: 0, name: 'x'" declare reactive state on a subtree
|
||||
* data-on-<event>="count++" run a statement in scope on an event
|
||||
* data-text="expr" element textContent follows an expression
|
||||
* data-wrnexus-csr="id" target for generated CSR fetch bindings
|
||||
* {{expr}} or {expr} interpolation inside text nodes
|
||||
*
|
||||
* Expressions are evaluated by a tiny parser instead of `eval`/`new Function`,
|
||||
* so production can use a strong CSP without `unsafe-eval`.
|
||||
*/
|
||||
declare const REACTIVE_RUNTIME: string;
|
||||
|
||||
/**
|
||||
* Client-side navigation runtime, served at `/__wrnexus/nav.js`.
|
||||
*
|
||||
* Progressive enhancement over normal links: intercepts same-origin `<a>`
|
||||
* clicks, fetches the target page's HTML, swaps the `#app` container in place,
|
||||
* updates history/title/scroll, ensures any framework runtimes the new page
|
||||
* needs are present, and re-hydrates. Anything unexpected (cross-origin,
|
||||
* modified click, non-HTML response, missing `#app`) falls back to a full
|
||||
* browser navigation, so behaviour degrades safely.
|
||||
*
|
||||
* Data "loaders": pages load their data on the server (SSR `api` bindings), so
|
||||
* the fetched HTML already contains fresh data — no separate client loader is
|
||||
* needed. Client-side (`csr`) bindings and reactive scopes re-hydrate after the
|
||||
* swap. Programmatic navigation is exposed as `window.__wrnexusNavigate(url)`.
|
||||
*/
|
||||
declare const NAV_RUNTIME: string;
|
||||
|
||||
/**
|
||||
* Client realtime runtime, served at `/__wrnexus/realtime.js`.
|
||||
*
|
||||
* Two ways to use it — no hand-written WebSocket code either way:
|
||||
*
|
||||
* 1. Declarative (zero JS). Put `data-room="<name>"` on a container; the runtime
|
||||
* connects, appends incoming messages to `[data-room-log]` using a
|
||||
* `<template data-room-item="<type>">` (fields via `%field%`, HTML-escaped),
|
||||
* reflects connection state on `[data-room-status]`, and sends a
|
||||
* `<form data-room-send>`'s named fields as JSON on submit (fields marked
|
||||
* `data-room-reset` clear after send). Optional `data-room-user` identifies
|
||||
* the connection.
|
||||
*
|
||||
* 2. Programmatic: `const room = wire.room("chat"); room.on("chat", fn);
|
||||
* room.send({ type: "chat", text })`. Handles connect, JSON, reconnect.
|
||||
*
|
||||
* Rebinds on `wrnexus:navigated` (client-side nav) and closes rooms whose
|
||||
* container has left the page.
|
||||
*/
|
||||
declare const REALTIME_RUNTIME: string;
|
||||
|
||||
/**
|
||||
* @wrnexus/csr — the browser reactive runtime.
|
||||
*
|
||||
* Components are `.wrn` files rendered on the SERVER (see @wrnexus/dev-server)
|
||||
* and hydrated in the browser by this single, generic runtime — served once at
|
||||
* `/__wrnexus/reactive.js` for any page that contains a `data-scope`. There are
|
||||
* no per-component browser bundles: SSR stays cleanly separated from CSR.
|
||||
*/
|
||||
|
||||
/** The reactive runtime served at `/__wrnexus/reactive.js` (plain browser JS). */
|
||||
declare function getReactiveRuntime(): string;
|
||||
/** The client-side navigation runtime served at `/__wrnexus/nav.js`. */
|
||||
declare function getNavRuntime(): string;
|
||||
/** The realtime client runtime served at `/__wrnexus/realtime.js`. */
|
||||
declare function getRealtimeRuntime(): string;
|
||||
|
||||
export { NAV_RUNTIME, REACTIVE_RUNTIME, REALTIME_RUNTIME, getNavRuntime, getReactiveRuntime, getRealtimeRuntime };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/csr</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>getReactiveRuntime(): string // → REACTIVE_RUNTIME
|
||||
getNavRuntime(): string // → NAV_RUNTIME
|
||||
getRealtimeRuntime(): string // → REALTIME_RUNTIME</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>wire.room(name): Room // open (or reuse) a room connection
|
||||
wire.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;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";
|
||||
|
||||
const routes: Record<string, string> = {
|
||||
"/__wrnexus/reactive.js": getReactiveRuntime(),
|
||||
"/__wrnexus/nav.js": getNavRuntime(),
|
||||
"/__wrnexus/realtime.js": getRealtimeRuntime(),
|
||||
};
|
||||
|
||||
Bun.serve({
|
||||
fetch(req) {
|
||||
const body = routes[new URL(req.url).pathname];
|
||||
if (body) {
|
||||
return new Response(body, {
|
||||
headers: { "content-type": "text/javascript; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#runtime-strings">Runtime strings</a><a class="toc-level-4" href="#accessor-functions">Accessor functions</a><a class="toc-level-4" href="#browser-reactive-directives">Browser: reactive directives</a><a class="toc-level-4" href="#browser-navigation">Browser: navigation</a><a class="toc-level-4" href="#browser-realtime-rooms">Browser: realtime rooms</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
page wrnexusdb {
|
||||
seo {
|
||||
title = "@wrnexus/db"
|
||||
description = "Database adapters, typed queries, models, migrations, and sessions."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Data</span><h1>@wrnexus/db</h1><p>Database adapters, typed queries, models, migrations, and sessions.</p><code>bun add @wrnexus/db@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Data</span><h1>@wrnexus/db</h1><p>Database adapters, typed queries, models, migrations, and sessions.</p><pre><code>bun add @wrnexus/db@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>The database layer for WRNexusJS: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based <code>Db</code> client, migrations, and a sqlc-style query generator.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/db</code> is the server-side data layer. You describe tables as TypeScript models (the <code>v</code> column builder + <code>table()</code>); those models drive migrations, coerce raw DB rows into typed objects, and feed the query generator. A thin <code>Driver</code> interface is implemented by adapters for SQLite (<code>bun:sqlite</code>), Postgres/MySQL (<code>Bun.SQL</code>), and MongoDB. The <code>Db</code> client adds ergonomics — model-mapped <code>all</code>/<code>one</code>, transactions, <code>createTable</code>, pagination, and batched relation loading. A process-wide registry (<code>getDb</code>/<code>setDb</code>) exposes configured connections to pages and API routes. Reach for it whenever a WRNexusJS app needs persistence.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/db</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>The core entry (<code>@wrnexus/db</code>) is dependency-free; adapters and connectors live in subpaths so importing the core doesn't pull in every driver.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Subpath</th><th>Exports</th></tr></thead>
|
||||
<tbody><tr><td><code>@wrnexus/db</code></td><td><code>v</code>, <code>table</code>, <code>Column</code>, <code>createDb</code>, <code>createTableSql</code>, the client registry (<code>setDb</code>/<code>getDb</code>/…), migrations, the query generator, and query helpers</td></tr><tr><td><code>@wrnexus/db/connect</code></td><td><code>connectFromConfig</code>, <code>resolveDbUrl</code>, <code>DbConfig</code> — resolve a config to a live SQL <code>Db</code></td></tr><tr><td><code>@wrnexus/db/session</code></td><td><code>sqliteSessionStore</code> — a <code>bun:sqlite</code> session backend for <code>@wrnexus/core</code></td></tr><tr><td><code>@wrnexus/db/sqlite</code></td><td><code>sqlite(url?)</code> driver</td></tr><tr><td><code>@wrnexus/db/postgres</code></td><td><code>postgres(url)</code> driver</td></tr><tr><td><code>@wrnexus/db/mysql</code></td><td><code>mysql(url)</code> driver</td></tr><tr><td><code>@wrnexus/db/mongo</code></td><td><code>mongo(url, dbName?)</code> document API</td></tr></tbody></table></div>
|
||||
<h4 id="schema-v-table-column">Schema — <code>v</code>, <code>table</code>, <code>Column</code></h4>
|
||||
<p><code>table(name, columns)</code> returns a <code>Model<T></code>. Columns are built with <code>v</code>:</p>
|
||||
<pre data-language="ts"><code>import { v, table } from "@wrnexus/db";
|
||||
|
||||
const users = table("users", {
|
||||
id: v.id(), // auto-increment primary key
|
||||
email: v.text().unique(),
|
||||
name: v.text().optional(), // NULLable
|
||||
age: v.int().default(0),
|
||||
active: v.bool().default(true),
|
||||
createdAt: v.timestamp().default("now"), // CURRENT_TIMESTAMP
|
||||
});</code></pre>
|
||||
<p>Column builders: <code>v.id</code>, <code>v.text</code> (alias <code>v.string</code>), <code>v.int</code>, <code>v.real</code> (alias <code>v.number</code>), <code>v.bool</code> (alias <code>v.boolean</code>), <code>v.timestamp</code>, <code>v.json</code>. <code>BaseType</code> values are <code>"id" | "text" | "int" | "real" | "bool" | "timestamp" | "json"</code>.</p>
|
||||
<p><code>Column</code> modifiers (chainable): <code>.optional()</code>, <code>.unique()</code>, <code>.default(value)</code> (use the sentinel <code>"now"</code> for a current-timestamp default), <code>.primaryKey()</code>, <code>.references(table, column = "id")</code>. <code>.coerce(raw)</code> converts a raw DB value to its JS type.</p>
|
||||
<p>A <code>Model<T></code> exposes: <code>name</code>, <code>columns</code>, <code>parse(row)</code> (coerces a raw row into a typed <code>T</code>; unknown columns pass through), and <code>describe()</code> (returns each column's <code>ColumnDef</code>, for migrations and the generator).</p>
|
||||
<h4 id="driver-client-createdb-db-driver">Driver & client — <code>createDb</code>, <code>Db</code>, <code>Driver</code></h4>
|
||||
<pre data-language="ts"><code>createDb(driver: Driver): Db</code></pre>
|
||||
<p>A <code>Driver</code> (implemented by adapters) exposes <code>dialect</code>, <code>query(sql, params?)</code>, <code>exec(sql, params?)</code>, <code>transaction(fn)</code>, and <code>close()</code>. <code>createDb</code> wraps it in a <code>Db</code>:</p>
|
||||
<ul>
|
||||
<li><code>all<T>(sql, params?, model?)</code> — all rows, mapped through <code>model.parse</code> when a model is given.</li>
|
||||
<li><code>one<T>(sql, params?, model?)</code> — first row or <code>null</code>.</li>
|
||||
<li><code>exec(sql, params?)</code> — <code>Promise<ExecResult></code> (<code>{ changes, lastInsertId? }</code>).</li>
|
||||
<li><code>tx(fn)</code> — run <code>fn(db)</code> in a transaction; rolls back on throw. Nested <code>tx</code> reuses the current transaction.</li>
|
||||
<li><code>createTable(model)</code> — runs the model's <code>CREATE TABLE IF NOT EXISTS</code> DDL.</li>
|
||||
<li><code>close()</code>.</li>
|
||||
</ul>
|
||||
<p>Every query is parameterized (positional params). <code>createTableSql(model, dialect, ifNotExists?)</code> renders <code>CREATE TABLE</code> directly; <code>Dialect</code> is <code>"sqlite" | "postgres" | "mysql"</code>.</p>
|
||||
<h4 id="client-registry-getdb-setdb">Client registry — <code>getDb</code> / <code>setDb</code></h4>
|
||||
<p>A process-wide registry the runtime configures at startup from <code>wrnexus.config.ts</code> (the <code>db</code> setting is the default; <code>databases.<name></code> entries are named).</p>
|
||||
<ul>
|
||||
<li><code>setDb(db)</code> / <code>setDb(name, db)</code> — set the default or a named connection.</li>
|
||||
<li><code>registerDb(name, db)</code> — alias of <code>setDb(name, db)</code>.</li>
|
||||
<li><code>getDb(name = "default")</code> — the default or a named <code>Db</code> (throws if unconfigured).</li>
|
||||
<li><code>hasDb(name?)</code>, <code>databaseNames()</code>, <code>closeDatabases()</code>.</li>
|
||||
</ul>
|
||||
<pre data-language="ts"><code>const users = await getDb().all("SELECT * FROM users");
|
||||
const events = await getDb("analytics").all("SELECT * FROM hits");</code></pre>
|
||||
<h4 id="adapters">Adapters</h4>
|
||||
<ul>
|
||||
<li><code>@wrnexus/db/sqlite</code> — <code>sqlite(url = ":memory:")</code>. <code>url</code> may be <code>file:./dev.db</code>, a raw path, or <code>:memory:</code>. Built on <code>bun:sqlite</code>; no external service.</li>
|
||||
<li><code>@wrnexus/db/postgres</code> — <code>postgres(url)</code> (e.g. <code>postgres://user:pass@host:5432/db</code>, placeholders <code>$N</code>).</li>
|
||||
<li><code>@wrnexus/db/mysql</code> — <code>mysql(url)</code> (e.g. <code>mysql://user:pass@host:3306/db</code>, placeholders <code>?</code>). Postgres/MySQL both use Bun's native <code>Bun.SQL</code> client and its pooled <code>begin()</code> for transactions.</li>
|
||||
<li><code>@wrnexus/db/mongo</code> — <code>mongo(url, dbName?)</code>. A document API, not SQL: <code>db.collection(model)</code> returns a <code>MongoRepo<T></code> with <code>find</code>, <code>findOne</code>, <code>insert</code>, <code>insertMany</code>, <code>update</code>, <code>delete</code>, <code>count</code>. Reads are coerced through <code>model.parse</code> (<code>_id</code> is mapped to <code>id</code>). The <code>mongodb</code> driver is imported lazily — install it to use Mongo.</li>
|
||||
</ul>
|
||||
<h4 id="migrations">Migrations</h4>
|
||||
<p>Migrations are <code>.sql</code> files (in e.g. <code>app/db/migrations</code>), each split into <code>-- +up</code> and <code>-- +down</code> sections. A file with no markers is treated entirely as <code>up</code>. Applied names are recorded in a <code>_wire_migrations</code> table so each runs once.</p>
|
||||
<ul>
|
||||
<li><code>parseMigration(name, content)</code> → <code>Migration</code> (<code>{ name, up, down }</code>).</li>
|
||||
<li><code>loadMigrations(dir)</code> — parse all <code>.sql</code> files, sorted by filename.</li>
|
||||
<li><code>appliedMigrations(db)</code> — applied names, oldest first.</li>
|
||||
<li><code>migrate(db, dir)</code> — apply all pending (each in a transaction); returns applied names.</li>
|
||||
<li><code>rollback(db, dir)</code> — roll back the most recent; returns its name or <code>null</code>.</li>
|
||||
<li><code>status(db, dir)</code> — <code>{ name, applied }[]</code> for every migration file.</li>
|
||||
<li><code>scaffoldMigration(dir, name, dialect, models?)</code> — write a new numbered migration; with <code>models</code> it generates <code>CREATE</code>/<code>DROP</code> for every table (referenced tables first via topological sort). Returns the file path.</li>
|
||||
</ul>
|
||||
<h4 id="query-generator-sqlc-style">Query generator (sqlc-style)</h4>
|
||||
<p>Turns annotated SQL into typed TS functions; params and result types are inferred from the models, and rows map back through <code>model.parse</code> when the selected columns are model columns.</p>
|
||||
<ul>
|
||||
<li><code>parseQueries(content)</code> → <code>QueryDef[]</code> from <code>-- name: X :one|:many|:exec</code> blocks.</li>
|
||||
<li><code>generateQueriesFile(queries, models, dialect)</code> → the <code>queries.gen.ts</code> source. <code>models</code> is a <code>ModelRef[]</code> (<code>{ varName, model }</code>). Rewrites <code>:name</code> placeholders to positional (<code>$N</code>/<code>?</code>) form.</li>
|
||||
</ul>
|
||||
<p><code>QueryKind</code> is <code>"one" | "many" | "exec"</code>.</p>
|
||||
<h4 id="query-helpers">Query helpers</h4>
|
||||
<ul>
|
||||
<li><code>paginate(db, { sql, params?, countSql?, model? }, opts?)</code> — offset pagination. Pass the base SELECT <strong>without</strong> a LIMIT; it appends the page window and derives <code>total</code> via a COUNT subquery. <code>PageOptions</code>: <code>{ page?, perPage?, maxPerPage? }</code> (defaults page 1, perPage 20, maxPerPage 100). Returns <code>Paginated<T></code> (<code>items, page, perPage, total, totalPages, hasNext, hasPrev</code>).</li>
|
||||
<li><code>loadRelated(db, parents, opts)</code> — load a relation for many parents in ONE query and attach it (no N+1). <code>RelationOptions</code>: <code>{ table, foreignKey, as, localKey?, single?, model? }</code> — <code>single: true</code> attaches one child (belongsTo), otherwise an array (hasMany). Table/foreign-key names are validated as identifiers.</li>
|
||||
</ul>
|
||||
<h4 id="session-store">Session store</h4>
|
||||
<p><code>@wrnexus/db/session</code> exports <code>sqliteSessionStore(path = "sessions.db")</code>, a persistent, process-shared <code>SessionBackend</code> (from <code>@wrnexus/core</code>) backed by <code>bun:sqlite</code> (WAL mode). Sessions survive restarts and are shared by every worker on the same file.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>Define models, connect, create tables, and query with typed results:</p>
|
||||
<pre data-language="ts"><code>import { v, table, createDb } from "@wrnexus/db";
|
||||
import { sqlite } from "@wrnexus/db/sqlite";
|
||||
|
||||
const users = table<{ id: number; email: string; name: string | null }>("users", {
|
||||
id: v.id(),
|
||||
email: v.text().unique(),
|
||||
name: v.text().optional(),
|
||||
createdAt: v.timestamp().default("now"),
|
||||
});
|
||||
|
||||
const db = createDb(sqlite("file:./dev.db"));
|
||||
await db.createTable(users);
|
||||
|
||||
await db.exec("INSERT INTO users (email) VALUES (?)", ["a@b.com"]);
|
||||
const list = await db.all("SELECT * FROM users", [], users); // rows typed + coerced
|
||||
const one = await db.one("SELECT * FROM users WHERE id = ?", [1], users);
|
||||
|
||||
await db.tx(async (tx) => {
|
||||
await tx.exec("UPDATE users SET name = ? WHERE id = ?", ["Ada", 1]);
|
||||
});</code></pre>
|
||||
<p>Resolve a config to a live SQL <code>Db</code>, and register it:</p>
|
||||
<pre data-language="ts"><code>import { connectFromConfig } from "@wrnexus/db/connect";
|
||||
import { setDb, getDb } from "@wrnexus/db";
|
||||
|
||||
setDb(connectFromConfig({ driver: "sqlite", url: "file:./dev.db" }, process.cwd()));
|
||||
const rows = await getDb().all("SELECT * FROM users");</code></pre>
|
||||
<p>Run migrations and paginate:</p>
|
||||
<pre data-language="ts"><code>import { migrate, paginate } from "@wrnexus/db";
|
||||
|
||||
await migrate(db, "app/db/migrations");
|
||||
const pageTwo = await paginate(
|
||||
db,
|
||||
{ sql: "SELECT * FROM users ORDER BY id", model: users },
|
||||
{ page: 2 },
|
||||
);</code></pre>
|
||||
<p>MongoDB (document API):</p>
|
||||
<pre data-language="ts"><code>import { mongo } from "@wrnexus/db/mongo";
|
||||
|
||||
const mdb = await mongo(process.env.MONGO_URL!, "app");
|
||||
const repo = mdb.collection(users);
|
||||
await repo.insert({ email: "a@b.com" });
|
||||
const active = await repo.find({ active: true });</code></pre>
|
||||
<h3 id="configuration">Configuration</h3>
|
||||
<p><code>connectFromConfig</code> (and the runtime) read a <code>DbConfig</code> (<code>{ driver, url }</code>) where <code>driver</code> is <code>sqlite | postgres | mysql</code>. <code>resolveDbUrl(url, appRoot?)</code> resolves a relative <code>file:</code>/<code>sqlite:</code> URL against the app root. MongoDB is not a SQL driver — use <code>@wrnexus/db/mongo</code> directly.</p>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Uses <code>bun:sqlite</code> (SQLite adapter + session store) and <code>Bun.SQL</code></li>
|
||||
<p>(Postgres/MySQL). Migrations/scaffolding use <code>node:fs</code>/<code>node:path</code>.</p>
|
||||
<li>Works with <code>@wrnexus/core</code> — <code>sqliteSessionStore</code> implements its</li>
|
||||
<p><code>SessionBackend</code>; <code>getDb</code>/<code>setDb</code> are wired by the WRNexusJS runtime from <code>wrnexus.config.ts</code>.</p>
|
||||
<li>The <code>mongodb</code> npm package is an optional, lazily-imported peer — install it</li>
|
||||
<p>only if you use <code>@wrnexus/db/mongo</code>. The core package stays dependency-free.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { M as Model } from './schema-tVurYsbL.js';
|
||||
export { B as BaseType, C as Column, a as ColumnDef, b as Columns, t as table, v } from './schema-tVurYsbL.js';
|
||||
import { a as Db, b as Dialect, R as Row } from './driver-DA53QHkO.js';
|
||||
export { D as Driver, E as ExecResult, T as TxHandle, c as createDb, d as createTableSql } from './driver-DA53QHkO.js';
|
||||
|
||||
/**
|
||||
* A process-wide database registry. The framework configures it at server
|
||||
* startup from `wrnexus.config.ts`: the `db` setting becomes the **default**
|
||||
* connection, and each entry under `databases` becomes a **named** connection.
|
||||
* Pages and API routes then call `getDb()` for the default, or `getDb("<name>")`
|
||||
* for a named one, to run queries (including the generated typed functions).
|
||||
*
|
||||
* const users = await getDb().all("SELECT * FROM users"); // default db
|
||||
* const events = await getDb("analytics").all("SELECT * FROM hits"); // named db
|
||||
*/
|
||||
|
||||
/** Set the default database (called by the runtime at startup). */
|
||||
declare function setDb(db: Db): Db;
|
||||
/** Set a named database (from `databases.<name>` in config). */
|
||||
declare function setDb(name: string, db: Db): Db;
|
||||
/** Register a named database. Alias of `setDb(name, db)` for readability. */
|
||||
declare function registerDb(name: string, db: Db): Db;
|
||||
/** The default database, or a named one. Throws if it isn't configured. */
|
||||
declare function getDb(name?: string): Db;
|
||||
/** Whether the default (or a named) database has been configured. */
|
||||
declare function hasDb(name?: string): boolean;
|
||||
/** Names of all configured databases (the default appears as "default"). */
|
||||
declare function databaseNames(): string[];
|
||||
/** Close every configured database and clear the registry. */
|
||||
declare function closeDatabases(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Migration runner. Migrations are `.sql` files in `app/db/migrations`, each
|
||||
* split into `-- +up` and `-- +down` sections. Applied migrations are recorded
|
||||
* in a `_wire_migrations` table so they run exactly once, newest-last.
|
||||
*
|
||||
* `scaffoldMigration(..., models)` writes an initial migration straight from the
|
||||
* TS models — the source of truth — so you don't hand-write the first schema.
|
||||
*/
|
||||
|
||||
interface Migration {
|
||||
name: string;
|
||||
up: string;
|
||||
down: string;
|
||||
}
|
||||
/** Split a migration file into its `up` and `down` SQL sections. */
|
||||
declare function parseMigration(name: string, content: string): Migration;
|
||||
/** Load and parse all migration files in a directory, sorted by filename. */
|
||||
declare function loadMigrations(dir: string): Migration[];
|
||||
/** Names of already-applied migrations, oldest first. */
|
||||
declare function appliedMigrations(db: Db): Promise<string[]>;
|
||||
/** Apply all pending migrations (each in a transaction). Returns applied names. */
|
||||
declare function migrate(db: Db, dir: string): Promise<string[]>;
|
||||
/** Roll back the most recently applied migration. Returns its name, or null. */
|
||||
declare function rollback(db: Db, dir: string): Promise<string | null>;
|
||||
/** Full status: every migration file with whether it has been applied. */
|
||||
declare function status(db: Db, dir: string): Promise<{
|
||||
name: string;
|
||||
applied: boolean;
|
||||
}[]>;
|
||||
/**
|
||||
* Write a new migration file. With `models`, the `up`/`down` are generated from
|
||||
* the TS models (create/drop every table); otherwise empty stubs are written.
|
||||
* Returns the created file path.
|
||||
*/
|
||||
declare function scaffoldMigration(dir: string, name: string, dialect: Dialect, models?: Model[]): string;
|
||||
|
||||
/**
|
||||
* sqlc-style query generator. Annotated SQL in `app/db/queries/*.sql` becomes
|
||||
* typed TS functions whose params + results are inferred from the TS models and
|
||||
* whose rows are mapped back through `model.parse`.
|
||||
*
|
||||
* -- name: GetUserByEmail :one
|
||||
* SELECT * FROM users WHERE email = :email;
|
||||
*
|
||||
* → GetUserByEmail(db, { email: string }): Promise<{…} | null>
|
||||
*
|
||||
* Type inference is best-effort (comparisons + INSERT column lists + SELECT list
|
||||
* vs the model); anything it can't resolve becomes `unknown`.
|
||||
*/
|
||||
|
||||
type QueryKind = "one" | "many" | "exec";
|
||||
interface QueryDef {
|
||||
name: string;
|
||||
kind: QueryKind;
|
||||
sql: string;
|
||||
}
|
||||
/** A model plus the variable name it is exported under (for imports). */
|
||||
interface ModelRef {
|
||||
varName: string;
|
||||
model: Model;
|
||||
}
|
||||
/** Parse annotated queries from one `.sql` file's contents. */
|
||||
declare function parseQueries(content: string): QueryDef[];
|
||||
/** Generate the full `queries.gen.ts` source. */
|
||||
declare function generateQueriesFile(queries: QueryDef[], models: ModelRef[], dialect: Dialect): string;
|
||||
|
||||
/**
|
||||
* Query ergonomics built on the `Db` client: offset pagination and a batched
|
||||
* relation loader (avoids N+1). Both are dialect-aware — placeholders follow the
|
||||
* driver's style (`$N` for Postgres, `?` for SQLite/MySQL).
|
||||
*/
|
||||
|
||||
interface PageOptions {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
/** Upper bound on perPage. Default 100. */
|
||||
maxPerPage?: number;
|
||||
}
|
||||
interface Paginated<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
/**
|
||||
* Paginate a SELECT. Pass the base query WITHOUT a LIMIT; the helper appends the
|
||||
* page window and derives the total with a COUNT over the same query.
|
||||
*
|
||||
* await paginate(db, { sql: "SELECT * FROM users ORDER BY name", model: users }, { page: 2 })
|
||||
*/
|
||||
declare function paginate<T = Row>(db: Db, query: {
|
||||
sql: string;
|
||||
params?: unknown[];
|
||||
countSql?: string;
|
||||
model?: Model<T>;
|
||||
}, opts?: PageOptions): Promise<Paginated<T>>;
|
||||
interface RelationOptions<C> {
|
||||
/** Parent field whose value matches the child's foreign key. Default "id". */
|
||||
localKey?: string;
|
||||
/** Child table to load from. */
|
||||
table: string;
|
||||
/** Child column that references the parent. */
|
||||
foreignKey: string;
|
||||
/** Property name to attach on each parent. */
|
||||
as: string;
|
||||
/** true → attach a single child (belongsTo); false → an array (hasMany). */
|
||||
single?: boolean;
|
||||
/** Map child rows through a model. */
|
||||
model?: Model<C>;
|
||||
}
|
||||
/**
|
||||
* Load a relation for a set of parent rows in ONE query and attach it to each
|
||||
* parent (no N+1). Returns the same parents, each with `opts.as` populated.
|
||||
*
|
||||
* await loadRelated(db, users, { table: "posts", foreignKey: "userId", as: "posts" })
|
||||
*/
|
||||
declare function loadRelated<P extends Row, C extends Row = Row>(db: Db, parents: P[], opts: RelationOptions<C>): Promise<(P & Record<string, C | C[] | null>)[]>;
|
||||
|
||||
export { Db, Dialect, type Migration, Model, type ModelRef, type PageOptions, type Paginated, type QueryDef, type QueryKind, type RelationOptions, Row, appliedMigrations, closeDatabases, databaseNames, generateQueriesFile, getDb, hasDb, loadMigrations, loadRelated, migrate, paginate, parseMigration, parseQueries, registerDb, rollback, scaffoldMigration, setDb, status };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/db</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>import { v, table } from "@wrnexus/db";
|
||||
|
||||
const users = table("users", {
|
||||
id: v.id(), // auto-increment primary key
|
||||
email: v.text().unique(),
|
||||
name: v.text().optional(), // NULLable
|
||||
age: v.int().default(0),
|
||||
active: v.bool().default(true),
|
||||
createdAt: v.timestamp().default("now"), // CURRENT_TIMESTAMP
|
||||
});</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>createDb(driver: Driver): Db</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>const users = await getDb().all("SELECT * FROM users");
|
||||
const events = await getDb("analytics").all("SELECT * FROM hits");</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#schema-v-table-column">Schema — v, table, Column</a><a class="toc-level-4" href="#driver-client-createdb-db-driver">Driver & client — createDb, Db, Driver</a><a class="toc-level-4" href="#client-registry-getdb-setdb">Client registry — getDb / setDb</a><a class="toc-level-4" href="#adapters">Adapters</a><a class="toc-level-4" href="#migrations">Migrations</a><a class="toc-level-4" href="#query-generator-sqlc-style">Query generator (sqlc-style)</a><a class="toc-level-4" href="#query-helpers">Query helpers</a><a class="toc-level-4" href="#session-store">Session store</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#configuration">Configuration</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
page wrnexusdevserver {
|
||||
seo {
|
||||
title = "@wrnexus/dev-server"
|
||||
description = "Development and production servers, HMR, assets, and gateways."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Runtime</span><h1>@wrnexus/dev-server</h1><p>Development and production servers, HMR, assets, and gateways.</p><code>bun add @wrnexus/dev-server@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Runtime</span><h1>@wrnexus/dev-server</h1><p>Development and production servers, HMR, assets, and gateways.</p><pre><code>bun add @wrnexus/dev-server@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>The WRNexusJS HTTP + WebSocket server runtime — request dispatch, SSR document assembly, live-reload (HMR), and the portable production handler.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p>This package is the server runtime that powers a WRNexusJS app in both development and production. A single <strong>request runtime</strong> (<code>createHandlers</code>) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about _how_ modules and assets are produced, so the dev and prod entry points wire in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app <strong>gateway</strong> (route several apps by <code>Host</code> header behind one port) and a portable <code>node:http</code> adapter for WinterCG hosts. It is entirely server-side and Bun-native (<code>Bun.serve</code>, <code>Bun.file</code>, <code>Bun.gzipSync</code>).</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/dev-server</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported for the full server; the <code>node:http</code> adapter is for WinterCG embedding only).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="main-entry-wrnexus-dev-server">Main entry (<code>@wrnexus/dev-server</code>)</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Kind</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>startServer(opts: ServeOptions)</code></td><td><code>Promise<RunningServer></code></td><td>Start the dev server on <code>Bun.serve</code>: builds the router, connects/migrates databases, wires assets + HMR, and starts the file watcher.</td></tr><tr><td><code>createHandlers(deps: RuntimeDeps)</code></td><td><code>Handlers</code></td><td>The shared request runtime (fetch + websocket handlers). Re-exported from <code>runtime.ts</code>.</td></tr><tr><td><code>createProductionServer(manifest, opts)</code></td><td><code>Bun.Server</code></td><td>Start the production server from a precompiled manifest.</td></tr><tr><td><code>createProductionHandlers(manifest, opts)</code></td><td><code>Handlers</code></td><td>Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam).</td></tr><tr><td><code>startGateway(opts: GatewayOptions)</code></td><td><code>Promise<RunningGateway></code></td><td>Boot multiple apps as child processes and route by <code>Host</code>.</td></tr><tr><td><code>toRequest</code>, <code>writeResponse</code>, <code>nodeListener</code>, <code>serveNode</code></td><td>functions</td><td><code>node:http</code> ↔ WinterCG <code>Request</code>/<code>Response</code> adapter.</td></tr><tr><td><code>RESTART_EXIT_CODE</code></td><td><code>number</code> (<code>97</code>)</td><td>Exit code the dev child uses to ask the supervisor for a fresh process.</td></tr><tr><td><code>STYLES_HREF</code>, <code>HMR_CLIENT_JS</code></td><td>constants</td><td>The global stylesheet URL and the inline HMR client script.</td></tr></tbody></table></div>
|
||||
<p>Exported types: <code>ServeOptions</code>, <code>RunningServer</code>, <code>RuntimeDeps</code>, <code>AssetServer</code>, <code>WsData</code>, <code>GatewayApp</code>, <code>GatewayOptions</code>, <code>GatewayAuth</code>, <code>GatewaySecurity</code>, <code>RunningGateway</code>, <code>FetchHandler</code>.</p>
|
||||
<h4 id="startserver-opts"><code>startServer(opts)</code></h4>
|
||||
<pre data-language="ts"><code>interface ServeOptions {
|
||||
appDir: string; // absolute/relative path to the app/ dir
|
||||
port?: number; // default 3000
|
||||
hostname?: string; // default "localhost"
|
||||
mode?: Mode; // "development" | "production"; default "development"
|
||||
hmr?: boolean; // inject live-reload client; default (mode === "development")
|
||||
styleEntry?: string | null; // resolved absolute path to the global CSS entry
|
||||
stylesConfig?: StylesConfig; // custom styles processor (e.g. Tailwind/PostCSS)
|
||||
head?: string; // raw HTML appended to every page <head>
|
||||
seo?: SeoConfig; // global SEO defaults
|
||||
security?: SecurityConfig; // security headers + CORS policy
|
||||
theme?: ThemeConfig; // design-token theme (merged over built-in light/dark)
|
||||
i18n?: I18nConfig; // default language + supported locales
|
||||
db?: { driver: string; url: string }; // default db → getDb(); dev auto-migrates
|
||||
databases?: Record<string, { driver: string; url: string }>; // named dbs → getDb("<name>")
|
||||
realtime?: { scale?: boolean; redisUrl?: string }; // bridge rooms over Redis across processes
|
||||
}
|
||||
|
||||
interface RunningServer {
|
||||
port: number;
|
||||
hostname: string;
|
||||
url: string;
|
||||
router: Router;
|
||||
stop(): void;
|
||||
}</code></pre>
|
||||
<p>In development, <code>startServer</code> also connects <code>app/db/migrations</code> (and <code>app/db/<name>/migrations</code>) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live; any other server change triggers <code>process.exit(RESTART_EXIT_CODE)</code> so the dev supervisor (<code>@wrnexus/cli</code>) respawns the process with fresh modules.</p>
|
||||
<h4 id="createhandlers-deps"><code>createHandlers(deps)</code></h4>
|
||||
<p>The core runtime shared by dev and prod. It handles CORS preflight, <code>/healthz</code> and <code>/__wrnexus/health</code>, request-body size limits (413), HMR socket upgrades (<code>/__wrnexus/hmr</code>), realtime WebSocket upgrades (<code>defineRoom</code> default export or a raw <code>websocket</code> export), the middleware pipeline, API routes (<code>/api/*</code>), framework assets (<code>/__wrnexus/*</code>), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).</p>
|
||||
<pre data-language="ts"><code>interface RuntimeDeps {
|
||||
mode: Mode;
|
||||
hmr: boolean; // inject the live-reload client into pages
|
||||
router: Router;
|
||||
loadModule(file: string): Promise<Record<string, unknown>>;
|
||||
getMiddleware(): Promise<Middleware[]>;
|
||||
assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
|
||||
hasStyles?: boolean; // inject the global stylesheet link
|
||||
hasUi?: boolean; // inject the Wire UI stylesheet (/__wrnexus/ui.css)
|
||||
theme?: ResolvedTheme; // enables /__wrnexus/theme.css + <html data-theme>
|
||||
i18n?: ResolvedI18n; // enables ctx.t, <html lang>, {t:key} markers
|
||||
inlineStyles?: string; // inline small prod stylesheets into <head>
|
||||
assetVersion?: string; // cache-busting ?v= on framework asset URLs
|
||||
head?: string; // raw HTML appended to every page <head>
|
||||
seo?: SeoConfig;
|
||||
security?: SecurityConfig;
|
||||
maxBodyBytes?: number; // 413 above this; default 10 MB
|
||||
hub?: HmrHub; // browser HMR sockets (dev only)
|
||||
realtimeBus?: RealtimeBus; // cross-process room bridge (Redis pub/sub)
|
||||
}
|
||||
|
||||
interface Handlers {
|
||||
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
|
||||
websocket: { open; message; close; drain };
|
||||
}</code></pre>
|
||||
<p><code>WsData</code> is the per-connection socket tag — a discriminated union of <code>{ kind: "realtime"; handler }</code>, <code>{ kind: "room"; meta }</code>, or <code>{ kind: "hmr" }</code>.</p>
|
||||
<h4 id="createproductionserver-manifest-opts-createproductionhandlers-manifest-opts"><code>createProductionServer(manifest, opts)</code> / <code>createProductionHandlers(manifest, opts)</code></h4>
|
||||
<p>Production runs the _same_ request runtime as dev, but with no filesystem scan and no runtime bundling. <code>wrnexus build</code> emits an entry that statically imports every route/component/layout module and passes them as a <code>ProdManifest</code>; the route-matching tables are rebuilt from the raw patterns.</p>
|
||||
<pre data-language="ts"><code>interface ProdManifest {
|
||||
pages: { raw: string; mod: RouteModule }[];
|
||||
api: { raw: string; mod: RouteModule }[];
|
||||
realtime: { raw: string; mod: RouteModule }[];
|
||||
middleware: Middleware[];
|
||||
components: { name: string; mod: RouteModule }[];
|
||||
layouts: { name: string; mod: RouteModule }[];
|
||||
}
|
||||
|
||||
interface ProdOptions {
|
||||
stylesPath?: string;
|
||||
inlineStyles?: string;
|
||||
reactivePath?: string;
|
||||
themePath?: string;
|
||||
themeJsPath?: string;
|
||||
theme?: ResolvedTheme;
|
||||
uiCssPath?: string;
|
||||
schemasJs?: string;
|
||||
i18n?: ResolvedI18n;
|
||||
db?: { driver: string; url: string };
|
||||
databases?: Record<string, { driver: string; url: string }>;
|
||||
realtime?: { scale?: boolean; redisUrl?: string };
|
||||
assetVersion?: string;
|
||||
publicDir?: string;
|
||||
head?: string;
|
||||
seo?: SeoConfig;
|
||||
security?: SecurityConfig;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
maxBodyBytes?: number;
|
||||
}</code></pre>
|
||||
<p><code>createProductionServer</code> also loads the <code>.env</code> cascade for the <code>production</code> profile, installs <code>SIGTERM</code>/<code>SIGINT</code> graceful shutdown, and binds <code>0.0.0.0</code> (port from <code>opts.port</code> or <code>$PORT</code>, default 3000). Migrations are <strong>not</strong> run here — apply them first (<code>wrnexus db migrate</code>). <code>createProductionHandlers</code> returns the bare handlers for edge/serverless/<code>node:http</code> deployment.</p>
|
||||
<h4 id="startgateway-opts-multi-app-gateway"><code>startGateway(opts)</code> — multi-app gateway</h4>
|
||||
<p>Serves several apps behind one port and routes each request to the right app by its <code>Host</code> header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. Apps communicate at runtime via <code>@wrnexus/pubsub</code> (use the Redis driver so messages cross processes).</p>
|
||||
<pre data-language="ts"><code>interface GatewayOptions {
|
||||
port?: number; // default 3000
|
||||
hostname?: string; // default "localhost"
|
||||
mode?: "development" | "production";
|
||||
apps: GatewayApp[];
|
||||
security?: GatewaySecurity;
|
||||
}
|
||||
|
||||
interface GatewayApp {
|
||||
name: string; // app id (for logs)
|
||||
dir: string; // app root (contains app/ + wrnexus.config.ts)
|
||||
domains: string[]; // host names routed here
|
||||
port?: number; // fixed internal port; else assigned
|
||||
auth?: GatewayAuth; // per-app edge access control
|
||||
}
|
||||
|
||||
interface GatewayAuth {
|
||||
basic?: { user: string; pass: string } | Array<{ user: string; pass: string }>;
|
||||
allowIps?: string[]; // exact-match IP allowlist
|
||||
forward?: { url: string }; // forward-auth (SSO): 2xx allows
|
||||
}
|
||||
|
||||
interface GatewaySecurity {
|
||||
trustedHostsOnly?: boolean; // 404 unknown hosts instead of first app
|
||||
rateLimit?: { max: number; windowMs?: number }; // global by client IP (429)
|
||||
headers?: boolean; // add baseline edge security headers
|
||||
forwardedHeaders?: boolean; // set X-Forwarded-* (default true)
|
||||
accessLog?: boolean;
|
||||
}</code></pre>
|
||||
<p>The gateway exposes <code>/__gateway/health</code> (JSON list of routed apps) and returns a <code>RunningGateway</code> (<code>{ port, url, stop() }</code>).</p>
|
||||
<h4 id="node-http-adapter-from-adapters-node-ts"><code>node:http</code> adapter (from <code>./adapters/node.ts</code>)</h4>
|
||||
<p>For embedding the WinterCG handler behind an existing Node server or a WinterCG host. Note the full app still needs Bun-compatible globals (<code>Bun.file</code>, <code>bun:sqlite</code>, etc.); only the <code>Request</code>/<code>Response</code> conversion is fully portable.</p>
|
||||
<pre data-language="ts"><code>type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;
|
||||
|
||||
toRequest(req: IncomingMessage, opts?): Promise<Request>
|
||||
writeResponse(res: ServerResponse, response: Response): Promise<void> // preserves multiple Set-Cookie
|
||||
nodeListener(handler: FetchHandler, opts?): (req, res) => Promise<void>
|
||||
serveNode(handler: FetchHandler, opts?): Promise<Server></code></pre>
|
||||
<h4 id="subpath-export-wrnexus-dev-server-serve-entry">Subpath export: <code>@wrnexus/dev-server/serve-entry</code></h4>
|
||||
<p>The child process the dev supervisor launches:</p>
|
||||
<pre data-language="bash"><code>bun run serve-entry.ts <appDir> <port> <mode></code></pre>
|
||||
<p>It loads the optional <code>wrnexus.config.ts</code>, resolves the style entry, calls <code>startServer</code>, and prints the route table (Pages / API / Realtime / Components). Because it runs in its own process, every restart re-imports all route modules fresh — that is how the supervisor delivers live reload of edited server code. <code>startGateway</code> resolves this entry via <code>import.meta.resolve("@wrnexus/dev-server/serve-entry")</code> to spawn each dev app.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<h4 id="programmatic-dev-server">Programmatic dev server</h4>
|
||||
<pre data-language="ts"><code>import { startServer } from "@wrnexus/dev-server";
|
||||
|
||||
const server = await startServer({
|
||||
appDir: "./app",
|
||||
port: 3000,
|
||||
mode: "development",
|
||||
theme: {/* design tokens */},
|
||||
db: { driver: "sqlite", url: "file:./data/app.db" },
|
||||
});
|
||||
|
||||
console.log(`Running at ${server.url}`);
|
||||
// server.stop();</code></pre>
|
||||
<h4 id="production-server-from-a-build-manifest">Production server from a build manifest</h4>
|
||||
<pre data-language="ts"><code>import { createProductionServer } from "@wrnexus/dev-server";
|
||||
import { manifest } from "./dist/manifest.js"; // generated by `wrnexus build`
|
||||
|
||||
createProductionServer(manifest, {
|
||||
stylesPath: "./dist/styles.css",
|
||||
reactivePath: "./dist/reactive.js",
|
||||
assetVersion: process.env.BUILD_ID,
|
||||
db: { driver: "postgres", url: process.env.DATABASE_URL! },
|
||||
port: Number(process.env.PORT) || 3000,
|
||||
});</code></pre>
|
||||
<h4 id="embedding-the-handler-on-node-http">Embedding the handler on <code>node:http</code></h4>
|
||||
<pre data-language="ts"><code>import { createProductionHandlers, serveNode } from "@wrnexus/dev-server";
|
||||
|
||||
const handlers = createProductionHandlers(manifest, opts);
|
||||
await serveNode(handlers.fetch, { port: 8080 });</code></pre>
|
||||
<h4 id="multi-app-gateway">Multi-app gateway</h4>
|
||||
<pre data-language="ts"><code>import { startGateway } from "@wrnexus/dev-server";
|
||||
|
||||
await startGateway({
|
||||
port: 3000,
|
||||
apps: [
|
||||
{ name: "web", dir: "./apps/web", domains: ["localhost", "web.localhost"] },
|
||||
{
|
||||
name: "admin",
|
||||
dir: "./apps/admin",
|
||||
domains: ["admin.localhost"],
|
||||
auth: { basic: { user: "root", pass: "s3cret" } },
|
||||
},
|
||||
],
|
||||
security: { trustedHostsOnly: true, rateLimit: { max: 600 } },
|
||||
});</code></pre>
|
||||
<h3 id="framework-asset-routes">Framework asset routes</h3>
|
||||
<p>The runtime serves these framework-owned paths (dev builds them live; prod serves pre-built/immutable versions):</p>
|
||||
<ul>
|
||||
<li><code>/__wrnexus/nav.js</code>, <code>/__wrnexus/reactive.js</code>, <code>/__wrnexus/realtime.js</code> — client runtimes</li>
|
||||
<li><code>/__wrnexus/validate.js</code>, <code>/__wrnexus/schemas.js</code>, <code>/__wrnexus/i18n.js</code> — validation + i18n runtimes</li>
|
||||
<li><code>/__wrnexus/theme.css</code>, <code>/__wrnexus/theme.js</code>, <code>/__wrnexus/ui.css</code>, <code>/__wrnexus/styles.css</code> — styles</li>
|
||||
<li><code>/__wrnexus/hmr</code> — dev-only HMR WebSocket</li>
|
||||
<li><code>/__wrnexus/csr</code> — server-evaluated CSR bindings for browser-side API fetches</li>
|
||||
</ul>
|
||||
<p>Pages get only the scripts they use: <code>nav.js</code> always, <code>reactive.js</code> when a page has a <code>data-scope</code>/CSR fetch, plus theme/validation/i18n/realtime runtimes when the relevant markup is present.</p>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Uses <code>Bun.serve</code> (HTTP + WebSocket), <code>Bun.file</code>, and <code>Bun.gzipSync</code>. The full app also relies on <code>bun:sqlite</code> / <code>Bun.SQL</code> via <code>@wrnexus/db</code>.</li>
|
||||
<li>Orchestrates the whole framework: <code>@wrnexus/core</code> (context, security, realtime registry), <code>@wrnexus/router</code>, <code>@wrnexus/ssr</code> (<code>renderDocument</code>), <code>@wrnexus/csr</code> (client runtimes), <code>@wrnexus/compiler</code> (<code>.wrn</code> → TS), <code>@wrnexus/styles</code>, <code>@wrnexus/ui</code>, <code>@wrnexus/validation</code>, <code>@wrnexus/i18n</code>, <code>@wrnexus/db</code>, and <code>@wrnexus/pubsub</code> (Redis-backed cross-process realtime).</li>
|
||||
<li><code>.wrn</code> files are compiled to TypeScript into a hidden sibling <code>.wrnexus/</code> cache dir and dynamically imported; the module cache means each edited server module needs a fresh process (dev) — hence the restart-on-change model.</li>
|
||||
<li>Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via <code>Cache-Control: no-transform</code>.</li>
|
||||
<p></content></p>
|
||||
</ul>
|
||||
<p></invoke></p></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { Mode, Middleware, SeoConfig, SecurityConfig, RealtimeBus, RealtimeConnectMeta } from '@wrnexus/core';
|
||||
import { Router } from '@wrnexus/router';
|
||||
import { ResolvedTheme, MobileConfig, PwaConfig, StylesConfig, ThemeConfig } from '@wrnexus/styles';
|
||||
import { ResolvedI18n, I18nConfig } from '@wrnexus/i18n';
|
||||
import { StorageConfig } from '@wrnexus/uploader';
|
||||
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
||||
|
||||
/**
|
||||
* HMR hub — tracks connected browser HMR sockets and broadcasts update events.
|
||||
*
|
||||
* Each open page holds one WebSocket to `/__wrnexus/hmr`. The in-process file
|
||||
* watcher (see index.ts) classifies a change and broadcasts a typed message:
|
||||
*
|
||||
* { type: "css" } -> the browser hot-swaps the stylesheet (no reload)
|
||||
* { type: "reload" } -> the browser asks for fresh HTML over the HMR socket
|
||||
*
|
||||
* Server-logic changes (pages/api/middleware/realtime) are NOT broadcast here:
|
||||
* they require a fresh process, so the child exits and the supervisor respawns
|
||||
* it. The browser then reconnects and performs a soft DOM morph automatically.
|
||||
*/
|
||||
type HmrMessage = {
|
||||
type: "css";
|
||||
version: number;
|
||||
} | {
|
||||
type: "reload";
|
||||
version: number;
|
||||
};
|
||||
/** Minimal shape of a Bun ServerWebSocket we rely on. */
|
||||
interface Socket {
|
||||
send(data: string): unknown;
|
||||
}
|
||||
declare class HmrHub {
|
||||
private sockets;
|
||||
private version;
|
||||
add(ws: Socket): void;
|
||||
remove(ws: Socket): void;
|
||||
broadcast(message: HmrMessage): void;
|
||||
get size(): number;
|
||||
css(): void;
|
||||
reload(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared request runtime used by BOTH the dev server and the production server.
|
||||
*
|
||||
* It owns the HTTP/WebSocket dispatch and the SSR document assembly, but knows
|
||||
* nothing about *how* modules or assets are produced — those come in via
|
||||
* `RuntimeDeps`. Dev wires in dynamic module loading + on-the-fly bundling;
|
||||
* prod wires in a static manifest + pre-built chunks on disk.
|
||||
*/
|
||||
|
||||
/** A realtime module's `websocket` export: a bag of optional lifecycle hooks. */
|
||||
type WsHandler = Record<string, (...args: any[]) => unknown>;
|
||||
/**
|
||||
* Per-connection socket data. A socket is either an app realtime connection or
|
||||
* an internal HMR connection — discriminated by `kind`.
|
||||
*/
|
||||
type WsData = {
|
||||
kind: "realtime";
|
||||
handler: WsHandler;
|
||||
} | {
|
||||
kind: "room";
|
||||
meta: RealtimeConnectMeta;
|
||||
} | {
|
||||
kind: "hmr";
|
||||
baseUrl: string;
|
||||
headers: [string, string][];
|
||||
};
|
||||
type RouteModule$1 = Record<string, unknown>;
|
||||
/** Serves framework-owned assets under `/__wrnexus/*` (islands, reactive, hmr). */
|
||||
interface AssetServer {
|
||||
serve(pathname: string): Promise<Response | null>;
|
||||
}
|
||||
interface RuntimeDeps {
|
||||
mode: Mode;
|
||||
/** When true, inject the live-reload client into rendered pages. */
|
||||
hmr: boolean;
|
||||
router: Router;
|
||||
/** Load a route module by absolute path (dev: dynamic import; prod: manifest). */
|
||||
loadModule(file: string): Promise<RouteModule$1>;
|
||||
/** Resolve the ordered middleware chain. */
|
||||
getMiddleware(): Promise<Middleware[]>;
|
||||
/** Serve `/__wrnexus/*` assets. */
|
||||
assets: AssetServer;
|
||||
/** When true, inject the global stylesheet link into every page head. */
|
||||
hasStyles?: boolean;
|
||||
/** When true, inject the Wire UI stylesheet link (`/__wrnexus/ui.css`). */
|
||||
hasUi?: boolean;
|
||||
/** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */
|
||||
theme?: ResolvedTheme;
|
||||
/** Resolved i18n bundle: enables `ctx.t`, `<html lang>`, and `{t:key}` markers. */
|
||||
i18n?: ResolvedI18n;
|
||||
/** Small production stylesheets can be inlined to avoid a render-blocking request. */
|
||||
inlineStyles?: string;
|
||||
/** Production cache-busting version appended to framework asset URLs. */
|
||||
assetVersion?: string;
|
||||
/** Raw HTML appended to every page head (e.g. CDN framework links). */
|
||||
head?: string;
|
||||
/** Global SEO defaults. */
|
||||
seo?: SeoConfig;
|
||||
mobile?: MobileConfig;
|
||||
pwa?: PwaConfig | false;
|
||||
/** Framework security headers and CORS policy. */
|
||||
security?: SecurityConfig;
|
||||
/** Max request body size in bytes (413 above this). Default 10 MB. */
|
||||
maxBodyBytes?: number;
|
||||
/** HMR hub for browser live-update sockets (dev only). */
|
||||
hub?: HmrHub;
|
||||
/**
|
||||
* Cross-process realtime bus. When provided, room broadcasts/`toUser` sends are
|
||||
* bridged to it so they reach clients on every app process/instance sharing the
|
||||
* bus (use the Redis pub/sub driver). Enables realtime across multiple apps.
|
||||
*/
|
||||
realtimeBus?: RealtimeBus;
|
||||
}
|
||||
interface UpgradeServer {
|
||||
upgrade(req: Request, opts: {
|
||||
data: WsData;
|
||||
}): boolean;
|
||||
/** Bun's per-request socket peer address (used for the non-spoofable client IP). */
|
||||
requestIP?(req: Request): {
|
||||
address: string;
|
||||
} | null;
|
||||
}
|
||||
/** The subset of Bun's ServerWebSocket the runtime touches. */
|
||||
interface Ws {
|
||||
data: WsData;
|
||||
send(data: string | Uint8Array): unknown;
|
||||
close(code?: number, reason?: string): void;
|
||||
}
|
||||
interface Handlers {
|
||||
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
|
||||
websocket: {
|
||||
open(ws: Ws): void;
|
||||
message(ws: Ws, message: string | Uint8Array): void;
|
||||
close(ws: Ws, code?: number, reason?: string): void;
|
||||
drain(ws: Ws): void;
|
||||
};
|
||||
}
|
||||
/** Build the fetch + websocket handlers from a set of dependencies. */
|
||||
declare function createHandlers(deps: RuntimeDeps): Handlers;
|
||||
|
||||
/**
|
||||
* The multi-app **gateway** — serves several WRNexusJS apps behind one port and
|
||||
* routes each request to the right app by its `Host` header (domain). This is how
|
||||
* a monorepo becomes a multi-domain SaaS: `app-a.com` → apps/a, `app-b.com` → apps/b.
|
||||
*
|
||||
* Each app runs as its own **process** (full isolation — its own database
|
||||
* registry, pubsub, in-memory state), and the gateway is a thin host-based
|
||||
* reverse proxy for both HTTP and WebSocket. Apps talk to each other at runtime
|
||||
* via @wrnexus/pubsub (use the Redis driver so messages cross processes).
|
||||
*/
|
||||
/** Per-app access control, enforced at the gateway before proxying. */
|
||||
interface GatewayAuth {
|
||||
/** HTTP Basic auth — one or more allowed user/password pairs. */
|
||||
basic?: {
|
||||
user: string;
|
||||
pass: string;
|
||||
} | Array<{
|
||||
user: string;
|
||||
pass: string;
|
||||
}>;
|
||||
/** Allow only these client IPs (exact match; others get 403). */
|
||||
allowIps?: string[];
|
||||
/**
|
||||
* Forward-auth (SSO): the gateway GETs `url` forwarding the request's cookies +
|
||||
* Authorization; a 2xx allows the request, anything else blocks it (its status
|
||||
* is returned). Point it at your own verify endpoint.
|
||||
*/
|
||||
forward?: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
interface GatewayApp {
|
||||
/** App id (for logs). */
|
||||
name: string;
|
||||
/** Path to the app root (the dir containing `app/` and wrnexus.config.ts). */
|
||||
dir: string;
|
||||
/** Host names routed to this app (e.g. ["localhost", "web.localhost"]). */
|
||||
domains: string[];
|
||||
/** Optional fixed internal port; otherwise assigned from the gateway port. */
|
||||
port?: number;
|
||||
/** Access control enforced at the edge for this app. */
|
||||
auth?: GatewayAuth;
|
||||
}
|
||||
/** Gateway-wide security controls, enforced for every app. */
|
||||
interface GatewaySecurity {
|
||||
/** Reject requests whose Host matches no app (404) instead of routing to the first. */
|
||||
trustedHostsOnly?: boolean;
|
||||
/** Global rate limit by client IP (429 over the limit). */
|
||||
rateLimit?: {
|
||||
max: number;
|
||||
windowMs?: number;
|
||||
};
|
||||
/** Add baseline security headers to responses (only where the app didn't set them). */
|
||||
headers?: boolean;
|
||||
/** Set X-Forwarded-For/Host/Proto so apps see the real client. Default true. */
|
||||
forwardedHeaders?: boolean;
|
||||
/** Log each request (host → app, method, path, status). */
|
||||
accessLog?: boolean;
|
||||
}
|
||||
interface GatewayOptions {
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
mode?: "development" | "production";
|
||||
apps: GatewayApp[];
|
||||
security?: GatewaySecurity;
|
||||
}
|
||||
interface RunningGateway {
|
||||
port: number;
|
||||
url: string;
|
||||
stop(): void;
|
||||
}
|
||||
/** Boot every app as a child process, then route by Host on one gateway port. */
|
||||
declare function startGateway(opts: GatewayOptions): Promise<RunningGateway>;
|
||||
|
||||
/**
|
||||
* @wrnexus/dev-server/prod — the production server (Point 4).
|
||||
*
|
||||
* Unlike dev, there is NO filesystem scan and NO on-the-fly bundling at runtime.
|
||||
* `wrnexus build` generates an entry that statically imports every route and
|
||||
* component module and hands them here as a manifest. We rebuild the (cheap)
|
||||
* route-matching tables from the raw patterns and run the exact same request
|
||||
* runtime as dev — just with production error pages and no live-reload client.
|
||||
*/
|
||||
|
||||
type RouteModule = Record<string, unknown>;
|
||||
interface ManifestRoute {
|
||||
/** URL pattern, e.g. `/users/[id]`. */
|
||||
raw: string;
|
||||
/** The statically-imported route module. */
|
||||
mod: RouteModule;
|
||||
}
|
||||
interface ProdManifest {
|
||||
pages: ManifestRoute[];
|
||||
api: ManifestRoute[];
|
||||
realtime: ManifestRoute[];
|
||||
middleware: Middleware[];
|
||||
/** Server-rendered components, statically imported and keyed by name. */
|
||||
components: {
|
||||
name: string;
|
||||
mod: RouteModule;
|
||||
}[];
|
||||
/** Named page layouts (from app/layouts/*.wrn). */
|
||||
layouts: {
|
||||
name: string;
|
||||
mod: RouteModule;
|
||||
}[];
|
||||
}
|
||||
interface ProdOptions {
|
||||
/** Absolute path to the pre-built global stylesheet, if any. */
|
||||
stylesPath?: string;
|
||||
/** Small production stylesheet inlined into the document head. */
|
||||
inlineStyles?: string;
|
||||
/** Absolute path to the pre-built reactive runtime. */
|
||||
reactivePath?: string;
|
||||
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
|
||||
themePath?: string;
|
||||
/** Absolute path to the pre-built theme runtime (`theme.js`). */
|
||||
themeJsPath?: string;
|
||||
/** Resolved theme config: enables `<html data-theme>` + `theme.css` link. */
|
||||
theme?: ResolvedTheme;
|
||||
/** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
|
||||
uiCssPath?: string;
|
||||
/** Pre-built `window.__wireSchemas = {...}` script for client validation. */
|
||||
schemasJs?: string;
|
||||
/** Resolved i18n bundle (default lang + locale messages). */
|
||||
i18n?: ResolvedI18n;
|
||||
/** Default database connection (driver + url); enables `getDb()`. */
|
||||
db?: {
|
||||
driver: string;
|
||||
url: string;
|
||||
};
|
||||
/** Named databases, reached with `getDb("<name>")`. */
|
||||
databases?: Record<string, {
|
||||
driver: string;
|
||||
url: string;
|
||||
}>;
|
||||
/**
|
||||
* Absolute path to the default db's migrations bundled into the build
|
||||
* (`dist/migrations`). When set, they are applied on startup — like dev.
|
||||
*/
|
||||
migrationsDir?: string;
|
||||
/** Bundled migrations dirs for named dbs (name → `dist/db/<name>/migrations`). */
|
||||
databaseMigrationDirs?: Record<string, string>;
|
||||
/**
|
||||
* Auto-apply bundled migrations on server startup (default: true). Set false
|
||||
* for deploys that migrate in a separate release step (e.g. multiple instances
|
||||
* behind a load balancer, where you migrate once before rolling out).
|
||||
*/
|
||||
autoMigrate?: boolean;
|
||||
/** Realtime scaling: bridge room broadcasts over Redis across app processes. */
|
||||
realtime?: {
|
||||
scale?: boolean;
|
||||
redisUrl?: string;
|
||||
};
|
||||
/** File-upload storage: named stores (local dir / S3). Local dirs resolve against cwd. */
|
||||
storage?: StorageConfig;
|
||||
/** Cache-busting version appended to framework asset URLs. */
|
||||
assetVersion?: string;
|
||||
/** Absolute path to copied public assets, if any. */
|
||||
publicDir?: string;
|
||||
/** Raw HTML appended to every page head. */
|
||||
head?: string;
|
||||
/** Global SEO defaults. */
|
||||
seo?: SeoConfig;
|
||||
mobile?: MobileConfig;
|
||||
pwa?: PwaConfig | false;
|
||||
/** Framework security headers and CORS policy. */
|
||||
security?: SecurityConfig;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
maxBodyBytes?: number;
|
||||
}
|
||||
/**
|
||||
* Build the portable request handler from a precompiled manifest — a
|
||||
* WinterCG-style `fetch(request) => Response` plus the websocket handlers, with
|
||||
* NO server bound. This is the deployment-adapter seam: `createProductionServer`
|
||||
* wraps it in `Bun.serve`, `serveNode` bridges it onto `node:http`, and edge or
|
||||
* serverless targets can call `fetch` directly.
|
||||
*/
|
||||
declare function createProductionHandlers(manifest: ProdManifest, opts: ProdOptions): ReturnType<typeof createHandlers>;
|
||||
/** Start the production server on Bun from a precompiled manifest. */
|
||||
declare function createProductionServer(manifest: ProdManifest, opts: ProdOptions): Promise<Bun.Server<WsData>>;
|
||||
|
||||
/**
|
||||
* node:http adapter — bridge a WinterCG `fetch(request) => Response` handler
|
||||
* onto a Node HTTP server, with no external dependencies. Converts a Node
|
||||
* `IncomingMessage` into a web `Request` and writes a web `Response` back into a
|
||||
* `ServerResponse` (preserving multiple `Set-Cookie` headers).
|
||||
*
|
||||
* Caveat: the production handler uses Bun-native APIs (Bun.file for assets,
|
||||
* Bun.serve for websockets, Bun.SQL / bun:sqlite for the database), so running
|
||||
* the FULL app under plain Node needs Bun-compatible globals. This adapter is
|
||||
* for WinterCG hosts and for embedding the handler behind an existing
|
||||
* `node:http` server; the Request/Response conversion itself is fully portable.
|
||||
*/
|
||||
|
||||
type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;
|
||||
/** Convert a Node IncomingMessage into a web Request (buffers the body). */
|
||||
declare function toRequest(req: IncomingMessage, opts?: {
|
||||
origin?: string;
|
||||
}): Promise<Request>;
|
||||
/** Write a web Response into a Node ServerResponse. */
|
||||
declare function writeResponse(res: ServerResponse, response: Response): Promise<void>;
|
||||
/** A `node:http` request listener that dispatches to a fetch handler. */
|
||||
declare function nodeListener(handler: FetchHandler, opts?: {
|
||||
origin?: string;
|
||||
}): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
||||
/** Create and start a `node:http` server for a fetch handler. */
|
||||
declare function serveNode(handler: FetchHandler, opts?: {
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
}): Promise<Server>;
|
||||
|
||||
/**
|
||||
* @wrnexus/dev-server — the development HTTP + WebSocket server.
|
||||
*
|
||||
* Thin Bun.serve wrapper around the shared runtime (runtime.ts). Dynamic module
|
||||
* loading makes it fast to iterate; the dev supervisor (see @wrnexus/cli)
|
||||
* restarts this process on file changes.
|
||||
*/
|
||||
|
||||
/** Exit code the child uses to ask the dev supervisor for a fresh process. */
|
||||
declare const RESTART_EXIT_CODE = 97;
|
||||
interface ServeOptions {
|
||||
appDir: string;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
mode?: Mode;
|
||||
/** Inject the live-reload client (defaults to true in development). */
|
||||
hmr?: boolean;
|
||||
/** Resolved absolute path to the global CSS entry, or null. */
|
||||
styleEntry?: string | null;
|
||||
/** Custom styles config (e.g. a Tailwind/PostCSS processor). */
|
||||
stylesConfig?: StylesConfig;
|
||||
/** Raw HTML appended to every page head (from wrnexus.config.ts). */
|
||||
head?: string;
|
||||
/** Global SEO defaults. */
|
||||
seo?: SeoConfig;
|
||||
/** Framework security headers and CORS policy. */
|
||||
security?: SecurityConfig;
|
||||
/** Design-token theme config (merged over the built-in light/dark). */
|
||||
theme?: ThemeConfig;
|
||||
/** i18n config (default language + supported locales). */
|
||||
i18n?: I18nConfig;
|
||||
/** Default database connection (driver + url). Enables `getDb()` and dev auto-migrate. */
|
||||
db?: {
|
||||
driver: string;
|
||||
url: string;
|
||||
};
|
||||
/** Named databases, reached with `getDb("<name>")`; migrations under app/db/<name>/. */
|
||||
databases?: Record<string, {
|
||||
driver: string;
|
||||
url: string;
|
||||
}>;
|
||||
/** Realtime scaling: bridge room broadcasts over Redis across app processes. */
|
||||
realtime?: {
|
||||
scale?: boolean;
|
||||
redisUrl?: string;
|
||||
};
|
||||
/** File-upload storage: named stores (local dir / S3), reached with `getStore()`. */
|
||||
storage?: StorageConfig;
|
||||
mobile?: MobileConfig;
|
||||
pwa?: PwaConfig | false;
|
||||
}
|
||||
interface RunningServer {
|
||||
port: number;
|
||||
hostname: string;
|
||||
url: string;
|
||||
router: Router;
|
||||
stop(): void;
|
||||
}
|
||||
declare function startServer(opts: ServeOptions): Promise<RunningServer>;
|
||||
|
||||
export { type AssetServer, type FetchHandler, type GatewayApp, type GatewayAuth, type GatewayOptions, type GatewaySecurity, RESTART_EXIT_CODE, type RunningGateway, type RunningServer, type RuntimeDeps, type ServeOptions, type WsData, createHandlers, createProductionHandlers, createProductionServer, nodeListener, serveNode, startGateway, startServer, toRequest, writeResponse };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/dev-server</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>interface ServeOptions {
|
||||
appDir: string; // absolute/relative path to the app/ dir
|
||||
port?: number; // default 3000
|
||||
hostname?: string; // default "localhost"
|
||||
mode?: Mode; // "development" | "production"; default "development"
|
||||
hmr?: boolean; // inject live-reload client; default (mode === "development")
|
||||
styleEntry?: string | null; // resolved absolute path to the global CSS entry
|
||||
stylesConfig?: StylesConfig; // custom styles processor (e.g. Tailwind/PostCSS)
|
||||
head?: string; // raw HTML appended to every page <head>
|
||||
seo?: SeoConfig; // global SEO defaults
|
||||
security?: SecurityConfig; // security headers + CORS policy
|
||||
theme?: ThemeConfig; // design-token theme (merged over built-in light/dark)
|
||||
i18n?: I18nConfig; // default language + supported locales
|
||||
db?: { driver: string; url: string }; // default db → getDb(); dev auto-migrates
|
||||
databases?: Record<string, { driver: string; url: string }>; // named dbs → getDb("<name>")
|
||||
realtime?: { scale?: boolean; redisUrl?: string }; // bridge rooms over Redis across processes
|
||||
}
|
||||
|
||||
interface RunningServer {
|
||||
port: number;
|
||||
hostname: string;
|
||||
url: string;
|
||||
router: Router;
|
||||
stop(): void;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>interface RuntimeDeps {
|
||||
mode: Mode;
|
||||
hmr: boolean; // inject the live-reload client into pages
|
||||
router: Router;
|
||||
loadModule(file: string): Promise<Record<string, unknown>>;
|
||||
getMiddleware(): Promise<Middleware[]>;
|
||||
assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
|
||||
hasStyles?: boolean; // inject the global stylesheet link
|
||||
hasUi?: boolean; // inject the Wire UI stylesheet (/__wrnexus/ui.css)
|
||||
theme?: ResolvedTheme; // enables /__wrnexus/theme.css + <html data-theme>
|
||||
i18n?: ResolvedI18n; // enables ctx.t, <html lang>, {t:key} markers
|
||||
inlineStyles?: string; // inline small prod stylesheets into <head>
|
||||
assetVersion?: string; // cache-busting ?v= on framework asset URLs
|
||||
head?: string; // raw HTML appended to every page <head>
|
||||
seo?: SeoConfig;
|
||||
security?: SecurityConfig;
|
||||
maxBodyBytes?: number; // 413 above this; default 10 MB
|
||||
hub?: HmrHub; // browser HMR sockets (dev only)
|
||||
realtimeBus?: RealtimeBus; // cross-process room bridge (Redis pub/sub)
|
||||
}
|
||||
|
||||
interface Handlers {
|
||||
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
|
||||
websocket: { open; message; close; drain };
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>interface ProdManifest {
|
||||
pages: { raw: string; mod: RouteModule }[];
|
||||
api: { raw: string; mod: RouteModule }[];
|
||||
realtime: { raw: string; mod: RouteModule }[];
|
||||
middleware: Middleware[];
|
||||
components: { name: string; mod: RouteModule }[];
|
||||
layouts: { name: string; mod: RouteModule }[];
|
||||
}
|
||||
|
||||
interface ProdOptions {
|
||||
stylesPath?: string;
|
||||
inlineStyles?: string;
|
||||
reactivePath?: string;
|
||||
themePath?: string;
|
||||
themeJsPath?: string;
|
||||
theme?: ResolvedTheme;
|
||||
uiCssPath?: string;
|
||||
schemasJs?: string;
|
||||
i18n?: ResolvedI18n;
|
||||
db?: { driver: string; url: string };
|
||||
databases?: Record<string, { driver: string; url: string }>;
|
||||
realtime?: { scale?: boolean; redisUrl?: string };
|
||||
assetVersion?: string;
|
||||
publicDir?: string;
|
||||
head?: string;
|
||||
seo?: SeoConfig;
|
||||
security?: SecurityConfig;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
maxBodyBytes?: number;
|
||||
}</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#main-entry-wrnexus-dev-server">Main entry (@wrnexus/dev-server)</a><a class="toc-level-4" href="#startserver-opts">startServer(opts)</a><a class="toc-level-4" href="#createhandlers-deps">createHandlers(deps)</a><a class="toc-level-4" href="#createproductionserver-manifest-opts-createproductionhandlers-manifest-opts">createProductionServer(manifest, opts) / createProductionHandlers(manifest, opts)</a><a class="toc-level-4" href="#startgateway-opts-multi-app-gateway">startGateway(opts) — multi-app gateway</a><a class="toc-level-4" href="#node-http-adapter-from-adapters-node-ts">node:http adapter (from ./adapters/node.ts)</a><a class="toc-level-4" href="#subpath-export-wrnexus-dev-server-serve-entry">Subpath export: @wrnexus/dev-server/serve-entry</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#programmatic-dev-server">Programmatic dev server</a><a class="toc-level-4" href="#production-server-from-a-build-manifest">Production server from a build manifest</a><a class="toc-level-4" href="#embedding-the-handler-on-node-http">Embedding the handler on node:http</a><a class="toc-level-4" href="#multi-app-gateway">Multi-app gateway</a><a class="toc-level-3" href="#framework-asset-routes">Framework asset routes</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
page wrnexusencryption {
|
||||
seo {
|
||||
title = "@wrnexus/encryption"
|
||||
description = "Hashing, HMAC, authenticated encryption, and key derivation."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Security</span><h1>@wrnexus/encryption</h1><p>Hashing, HMAC, authenticated encryption, and key derivation.</p><code>bun add @wrnexus/encryption@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Security</span><h1>@wrnexus/encryption</h1><p>Hashing, HMAC, authenticated encryption, and key derivation.</p><pre><code>bun add @wrnexus/encryption@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Dependency-free crypto helpers for WRNexusJS: authenticated symmetric encryption (AES-256-GCM), hashing, and HMAC signing.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p>This package provides small, focused cryptographic primitives for server-side use: encrypting secrets/tokens/database fields at rest with AES-256-GCM, deriving keys from passwords via PBKDF2, computing SHA-256 digests, and signing/verifying payloads with HMAC-SHA256. It is built entirely on the standard <strong>Web Crypto API</strong> (<code>crypto.subtle</code>) plus <code>btoa</code>/<code>atob</code> and <code>TextEncoder</code>/<code>TextDecoder</code> — no third-party dependencies. Reach for it whenever you need to protect sensitive values or verify webhook signatures. All functions are <code>async</code> (Web Crypto is promise-based).</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/encryption</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>All keys are exchanged as <strong>base64 strings</strong> and all digests/signatures as <strong>hex strings</strong>.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>generateKey</code></td><td><code>() => Promise<string></code></td><td>Generate a random 256-bit AES key, base64-encoded. Store it as a secret.</td></tr><tr><td><code>deriveKey</code></td><td><code>(password: string, salt: string) => Promise<string></code></td><td>Derive a base64 AES-256 key from a password + salt using PBKDF2 (100,000 iterations, SHA-256).</td></tr><tr><td><code>encrypt</code></td><td><code>(plaintext: string, key: string) => Promise<string></code></td><td>AES-256-GCM encrypt a string. Returns base64 of <code>iv(12 bytes) ‖ ciphertext+tag</code>. A fresh random IV is used each call.</td></tr><tr><td><code>decrypt</code></td><td><code>(payload: string, key: string) => Promise<string></code></td><td>Decrypt a value produced by <code>encrypt</code>. Throws if the key is wrong or the data was tampered with.</td></tr><tr><td><code>sha256</code></td><td><code>(data: string) => Promise<string></code></td><td>SHA-256 hex digest of a string (e.g. content hashing, dedup keys).</td></tr><tr><td><code>hmacSign</code></td><td><code>(data: string, secret: string) => Promise<string></code></td><td>HMAC-SHA256 hex signature of <code>data</code> with <code>secret</code> (e.g. signing webhooks).</td></tr><tr><td><code>hmacVerify</code></td><td><code>(data: string, secret: string, signature: string) => Promise<boolean></code></td><td>Constant-time verify of an HMAC-SHA256 hex signature.</td></tr></tbody></table></div>
|
||||
<p>Notes:</p>
|
||||
<ul>
|
||||
<li><code>generateKey</code> produces a 32-byte (256-bit) key via <code>crypto.getRandomValues</code>.</li>
|
||||
<li><code>encrypt</code>/<code>decrypt</code> require a base64-encoded 256-bit key; anything else throws <code>"Encryption key must be a base64 256-bit key"</code>.</li>
|
||||
<li><code>decrypt</code> throws <code>"Invalid ciphertext"</code> if the payload is shorter than the 12-byte IV, and the underlying Web Crypto call throws on any authentication (tag) mismatch.</li>
|
||||
<li><code>hmacVerify</code> compares in constant time (length check plus XOR accumulation) to avoid timing leaks.</li>
|
||||
</ul>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>Symmetric encryption of a secret at rest:</p>
|
||||
<pre data-language="ts"><code>import { generateKey, encrypt, decrypt } from "@wrnexus/encryption";
|
||||
|
||||
const key = await generateKey(); // store this safely (env/secret manager)
|
||||
|
||||
const box = await encrypt("card #1234", key); // opaque base64 string, safe to persist
|
||||
const plain = await decrypt(box, key); // "card #1234"</code></pre>
|
||||
<p>Deriving a key from a user password instead of a random key:</p>
|
||||
<pre data-language="ts"><code>import { deriveKey, encrypt } from "@wrnexus/encryption";
|
||||
|
||||
const key = await deriveKey("correct horse battery staple", "per-user-salt");
|
||||
const box = await encrypt("secret note", key);</code></pre>
|
||||
<p>Hashing and webhook signature verification:</p>
|
||||
<pre data-language="ts"><code>import { sha256, hmacSign, hmacVerify } from "@wrnexus/encryption";
|
||||
|
||||
const digest = await sha256("some content"); // 64-char hex string
|
||||
|
||||
const signature = await hmacSign(rawBody, webhookSecret);
|
||||
const ok = await hmacVerify(rawBody, webhookSecret, incomingSignatureHeader);
|
||||
if (!ok) throw new Error("Invalid webhook signature");</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Relies on the Web Crypto API (<code>crypto.subtle</code>, <code>crypto.getRandomValues</code>) and the global <code>btoa</code>/<code>atob</code>, <code>TextEncoder</code>/<code>TextDecoder</code> — all available in Bun's runtime.</li>
|
||||
<li><strong>No dependencies.</strong> The package has an empty dependency set; nothing is bundled beyond standard runtime APIs.</li>
|
||||
<li>Algorithms: AES-256-GCM (encryption), PBKDF2 with 100k SHA-256 iterations (key derivation), SHA-256 (digest), HMAC-SHA256 (signing).</li>
|
||||
<li>Keep generated/derived keys and HMAC secrets out of source control; treat them as first-class secrets.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* @wrnexus/encryption — authenticated symmetric encryption (AES-256-GCM) via
|
||||
* WebCrypto, dependency-free. Use it to encrypt secrets, tokens, or database
|
||||
* fields at rest.
|
||||
*
|
||||
* const key = await generateKey(); // store this safely
|
||||
* const box = await encrypt("card #1234", key); // opaque base64 string
|
||||
* const plain = await decrypt(box, key); // "card #1234"
|
||||
*
|
||||
* A key derived from a password (PBKDF2) is also supported via `deriveKey`.
|
||||
*/
|
||||
/** SHA-256 hex digest of a string (e.g. content hashing, dedup keys). */
|
||||
declare function sha256(data: string): Promise<string>;
|
||||
/** HMAC-SHA256 hex signature of `data` with `secret` (e.g. signing webhooks). */
|
||||
declare function hmacSign(data: string, secret: string): Promise<string>;
|
||||
/** Constant-time verify of an HMAC-SHA256 signature. */
|
||||
declare function hmacVerify(data: string, secret: string, signature: string): Promise<boolean>;
|
||||
/** Generate a random 256-bit key, base64-encoded. Store it as a secret. */
|
||||
declare function generateKey(): Promise<string>;
|
||||
/**
|
||||
* Encrypt a string. Output is base64 of `iv(12) || ciphertext+tag`, safe to
|
||||
* store or transmit. Each call uses a fresh random IV.
|
||||
*/
|
||||
declare function encrypt(plaintext: string, key: string): Promise<string>;
|
||||
/** Decrypt a value produced by `encrypt`. Throws if the key is wrong or data tampered. */
|
||||
declare function decrypt(payload: string, key: string): Promise<string>;
|
||||
/** Derive a base64 AES key from a password + salt (PBKDF2, 100k iterations). */
|
||||
declare function deriveKey(password: string, salt: string): Promise<string>;
|
||||
|
||||
export { decrypt, deriveKey, encrypt, generateKey, hmacSign, hmacVerify, sha256 };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/encryption</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>import { generateKey, encrypt, decrypt } from "@wrnexus/encryption";
|
||||
|
||||
const key = await generateKey(); // store this safely (env/secret manager)
|
||||
|
||||
const box = await encrypt("card #1234", key); // opaque base64 string, safe to persist
|
||||
const plain = await decrypt(box, key); // "card #1234"</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>import { deriveKey, encrypt } from "@wrnexus/encryption";
|
||||
|
||||
const key = await deriveKey("correct horse battery staple", "per-user-salt");
|
||||
const box = await encrypt("secret note", key);</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import { sha256, hmacSign, hmacVerify } from "@wrnexus/encryption";
|
||||
|
||||
const digest = await sha256("some content"); // 64-char hex string
|
||||
|
||||
const signature = await hmacSign(rawBody, webhookSecret);
|
||||
const ok = await hmacVerify(rawBody, webhookSecret, incomingSignatureHeader);
|
||||
if (!ok) throw new Error("Invalid webhook signature");</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
page wrnexusi18n {
|
||||
seo {
|
||||
title = "@wrnexus/i18n"
|
||||
description = "Translation loading, locale resolution, and Intl formatting."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h1>@wrnexus/i18n</h1><p>Translation loading, locale resolution, and Intl formatting.</p><code>bun add @wrnexus/i18n@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend</span><h1>@wrnexus/i18n</h1><p>Translation loading, locale resolution, and Intl formatting.</p><pre><code>bun add @wrnexus/i18n@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Per-request translations plus locale-aware number, date, and currency formatting for WRNexusJS apps.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/i18n</code> loads locale files from <code>app/locales/<lang>.json</code>, resolves the active language for each request (cookie → <code>Accept-Language</code> → default), and builds a <code>t(key, params)</code> translator used both in server code and in <code>.wrn</code> views. It also ships Intl-based formatting helpers and a tiny client runtime that wires up a language switcher. Translation lookup, language resolution, and HTML marker rewriting run server-side; only the small <code>I18N_RUNTIME</code> snippet runs in the browser.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/i18n</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="loading-resolving">Loading & resolving</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>loadLocales</code></td><td><code>(dir: string) => Record<string, Messages></code></td><td>Reads every <code><lang>.json</code> in <code>dir</code> into a <code>{ lang: messages }</code> map. Missing dir → <code>{}</code>; a bad file is warned and skipped.</td></tr><tr><td><code>resolveI18n</code></td><td><code>(messages: Record<string, Messages>, config?: I18nConfig) => ResolvedI18n</code></td><td>Merges loaded messages + config into a resolved bundle (default lang, supported langs, messages).</td></tr><tr><td><code>resolveLang</code></td><td>`(i18n: ResolvedI18n, cookieValue: string \</td><td>undefined, acceptLanguage: string \</td><td>null) => string`</td><td>Picks the active language: matching cookie → best <code>Accept-Language</code> tag (falls back to base tag, e.g. <code>en-US</code> → <code>en</code>) → <code>i18n.default</code>.</td></tr><tr><td><code>makeT</code></td><td><code>(i18n: ResolvedI18n, lang: string) => TFunction</code></td><td>Builds a translator resolving current language → default → the key itself, with <code>{param}</code> interpolation.</td></tr></tbody></table></div>
|
||||
<h4 id="types-constants">Types & constants</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>Messages</code></td><td><code>type</code></td><td><code>Record<string, unknown></code> — a locale's messages (supports nested/dotted keys).</td></tr><tr><td><code>I18nConfig</code></td><td><code>interface</code></td><td><code>{ default?: string; locales?: string[] }</code>.</td></tr><tr><td><code>ResolvedI18n</code></td><td><code>interface</code></td><td><code>{ default: string; langs: string[]; messages: Record<string, Messages> }</code>.</td></tr><tr><td><code>LANG_COOKIE</code></td><td><code>const</code></td><td><code>"wire-lang"</code> — the cookie the language is read from / written to.</td></tr><tr><td><code>I18N_JS_HREF</code></td><td><code>const</code></td><td><code>"/__wrnexus/i18n.js"</code> — URL the client runtime is served at.</td></tr></tbody></table></div>
|
||||
<h4 id="html-client-runtime">HTML & client runtime</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>translateHtml</code></td><td><code>(html: string, t: TFunction) => string</code></td><td>Rewrites markers in rendered HTML: <code>t:<attr>="key"</code> → <code><attr>="<translation>"</code> (attribute-escaped) and <code><tag data-t="key">…</tag></code> → element text becomes the translation (HTML-escaped). No-op unless a marker is present.</td></tr><tr><td><code>renderI18nData</code></td><td><code>(i18n: ResolvedI18n, lang: string) => string</code></td><td>JS snippet setting <code>window.__wireI18n = { lang, langs, default }</code> for the client switcher.</td></tr><tr><td><code>I18N_RUNTIME</code></td><td><code>const string</code></td><td>Browser IIFE that binds <code>[data-wire-lang-set="es"]</code> clicks and <code>select[data-wire-lang]</code> changes to set the <code>wire-lang</code> cookie and reload. Exposes <code>window.__wireLang.set(lang)</code>.</td></tr></tbody></table></div>
|
||||
<h4 id="formatting-helpers-re-exported-from-format-ts">Formatting helpers (re-exported from <code>./format.ts</code>)</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Example</th></tr></thead>
|
||||
<tbody><tr><td><code>formatNumber</code></td><td><code>(value: number, lang: string, options?: Intl.NumberFormatOptions) => string</code></td><td><code>1234.5 → "1,234.5"</code></td></tr><tr><td><code>formatCurrency</code></td><td><code>(value: number, currency: string, lang: string) => string</code></td><td><code>9.99, "USD" → "$9.99"</code></td></tr><tr><td><code>formatDate</code></td><td>`(value: Date \</td><td>number \</td><td>string, lang: string, options?: Intl.DateTimeFormatOptions) => string`</td><td>defaults to <code>{ dateStyle: "medium" }</code></td></tr><tr><td><code>formatRelativeTime</code></td><td><code>(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string) => string</code></td><td><code>-3, "day" → "3 days ago"</code> (<code>numeric: "auto"</code>)</td></tr><tr><td><code>plural</code></td><td><code>(count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>>, lang: string) => string</code></td><td>picks CLDR form; <code>#</code> is replaced by <code>count</code></td></tr></tbody></table></div>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<h4 id="server-load-resolve-translate">Server: load, resolve, translate</h4>
|
||||
<pre data-language="ts"><code>import {
|
||||
loadLocales,
|
||||
resolveI18n,
|
||||
resolveLang,
|
||||
makeT,
|
||||
translateHtml,
|
||||
LANG_COOKIE,
|
||||
} from "@wrnexus/i18n";
|
||||
|
||||
// app/locales/en.json, app/locales/es.json
|
||||
const messages = loadLocales("app/locales");
|
||||
const i18n = resolveI18n(messages, { default: "en", locales: ["en", "es"] });
|
||||
|
||||
// Per request:
|
||||
const lang = resolveLang(i18n, req.cookies?.[LANG_COOKIE], req.headers.get("accept-language"));
|
||||
const t = makeT(i18n, lang);
|
||||
|
||||
t("nav.home"); // dotted key → "Home"
|
||||
t("greeting", { name: "Ada" }); // "Hello, {name}" → "Hello, Ada"
|
||||
|
||||
// After rendering a .wrn view, resolve translation markers in the HTML:
|
||||
const finalHtml = translateHtml(renderedHtml, t);</code></pre>
|
||||
<p><code>app/locales/en.json</code>:</p>
|
||||
<pre data-language="json"><code>{
|
||||
"nav": { "home": "Home" },
|
||||
"greeting": "Hello, {name}"
|
||||
}</code></pre>
|
||||
<h4 id="views-translation-markers">Views: translation markers</h4>
|
||||
<pre data-language="html"><code><h1 data-t="nav.home">Home</h1>
|
||||
<input t:placeholder="search.placeholder" /></code></pre>
|
||||
<p><code>translateHtml</code> replaces the element text for <code>data-t</code> and the attribute value for any <code>t:<attr></code> (e.g. <code>t:placeholder</code>, <code>t:aria-label</code>).</p>
|
||||
<h4 id="client-language-switcher">Client: language switcher</h4>
|
||||
<pre data-language="ts"><code>import { renderI18nData, I18N_RUNTIME, I18N_JS_HREF } from "@wrnexus/i18n";
|
||||
|
||||
// In the document <head>:
|
||||
const head = `
|
||||
<script>${renderI18nData(i18n, lang)}</script>
|
||||
<script src="${I18N_JS_HREF}"></script>
|
||||
`;
|
||||
|
||||
// Serve I18N_RUNTIME at I18N_JS_HREF; then in markup:
|
||||
// <button data-wire-lang-set="es">Español</button>
|
||||
// <select data-wire-lang>…</select></code></pre>
|
||||
<h4 id="formatting">Formatting</h4>
|
||||
<pre data-language="ts"><code>import {
|
||||
formatNumber,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
plural,
|
||||
} from "@wrnexus/i18n";
|
||||
|
||||
formatNumber(1234.5, lang); // "1,234.5"
|
||||
formatCurrency(9.99, "USD", lang); // "$9.99"
|
||||
formatDate(Date.now(), lang); // "Jul 4, 2026"
|
||||
formatRelativeTime(-3, "day", lang); // "3 days ago"
|
||||
plural(2, { one: "# item", other: "# items" }, lang); // "2 items"</code></pre>
|
||||
<h3 id="configuration">Configuration</h3>
|
||||
<p><code>resolveI18n</code> accepts an <code>I18nConfig</code>:</p>
|
||||
<ul>
|
||||
<li><code>default</code> — fallback language; used when nothing else matches. Ignored if it has</li>
|
||||
<p>no loaded messages, in which case the first supported language is used.</p>
|
||||
<li><code>locales</code> — explicit supported-language list; defaults to the loaded locale names.</li>
|
||||
</ul>
|
||||
<p>Language resolution order at request time (<code>resolveLang</code>): a supported <code>wire-lang</code> cookie value → the first matching <code>Accept-Language</code> tag (or its base subtag) → the resolved default.</p>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Locale loading uses <code>node:fs</code> (<code>existsSync</code>, <code>readdirSync</code>,</li>
|
||||
<p><code>readFileSync</code>) and <code>node:path</code>; formatting relies on the platform <code>Intl</code> APIs.</p>
|
||||
<li>Works with [<code>@wrnexus/core</code>](../core) — <code>TFunction</code> (the <code>t(key, params)</code> type)</li>
|
||||
<p>comes from core, and the resolved translator is exposed as <code>ctx.t</code> / <code>ctx.lang</code> in request handling.</p>
|
||||
<li>Nested message objects are supported: keys are looked up whole first, then split</li>
|
||||
<p>on <code>.</code> to walk the object tree.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { TFunction } from '@wrnexus/core';
|
||||
|
||||
/**
|
||||
* Locale-aware formatting helpers (Intl-based) + pluralization. Pair with the
|
||||
* request language (`ctx.lang`) so numbers, dates, and currencies render right
|
||||
* for each user.
|
||||
*/
|
||||
/** Format a number for a locale (e.g. 1234.5 → "1,234.5"). */
|
||||
declare function formatNumber(value: number, lang: string, options?: Intl.NumberFormatOptions): string;
|
||||
/** Format a currency amount (e.g. 9.99, "USD" → "$9.99"). */
|
||||
declare function formatCurrency(value: number, currency: string, lang: string): string;
|
||||
/** Format a date/timestamp for a locale. */
|
||||
declare function formatDate(value: Date | number | string, lang: string, options?: Intl.DateTimeFormatOptions): string;
|
||||
/** Relative time, e.g. -3 days → "3 days ago" (localized). */
|
||||
declare function formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string): string;
|
||||
/**
|
||||
* Pick a plural form for `count` in `lang` using CLDR rules, e.g.
|
||||
* `plural(n, { one: "1 item", other: "# items" }, lang)` — "#" is replaced by n.
|
||||
*/
|
||||
declare function plural(count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>>, lang: string): string;
|
||||
|
||||
/**
|
||||
* @wrnexus/i18n — translations for pages and API responses.
|
||||
*
|
||||
* Locales live in `app/locales/<lang>.json`. Per request the active language is
|
||||
* resolved from the `wire-lang` cookie, then Accept-Language, then the default.
|
||||
* `ctx.t(key, params)` translates on the server; in `.wrn` views `{t:key}` and
|
||||
* `t:attr="key"` markers are resolved by `translateHtml` before the HTML is sent.
|
||||
*/
|
||||
|
||||
type Messages = Record<string, unknown>;
|
||||
/** Load `<dir>/<lang>.json` files into a `{ lang: messages }` map. */
|
||||
declare function loadLocales(dir: string): Record<string, Messages>;
|
||||
interface I18nConfig {
|
||||
/** Default language, used as the fallback and when nothing else matches. */
|
||||
default?: string;
|
||||
/** Explicit set of supported languages (defaults to the loaded locale names). */
|
||||
locales?: string[];
|
||||
}
|
||||
interface ResolvedI18n {
|
||||
default: string;
|
||||
langs: string[];
|
||||
messages: Record<string, Messages>;
|
||||
}
|
||||
declare const LANG_COOKIE = "wire-lang";
|
||||
declare const I18N_JS_HREF = "/__wrnexus/i18n.js";
|
||||
/** Merge loaded locale messages + config into a resolved i18n bundle. */
|
||||
declare function resolveI18n(messages: Record<string, Messages>, config?: I18nConfig): ResolvedI18n;
|
||||
/** Build a `t()` for a language: current → default → the key itself. */
|
||||
declare function makeT(i18n: ResolvedI18n, lang: string): TFunction;
|
||||
/** Resolve the active language from a cookie, Accept-Language, then default. */
|
||||
declare function resolveLang(i18n: ResolvedI18n, cookieValue: string | undefined, acceptLanguage: string | null): string;
|
||||
/**
|
||||
* Resolve translation markers in rendered HTML:
|
||||
* t:<attr>="key" → <attr>="<translation>" (e.g. t:placeholder, t:aria-label)
|
||||
* <tag data-t="key">…</tag> → element text becomes the translation
|
||||
* Only runs when the HTML actually contains a marker.
|
||||
*/
|
||||
declare function translateHtml(html: string, t: TFunction): string;
|
||||
/** `window.__wireI18n = { lang, langs }` for the client language switcher. */
|
||||
declare function renderI18nData(i18n: ResolvedI18n, lang: string): string;
|
||||
/**
|
||||
* Client runtime: binds `[data-wire-lang-set="es"]` elements to set the
|
||||
* `wire-lang` cookie and reload, so the server re-renders in the new language.
|
||||
*/
|
||||
declare const I18N_RUNTIME: string;
|
||||
|
||||
export { I18N_JS_HREF, I18N_RUNTIME, type I18nConfig, LANG_COOKIE, type Messages, type ResolvedI18n, formatCurrency, formatDate, formatNumber, formatRelativeTime, loadLocales, makeT, plural, renderI18nData, resolveI18n, resolveLang, translateHtml };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/i18n</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>import {
|
||||
loadLocales,
|
||||
resolveI18n,
|
||||
resolveLang,
|
||||
makeT,
|
||||
translateHtml,
|
||||
LANG_COOKIE,
|
||||
} from "@wrnexus/i18n";
|
||||
|
||||
// app/locales/en.json, app/locales/es.json
|
||||
const messages = loadLocales("app/locales");
|
||||
const i18n = resolveI18n(messages, { default: "en", locales: ["en", "es"] });
|
||||
|
||||
// Per request:
|
||||
const lang = resolveLang(i18n, req.cookies?.[LANG_COOKIE], req.headers.get("accept-language"));
|
||||
const t = makeT(i18n, lang);
|
||||
|
||||
t("nav.home"); // dotted key → "Home"
|
||||
t("greeting", { name: "Ada" }); // "Hello, {name}" → "Hello, Ada"
|
||||
|
||||
// After rendering a .wrn view, resolve translation markers in the HTML:
|
||||
const finalHtml = translateHtml(renderedHtml, t);</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="json"><code>{
|
||||
"nav": { "home": "Home" },
|
||||
"greeting": "Hello, {name}"
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="html"><code><h1 data-t="nav.home">Home</h1>
|
||||
<input t:placeholder="search.placeholder" /></code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#loading-resolving">Loading & resolving</a><a class="toc-level-4" href="#types-constants">Types & constants</a><a class="toc-level-4" href="#html-client-runtime">HTML & client runtime</a><a class="toc-level-4" href="#formatting-helpers-re-exported-from-format-ts">Formatting helpers (re-exported from ./format.ts)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#server-load-resolve-translate">Server: load, resolve, translate</a><a class="toc-level-4" href="#views-translation-markers">Views: translation markers</a><a class="toc-level-4" href="#client-language-switcher">Client: language switcher</a><a class="toc-level-4" href="#formatting">Formatting</a><a class="toc-level-3" href="#configuration">Configuration</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
page wrnexusjwt {
|
||||
seo {
|
||||
title = "@wrnexus/jwt"
|
||||
description = "HS256 JWT signing, verification, and bearer authentication."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Security</span><h1>@wrnexus/jwt</h1><p>HS256 JWT signing, verification, and bearer authentication.</p><code>bun add @wrnexus/jwt@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Security</span><h1>@wrnexus/jwt</h1><p>HS256 JWT signing, verification, and bearer authentication.</p><pre><code>bun add @wrnexus/jwt@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Dependency-free JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WRNexusJS.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/jwt</code> signs and verifies stateless JSON Web Tokens using the <strong>HS256</strong> (HMAC-SHA-256) algorithm. It has no runtime dependencies — signing and verification are implemented directly on the standard <strong>Web Crypto</strong> API (<code>crypto.subtle</code>), which Bun provides natively. It runs server-side and pairs with the session-based auth in <code>@wrnexus/core</code>, giving you a stateless option for API and mobile clients. Reach for it when you need bearer-token auth rather than cookie sessions.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/jwt</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>Single entry point (<code>@wrnexus/jwt</code>). All functions are async and return Promises.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Kind</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>signJwt(payload, secret, options?)</code></td><td>function</td><td>Sign claims into an HS256 token string.</td></tr><tr><td><code>verifyJwt<T>(token, secret, options?)</code></td><td>function</td><td>Verify a token and return its claims, or throw.</td></tr><tr><td><code>jwtAuth(options)</code></td><td>function</td><td>Middleware that verifies a bearer JWT and sets <code>ctx.user</code>.</td></tr><tr><td><code>JwtError</code></td><td>class</td><td>Error thrown on any signature/payload/expiry failure.</td></tr><tr><td><code>JwtClaims</code></td><td>interface</td><td>Claims shape (<code>sub</code>, <code>iat</code>, <code>exp</code>, <code>nbf</code>, plus arbitrary keys).</td></tr><tr><td><code>SignOptions</code></td><td>interface</td><td>Options for <code>signJwt</code>.</td></tr><tr><td><code>JwtAuthOptions</code></td><td>interface</td><td>Options for <code>jwtAuth</code>.</td></tr></tbody></table></div>
|
||||
<h4 id="signjwt-payload-secret-options"><code>signJwt(payload, secret, options?)</code></h4>
|
||||
<pre data-language="ts"><code>function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;</code></pre>
|
||||
<p>Signs <code>payload</code> with <code>secret</code> using HS256 and returns the encoded token (<code>header.body.signature</code>). An <code>iat</code> (issued-at) claim is always added.</p>
|
||||
<p><code>SignOptions</code>:</p>
|
||||
<ul>
|
||||
<li><code>expiresIn?: number</code> — seconds until expiry; sets the <code>exp</code> claim.</li>
|
||||
<li><code>now?: number</code> — override the issued-at time (seconds), useful for testing.</li>
|
||||
</ul>
|
||||
<h4 id="verifyjwt-t-token-secret-options"><code>verifyJwt<T>(token, secret, options?)</code></h4>
|
||||
<pre data-language="ts"><code>function verifyJwt<T extends JwtClaims = JwtClaims>(
|
||||
token: string,
|
||||
secret: string,
|
||||
options?: { now?: number },
|
||||
): Promise<T>;</code></pre>
|
||||
<p>Verifies the HS256 signature and returns the decoded claims typed as <code>T</code>. Throws <code>JwtError</code> when the token is malformed, the signature is invalid, the payload is not valid JSON, the token is expired (<code>exp</code>), or not yet valid (<code>nbf</code>). Pass <code>now</code> (seconds) to override the reference time for the <code>exp</code>/<code>nbf</code> checks.</p>
|
||||
<h4 id="jwtauth-options"><code>jwtAuth(options)</code></h4>
|
||||
<pre data-language="ts"><code>function jwtAuth(options: JwtAuthOptions): Middleware;</code></pre>
|
||||
<p>Returns a WRNexusJS <code>Middleware</code> that reads a token, verifies it, and assigns the claims to <code>ctx.user</code>.</p>
|
||||
<p><code>JwtAuthOptions</code>:</p>
|
||||
<ul>
|
||||
<li><code>secret: string</code> — the HMAC secret used to verify tokens.</li>
|
||||
<li><code>getToken?: (ctx: Context) => string | undefined</code> — how to extract the token.</li>
|
||||
<p>Defaults to reading <code>Authorization: Bearer <token></code>.</p>
|
||||
<li><code>required?: boolean</code> — when <code>true</code> (default), a missing or invalid token</li>
|
||||
<p>responds with <code>401 { ok: false, error: "Unauthorized" }</code>. When <code>false</code>, requests pass through and <code>ctx.user</code> is only set if a valid token is present.</p>
|
||||
</ul>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre data-language="ts"><code>import { signJwt, verifyJwt, jwtAuth, JwtError } from "@wrnexus/jwt";
|
||||
|
||||
const secret = process.env.JWT_SECRET!;
|
||||
|
||||
// Sign a token that expires in one hour
|
||||
const token = await signJwt({ sub: user.id, role: "admin" }, secret, {
|
||||
expiresIn: 3600,
|
||||
});
|
||||
|
||||
// Verify it later
|
||||
try {
|
||||
const claims = await verifyJwt<{ sub: string; role: string }>(token, secret);
|
||||
console.log(claims.sub, claims.role);
|
||||
} catch (err) {
|
||||
if (err instanceof JwtError) {
|
||||
// invalid signature, expired, malformed, etc.
|
||||
}
|
||||
}</code></pre>
|
||||
<p>Protecting routes with the middleware:</p>
|
||||
<pre data-language="ts"><code>import { jwtAuth } from "@wrnexus/jwt";
|
||||
|
||||
// Require a valid bearer token; ctx.user holds the verified claims
|
||||
app.use(jwtAuth({ secret: process.env.JWT_SECRET! }));
|
||||
|
||||
// Optional auth — populate ctx.user when present, but don't 401
|
||||
app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false }));</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Uses the standard Web Crypto API (<code>crypto.subtle.importKey</code>,</li>
|
||||
<p><code>sign</code>, <code>verify</code>) plus <code>btoa</code>/<code>atob</code> and <code>TextEncoder</code>/<code>TextDecoder</code> — all provided by Bun. No third-party crypto dependency.</p>
|
||||
<li><strong>Algorithm:</strong> HS256 (HMAC with SHA-256) only. Asymmetric algorithms (RS/ES)</li>
|
||||
<p>are not supported.</p>
|
||||
<li>Integrates with [<code>@wrnexus/core</code>](../core) for <code>Context</code>, <code>Middleware</code>, and</li>
|
||||
<p><code>ctx.user</code>; it complements the framework's cookie/session auth with a stateless bearer-token flow for API and mobile clients.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { Context, Middleware } from '@wrnexus/core';
|
||||
|
||||
/**
|
||||
* @wrnexus/jwt — dependency-free JSON Web Tokens (HS256) via WebCrypto, plus a
|
||||
* bearer-token auth middleware. Pairs with the session auth in @wrnexus/core for
|
||||
* stateless (API/mobile) authentication.
|
||||
*
|
||||
* const token = await signJwt({ sub: user.id, role: "admin" }, secret, { expiresIn: 3600 });
|
||||
* const claims = await verifyJwt(token, secret); // throws JwtError if invalid/expired
|
||||
*/
|
||||
|
||||
declare class JwtError extends Error {
|
||||
constructor(message: string);
|
||||
}
|
||||
interface JwtClaims {
|
||||
/** Subject (user id). */
|
||||
sub?: string;
|
||||
/** Issued-at (seconds). */
|
||||
iat?: number;
|
||||
/** Expiry (seconds). */
|
||||
exp?: number;
|
||||
/** Not-before (seconds). */
|
||||
nbf?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
interface SignOptions {
|
||||
/** Seconds until expiry (sets `exp`). */
|
||||
expiresIn?: number;
|
||||
/** Override issued-at (seconds). */
|
||||
now?: number;
|
||||
}
|
||||
/** Sign a payload into a JWT (HS256). */
|
||||
declare function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;
|
||||
/** Verify a JWT and return its claims. Throws `JwtError` on any failure. */
|
||||
declare function verifyJwt<T extends JwtClaims = JwtClaims>(token: string, secret: string, options?: {
|
||||
now?: number;
|
||||
}): Promise<T>;
|
||||
interface JwtAuthOptions {
|
||||
secret: string;
|
||||
/** Where to read the token. Default: `Authorization: Bearer <token>`. */
|
||||
getToken?: (ctx: Context) => string | undefined;
|
||||
/** Reject unauthenticated requests with 401. Default true. */
|
||||
required?: boolean;
|
||||
}
|
||||
/**
|
||||
* Middleware that verifies a bearer JWT and sets `ctx.user` to its claims.
|
||||
* When `required` (default), a missing/invalid token gets a 401.
|
||||
*/
|
||||
declare function jwtAuth(options: JwtAuthOptions): Middleware;
|
||||
|
||||
export { type JwtAuthOptions, type JwtClaims, JwtError, type SignOptions, jwtAuth, signJwt, verifyJwt };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/jwt</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>function verifyJwt<T extends JwtClaims = JwtClaims>(
|
||||
token: string,
|
||||
secret: string,
|
||||
options?: { now?: number },
|
||||
): Promise<T>;</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>function jwtAuth(options: JwtAuthOptions): Middleware;</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#signjwt-payload-secret-options">signJwt(payload, secret, options?)</a><a class="toc-level-4" href="#verifyjwt-t-token-secret-options">verifyJwt<T>(token, secret, options?)</a><a class="toc-level-4" href="#jwtauth-options">jwtAuth(options)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
page wrnexusmobile {
|
||||
seo {
|
||||
title = "@wrnexus/mobile"
|
||||
description = "SSR-safe compatibility access to Capacitor plugins."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Native</span><h1>@wrnexus/mobile</h1><p>SSR-safe compatibility access to Capacitor plugins.</p><code>bun add @wrnexus/mobile@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Native</span><h1>@wrnexus/mobile</h1><p>SSR-safe compatibility access to Capacitor plugins.</p><pre><code>bun add @wrnexus/mobile@0.2.12</code></pre></section><section id="guide" class="prose"><p>SSR-safe access to Capacitor plugins from WRNexusJS browser code.</p>
|
||||
<pre data-language="bash"><code>wrnexus mobile add @capacitor/camera</code></pre>
|
||||
<pre data-language="ts"><code>import { Camera } from "@capacitor/camera";
|
||||
import { mobile } from "@wrnexus/mobile";
|
||||
|
||||
if (mobile.isNative()) {
|
||||
mobile.registerPlugin("Camera", Camera);
|
||||
const photo = await mobile.invoke("Camera", "getPhoto", { resultType: "uri" });
|
||||
}</code></pre>
|
||||
<p><code>isNative()</code> is false and <code>platform()</code> is <code>web</code> during SSR. <code>plugin()</code> returns <code>undefined</code> when unavailable; <code>requirePlugin()</code> and <code>invoke()</code> throw an actionable <code>MobileUnavailableError</code>.</p>
|
||||
<p>Import and register Capacitor packages only from browser-owned code. Do not import them in server routes, SSR helpers, or other Bun-only modules.</p></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>export { native } from '@wrnexus/native';
|
||||
|
||||
/** @wrnexus/mobile — SSR-safe access to Capacitor's native bridge. */
|
||||
|
||||
type MobilePlatform = "ios" | "android" | "web" | string;
|
||||
interface CapacitorBridge {
|
||||
isNativePlatform?: () => boolean;
|
||||
getPlatform?: () => MobilePlatform;
|
||||
Plugins?: Record<string, unknown>;
|
||||
}
|
||||
declare class MobileUnavailableError extends Error {
|
||||
constructor(message?: string);
|
||||
}
|
||||
/** Register a plugin imported by browser-only application code. */
|
||||
declare function registerPlugin<T extends object>(name: string, instance: T): T;
|
||||
/** True only inside a native Capacitor iOS or Android WebView. SSR-safe. */
|
||||
declare function isNative(): boolean;
|
||||
/** Current Capacitor platform, falling back to `web` during SSR and in browsers. */
|
||||
declare function platform(): MobilePlatform;
|
||||
/** Return an injected Capacitor plugin, or undefined when it is unavailable. */
|
||||
declare function plugin<T extends object>(name: string): T | undefined;
|
||||
/** Require an installed native plugin and produce a useful error when absent. */
|
||||
declare function requirePlugin<T extends object>(name: string): T;
|
||||
/** Invoke a plugin method without importing native code into an SSR module. */
|
||||
declare function invoke<TResult = unknown>(pluginName: string, method: string, options?: unknown): Promise<TResult>;
|
||||
/** Run native behavior when available, with an optional SSR/web fallback. */
|
||||
declare function whenNative<T>(native: () => T | Promise<T>, fallback?: () => T | Promise<T>): Promise<T | undefined>;
|
||||
declare const mobile: {
|
||||
isNative: typeof isNative;
|
||||
platform: typeof platform;
|
||||
registerPlugin: typeof registerPlugin;
|
||||
plugin: typeof plugin;
|
||||
requirePlugin: typeof requirePlugin;
|
||||
invoke: typeof invoke;
|
||||
whenNative: typeof whenNative;
|
||||
};
|
||||
|
||||
export { type CapacitorBridge, type MobilePlatform, MobileUnavailableError, invoke, isNative, mobile, platform, plugin, registerPlugin, requirePlugin, whenNative };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>wrnexus mobile add @capacitor/camera</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>import { Camera } from "@capacitor/camera";
|
||||
import { mobile } from "@wrnexus/mobile";
|
||||
|
||||
if (mobile.isNative()) {
|
||||
mobile.registerPlugin("Camera", Camera);
|
||||
const photo = await mobile.invoke("Camera", "getPhoto", { resultType: "uri" });
|
||||
}</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
page wrnexusnative {
|
||||
seo {
|
||||
title = "@wrnexus/native"
|
||||
description = "Cross-platform browser and Capacitor capability registry."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Native</span><h1>@wrnexus/native</h1><p>Cross-platform browser and Capacitor capability registry.</p><code>bun add @wrnexus/native@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Native</span><h1>@wrnexus/native</h1><p>Cross-platform browser and Capacitor capability registry.</p><pre><code>bun add @wrnexus/native@0.2.12</code></pre></section><section id="guide" class="prose"><p>Cross-platform capabilities for browsers, Capacitor WebViews, and compiled native apps.</p>
|
||||
<pre data-language="ts"><code>import { native } from "@wrnexus/native";
|
||||
|
||||
if (native.supports("share")) await native.run("share", { title: "WRNexusJS", url: location.href });</code></pre>
|
||||
<p>Built-ins include <code>camera</code>, <code>clipboard.write</code>, <code>share</code>, <code>geolocation</code>, <code>network</code>, <code>haptics</code>, storage, filesystem, notifications, and device information. Browser capabilities use Web APIs; mobile capabilities use installed Capacitor plugins.</p>
|
||||
<p><code>platform()</code> returns <code>server</code> during SSR, <code>browser</code> on the web, and the Capacitor platform in a native WebView. Unsupported operations reject with <code>NativeUnavailableError</code>; use <code>supports()</code> before presenting optional UI.</p></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { N as NativePlatform, a as NativeCapability, b as NativeRunOptions, c as NativeTarget } from './types-CDShWg0i.js';
|
||||
export { d as NativeAdapter, e as NativeBrowserRuntime } from './types-CDShWg0i.js';
|
||||
export { browserCapabilities } from './browser.js';
|
||||
export { mobileCapabilities } from './mobile.js';
|
||||
|
||||
declare class NativeUnavailableError extends Error {
|
||||
constructor(message: string);
|
||||
}
|
||||
declare function isMobile(): boolean;
|
||||
declare function platform(): NativePlatform;
|
||||
declare function register<TOptions = unknown, TResult = unknown>(name: string, capability: NativeCapability<TOptions, TResult>): () => void;
|
||||
declare function registered(): string[];
|
||||
declare function supports(name: string, target?: NativeTarget): boolean;
|
||||
declare function run<TResult = unknown>(name: string, options?: unknown, runOptions?: NativeRunOptions): Promise<TResult>;
|
||||
declare function clearRegistry(): void;
|
||||
|
||||
declare const native: {
|
||||
isMobile: typeof isMobile;
|
||||
platform: typeof platform;
|
||||
register: typeof register;
|
||||
registered: typeof registered;
|
||||
run: typeof run;
|
||||
supports: typeof supports;
|
||||
};
|
||||
|
||||
export { NativeCapability, NativePlatform, NativeRunOptions, NativeTarget, NativeUnavailableError, clearRegistry, isMobile, native, platform, register, registered, run, supports };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="ts"><code>import { native } from "@wrnexus/native";
|
||||
|
||||
if (native.supports("share")) await native.run("share", { title: "WRNexusJS", url: location.href });</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
page wrnexusoauth {
|
||||
seo {
|
||||
title = "@wrnexus/oauth"
|
||||
description = "OAuth 2.0, PKCE, provider presets, and profile mapping."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Security</span><h1>@wrnexus/oauth</h1><p>OAuth 2.0, PKCE, provider presets, and profile mapping.</p><code>bun add @wrnexus/oauth@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Security</span><h1>@wrnexus/oauth</h1><p>OAuth 2.0, PKCE, provider presets, and profile mapping.</p><pre><code>bun add @wrnexus/oauth@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/oauth</code> implements the OAuth 2.0 Authorization Code flow (with PKCE) for server-side sign-in. It ships ready-made provider presets and a <code>defineProvider</code> helper for custom providers, then gives you two flow functions — <code>startAuth</code> (build the redirect) and <code>completeAuth</code> (exchange the code and fetch the user's profile). It has no runtime dependencies: it uses the platform <code>fetch</code> and WebCrypto only. Pairs naturally with <code>@wrnexus/core</code>'s <code>logIn</code> to establish a session once you have a normalized profile.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/oauth</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="providers">Providers</h4>
|
||||
<p>Each preset takes <code>ProviderCredentials</code> and returns an <code>OAuthProvider</code>.</p>
|
||||
<pre data-language="ts"><code>interface ProviderCredentials {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes?: string[]; // override the preset's default scopes
|
||||
}</code></pre>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Default scopes</th><th>Notes</th></tr></thead>
|
||||
<tbody><tr><td><code>google(creds)</code></td><td><code>openid</code>, <code>email</code>, <code>profile</code></td><td>Sets <code>access_type: offline</code> for refresh tokens.</td></tr><tr><td><code>github(creds)</code></td><td><code>read:user</code>, <code>user:email</code></td><td>Maps <code>name</code> (falls back to <code>login</code>) and <code>avatar_url</code>.</td></tr><tr><td><code>discord(creds)</code></td><td><code>identify</code>, <code>email</code></td><td>Builds the avatar CDN URL from the user id + hash.</td></tr><tr><td><code>defineProvider(config)</code></td><td>—</td><td>Pass a full <code>OAuthProvider</code> to define a custom OAuth 2.0 provider.</td></tr></tbody></table></div>
|
||||
<p>An <code>OAuthProvider</code> describes the endpoints, scopes, credentials, optional extra authorize params, and a <code>mapProfile</code> normalizer:</p>
|
||||
<pre data-language="ts"><code>interface OAuthProvider {
|
||||
name: string;
|
||||
authorizeUrl: string;
|
||||
tokenUrl: string;
|
||||
userInfoUrl: string;
|
||||
scopes: string[];
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
authorizeParams?: Record<string, string>; // e.g. access_type, prompt
|
||||
mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
|
||||
}</code></pre>
|
||||
<h4 id="flow">Flow</h4>
|
||||
<h4 id="startauth-provider-options-promise-startauthresult"><code>startAuth(provider, options): Promise<StartAuthResult></code></h4>
|
||||
<p>Builds the authorize redirect URL with a generated PKCE challenge and CSRF <code>state</code>. Store the returned <code>state</code> and <code>verifier</code> (session/cookie), then 302 the user to <code>url</code>.</p>
|
||||
<pre data-language="ts"><code>interface StartAuthOptions {
|
||||
redirectUri: string;
|
||||
state?: string; // reuse a state instead of generating one
|
||||
params?: Record<string, string>; // extra authorize params, merged last
|
||||
}
|
||||
|
||||
interface StartAuthResult {
|
||||
url: string; // authorize URL to redirect to
|
||||
state: string; // CSRF state — verify on callback
|
||||
verifier: string; // PKCE code verifier — pass to completeAuth
|
||||
}</code></pre>
|
||||
<h4 id="completeauth-provider-options-promise-tokens-profile"><code>completeAuth(provider, options): Promise<{ tokens, profile }></code></h4>
|
||||
<p>On the callback: exchanges the authorization <code>code</code> for tokens, then fetches and normalizes the user profile. Convenience wrapper over <code>exchangeCode</code> + <code>fetchProfile</code>.</p>
|
||||
<pre data-language="ts"><code>interface CompleteAuthOptions {
|
||||
code: string;
|
||||
redirectUri: string;
|
||||
verifier?: string; // the PKCE verifier from startAuth
|
||||
fetch?: typeof fetch; // inject a fetch implementation (tests)
|
||||
}</code></pre>
|
||||
<h4 id="lower-level-helpers">Lower-level helpers</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>exchangeCode(provider, options)</code></td><td><code>→ Promise<OAuthTokens></code></td><td>Exchange an authorization code for tokens.</td></tr><tr><td><code>fetchProfile(provider, tokens, fetch?)</code></td><td><code>→ Promise<OAuthProfile></code></td><td>Fetch + normalize the user's profile.</td></tr><tr><td><code>randomToken(bytes?)</code></td><td><code>→ string</code></td><td>Random URL-safe token (default 32 bytes) for <code>state</code>/verifiers.</td></tr></tbody></table></div>
|
||||
<h4 id="types">Types</h4>
|
||||
<pre data-language="ts"><code>interface OAuthTokens {
|
||||
access_token: string;
|
||||
token_type?: string;
|
||||
refresh_token?: string;
|
||||
expires_in?: number;
|
||||
id_token?: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
interface OAuthProfile {
|
||||
id: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
raw: Record<string, unknown>;
|
||||
}</code></pre>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre data-language="ts"><code>import { google, startAuth, completeAuth } from "@wrnexus/oauth";
|
||||
import { logIn } from "@wrnexus/core";
|
||||
|
||||
const provider = google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
});
|
||||
|
||||
const redirectUri = "https://example.com/auth/callback";
|
||||
|
||||
// 1. Kick off sign-in: redirect the user to the provider.
|
||||
async function beginLogin(ctx) {
|
||||
const { url, state, verifier } = await startAuth(provider, { redirectUri });
|
||||
// Persist state + verifier in the session, then redirect.
|
||||
ctx.session.set("oauth_state", state);
|
||||
ctx.session.set("oauth_verifier", verifier);
|
||||
return Response.redirect(url, 302);
|
||||
}
|
||||
|
||||
// 2. Handle the callback.
|
||||
async function handleCallback(ctx, code: string, state: string) {
|
||||
if (state !== ctx.session.get("oauth_state")) throw new Error("bad state");
|
||||
|
||||
const { profile } = await completeAuth(provider, {
|
||||
code,
|
||||
redirectUri,
|
||||
verifier: ctx.session.get("oauth_verifier"),
|
||||
});
|
||||
|
||||
logIn(ctx, { id: profile.id, email: profile.email });
|
||||
}</code></pre>
|
||||
<p>Custom provider with <code>defineProvider</code>:</p>
|
||||
<pre data-language="ts"><code>import { defineProvider, startAuth } from "@wrnexus/oauth";
|
||||
|
||||
const gitlab = defineProvider({
|
||||
name: "gitlab",
|
||||
authorizeUrl: "https://gitlab.com/oauth/authorize",
|
||||
tokenUrl: "https://gitlab.com/oauth/token",
|
||||
userInfoUrl: "https://gitlab.com/api/v4/user",
|
||||
scopes: ["read_user"],
|
||||
clientId: process.env.GITLAB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITLAB_CLIENT_SECRET!,
|
||||
mapProfile: (raw) => ({
|
||||
id: String(raw.id),
|
||||
email: raw.email as string | undefined,
|
||||
name: raw.name as string | undefined,
|
||||
avatar: raw.avatar_url as string | undefined,
|
||||
raw,
|
||||
}),
|
||||
});</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Relies on the global <code>fetch</code> and WebCrypto (<code>crypto.getRandomValues</code>,</li>
|
||||
<p><code>crypto.subtle.digest</code>) — no other runtime dependencies.</p>
|
||||
<li>The flow is stateless by design: you are responsible for storing <code>state</code> and</li>
|
||||
<p><code>verifier</code> between <code>startAuth</code> and <code>completeAuth</code> (session or signed cookie).</p>
|
||||
<li>Pairs with [<code>@wrnexus/core</code>](../core) — feed the normalized <code>OAuthProfile</code> into</li>
|
||||
<p><code>logIn</code> to establish a session.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* @wrnexus/oauth — OAuth 2.0 sign-in with any provider. Ships presets for Google,
|
||||
* GitHub, and Discord, and `defineProvider` for a custom one. Dependency-free
|
||||
* (uses `fetch` + WebCrypto for PKCE). Pairs with @wrnexus/core's `logIn`.
|
||||
*
|
||||
* const provider = google({ clientId, clientSecret });
|
||||
* // 1. send the user to the provider:
|
||||
* const { url, state, verifier } = await startAuth(provider, { redirectUri });
|
||||
* // (store `state` + `verifier` in the session, then 302 to `url`)
|
||||
* // 2. on the callback:
|
||||
* const { profile } = await completeAuth(provider, { code, redirectUri, verifier });
|
||||
* logIn(ctx, { id: profile.id, email: profile.email });
|
||||
*/
|
||||
interface OAuthTokens {
|
||||
access_token: string;
|
||||
token_type?: string;
|
||||
refresh_token?: string;
|
||||
expires_in?: number;
|
||||
id_token?: string;
|
||||
scope?: string;
|
||||
}
|
||||
interface OAuthProfile {
|
||||
id: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
interface OAuthProvider {
|
||||
name: string;
|
||||
authorizeUrl: string;
|
||||
tokenUrl: string;
|
||||
userInfoUrl: string;
|
||||
scopes: string[];
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
/** Extra params for the authorize request (e.g. `access_type`, `prompt`). */
|
||||
authorizeParams?: Record<string, string>;
|
||||
/** Normalize the provider's raw userinfo into an OAuthProfile. */
|
||||
mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
|
||||
}
|
||||
interface ProviderCredentials {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes?: string[];
|
||||
}
|
||||
type FetchLike = typeof fetch;
|
||||
declare function google(creds: ProviderCredentials): OAuthProvider;
|
||||
declare function github(creds: ProviderCredentials): OAuthProvider;
|
||||
declare function discord(creds: ProviderCredentials): OAuthProvider;
|
||||
/** Define a custom OAuth2 provider. */
|
||||
declare function defineProvider(config: OAuthProvider): OAuthProvider;
|
||||
/** A random URL-safe token (for `state` and the PKCE verifier). */
|
||||
declare function randomToken(bytes?: number): string;
|
||||
interface StartAuthOptions {
|
||||
redirectUri: string;
|
||||
/** Provide to reuse a state (else one is generated). */
|
||||
state?: string;
|
||||
/** Extra authorize params (merged over the provider's). */
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
interface StartAuthResult {
|
||||
/** The full authorize URL to redirect the user to. */
|
||||
url: string;
|
||||
/** CSRF state — store it (session/cookie) and verify on callback. */
|
||||
state: string;
|
||||
/** PKCE code verifier — store it and pass to `completeAuth`. */
|
||||
verifier: string;
|
||||
}
|
||||
/** Build the authorize redirect (with PKCE + state). */
|
||||
declare function startAuth(provider: OAuthProvider, options: StartAuthOptions): Promise<StartAuthResult>;
|
||||
interface CompleteAuthOptions {
|
||||
code: string;
|
||||
redirectUri: string;
|
||||
/** The PKCE verifier from `startAuth`. */
|
||||
verifier?: string;
|
||||
/** Inject a fetch implementation (tests). */
|
||||
fetch?: FetchLike;
|
||||
}
|
||||
/** Exchange the authorization code for tokens, then fetch the user profile. */
|
||||
declare function completeAuth(provider: OAuthProvider, options: CompleteAuthOptions): Promise<{
|
||||
tokens: OAuthTokens;
|
||||
profile: OAuthProfile;
|
||||
}>;
|
||||
/** Exchange an authorization code for tokens. */
|
||||
declare function exchangeCode(provider: OAuthProvider, options: CompleteAuthOptions): Promise<OAuthTokens>;
|
||||
/** Fetch + normalize the user's profile from the provider. */
|
||||
declare function fetchProfile(provider: OAuthProvider, tokens: OAuthTokens, fetchImpl?: FetchLike): Promise<OAuthProfile>;
|
||||
|
||||
export { type CompleteAuthOptions, type OAuthProfile, type OAuthProvider, type OAuthTokens, type ProviderCredentials, type StartAuthOptions, type StartAuthResult, completeAuth, defineProvider, discord, exchangeCode, fetchProfile, github, google, randomToken, startAuth };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/oauth</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>interface ProviderCredentials {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes?: string[]; // override the preset's default scopes
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>interface OAuthProvider {
|
||||
name: string;
|
||||
authorizeUrl: string;
|
||||
tokenUrl: string;
|
||||
userInfoUrl: string;
|
||||
scopes: string[];
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
authorizeParams?: Record<string, string>; // e.g. access_type, prompt
|
||||
mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>interface StartAuthOptions {
|
||||
redirectUri: string;
|
||||
state?: string; // reuse a state instead of generating one
|
||||
params?: Record<string, string>; // extra authorize params, merged last
|
||||
}
|
||||
|
||||
interface StartAuthResult {
|
||||
url: string; // authorize URL to redirect to
|
||||
state: string; // CSRF state — verify on callback
|
||||
verifier: string; // PKCE code verifier — pass to completeAuth
|
||||
}</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#providers">Providers</a><a class="toc-level-4" href="#flow">Flow</a><a class="toc-level-4" href="#startauth-provider-options-promise-startauthresult">startAuth(provider, options): Promise<StartAuthResult></a><a class="toc-level-4" href="#completeauth-provider-options-promise-tokens-profile">completeAuth(provider, options): Promise<{ tokens, profile }></a><a class="toc-level-4" href="#lower-level-helpers">Lower-level helpers</a><a class="toc-level-4" href="#types">Types</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
page wrnexuspubsub {
|
||||
seo {
|
||||
title = "@wrnexus/pubsub"
|
||||
description = "In-process and Redis-backed publish/subscribe."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Realtime</span><h1>@wrnexus/pubsub</h1><p>In-process and Redis-backed publish/subscribe.</p><code>bun add @wrnexus/pubsub@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Realtime</span><h1>@wrnexus/pubsub</h1><p>In-process and Redis-backed publish/subscribe.</p><pre><code>bun add @wrnexus/pubsub@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Topic-based publish/subscribe with a pluggable driver — in-process by default, Redis for cross-process messaging.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/pubsub</code> is a small server-side pub/sub bus. You publish messages to a topic and subscribe with topic patterns; handlers fire for matching topics. The default driver keeps everything in-process, and you can swap in the Redis driver (<code>@wrnexus/pubsub/redis</code>) to fan messages out across processes or hosts. It also backs <code>@wrnexus/core</code>'s realtime bridge for horizontal scaling.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/pubsub</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="createpubsub-driver-pubsub"><code>createPubSub(driver?): PubSub</code></h4>
|
||||
<p>Creates a bus over a driver. Defaults to <code>memoryDriver()</code> (in-process).</p>
|
||||
<pre data-language="ts"><code>interface PubSub {
|
||||
publish<T = unknown>(topic: string, message: T): Promise<void>;
|
||||
subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
|
||||
}
|
||||
|
||||
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;</code></pre>
|
||||
<ul>
|
||||
<li><code>publish(topic, message)</code> — resolves once the driver has dispatched the message.</li>
|
||||
<li><code>subscribe(pattern, handler)</code> — returns an unsubscribe function.</li>
|
||||
</ul>
|
||||
<h4 id="pattern-matching">Pattern matching</h4>
|
||||
<p>Subscription patterns match in three ways:</p>
|
||||
<ul>
|
||||
<li><strong>Exact</strong> — <code>"order:created"</code> matches only that topic.</li>
|
||||
<li><strong>Prefix</strong> — <code>"order:*"</code> matches any topic starting with <code>"order:"</code>.</li>
|
||||
<li><strong>Everything</strong> — <code>"*"</code> matches all topics.</li>
|
||||
</ul>
|
||||
<h4 id="memorydriver-pubsubdriver"><code>memoryDriver(): PubSubDriver</code></h4>
|
||||
<p>The default in-process driver. Handlers are invoked synchronously (fire-and-forget for async handlers) whenever a published topic matches a registered pattern.</p>
|
||||
<pre data-language="ts"><code>interface PubSubDriver {
|
||||
publish(topic: string, message: unknown): void | Promise<void>;
|
||||
subscribe(pattern: string, handler: Handler): () => void;
|
||||
}</code></pre>
|
||||
<h4 id="wrnexus-pubsub-redis-redisdriver-url"><code>@wrnexus/pubsub/redis</code> — <code>redisDriver(url?)</code></h4>
|
||||
<p>A cross-process driver backed by Redis. It speaks RESP over a raw TCP socket via <code>Bun.connect</code>, so it adds <strong>no npm dependency</strong>. <code>url</code> defaults to <code>$REDIS_URL</code>, then <code>redis://localhost:6379</code>. The URL may carry a password and a database index (e.g. <code>redis://:secret@host:6379/2</code>).</p>
|
||||
<pre data-language="ts"><code>function redisDriver(url?: string): PubSubDriver & { close(): void };</code></pre>
|
||||
<ul>
|
||||
<li>Exact topics use Redis <code>SUBSCRIBE</code>; wildcard patterns (<code>ns:*</code>, <code>*</code>) use</li>
|
||||
<p><code>PSUBSCRIBE</code>, whose glob semantics line up with this library's matching.</p>
|
||||
<li>Messages are JSON-stringified on publish and <code>JSON.parse</code>d on receipt; a payload</li>
|
||||
<p>that isn't valid JSON is delivered as the raw string.</p>
|
||||
<li><code>close()</code> tears down both the subscriber and publisher connections.</li>
|
||||
</ul>
|
||||
<h4 id="resp-codec-internal">RESP codec (internal)</h4>
|
||||
<p><code>redis.ts</code> uses a minimal RESP implementation exported from <code>resp.ts</code> (<code>encodeCommand</code>, <code>parseReply</code>, <code>concat</code>, and the <code>RespValue</code> type). These are implementation details of the Redis driver, not part of the public package entry.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>In-process (default):</p>
|
||||
<pre data-language="ts"><code>import { createPubSub } from "@wrnexus/pubsub";
|
||||
|
||||
const bus = createPubSub();
|
||||
|
||||
const off = bus.subscribe("order:*", (msg, topic) => {
|
||||
console.log(topic, msg);
|
||||
});
|
||||
|
||||
await bus.publish("order:created", { id: 7 });
|
||||
|
||||
off(); // unsubscribe</code></pre>
|
||||
<p>Cross-process with Redis:</p>
|
||||
<pre data-language="ts"><code>import { createPubSub } from "@wrnexus/pubsub";
|
||||
import { redisDriver } from "@wrnexus/pubsub/redis";
|
||||
|
||||
const driver = redisDriver("redis://localhost:6379");
|
||||
const bus = createPubSub(driver);
|
||||
|
||||
bus.subscribe("order:*", (msg, topic) => {
|
||||
// received on any app process subscribed to this pattern
|
||||
});
|
||||
|
||||
await bus.publish("order:created", { id: 7 });
|
||||
|
||||
// on shutdown
|
||||
driver.close();</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> The Redis driver depends on <code>Bun.connect</code>; it throws</li>
|
||||
<p><code>redisDriver requires the Bun runtime (Bun.connect).</code> outside Bun. The default in-memory driver has no runtime dependencies.</p>
|
||||
<li>The Redis driver reads <code>REDIS_URL</code> from the environment when no <code>url</code> is passed.</li>
|
||||
<li>Backs [<code>@wrnexus/core</code>](../core)'s realtime bridge for horizontal scaling.</li>
|
||||
<li>No external npm dependencies — the Redis client is a self-contained RESP codec.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* @wrnexus/pubsub — topic-based publish/subscribe with a pluggable driver.
|
||||
* The default is in-process; swap in a Redis/NATS driver for cross-instance
|
||||
* messaging (it also backs @wrnexus/core's realtime bridge).
|
||||
*
|
||||
* const bus = createPubSub();
|
||||
* const off = bus.subscribe("order:*", (msg, topic) => {...});
|
||||
* await bus.publish("order:created", { id: 7 });
|
||||
*
|
||||
* Subscriptions match exact topics, "ns:*" prefixes, and "*" (everything).
|
||||
*/
|
||||
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;
|
||||
interface PubSubDriver {
|
||||
publish(topic: string, message: unknown): void | Promise<void>;
|
||||
subscribe(pattern: string, handler: Handler): () => void;
|
||||
}
|
||||
interface PubSub {
|
||||
publish<T = unknown>(topic: string, message: T): Promise<void>;
|
||||
subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
|
||||
}
|
||||
/** In-process pub/sub driver (default). */
|
||||
declare function memoryDriver(): PubSubDriver;
|
||||
/** Create a pub/sub bus over a driver (in-memory by default). */
|
||||
declare function createPubSub(driver?: PubSubDriver): PubSub;
|
||||
|
||||
export { type Handler, type PubSub, type PubSubDriver, createPubSub, memoryDriver };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/pubsub</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>interface PubSub {
|
||||
publish<T = unknown>(topic: string, message: T): Promise<void>;
|
||||
subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
|
||||
}
|
||||
|
||||
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>interface PubSubDriver {
|
||||
publish(topic: string, message: unknown): void | Promise<void>;
|
||||
subscribe(pattern: string, handler: Handler): () => void;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>function redisDriver(url?: string): PubSubDriver & { close(): void };</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#createpubsub-driver-pubsub">createPubSub(driver?): PubSub</a><a class="toc-level-4" href="#pattern-matching">Pattern matching</a><a class="toc-level-4" href="#memorydriver-pubsubdriver">memoryDriver(): PubSubDriver</a><a class="toc-level-4" href="#wrnexus-pubsub-redis-redisdriver-url">@wrnexus/pubsub/redis — redisDriver(url?)</a><a class="toc-level-4" href="#resp-codec-internal">RESP codec (internal)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
page wrnexusqueue {
|
||||
seo {
|
||||
title = "@wrnexus/queue"
|
||||
description = "Background jobs with delay, concurrency, retry, and repetition."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Data</span><h1>@wrnexus/queue</h1><p>Background jobs with delay, concurrency, retry, and repetition.</p><code>bun add @wrnexus/queue@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Data</span><h1>@wrnexus/queue</h1><p>Background jobs with delay, concurrency, retry, and repetition.</p><pre><code>bun add @wrnexus/queue@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>A background job queue with delays, retries + exponential backoff, recurring jobs, and concurrent workers.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/queue</code> is a server-side in-process job queue. You register named workers, enqueue jobs (optionally delayed or recurring), and let the queue poll and run them on a timer — with per-job retry limits and doubling backoff between attempts. The default store lives in memory; the design allows a pluggable driver to back it with Redis/SQL for durability across restarts. Reach for it when you need to defer work (emails, webhooks, cleanup) off the request path without a heavyweight external broker. Tests can drive it deterministically via <code>drain()</code>.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/queue</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>The package exports a single factory plus its supporting types.</p>
|
||||
<h4 id="createqueue-options-queue"><code>createQueue(options?): Queue</code></h4>
|
||||
<p>Creates a new queue instance.</p>
|
||||
<pre data-language="ts"><code>function createQueue(options?: QueueOptions): Queue;</code></pre>
|
||||
<h4 id="queueoptions"><code>QueueOptions</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>maxAttempts</code></td><td><code>number</code></td><td><code>3</code></td><td>Default max attempts per job before it is dead-lettered.</td></tr><tr><td><code>backoffMs</code></td><td><code>number</code></td><td><code>1000</code></td><td>Base retry backoff in ms; doubles per attempt.</td></tr><tr><td><code>pollMs</code></td><td><code>number</code></td><td><code>250</code></td><td>Poll interval used once <code>start()</code> is called (ms).</td></tr><tr><td><code>onFailed</code></td><td><code>(job: Job, error: unknown) => void</code></td><td>—</td><td>Called when a job exhausts its attempts.</td></tr><tr><td><code>now</code></td><td><code>() => number</code></td><td><code>Date.now</code></td><td>Clock injection for deterministic tests.</td></tr></tbody></table></div>
|
||||
<h4 id="queue"><code>Queue</code></h4>
|
||||
<p>The object returned by <code>createQueue</code>.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>add</code></td><td><code>add<T>(name, data: T, options?: AddOptions): Promise<Job<T>></code></td><td>Enqueue a job under a worker name. Returns the created job.</td></tr><tr><td><code>process</code></td><td><code>process<T>(name, handler: JobHandler<T>): void</code></td><td>Register the worker that runs jobs of the given name.</td></tr><tr><td><code>drain</code></td><td><code>drain(now?: number): Promise<number></code></td><td>Run every job whose <code>runAt ≤ now</code>, once. Returns how many ran.</td></tr><tr><td><code>start</code></td><td><code>start(): void</code></td><td>Begin polling every <code>pollMs</code>. No-op if already started.</td></tr><tr><td><code>stop</code></td><td><code>stop(): void</code></td><td>Stop the poll timer.</td></tr><tr><td><code>size</code></td><td><code>size(): number</code></td><td>Number of jobs currently queued.</td></tr></tbody></table></div>
|
||||
<h4 id="addoptions"><code>AddOptions</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>delayMs</code></td><td><code>number</code></td><td>Delay before the job becomes runnable (ms).</td></tr><tr><td><code>maxAttempts</code></td><td><code>number</code></td><td>Max attempts before dead-lettering. Defaults to the queue's <code>maxAttempts</code>.</td></tr><tr><td><code>repeat</code></td><td><code>number</code></td><td>Re-enqueue this job this many ms after each successful run (recurring).</td></tr></tbody></table></div>
|
||||
<h4 id="jobhandler-t"><code>JobHandler<T></code></h4>
|
||||
<pre data-language="ts"><code>type JobHandler<T = unknown> = (job: Job<T>) => void | Promise<void>;</code></pre>
|
||||
<h4 id="job-t"><code>Job<T></code></h4>
|
||||
<pre data-language="ts"><code>interface Job<T = unknown> {
|
||||
id: string; // e.g. "job_1"
|
||||
name: string;
|
||||
data: T;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
runAt: number; // epoch ms; job runs when now ≥ runAt
|
||||
repeat?: number; // if set, re-enqueue this many ms after each success
|
||||
}</code></pre>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>Register workers, enqueue jobs, then start the poller:</p>
|
||||
<pre data-language="ts"><code>import { createQueue } from "@wrnexus/queue";
|
||||
|
||||
const queue = createQueue({ maxAttempts: 3, backoffMs: 1000 });
|
||||
|
||||
// Register a worker for the "email" job name.
|
||||
queue.process<{ to: string }>("email", async (job) => {
|
||||
await send(job.data.to);
|
||||
});
|
||||
|
||||
// Enqueue a delayed job with up to 3 attempts.
|
||||
await queue.add("email", { to: "a@b.com" }, { delayMs: 5000, maxAttempts: 3 });
|
||||
|
||||
queue.start(); // begin polling; queue.stop() to halt</code></pre>
|
||||
<h4 id="recurring-jobs">Recurring jobs</h4>
|
||||
<p>Pass <code>repeat</code> to re-enqueue a job a fixed interval after each successful run:</p>
|
||||
<pre data-language="ts"><code>queue.process("heartbeat", async () => ping());
|
||||
await queue.add("heartbeat", {}, { repeat: 60_000 }); // runs ~every minute</code></pre>
|
||||
<h4 id="handling-permanent-failures">Handling permanent failures</h4>
|
||||
<p>When a job's <code>attempts</code> reaches <code>maxAttempts</code>, it is dropped and <code>onFailed</code> fires instead of retrying:</p>
|
||||
<pre data-language="ts"><code>const queue = createQueue({
|
||||
onFailed: (job, error) => {
|
||||
console.error(`job ${job.id} (${job.name}) gave up`, error);
|
||||
},
|
||||
});</code></pre>
|
||||
<h4 id="deterministic-testing">Deterministic testing</h4>
|
||||
<p>Instead of <code>start()</code>, inject a clock and drive the queue with <code>drain()</code>:</p>
|
||||
<pre data-language="ts"><code>let clock = 0;
|
||||
const queue = createQueue({ now: () => clock });
|
||||
|
||||
queue.process("task", async () => {
|
||||
/* ... */
|
||||
});
|
||||
await queue.add("task", {}, { delayMs: 5000 });
|
||||
|
||||
clock = 5000;
|
||||
const ran = await queue.drain(); // => 1</code></pre>
|
||||
<h3 id="retry-backoff-behavior">Retry & backoff behavior</h3>
|
||||
<ul>
|
||||
<li>On a thrown handler error, the job is retried while <code>attempts < maxAttempts</code>.</li>
|
||||
<li>The next <code>runAt</code> is set to <code>now + backoffMs * 2^(attempts - 1)</code> (exponential</li>
|
||||
<p>backoff): with <code>backoffMs: 1000</code> the delays are 1s, 2s, 4s, …</p>
|
||||
<li>A job whose worker name has no registered handler stays queued until one is</li>
|
||||
<p>registered (it is not counted as runnable by <code>drain</code>).</p>
|
||||
<li><code>drain</code> is re-entrant-safe: overlapping calls are skipped while one is running.</li>
|
||||
</ul>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only</strong> runtime (Node is not supported), consistent with the rest of the</li>
|
||||
<p>WRNexusJS framework. The queue itself relies only on standard timers (<code>setInterval</code>/<code>clearInterval</code>) and has no runtime dependencies.</p>
|
||||
<li>The default store is in-process, so queued jobs do not survive a restart; a</li>
|
||||
<p>pluggable driver is intended for backing it with Redis/SQL for durability.</p>
|
||||
<li>Works alongside <code>@wrnexus/core</code> for offloading work from the request path.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* @wrnexus/queue — a background job queue with delays, retries + backoff, and
|
||||
* concurrent workers. The default store is in-process; a pluggable driver lets
|
||||
* you back it with Redis/SQL for durability across restarts.
|
||||
*
|
||||
* const queue = createQueue();
|
||||
* queue.process("email", async (job) => { await send(job.data); });
|
||||
* await queue.add("email", { to: "a@b.com" }, { delayMs: 5000, maxAttempts: 3 });
|
||||
* queue.start(); // begin polling; queue.stop() to halt
|
||||
*
|
||||
* Tests can drive it deterministically with `await queue.drain(now)`.
|
||||
*/
|
||||
interface Job<T = unknown> {
|
||||
id: string;
|
||||
name: string;
|
||||
data: T;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
runAt: number;
|
||||
/** If set, re-enqueue this job this many ms after each successful run. */
|
||||
repeat?: number;
|
||||
}
|
||||
type JobHandler<T = unknown> = (job: Job<T>) => void | Promise<void>;
|
||||
interface AddOptions {
|
||||
/** Delay before the job becomes runnable (ms). */
|
||||
delayMs?: number;
|
||||
/** Max attempts before it's dead-lettered. Default from queue options. */
|
||||
maxAttempts?: number;
|
||||
/** Re-enqueue this job this many ms after each successful run (recurring). */
|
||||
repeat?: number;
|
||||
}
|
||||
interface QueueOptions {
|
||||
/** Default max attempts per job. Default 3. */
|
||||
maxAttempts?: number;
|
||||
/** Base retry backoff (ms); doubles per attempt. Default 1000. */
|
||||
backoffMs?: number;
|
||||
/** Poll interval when started (ms). Default 250. */
|
||||
pollMs?: number;
|
||||
/** Called when a job exhausts its attempts. */
|
||||
onFailed?: (job: Job, error: unknown) => void;
|
||||
/** Clock injection (tests). Default Date.now. */
|
||||
now?: () => number;
|
||||
}
|
||||
interface Queue {
|
||||
add<T>(name: string, data: T, options?: AddOptions): Promise<Job<T>>;
|
||||
process<T>(name: string, handler: JobHandler<T>): void;
|
||||
/** Run every job whose runAt ≤ now, once. Returns how many ran. */
|
||||
drain(now?: number): Promise<number>;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
size(): number;
|
||||
}
|
||||
declare function createQueue(options?: QueueOptions): Queue;
|
||||
|
||||
export { type AddOptions, type Job, type JobHandler, type Queue, type QueueOptions, createQueue };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/queue</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>function createQueue(options?: QueueOptions): Queue;</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>type JobHandler<T = unknown> = (job: Job<T>) => void | Promise<void>;</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>interface Job<T = unknown> {
|
||||
id: string; // e.g. "job_1"
|
||||
name: string;
|
||||
data: T;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
runAt: number; // epoch ms; job runs when now ≥ runAt
|
||||
repeat?: number; // if set, re-enqueue this many ms after each success
|
||||
}</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#createqueue-options-queue">createQueue(options?): Queue</a><a class="toc-level-4" href="#queueoptions">QueueOptions</a><a class="toc-level-4" href="#queue">Queue</a><a class="toc-level-4" href="#addoptions">AddOptions</a><a class="toc-level-4" href="#jobhandler-t">JobHandler<T></a><a class="toc-level-4" href="#job-t">Job<T></a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#recurring-jobs">Recurring jobs</a><a class="toc-level-4" href="#handling-permanent-failures">Handling permanent failures</a><a class="toc-level-4" href="#deterministic-testing">Deterministic testing</a><a class="toc-level-3" href="#retry-backoff-behavior">Retry & backoff behavior</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
page wrnexusreactive {
|
||||
seo {
|
||||
title = "@wrnexus/reactive"
|
||||
description = "Small type-safe reactive signal primitives."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h1>@wrnexus/reactive</h1><p>Small type-safe reactive signal primitives.</p><code>bun add @wrnexus/reactive@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend</span><h1>@wrnexus/reactive</h1><p>Small type-safe reactive signal primitives.</p><pre><code>bun add @wrnexus/reactive@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Tiny, type-safe reactive primitives (signals) with zero dependencies.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/reactive</code> is the seed of WRNexusJS's reactivity layer: a minimal <code>signal</code> primitive that holds a value, notifies subscribers when it changes, and hands back an unsubscribe function. It is deliberately small and framework-agnostic — it powers nothing on its own, but is shaped so client islands (and later the <code>.wrn</code> compiler's <code>state</code> blocks) can build reactive bindings on top of it. Reach for it when you need observable state without pulling in a full reactivity library.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/reactive</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>The package has a single entry point (<code>.</code>) exporting one function and three types.</p>
|
||||
<h4 id="signal-t-initial-t-signal-t"><code>signal<T>(initial: T): Signal<T></code></h4>
|
||||
<p>Creates a reactive signal seeded with <code>initial</code>. Returns a <code>Signal<T></code>:</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Member</th><th>Signature</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>get</code></td><td><code>(): T</code></td><td>Read the current value.</td></tr><tr><td><code>set</code></td><td><code>(next: T): void</code></td><td>Write a new value. Subscribers run <strong>only when the value actually changes</strong> (compared with <code>Object.is</code>).</td></tr><tr><td><code>update</code></td><td><code>(fn: (current: T) => T): void</code></td><td>Apply a function to the current value; equivalent to <code>set(fn(get()))</code>.</td></tr><tr><td><code>subscribe</code></td><td><code>(fn: Subscriber<T>): Unsubscribe</code></td><td>Register a subscriber; returns a function that removes it.</td></tr></tbody></table></div>
|
||||
<h4 id="types">Types</h4>
|
||||
<pre data-language="ts"><code>type Subscriber<T> = (value: T) => void;
|
||||
type Unsubscribe = () => void;
|
||||
|
||||
interface Signal<T> {
|
||||
get(): T;
|
||||
set(next: T): void;
|
||||
update(fn: (current: T) => T): void;
|
||||
subscribe(fn: Subscriber<T>): Unsubscribe;
|
||||
}</code></pre>
|
||||
<p>Notes on semantics:</p>
|
||||
<ul>
|
||||
<li><strong>No-op updates are skipped.</strong> <code>set</code> compares the incoming value to the current</li>
|
||||
<p>one with <code>Object.is</code>; identical values do not notify subscribers.</p>
|
||||
<li><strong>Safe unsubscribe during notification.</strong> Subscribers are iterated over a copy of</li>
|
||||
<p>the subscriber set, so a subscriber may call its own (or another's) unsubscribe while a notification is in flight.</p>
|
||||
</ul>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre data-language="ts"><code>import { signal } from "@wrnexus/reactive";
|
||||
|
||||
const count = signal(0);
|
||||
|
||||
count.get(); // 0
|
||||
|
||||
// Subscribe; the returned function unsubscribes.
|
||||
const off = count.subscribe((value) => {
|
||||
console.log("count is now", value);
|
||||
});
|
||||
|
||||
count.set(1); // logs: count is now 1
|
||||
count.set(1); // no-op — value unchanged, no notification
|
||||
count.update((n) => n + 1); // logs: count is now 2
|
||||
|
||||
off(); // stop listening
|
||||
count.set(3); // nothing logged</code></pre>
|
||||
<p>Typed signals infer <code>T</code> from the initial value, or can be annotated explicitly:</p>
|
||||
<pre data-language="ts"><code>import { signal, type Signal } from "@wrnexus/reactive";
|
||||
|
||||
const user: Signal<{ name: string } | null> = signal(null);
|
||||
user.set({ name: "Ada" });</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Distributed as TypeScript source (<code>main</code>/<code>exports</code> point at</li>
|
||||
<p><code>src/index.ts</code>); consume it under Bun, which runs <code>.ts</code> directly.</p>
|
||||
<li><strong>Zero dependencies.</strong> The only runtime API used is the standard <code>Object.is</code>.</li>
|
||||
<li>Foundational primitive for WRNexusJS client islands and the forthcoming <code>.wrn</code></li>
|
||||
<p>compiler <code>state</code> blocks.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* A minimal, type-safe reactive signal with zero dependencies.
|
||||
*
|
||||
* This is the seed of the framework's reactivity. Today it powers nothing on
|
||||
* its own, but it is shaped so client islands (and later the `.wrn` compiler's
|
||||
* `state` blocks) can build reactive bindings on top of it.
|
||||
*
|
||||
* const count = signal(0)
|
||||
* count.get() // 0
|
||||
* count.set(1) // notifies subscribers
|
||||
* const off = count.subscribe(v => console.log(v))
|
||||
* off() // unsubscribe
|
||||
*/
|
||||
type Subscriber<T> = (value: T) => void;
|
||||
type Unsubscribe = () => void;
|
||||
interface Signal<T> {
|
||||
/** Read the current value. */
|
||||
get(): T;
|
||||
/** Write a new value; subscribers run only when the value actually changes. */
|
||||
set(next: T): void;
|
||||
/** Apply a function to the current value. */
|
||||
update(fn: (current: T) => T): void;
|
||||
/** Subscribe to changes; returns an unsubscribe function. */
|
||||
subscribe(fn: Subscriber<T>): Unsubscribe;
|
||||
}
|
||||
declare function signal<T>(initial: T): Signal<T>;
|
||||
|
||||
export { type Signal, type Subscriber, type Unsubscribe, signal };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/reactive</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>type Subscriber<T> = (value: T) => void;
|
||||
type Unsubscribe = () => void;
|
||||
|
||||
interface Signal<T> {
|
||||
get(): T;
|
||||
set(next: T): void;
|
||||
update(fn: (current: T) => T): void;
|
||||
subscribe(fn: Subscriber<T>): Unsubscribe;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>import { signal } from "@wrnexus/reactive";
|
||||
|
||||
const count = signal(0);
|
||||
|
||||
count.get(); // 0
|
||||
|
||||
// Subscribe; the returned function unsubscribes.
|
||||
const off = count.subscribe((value) => {
|
||||
console.log("count is now", value);
|
||||
});
|
||||
|
||||
count.set(1); // logs: count is now 1
|
||||
count.set(1); // no-op — value unchanged, no notification
|
||||
count.update((n) => n + 1); // logs: count is now 2
|
||||
|
||||
off(); // stop listening
|
||||
count.set(3); // nothing logged</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import { signal, type Signal } from "@wrnexus/reactive";
|
||||
|
||||
const user: Signal<{ name: string } | null> = signal(null);
|
||||
user.set({ name: "Ada" });</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#signal-t-initial-t-signal-t">signal<T>(initial: T): Signal<T></a><a class="toc-level-4" href="#types">Types</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
page wrnexusrouter {
|
||||
seo {
|
||||
title = "@wrnexus/router"
|
||||
description = "Filesystem discovery, route matching, and typed route generation."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Core</span><h1>@wrnexus/router</h1><p>Filesystem discovery, route matching, and typed route generation.</p><code>bun add @wrnexus/router@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Core</span><h1>@wrnexus/router</h1><p>Filesystem discovery, route matching, and typed route generation.</p><pre><code>bun add @wrnexus/router@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>File-based router that maps an <code>app/</code> directory onto route tables and matches request paths against them.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/router</code> scans an application's <code>app/</code> directory once at startup and builds route tables for pages, API endpoints, realtime channels, middleware, server-rendered <code>.wrn</code> components, layouts, and validation schemas. It also compiles URL patterns (<code>/users/[id]</code>) into RegExps and matches request paths against them. Request input is never turned into a file path, which makes the router immune to path traversal. This is a server-side package used by the WRNexusJS runtime to resolve incoming requests, plus a codegen helper for compile-time typed links.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/router</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="directory-conventions">Directory conventions</h3>
|
||||
<p>The router maps files under <code>appDir</code> onto routes:</p>
|
||||
<pre data-language=""><code>app/pages/index.tsx -> GET /
|
||||
app/pages/about.tsx -> GET /about
|
||||
app/pages/users/[id].tsx -> GET /users/:id
|
||||
app/api/hello.ts -> /api/hello
|
||||
app/realtime/chat.ts -> /realtime/chat
|
||||
app/pages/*.wrn (api) -> embedded /api/* routes
|
||||
app/pages/*.wrn (rt) -> embedded /realtime/* routes
|
||||
app/middleware/*.ts -> global middleware (alphabetical)
|
||||
app/components/*.wrn -> server-rendered components (by basename)
|
||||
app/layouts/*.wrn -> named page layouts
|
||||
app/schemas/*.ts -> validation schemas</code></pre>
|
||||
<p>Allowed route extensions are <code>.ts</code>, <code>.tsx</code>, and <code>.wrn</code>. Dotfiles and underscore-prefixed files are ignored. A trailing <code>index</code> segment is dropped from the route. <code>.wrn</code> pages may embed <code>api</code> and <code>realtime</code> blocks, which the router extracts and mounts under <code>/api/*</code> and <code>/realtime/*</code>.</p>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="buildrouter-appdir-opts-router"><code>buildRouter(appDir, opts?): Router</code></h4>
|
||||
<p>Scan an app directory and build all route tables.</p>
|
||||
<pre data-language="ts"><code>function buildRouter(appDir: string, opts?: RouterOptions): Router;
|
||||
|
||||
interface RouterOptions {
|
||||
/** Extra dirs scanned for `.wrn` components (e.g. `@wrnexus/ui`), before
|
||||
* `app/components`, so an app component of the same name wins. */
|
||||
componentDirs?: string[];
|
||||
}</code></pre>
|
||||
<p>The returned <code>Router</code> exposes the built tables plus per-kind matchers:</p>
|
||||
<pre data-language="ts"><code>interface Router {
|
||||
pages: Route[];
|
||||
api: Route[];
|
||||
realtime: Route[];
|
||||
/** Absolute paths of middleware modules, in execution order (alphabetical). */
|
||||
middlewareFiles: string[];
|
||||
/** Server-rendered `.wrn` components, mounted via `data-component`. */
|
||||
components: ComponentRef[];
|
||||
/** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
|
||||
layouts: ComponentRef[];
|
||||
/** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
|
||||
schemas: ComponentRef[];
|
||||
matchPage(pathname: string): RouteMatch | null;
|
||||
matchApi(pathname: string): RouteMatch | null;
|
||||
matchRealtime(pathname: string): RouteMatch | null;
|
||||
}
|
||||
|
||||
interface ComponentRef {
|
||||
/** Validated component name (matches a `data-component` attribute). */
|
||||
name: string;
|
||||
/** Absolute path to the component's `.wrn` module. */
|
||||
file: string;
|
||||
}</code></pre>
|
||||
<p>Component, layout, and schema names are validated with <code>isSafeIslandName</code> from <code>@wrnexus/core</code>; unsafe names are skipped with a warning. Realtime channel names are validated the same way.</p>
|
||||
<h4 id="route-matching">Route matching</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>compileRoutePattern</code></td><td>`(raw: string) => Pick<Route, "regex" \</td><td>"paramNames">`</td><td>Compile a <code>/users/[id]</code> pattern into a RegExp (with optional trailing slash) plus ordered param names.</td></tr><tr><td><code>matchRoute</code></td><td>`(routes: Route[], pathname: string) => RouteMatch \</td><td>null`</td><td>Return the first route whose regex matches; captured params are <code>decodeURIComponent</code>-decoded.</td></tr><tr><td><code>sortRoutes</code></td><td><code>(routes: Route[]) => Route[]</code></td><td>Order routes so static routes win over dynamic ones (fewer params first), then longer/more specific patterns first.</td></tr></tbody></table></div>
|
||||
<pre data-language="ts"><code>interface Route {
|
||||
raw: string; // e.g. "/users/[id]"
|
||||
file: string; // absolute path to the handling module
|
||||
regex: RegExp; // compiled matcher
|
||||
paramNames: string[]; // ordered dynamic param names
|
||||
}
|
||||
|
||||
interface RouteMatch {
|
||||
route: Route;
|
||||
params: Record<string, string>;
|
||||
}</code></pre>
|
||||
<h4 id="typed-routes-codegen">Typed-routes codegen</h4>
|
||||
<pre data-language="ts"><code>function generateRoutesFile(pages: Route[]): string;</code></pre>
|
||||
<p>Emits the source for <code>app/routes.gen.ts</code>: a <code>Routes</code> map (each page path → its <code>[param]</code> types), a <code>RoutePath</code> union, and an <code>href()</code> builder that fills params and rejects unknown paths at compile time. Entries are de-duplicated and sorted by path.</p>
|
||||
<h4 id="re-exports">Re-exports</h4>
|
||||
<p><code>Middleware</code> (the type from <code>@wrnexus/core</code>) is re-exported for callers that load middleware modules themselves.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre data-language="ts"><code>import { buildRouter } from "@wrnexus/router";
|
||||
|
||||
const router = buildRouter("./app", {
|
||||
componentDirs: ["./node_modules/@wrnexus/ui/components"],
|
||||
});
|
||||
|
||||
// Resolve an incoming request.
|
||||
const match = router.matchPage("/users/42");
|
||||
if (match) {
|
||||
console.log(match.route.file); // absolute path to the page module
|
||||
console.log(match.params); // { id: "42" }
|
||||
}
|
||||
|
||||
const api = router.matchApi("/api/hello");
|
||||
const rt = router.matchRealtime("/realtime/chat");</code></pre>
|
||||
<p>Generating the typed-routes file (as <code>wrnexus dev</code> does):</p>
|
||||
<pre data-language="ts"><code>import { generateRoutesFile } from "@wrnexus/router";
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const router = buildRouter("./app");
|
||||
writeFileSync("./app/routes.gen.ts", generateRoutesFile(router.pages));</code></pre>
|
||||
<pre data-language="ts"><code>// Then, in app code, links are checked at compile time:
|
||||
import { href } from "./routes.gen.ts";
|
||||
|
||||
href("/users/[id]", { id: "42" }); // "/users/42"
|
||||
href("/about"); // "/about"
|
||||
href("/nope"); // type error: unknown path</code></pre>
|
||||
<p>Lower-level pattern matching, if you need it directly:</p>
|
||||
<pre data-language="ts"><code>import { compileRoutePattern, matchRoute, sortRoutes, type Route } from "@wrnexus/router";
|
||||
|
||||
const { regex, paramNames } = compileRoutePattern("/posts/[slug]");
|
||||
const routes = sortRoutes([{ raw: "/posts/[slug]", file: "…", regex, paramNames }]);
|
||||
const m = matchRoute(routes, "/posts/hello"); // { route, params: { slug: "hello" } }</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li>Scanning uses <code>node:fs</code> (<code>existsSync</code>, <code>readdirSync</code>, <code>statSync</code>) and <code>node:path</code> — runs under Bun.</li>
|
||||
<li>Depends on [<code>@wrnexus/compiler</code>](../compiler) to <code>parse</code> <code>.wrn</code> pages and extract embedded <code>api</code> / <code>realtime</code> blocks.</li>
|
||||
<li>Depends on [<code>@wrnexus/core</code>](../core) for <code>isSafeIslandName</code> (name validation) and the <code>Middleware</code> type.</li>
|
||||
<li>Missing route directories are tolerated — a route kind you don't use simply yields an empty table.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>export { Middleware } from '@wrnexus/core';
|
||||
|
||||
/**
|
||||
* Route compilation + matching.
|
||||
*
|
||||
* A "route" is a URL pattern compiled to a RegExp. We support static segments
|
||||
* and dynamic `[param]` segments, e.g. `/users/[id]` -> `{ id }`.
|
||||
*/
|
||||
interface Route {
|
||||
/** The human-readable route pattern, e.g. `/users/[id]`. */
|
||||
raw: string;
|
||||
/** Absolute path to the module that handles this route. */
|
||||
file: string;
|
||||
/** Compiled matcher. */
|
||||
regex: RegExp;
|
||||
/** Ordered names of dynamic params captured by `regex`. */
|
||||
paramNames: string[];
|
||||
}
|
||||
interface RouteMatch {
|
||||
route: Route;
|
||||
params: Record<string, string>;
|
||||
}
|
||||
/** Compile a `/users/[id]` style pattern into a RegExp + param names. */
|
||||
declare function compileRoutePattern(raw: string): Pick<Route, "regex" | "paramNames">;
|
||||
/**
|
||||
* Order routes so that static routes win over dynamic ones, and longer/more
|
||||
* specific routes win over shorter ones. Sorting once keeps matching simple.
|
||||
*/
|
||||
declare function sortRoutes(routes: Route[]): Route[];
|
||||
/** Find the first route whose pattern matches `pathname`. */
|
||||
declare function matchRoute(routes: Route[], pathname: string): RouteMatch | null;
|
||||
|
||||
/**
|
||||
* Typed-routes codegen. From the scanned page routes, emit `app/routes.gen.ts`
|
||||
* with a `Routes` map (path → param types) and an `href()` builder — so links
|
||||
* are checked at compile time (unknown path or missing param = type error).
|
||||
*/
|
||||
|
||||
declare function generateRoutesFile(pages: Route[]): string;
|
||||
|
||||
/**
|
||||
* @wrnexus/router — file-based router.
|
||||
*
|
||||
* Maps the `app/` directory onto route tables:
|
||||
* app/pages/index.tsx -> GET /
|
||||
* app/pages/about.tsx -> GET /about
|
||||
* app/pages/users/[id].tsx-> GET /users/:id
|
||||
* app/api/hello.ts -> /api/hello
|
||||
* app/realtime/chat.ts -> /realtime/chat
|
||||
* app/pages/*.wrn api -> embedded /api/* routes
|
||||
* app/pages/*.wrn realtime -> embedded /realtime/* routes
|
||||
* app/middleware/*.ts -> global middleware (alphabetical)
|
||||
* app/components/*.wrn -> server-rendered components (by basename),
|
||||
* mounted in a page via data-component="<name>"
|
||||
*/
|
||||
|
||||
interface ComponentRef {
|
||||
/** Validated component name (matches a `data-component` attribute). */
|
||||
name: string;
|
||||
/** Absolute path to the component's `.wrn` module. */
|
||||
file: string;
|
||||
}
|
||||
interface Router {
|
||||
pages: Route[];
|
||||
api: Route[];
|
||||
realtime: Route[];
|
||||
/** Absolute paths of middleware modules, in execution order. */
|
||||
middlewareFiles: string[];
|
||||
/** Server-rendered `.wrn` components, mounted via `data-component`. */
|
||||
components: ComponentRef[];
|
||||
/** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
|
||||
layouts: ComponentRef[];
|
||||
/** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
|
||||
schemas: ComponentRef[];
|
||||
matchPage(pathname: string): RouteMatch | null;
|
||||
matchApi(pathname: string): RouteMatch | null;
|
||||
matchRealtime(pathname: string): RouteMatch | null;
|
||||
}
|
||||
interface RouterOptions {
|
||||
/**
|
||||
* Extra directories to scan for `.wrn` components (e.g. `@wrnexus/ui`).
|
||||
* Scanned before `app/components`, so an app component of the same name wins.
|
||||
*/
|
||||
componentDirs?: string[];
|
||||
}
|
||||
/** Scan an app directory and build all route tables. */
|
||||
declare function buildRouter(appDir: string, opts?: RouterOptions): Router;
|
||||
|
||||
export { type ComponentRef, type Route, type RouteMatch, type Router, type RouterOptions, buildRouter, compileRoutePattern, generateRoutesFile, matchRoute, sortRoutes };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/router</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="text"><code>app/pages/index.tsx -> GET /
|
||||
app/pages/about.tsx -> GET /about
|
||||
app/pages/users/[id].tsx -> GET /users/:id
|
||||
app/api/hello.ts -> /api/hello
|
||||
app/realtime/chat.ts -> /realtime/chat
|
||||
app/pages/*.wrn (api) -> embedded /api/* routes
|
||||
app/pages/*.wrn (rt) -> embedded /realtime/* routes
|
||||
app/middleware/*.ts -> global middleware (alphabetical)
|
||||
app/components/*.wrn -> server-rendered components (by basename)
|
||||
app/layouts/*.wrn -> named page layouts
|
||||
app/schemas/*.ts -> validation schemas</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>function buildRouter(appDir: string, opts?: RouterOptions): Router;
|
||||
|
||||
interface RouterOptions {
|
||||
/** Extra dirs scanned for `.wrn` components (e.g. `@wrnexus/ui`), before
|
||||
* `app/components`, so an app component of the same name wins. */
|
||||
componentDirs?: string[];
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>interface Router {
|
||||
pages: Route[];
|
||||
api: Route[];
|
||||
realtime: Route[];
|
||||
/** Absolute paths of middleware modules, in execution order (alphabetical). */
|
||||
middlewareFiles: string[];
|
||||
/** Server-rendered `.wrn` components, mounted via `data-component`. */
|
||||
components: ComponentRef[];
|
||||
/** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
|
||||
layouts: ComponentRef[];
|
||||
/** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
|
||||
schemas: ComponentRef[];
|
||||
matchPage(pathname: string): RouteMatch | null;
|
||||
matchApi(pathname: string): RouteMatch | null;
|
||||
matchRealtime(pathname: string): RouteMatch | null;
|
||||
}
|
||||
|
||||
interface ComponentRef {
|
||||
/** Validated component name (matches a `data-component` attribute). */
|
||||
name: string;
|
||||
/** Absolute path to the component's `.wrn` module. */
|
||||
file: string;
|
||||
}</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#directory-conventions">Directory conventions</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#buildrouter-appdir-opts-router">buildRouter(appDir, opts?): Router</a><a class="toc-level-4" href="#route-matching">Route matching</a><a class="toc-level-4" href="#typed-routes-codegen">Typed-routes codegen</a><a class="toc-level-4" href="#re-exports">Re-exports</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
page wrnexusssr {
|
||||
seo {
|
||||
title = "@wrnexus/ssr"
|
||||
description = "Secure HTML document rendering and SEO metadata."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Runtime</span><h1>@wrnexus/ssr</h1><p>Secure HTML document rendering and SEO metadata.</p><code>bun add @wrnexus/ssr@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Runtime</span><h1>@wrnexus/ssr</h1><p>Secure HTML document rendering and SEO metadata.</p><pre><code>bun add @wrnexus/ssr@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Server-side rendering: wraps a page's HTML body in a complete HTML document with a metadata-driven <code><head></code>.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p>Pages in WRNexusJS return an HTML string for the body. <code>@wrnexus/ssr</code> takes that body and produces a full HTML document — building the <code><head></code> from page metadata and global SEO defaults, resolving canonical/Open Graph/Twitter tags, and injecting module preloads and <code><script type="module"></code> tags. It is deliberately server-only: nothing in this package touches the DOM or ships to the browser, keeping server code genuinely server-only. Reach for it on the server when turning a rendered page body into a response document.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/ssr</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>The package has a single export.</p>
|
||||
<h4 id="renderdocument-opts-renderoptions-string"><code>renderDocument(opts: RenderOptions): string</code></h4>
|
||||
<p>Renders a complete HTML document as a string, beginning with <code><!doctype html></code>. All metadata is HTML-escaped (via <code>escapeHtml</code> from <code>@wrnexus/core</code>), so a malicious title or description cannot break out of its element or attribute. The body is placed inside <code><div id="app"></code>.</p>
|
||||
<h4 id="renderoptions"><code>RenderOptions</code></h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>meta</code></td><td><code>PageMeta</code></td><td>Page metadata for the document head (required).</td></tr><tr><td><code>body</code></td><td><code>string</code></td><td>Rendered HTML for the body, placed inside <code>#app</code> (required).</td></tr><tr><td><code>seo</code></td><td><code>SeoConfig</code></td><td>Global SEO defaults, typically from <code>wrnexus.config.ts</code>.</td></tr><tr><td><code>url</code></td><td><code>URL</code></td><td>Current request URL, used to resolve canonical/Open Graph URLs.</td></tr><tr><td><code>scripts</code></td><td><code>string[]</code></td><td>URLs of <code><script type="module"></code> tags to load (e.g. per-island chunks or the reactive runtime). Each also gets a <code><link rel="modulepreload"></code>.</td></tr><tr><td><code>defaultTitle</code></td><td><code>string</code></td><td>Default document title used when <code>meta.title</code> is absent.</td></tr><tr><td><code>extraHead</code></td><td><code>string</code></td><td>Raw HTML injected at the end of <code><head></code> (trusted, framework-controlled — not escaped).</td></tr><tr><td><code>extraBody</code></td><td><code>string</code></td><td>Raw HTML injected at the end of <code><body></code> (trusted, framework-controlled — not escaped).</td></tr><tr><td><code>htmlAttrs</code></td><td><code>string</code></td><td>Attributes for the <code><html></code> element, e.g. <code> data-theme="dark"</code> (trusted).</td></tr></tbody></table></div>
|
||||
<p><code>PageMeta</code> and <code>SeoConfig</code> come from <code>@wrnexus/core</code>. <code>PageMeta</code> is an alias of <code>SeoConfig</code>, whose fields are all optional:</p>
|
||||
<pre data-language="ts"><code>type SeoConfig = {
|
||||
title?: string;
|
||||
titleTemplate?: string; // e.g. "%s — My Site"; %s is replaced with the page title
|
||||
description?: string;
|
||||
canonical?: string;
|
||||
canonicalBase?: string; // origin used to absolutize canonical/image URLs
|
||||
robots?: string;
|
||||
keywords?: string | string[];
|
||||
image?: string;
|
||||
siteName?: string;
|
||||
type?: string; // Open Graph type; defaults to "website"
|
||||
locale?: string;
|
||||
twitterCard?: string; // defaults to "summary"
|
||||
twitterSite?: string;
|
||||
themeColor?: string;
|
||||
};</code></pre>
|
||||
<h4 id="metadata-resolution">Metadata resolution</h4>
|
||||
<p><code>renderDocument</code> merges page metadata (<code>meta</code>) over global defaults (<code>seo</code>), field by field, so per-page values win. Notable behavior:</p>
|
||||
<ul>
|
||||
<li><strong>Title</strong>: uses <code>meta.title</code>, else <code>seo.title</code>, else <code>defaultTitle</code>, else <code>"WRNexusJS"</code>. When the page sets its own title and <code>seo.titleTemplate</code> contains <code>%s</code>, the template is applied.</li>
|
||||
<li><strong>Canonical / image URLs</strong>: resolved against <code>canonicalBase</code> (or the request <code>url</code>'s origin) into absolute URLs when possible.</li>
|
||||
<li><strong>Keywords</strong>: an array is joined with <code>", "</code>.</li>
|
||||
<li><strong>Emitted tags</strong>: <code><title></code>, and as applicable <code>description</code>, <code>robots</code>, <code>keywords</code>, <code>theme-color</code>, and <code>canonical</code> link, plus Open Graph (<code>og:title</code>, <code>og:description</code>, <code>og:type</code>, <code>og:url</code>, <code>og:site_name</code>, <code>og:locale</code>, <code>og:image</code>) and Twitter (<code>twitter:card</code>, <code>twitter:title</code>, <code>twitter:description</code>, <code>twitter:image</code>, <code>twitter:site</code>) meta tags. The document always includes <code>charset</code>, <code>viewport</code>, and a <code>/favicon.ico</code> icon link.</li>
|
||||
</ul>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre data-language="ts"><code>import { renderDocument } from "@wrnexus/ssr";
|
||||
|
||||
const html = renderDocument({
|
||||
meta: {
|
||||
title: "About Us",
|
||||
description: "Learn more about our team.",
|
||||
},
|
||||
seo: {
|
||||
titleTemplate: "%s — Acme",
|
||||
siteName: "Acme",
|
||||
canonicalBase: "https://acme.example",
|
||||
twitterSite: "@acme",
|
||||
},
|
||||
url: new URL("https://acme.example/about"),
|
||||
body: "<h1>About Us</h1>",
|
||||
scripts: ["/_wire/runtime.js", "/_wire/islands/about.js"],
|
||||
htmlAttrs: ' data-theme="dark"',
|
||||
});
|
||||
|
||||
return new Response(html, {
|
||||
headers: { "content-type": "text/html; charset=utf-8" },
|
||||
});</code></pre>
|
||||
<p>The produced document has <code><title>About Us — Acme</title></code>, the SEO/Open Graph/Twitter tags derived from the merged metadata, a <code>modulepreload</code> link and module <code><script></code> for each entry in <code>scripts</code>, and the body wrapped in <code><div id="app"></code>.</p>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Server-only.</strong> This module never imports or touches the DOM and is safe to keep out of client bundles.</li>
|
||||
<li><strong>Depends on [<code>@wrnexus/core</code>](../core)</strong> for <code>escapeHtml</code> and the <code>PageMeta</code> / <code>SeoConfig</code> types.</li>
|
||||
<li><strong>Bun-only</strong> — like the rest of WRNexusJS, this package targets the Bun runtime (Node is not supported).</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { PageMeta, SeoConfig } from '@wrnexus/core';
|
||||
|
||||
/**
|
||||
* @wrnexus/ssr — server-side rendering.
|
||||
*
|
||||
* Pages return an HTML string for the body; this module wraps that body in a
|
||||
* full document with a `<head>` built from page metadata. It is intentionally
|
||||
* isolated from any client runtime: nothing here touches the DOM or ships to
|
||||
* the browser, which keeps "server-only code" genuinely server-only.
|
||||
*/
|
||||
|
||||
interface RenderOptions {
|
||||
/** Page metadata for the document head. */
|
||||
meta: PageMeta;
|
||||
/** Global SEO defaults from `wrnexus.config.ts`. */
|
||||
seo?: SeoConfig;
|
||||
/** Current request URL, used to resolve canonical/Open Graph URLs. */
|
||||
url?: URL;
|
||||
/** Rendered HTML for the body (placed inside `#app`). */
|
||||
body: string;
|
||||
/**
|
||||
* URLs of `<script type="module">` tags to load (e.g. per-island chunks or
|
||||
* the reactive runtime). Only the scripts a page actually needs are passed.
|
||||
*/
|
||||
scripts?: string[];
|
||||
/** Optional default document title used when meta.title is absent. */
|
||||
defaultTitle?: string;
|
||||
/** Raw HTML injected at the end of `<head>` (trusted, framework-controlled). */
|
||||
extraHead?: string;
|
||||
/** Raw HTML injected at the end of `<body>` (trusted, framework-controlled). */
|
||||
extraBody?: string;
|
||||
/** Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted). */
|
||||
htmlAttrs?: string;
|
||||
}
|
||||
/**
|
||||
* Render a complete HTML document.
|
||||
*
|
||||
* Metadata is HTML-escaped so a malicious title/description can never break
|
||||
* out of its element or attribute.
|
||||
*/
|
||||
declare function renderDocument(opts: RenderOptions): string;
|
||||
|
||||
export { type RenderOptions, renderDocument };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/ssr</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>type SeoConfig = {
|
||||
title?: string;
|
||||
titleTemplate?: string; // e.g. "%s — My Site"; %s is replaced with the page title
|
||||
description?: string;
|
||||
canonical?: string;
|
||||
canonicalBase?: string; // origin used to absolutize canonical/image URLs
|
||||
robots?: string;
|
||||
keywords?: string | string[];
|
||||
image?: string;
|
||||
siteName?: string;
|
||||
type?: string; // Open Graph type; defaults to "website"
|
||||
locale?: string;
|
||||
twitterCard?: string; // defaults to "summary"
|
||||
twitterSite?: string;
|
||||
themeColor?: string;
|
||||
};</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>import { renderDocument } from "@wrnexus/ssr";
|
||||
|
||||
const html = renderDocument({
|
||||
meta: {
|
||||
title: "About Us",
|
||||
description: "Learn more about our team.",
|
||||
},
|
||||
seo: {
|
||||
titleTemplate: "%s — Acme",
|
||||
siteName: "Acme",
|
||||
canonicalBase: "https://acme.example",
|
||||
twitterSite: "@acme",
|
||||
},
|
||||
url: new URL("https://acme.example/about"),
|
||||
body: "<h1>About Us</h1>",
|
||||
scripts: ["/_wire/runtime.js", "/_wire/islands/about.js"],
|
||||
htmlAttrs: ' data-theme="dark"',
|
||||
});
|
||||
|
||||
return new Response(html, {
|
||||
headers: { "content-type": "text/html; charset=utf-8" },
|
||||
});</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#renderdocument-opts-renderoptions-string">renderDocument(opts: RenderOptions): string</a><a class="toc-level-4" href="#renderoptions">RenderOptions</a><a class="toc-level-4" href="#metadata-resolution">Metadata resolution</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
page wrnexusstyles {
|
||||
seo {
|
||||
title = "@wrnexus/styles"
|
||||
description = "CSS pipeline, themes, fonts, profiles, and application config."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h1>@wrnexus/styles</h1><p>CSS pipeline, themes, fonts, profiles, and application config.</p><code>bun add @wrnexus/styles@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend</span><h1>@wrnexus/styles</h1><p>CSS pipeline, themes, fonts, profiles, and application config.</p><pre><code>bun add @wrnexus/styles@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Global CSS bundling, the <code>--wire-*</code> design-token theme system, and the <code>wrnexus.config.ts</code> app-config loader for WRNexusJS apps.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p>This package owns three server-side concerns that shape every page a WRNexusJS app renders:</p>
|
||||
<p>1. <strong>Global stylesheet pipeline</strong> — finds <code>app/styles/global.css</code> (or aggregates <code>app/styles/*.css</code>), bundles it with Bun's CSS bundler (which resolves <code>@import</code>, including from <code>node_modules</code>), and produces one stylesheet that is <code><link></code>ed into every page's <code><head></code>. Because it is a plain global sheet, it styles server-rendered markup and hydrated client islands identically. A custom <code>process</code> hook lets you swap in Tailwind / PostCSS / Sass. 2. <strong>Theme system</strong> — design tokens exposed as CSS custom properties (<code>--wire-<key></code>), with built-in <code>light</code>/<code>dark</code> sets, deep-merged user overrides, an SSR <code><html data-theme></code> render (no flash), and a tiny client runtime to toggle/persist the choice. 3. <strong>App config</strong> — loads <code>wrnexus.config.ts</code> (the <code>AppConfig</code> type), applies named profile overrides, and loads the <code>.env</code> cascade.</p>
|
||||
<p>It runs server-side / at build time. Reach for it when configuring an app, defining themes, or customising how global CSS is produced.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/styles</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<p>Everything is exported from the package root (<code>@wrnexus/styles</code>).</p>
|
||||
<h4 id="config-loading">Config loading</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>loadAppConfig</code></td><td><code>(appRoot: string, profile?: string) => Promise<AppConfig></code></td><td>Load <code>wrnexus.config.*</code> with the active profile deep-merged in (<code>profiles</code> stripped from the result).</td></tr><tr><td><code>loadRawConfig</code></td><td><code>(appRoot: string) => Promise<AppConfig></code></td><td>Load the raw config with the <code>profiles</code> map intact; returns <code>{}</code> if no config file exists.</td></tr><tr><td><code>resolveProfile</code></td><td><code>(options?: { explicit?; mode? }) => string</code></td><td>Resolve the active profile: explicit arg > <code>WRNEXUS_PROFILE</code> env var > mode-based default (<code>production</code> in prod, else <code>development</code>).</td></tr><tr><td><code>loadEnv</code></td><td><code>(appRoot: string, profile: string) => Record<string, string></code></td><td>Load the <code>.env</code> cascade for a profile into <code>process.env</code> without clobbering real env vars. Returns what it loaded.</td></tr><tr><td><code>headToString</code></td><td>`(head?: string \</td><td>string[]) => string`</td><td>Flatten <code>AppConfig.head</code> into a single HTML string.</td></tr></tbody></table></div>
|
||||
<p>Config file names probed, in order: <code>wrnexus.config.ts</code>, <code>wrnexus.config.js</code>, <code>wrnexus.config.mjs</code>.</p>
|
||||
<p><code>.env</code> cascade precedence (low → high): <code>.env</code> < <code>.env.<profile></code> < <code>.env.local</code> < <code>.env.<profile>.local</code>. Variables already present in the real environment always win.</p>
|
||||
<h4 id="appconfig"><code>AppConfig</code></h4>
|
||||
<p>The type of the object your <code>wrnexus.config.ts</code> default-exports. Every field is optional.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>head</code></td><td>`string \</td><td>string[]`</td><td>Raw HTML appended to every page's <code><head></code> (e.g. CDN stylesheet/script links).</td></tr><tr><td><code>seo</code></td><td><code>SeoConfig</code></td><td>Global SEO defaults, merged with each page's exported <code>meta</code>. (from <code>@wrnexus/core</code>)</td></tr><tr><td><code>security</code></td><td><code>SecurityConfig</code></td><td>Framework security headers and optional CORS policy. (from <code>@wrnexus/core</code>)</td></tr><tr><td><code>styles</code></td><td><code>StylesConfig</code></td><td>Global stylesheet pipeline config (see below).</td></tr><tr><td><code>theme</code></td><td><code>ThemeConfig</code></td><td>Design-token themes, deep-merged over the built-in light/dark.</td></tr><tr><td><code>i18n</code></td><td><code>{ default?: string; locales?: string[] }</code></td><td>Default language + supported locales (strings live in <code>app/locales/*.json</code>).</td></tr><tr><td><code>db</code></td><td>`{ driver: "sqlite" \</td><td>"postgres" \</td><td>"mysql" \</td><td>"mongo"; url: string }`</td><td>Default database connection; reached with <code>getDb()</code>.</td></tr><tr><td><code>databases</code></td><td><code>Record<string, { driver; url }></code></td><td>Additional named databases, reached with <code>getDb("<name>")</code>; each has its own <code>app/db/<name>/</code> migrations/queries.</td></tr><tr><td><code>realtime</code></td><td><code>{ scale?: boolean; redisUrl?: string }</code></td><td>When <code>scale</code> is true (or <code>redisUrl</code> is set), room broadcasts bridge over Redis pub/sub so they reach clients on every app process.</td></tr><tr><td><code>port</code></td><td><code>number</code></td><td>Default server port.</td></tr><tr><td><code>profiles</code></td><td><code>Record<string, Partial<Omit<AppConfig, "profiles">>></code></td><td>Named profiles (dev, prod, uat, test, …). The active profile's overrides are deep-merged over the base config. Selected via <code>--profile=<name></code> or <code>WRNEXUS_PROFILE</code>.</td></tr></tbody></table></div>
|
||||
<h4 id="styles-pipeline">Styles pipeline</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>findStyleEntry</code></td><td>`(appDir, appRoot, override?) => string \</td><td>null`</td><td>Resolve the CSS entry: <code>override</code> (relative to <code>appRoot</code>) → <code>app/styles/global.css</code> → an aggregate of all <code>app/styles/*.css</code> (written to <code>app/.wrnexus/styles-entry.css</code>). <code>null</code> if the app has no styles.</td></tr><tr><td><code>bundleCss</code></td><td><code>(entryPath: string, mode: Mode) => Promise<string></code></td><td>Bundle an entry with <code>Bun.build</code> (CSS bundler). Resolves <code>@import</code> (local + node_modules), handles nesting, minifies when <code>mode === "production"</code>.</td></tr><tr><td><code>renderStyles</code></td><td><code>(ctx: StyleProcessContext, styles?: StylesConfig) => Promise<string></code></td><td>Produce final CSS: runs <code>styles.process(ctx)</code> if provided, else <code>bundleCss</code>. Returns <code>""</code> when <code>ctx.entryPath</code> is null.</td></tr></tbody></table></div>
|
||||
<p><code>StylesConfig</code>:</p>
|
||||
<pre data-language="ts"><code>interface StylesConfig {
|
||||
/** CSS entry path relative to the app root. Default: app/styles/global.css */
|
||||
entry?: string;
|
||||
/** Custom processor — return the final CSS string (Tailwind/PostCSS/Sass). */
|
||||
process?: (ctx: StyleProcessContext) => string | Promise<string>;
|
||||
}
|
||||
|
||||
interface StyleProcessContext {
|
||||
entryPath: string | null; // resolved absolute CSS entry, or null
|
||||
appDir: string;
|
||||
appRoot: string;
|
||||
mode: Mode; // "development" | "production"
|
||||
}</code></pre>
|
||||
<h4 id="theme-system">Theme system</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Type / Signature</th><th>Purpose</th></tr></thead>
|
||||
<tbody><tr><td><code>DEFAULT_THEMES</code></td><td><code>Record<string, ThemeTokens></code></td><td>Built-in <code>light</code> and <code>dark</code> token maps.</td></tr><tr><td><code>THEME_COOKIE</code></td><td><code>"wire-theme"</code></td><td>Cookie the resolved theme is read from / persisted to.</td></tr><tr><td><code>THEME_CSS_HREF</code></td><td><code>"/__wrnexus/theme.css"</code></td><td>URL the generated theme stylesheet is served at.</td></tr><tr><td><code>THEME_JS_HREF</code></td><td><code>"/__wrnexus/theme.js"</code></td><td>URL the client theme runtime is served at.</td></tr><tr><td><code>resolveThemeConfig</code></td><td><code>(config?: ThemeConfig) => ResolvedTheme</code></td><td>Deep-merge the user's <code>theme</code> config over the defaults; pick the default theme (config's <code>default</code> if valid, else <code>dark</code>, else the first).</td></tr><tr><td><code>resolveThemeName</code></td><td>`(cookieValue: string \</td><td>undefined, theme: ResolvedTheme) => string`</td><td>Pick a valid theme name from a cookie, falling back to <code>theme.default</code>.</td></tr><tr><td><code>renderThemeCss</code></td><td><code>(theme: ResolvedTheme) => string</code></td><td>Generate the theme stylesheet: a <code>:root{…}</code> default plus one <code>[data-theme="<name>"]{…}</code> block per theme.</td></tr><tr><td><code>renderThemeRuntime</code></td><td><code>(theme: ResolvedTheme) => string</code></td><td>Generate the client runtime (see below).</td></tr></tbody></table></div>
|
||||
<p>Tokens are emitted as <code>--wire-<key></code> custom properties, <strong>except</strong> the reserved key <code>color-scheme</code>, which is emitted as the native <code>color-scheme</code> CSS property so form controls and scrollbars match the theme.</p>
|
||||
<p><code>ThemeConfig</code> / <code>ThemeTokens</code> / <code>ResolvedTheme</code>:</p>
|
||||
<pre data-language="ts"><code>type ThemeTokens = Record<string, string>;
|
||||
|
||||
interface ThemeConfig {
|
||||
default?: string; // theme used when no cookie is present
|
||||
themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
|
||||
}
|
||||
|
||||
interface ResolvedTheme {
|
||||
default: string;
|
||||
names: string[];
|
||||
themes: Record<string, ThemeTokens>;
|
||||
}</code></pre>
|
||||
<p>Built-in token keys (both <code>light</code> and <code>dark</code>): <code>color-scheme</code>, <code>color-bg</code>, <code>color-surface</code>, <code>color-surface-2</code>, <code>color-text</code>, <code>color-muted</code>, <code>color-border</code>, <code>color-primary</code>, <code>color-primary-hover</code>, <code>color-primary-contrast</code>, <code>color-danger</code>, <code>color-success</code>, <code>color-warning</code>, <code>radius</code>, <code>radius-sm</code>, <code>font-sans</code>, <code>shadow-1</code>.</p>
|
||||
<p>The client runtime (<code>renderThemeRuntime</code>) exposes <code>window.wireTheme</code> with <code>{ get, set, toggle, bind, themes }</code>, wires up any <code>[data-wire-theme-toggle]</code> and <code>[data-wire-theme-set]</code> elements on load, and persists the choice to the <code>wire-theme</code> cookie (<code>max-age</code> 1 year, <code>samesite=lax</code>). <code>toggle()</code> cycles through the configured theme names in order.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<h4 id="wrnexus-config-ts"><code>wrnexus.config.ts</code></h4>
|
||||
<pre data-language="ts"><code>import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
export default {
|
||||
head: [
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">',
|
||||
],
|
||||
port: 3000,
|
||||
db: { driver: "sqlite", url: "app.db" },
|
||||
theme: {
|
||||
default: "dark",
|
||||
themes: {
|
||||
light: { "color-primary": "#7c3aed" }, // override one token; rest inherited
|
||||
brand: {
|
||||
// add a whole new theme
|
||||
"color-scheme": "dark",
|
||||
"color-bg": "#0a0a0a",
|
||||
"color-primary": "#22d3ee",
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
entry: "app/styles/main.css",
|
||||
},
|
||||
profiles: {
|
||||
production: {
|
||||
db: { driver: "postgres", url: process.env.DATABASE_URL! },
|
||||
},
|
||||
},
|
||||
} satisfies AppConfig;</code></pre>
|
||||
<h4 id="loading-config-producing-css">Loading config + producing CSS</h4>
|
||||
<pre data-language="ts"><code>import {
|
||||
loadAppConfig,
|
||||
resolveProfile,
|
||||
loadEnv,
|
||||
findStyleEntry,
|
||||
renderStyles,
|
||||
} from "@wrnexus/styles";
|
||||
|
||||
const appRoot = process.cwd();
|
||||
const mode = "production" as const;
|
||||
|
||||
const profile = resolveProfile({ mode });
|
||||
loadEnv(appRoot, profile);
|
||||
|
||||
const config = await loadAppConfig(appRoot, profile);
|
||||
|
||||
const appDir = `${appRoot}/app`;
|
||||
const entryPath = findStyleEntry(appDir, appRoot, config.styles?.entry);
|
||||
const css = await renderStyles({ entryPath, appDir, appRoot, mode }, config.styles);</code></pre>
|
||||
<h4 id="rendering-the-theme">Rendering the theme</h4>
|
||||
<pre data-language="ts"><code>import {
|
||||
resolveThemeConfig,
|
||||
resolveThemeName,
|
||||
renderThemeCss,
|
||||
renderThemeRuntime,
|
||||
THEME_COOKIE,
|
||||
} from "@wrnexus/styles";
|
||||
|
||||
const theme = resolveThemeConfig(config.theme);
|
||||
|
||||
// Server: pick the active theme from the request cookie (no flash).
|
||||
const active = resolveThemeName(cookies[THEME_COOKIE], theme);
|
||||
// → render <html data-theme={active}>
|
||||
|
||||
const themeCss = renderThemeCss(theme); // served at THEME_CSS_HREF
|
||||
const themeJs = renderThemeRuntime(theme); // served at THEME_JS_HREF</code></pre>
|
||||
<p>In templates, consume tokens via the custom properties:</p>
|
||||
<pre data-language="css"><code>.card {
|
||||
background: var(--wire-color-surface);
|
||||
color: var(--wire-color-text);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: var(--wire-radius);
|
||||
box-shadow: var(--wire-shadow-1);
|
||||
}</code></pre>
|
||||
<pre data-language="html"><code><button data-wire-theme-toggle>Toggle theme</button>
|
||||
<button data-wire-theme-set="brand">Brand theme</button></code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> <code>bundleCss</code> uses <code>Bun.build</code>'s CSS bundler for <code>@import</code> resolution, nesting, and minification. Node is not supported.</li>
|
||||
<li>Config and env loading use <code>node:fs</code> / <code>node:path</code> / <code>node:url</code> and read from <code>process.env</code>.</li>
|
||||
<li>Peer package: <code>@wrnexus/core</code> supplies the <code>SeoConfig</code> and <code>SecurityConfig</code> types referenced by <code>AppConfig</code>.</li>
|
||||
<li>The bundled global stylesheet, the theme stylesheet (<code>THEME_CSS_HREF</code>), and the theme runtime (<code>THEME_JS_HREF</code>) are wired into pages by the framework's server; this package only produces their contents.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { SeoConfig, SecurityConfig } from '@wrnexus/core';
|
||||
import { StorageConfig } from '@wrnexus/uploader';
|
||||
|
||||
/**
|
||||
* Theme system — design tokens that work SSR and client-side.
|
||||
*
|
||||
* Tokens are plain CSS custom properties (`--wire-<key>`) so they cascade and
|
||||
* can be overridden by user CSS. Each theme is a flat token map; the framework
|
||||
* ships default `light`/`dark` sets and the user's config deep-merges over them.
|
||||
*
|
||||
* The server renders `<html data-theme="…">` from the `wire-theme` cookie (no
|
||||
* flash), and a tiny client runtime toggles/persists it. The reserved token key
|
||||
* `color-scheme` is emitted as the native CSS property (not a variable) so form
|
||||
* controls and scrollbars match the theme.
|
||||
*/
|
||||
type ThemeTokens = Record<string, string>;
|
||||
interface ThemeConfig {
|
||||
/** Name of the theme used when no `wire-theme` cookie is present. */
|
||||
default?: string;
|
||||
/** Named token maps. Deep-merged over the framework's built-in light/dark. */
|
||||
themes?: Record<string, ThemeTokens>;
|
||||
}
|
||||
interface ResolvedTheme {
|
||||
default: string;
|
||||
names: string[];
|
||||
themes: Record<string, ThemeTokens>;
|
||||
}
|
||||
/** Cookie the resolved theme is read from / persisted to. */
|
||||
declare const THEME_COOKIE = "wire-theme";
|
||||
declare const THEME_CSS_HREF = "/__wrnexus/theme.css";
|
||||
declare const THEME_JS_HREF = "/__wrnexus/theme.js";
|
||||
/** Built-in themes so components have tokens out of the box. */
|
||||
declare const DEFAULT_THEMES: Record<string, ThemeTokens>;
|
||||
/** Merge the user's theme config over the built-in defaults. */
|
||||
declare function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme;
|
||||
/** Pick a valid theme name from a cookie value, falling back to the default. */
|
||||
declare function resolveThemeName(cookieValue: string | undefined, theme: ResolvedTheme): string;
|
||||
/** Generate the theme stylesheet: a `:root` default plus one block per theme. */
|
||||
declare function renderThemeCss(theme: ResolvedTheme): string;
|
||||
/**
|
||||
* Generate the client theme runtime. It exposes `window.wireTheme` and binds
|
||||
* `[data-wire-theme-toggle]` / `[data-wire-theme-set]` elements. The configured
|
||||
* theme names are baked in so `toggle()` cycles through them in order.
|
||||
*/
|
||||
declare function renderThemeRuntime(theme: ResolvedTheme): string;
|
||||
|
||||
/**
|
||||
* Font configuration.
|
||||
*
|
||||
* Declare fonts in `wrnexus.config.ts` under `fonts` and the framework emits
|
||||
* optimized `<head>` markup for you:
|
||||
* - Google Fonts: `preconnect` hints + a single subsetted stylesheet request
|
||||
* (only the weights you list) with `font-display`. The CSP is auto-extended
|
||||
* so the fonts load under the default security policy (see loadAppConfig).
|
||||
* - Self-hosted fonts: generated `@font-face` rules + optional `<link rel=preload>`
|
||||
* for above-the-fold text (the fastest, no-third-party option).
|
||||
* - Family stacks: `sans`/`mono`/`serif` become `--wrn-font-*` CSS variables,
|
||||
* and `sans` is applied to `body`.
|
||||
*/
|
||||
type FontDisplay = "auto" | "block" | "swap" | "fallback" | "optional";
|
||||
interface GoogleFont {
|
||||
/** Family name as it appears on fonts.google.com, e.g. "Inter". */
|
||||
family: string;
|
||||
/** Weights to load — ONLY these are fetched. Default: [400]. */
|
||||
weights?: (number | string)[];
|
||||
/** Also load italic styles for each weight. */
|
||||
italic?: boolean;
|
||||
/** Per-font `font-display` override (else the config default). */
|
||||
display?: FontDisplay;
|
||||
}
|
||||
interface LocalFontFace {
|
||||
/** `font-family` name this face defines. */
|
||||
family: string;
|
||||
/** URL to the font file, typically served from `public/` (e.g. "/fonts/inter.woff2"). */
|
||||
src: string;
|
||||
/** e.g. 400, "700", or "100 900" for a variable font. Default: 400. */
|
||||
weight?: number | string;
|
||||
style?: "normal" | "italic";
|
||||
/** CSS `src` format; inferred from the file extension when omitted. */
|
||||
format?: string;
|
||||
display?: FontDisplay;
|
||||
/** Emit `<link rel="preload" as="font">` — use for the primary above-the-fold face. */
|
||||
preload?: boolean;
|
||||
/** Optional `unicode-range` subset. */
|
||||
unicodeRange?: string;
|
||||
}
|
||||
interface FontConfig {
|
||||
/** Google Fonts, loaded with preconnect + weight subsetting + `font-display`. */
|
||||
google?: GoogleFont[];
|
||||
/** Self-hosted `@font-face` definitions (files served from `public/`). */
|
||||
local?: LocalFontFace[];
|
||||
/** Default `font-display` for faces that don't set their own. Default: "swap". */
|
||||
display?: FontDisplay;
|
||||
/** Body / default family stack → `--wrn-font-sans` + `body { font-family }`. */
|
||||
sans?: string;
|
||||
/** Monospace family stack → `--wrn-font-mono`. */
|
||||
mono?: string;
|
||||
/** Serif family stack → `--wrn-font-serif`. */
|
||||
serif?: string;
|
||||
}
|
||||
/**
|
||||
* Render all `<head>` markup for a font config. Returns "" when nothing is
|
||||
* configured. The output is trusted, framework-controlled HTML.
|
||||
*/
|
||||
declare function renderFontHead(fonts?: FontConfig): string;
|
||||
/**
|
||||
* CSP source hosts required by the configured fonts, so the policy can be
|
||||
* auto-extended (Google Fonts need their CSS + static hosts allow-listed).
|
||||
*/
|
||||
declare function fontCspSources(fonts?: FontConfig): {
|
||||
style: string[];
|
||||
font: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* App configuration loader (`wrnexus.config.ts`).
|
||||
*
|
||||
* The config is optional. It lets an app inject arbitrary `<head>` HTML (ideal
|
||||
* for CDN-delivered CSS frameworks like Bootstrap or the Tailwind Play CDN) and
|
||||
* customise the global stylesheet pipeline (entry file or a custom processor for
|
||||
* Tailwind / PostCSS / Sass).
|
||||
*/
|
||||
|
||||
type Mode = "development" | "production";
|
||||
interface StyleProcessContext {
|
||||
/** Resolved absolute path to the CSS entry, or null if there is none. */
|
||||
entryPath: string | null;
|
||||
appDir: string;
|
||||
appRoot: string;
|
||||
mode: Mode;
|
||||
}
|
||||
interface StylesConfig {
|
||||
/** Path to the CSS entry, relative to the app root. Default: app/styles/global.css */
|
||||
entry?: string;
|
||||
/**
|
||||
* Optional custom processor. Return the final CSS string. Use this to run
|
||||
* Tailwind, PostCSS, Sass, etc. When omitted, the built-in Bun CSS bundler is
|
||||
* used (which already resolves `@import`, including from node_modules).
|
||||
*/
|
||||
process?: (ctx: StyleProcessContext) => string | Promise<string>;
|
||||
}
|
||||
interface MobileConfig {
|
||||
enabled?: boolean;
|
||||
/** Mobile renderer. `webview` uses Capacitor; `native` scaffolds an Expo/React Native app. */
|
||||
mode?: "webview" | "native";
|
||||
appId?: string;
|
||||
appName?: string;
|
||||
serverUrl?: string;
|
||||
userAgent?: string;
|
||||
layout?: string;
|
||||
backgroundColor?: string;
|
||||
icon?: string;
|
||||
errorTitle?: string;
|
||||
errorMessage?: string;
|
||||
/** Base URL used by a fully native client for WRNexusJS API and realtime requests. */
|
||||
apiUrl?: string;
|
||||
/** URL scheme used for native deep links (defaults to a slug of appName). */
|
||||
scheme?: string;
|
||||
/** Advanced Expo app config fields merged into generated app.config.ts. */
|
||||
expo?: Record<string, unknown>;
|
||||
/** Advanced CapacitorConfig fields merged into generated capacitor.config.ts. */
|
||||
capacitor?: Record<string, unknown>;
|
||||
}
|
||||
interface PwaScreenshot {
|
||||
src: string;
|
||||
sizes: string;
|
||||
type?: string;
|
||||
formFactor?: "wide" | "narrow";
|
||||
label?: string;
|
||||
}
|
||||
interface PwaShortcut {
|
||||
name: string;
|
||||
shortName?: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
icons?: Array<{
|
||||
src: string;
|
||||
sizes: string;
|
||||
type?: string;
|
||||
purpose?: string;
|
||||
}>;
|
||||
}
|
||||
interface PwaConfig {
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
shortName?: string;
|
||||
description?: string;
|
||||
startUrl?: string;
|
||||
scope?: string;
|
||||
lang?: string;
|
||||
display?: "standalone" | "fullscreen" | "minimal-ui" | "browser";
|
||||
orientation?: "any" | "natural" | "landscape" | "landscape-primary" | "landscape-secondary" | "portrait" | "portrait-primary" | "portrait-secondary";
|
||||
themeColor?: string;
|
||||
backgroundColor?: string;
|
||||
icons?: Array<{
|
||||
src: string;
|
||||
sizes: string;
|
||||
type?: string;
|
||||
purpose?: string;
|
||||
}>;
|
||||
categories?: string[];
|
||||
screenshots?: PwaScreenshot[];
|
||||
shortcuts?: PwaShortcut[];
|
||||
/** Disable service-worker registration while keeping the web manifest. */
|
||||
serviceWorker?: boolean;
|
||||
/** Navigation shown when both the network and requested page cache are unavailable. */
|
||||
offlineUrl?: string;
|
||||
/** Additional same-origin URLs precached during service-worker installation. */
|
||||
cacheUrls?: string[];
|
||||
/** Service-worker cache key. Change it to invalidate existing PWA caches. */
|
||||
cacheName?: string;
|
||||
}
|
||||
interface AppConfig {
|
||||
/** Raw HTML appended to every page's `<head>` (e.g. CDN stylesheet links). */
|
||||
head?: string | string[];
|
||||
/** Global SEO defaults merged with every page's exported `meta`. */
|
||||
seo?: SeoConfig;
|
||||
/** Framework security headers and optional CORS policy. */
|
||||
security?: SecurityConfig;
|
||||
styles?: StylesConfig;
|
||||
/** Capacitor/native shell defaults and mobile-only page rendering. */
|
||||
mobile?: MobileConfig;
|
||||
/** Progressive Web App metadata. Enabled by default unless set to false. */
|
||||
pwa?: PwaConfig | false;
|
||||
/**
|
||||
* Fonts. Declare Google Fonts (subsetted + preconnect + `font-display`) and/or
|
||||
* self-hosted `@font-face` (with preload), and set `sans`/`mono`/`serif` family
|
||||
* stacks. Google Fonts auto-extend the CSP so they load under the default policy.
|
||||
*/
|
||||
fonts?: FontConfig;
|
||||
/** Design-token themes (deep-merged over the built-in light/dark). */
|
||||
theme?: ThemeConfig;
|
||||
/** i18n: default language + supported locales (strings live in app/locales/*.json). */
|
||||
i18n?: {
|
||||
default?: string;
|
||||
locales?: string[];
|
||||
};
|
||||
/** Default database connection (driver + url); reached with `getDb()`. */
|
||||
db?: {
|
||||
driver: "sqlite" | "postgres" | "mysql" | "mongo";
|
||||
url: string;
|
||||
};
|
||||
/**
|
||||
* File-upload storage. Declare named stores (local dir or S3-compatible),
|
||||
* upload with `handleUpload`/`upload` from `@wrnexus/uploader`, and serve
|
||||
* files back. Each store is `access: "public" | "private"`.
|
||||
*/
|
||||
storage?: StorageConfig;
|
||||
/**
|
||||
* Additional named databases, reached with `getDb("<name>")`. Each has its own
|
||||
* migrations/queries under `app/db/<name>/`. Connect to as many as you like and
|
||||
* read/write to any of them per request.
|
||||
*
|
||||
* databases: { analytics: { driver: "postgres", url: "…" } }
|
||||
*/
|
||||
databases?: Record<string, {
|
||||
driver: "sqlite" | "postgres" | "mysql" | "mongo";
|
||||
url: string;
|
||||
}>;
|
||||
/**
|
||||
* Realtime scaling. When `scale` is true (or `redisUrl` is set), room
|
||||
* broadcasts are bridged over Redis pub/sub so they reach clients on **every**
|
||||
* app process/instance — realtime that works with multiple running apps.
|
||||
*/
|
||||
realtime?: {
|
||||
scale?: boolean;
|
||||
redisUrl?: string;
|
||||
};
|
||||
/** Default server port. */
|
||||
port?: number;
|
||||
/**
|
||||
* Named config profiles (dev, prod, uat, test, …). When a profile is active
|
||||
* its overrides are DEEP-MERGED over the base config. Select with
|
||||
* `--profile=<name>` or the `WRNEXUS_PROFILE` env var.
|
||||
*/
|
||||
profiles?: Record<string, Partial<Omit<AppConfig, "profiles">>>;
|
||||
}
|
||||
/**
|
||||
* Resolve the active profile name: explicit argument > `WRNEXUS_PROFILE` env var
|
||||
* > a mode-based default ("production" in prod, else "development").
|
||||
*/
|
||||
declare function resolveProfile(options?: {
|
||||
explicit?: string;
|
||||
mode?: Mode;
|
||||
}): string;
|
||||
/** Load the raw `wrnexus.config.*` (with the `profiles` map intact), or `{}`. */
|
||||
declare function loadRawConfig(appRoot: string): Promise<AppConfig>;
|
||||
/** Load `wrnexus.config.*`, applying the active profile's overrides. */
|
||||
declare function loadAppConfig(appRoot: string, profile?: string): Promise<AppConfig>;
|
||||
/**
|
||||
* Load the `.env` cascade for a profile into `process.env`, WITHOUT clobbering
|
||||
* variables already set in the real environment (which always win). Order, low
|
||||
* → high precedence: `.env` < `.env.<profile>` < `.env.local` < `.env.<profile>.local`.
|
||||
* Returns the variables it loaded.
|
||||
*/
|
||||
declare function loadEnv(appRoot: string, profile: string): Record<string, string>;
|
||||
/** Flatten a head config into a single HTML string. */
|
||||
declare function headToString(head?: string | string[]): string;
|
||||
|
||||
/**
|
||||
* Global stylesheet pipeline.
|
||||
*
|
||||
* Convention: `app/styles/global.css` is the entry. If it is absent but other
|
||||
* `app/styles/*.css` files exist, they are aggregated into one entry. The entry
|
||||
* is bundled by Bun's CSS bundler, which resolves `@import` — including from
|
||||
* node_modules — so any npm CSS framework (Bootstrap, etc.) works by importing
|
||||
* it. A custom `process` hook can replace the bundler for Tailwind/PostCSS/Sass.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolve the CSS entry for an app.
|
||||
* - `override` (from config.styles.entry) is resolved relative to `appRoot`.
|
||||
* - otherwise prefer `app/styles/global.css`.
|
||||
* - otherwise aggregate all `app/styles/*.css` into a generated entry.
|
||||
* Returns null when the app has no styles.
|
||||
*/
|
||||
declare function findStyleEntry(appDir: string, appRoot: string, override?: string): string | null;
|
||||
/**
|
||||
* Bundle a CSS entry into a single stylesheet string using Bun's CSS bundler.
|
||||
* Resolves `@import` (local and node_modules), handles nesting, minifies in prod.
|
||||
*/
|
||||
declare function bundleCss(entryPath: string, mode: Mode): Promise<string>;
|
||||
|
||||
/**
|
||||
* @wrnexus/styles — global stylesheet pipeline + app config.
|
||||
*
|
||||
* Works for SSR and CSR: the bundled stylesheet is `<link>`ed into every page's
|
||||
* `<head>`, so it styles server-rendered markup and hydrated client islands
|
||||
* alike. Use any CSS framework via `@import` in global.css (npm) or via a CDN
|
||||
* link in `wrnexus.config.ts`'s `head` field.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Produce the final CSS for an entry: run the config's custom processor if one
|
||||
* is provided (Tailwind/PostCSS/Sass), otherwise use the built-in Bun bundler.
|
||||
*
|
||||
* If a custom processor throws (e.g. Tailwind can't resolve `tailwindcss`
|
||||
* because deps aren't installed), we DON'T crash every request — we log a clear,
|
||||
* actionable message and fall back to best-effort CSS so the app keeps serving.
|
||||
*/
|
||||
declare function renderStyles(ctx: StyleProcessContext, styles?: StylesConfig): Promise<string>;
|
||||
|
||||
export { type AppConfig, DEFAULT_THEMES, type FontConfig, type FontDisplay, type GoogleFont, type LocalFontFace, type MobileConfig, type Mode, type PwaConfig, type ResolvedTheme, type StyleProcessContext, type StylesConfig, type Mode as StylesMode, THEME_COOKIE, THEME_CSS_HREF, THEME_JS_HREF, type ThemeConfig, type ThemeTokens, bundleCss, findStyleEntry, fontCspSources, headToString, loadAppConfig, loadEnv, loadRawConfig, renderFontHead, renderStyles, renderThemeCss, renderThemeRuntime, resolveProfile, resolveThemeConfig, resolveThemeName };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/styles</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>interface StylesConfig {
|
||||
/** CSS entry path relative to the app root. Default: app/styles/global.css */
|
||||
entry?: string;
|
||||
/** Custom processor — return the final CSS string (Tailwind/PostCSS/Sass). */
|
||||
process?: (ctx: StyleProcessContext) => string | Promise<string>;
|
||||
}
|
||||
|
||||
interface StyleProcessContext {
|
||||
entryPath: string | null; // resolved absolute CSS entry, or null
|
||||
appDir: string;
|
||||
appRoot: string;
|
||||
mode: Mode; // "development" | "production"
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>type ThemeTokens = Record<string, string>;
|
||||
|
||||
interface ThemeConfig {
|
||||
default?: string; // theme used when no cookie is present
|
||||
themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
|
||||
}
|
||||
|
||||
interface ResolvedTheme {
|
||||
default: string;
|
||||
names: string[];
|
||||
themes: Record<string, ThemeTokens>;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
export default {
|
||||
head: [
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">',
|
||||
],
|
||||
port: 3000,
|
||||
db: { driver: "sqlite", url: "app.db" },
|
||||
theme: {
|
||||
default: "dark",
|
||||
themes: {
|
||||
light: { "color-primary": "#7c3aed" }, // override one token; rest inherited
|
||||
brand: {
|
||||
// add a whole new theme
|
||||
"color-scheme": "dark",
|
||||
"color-bg": "#0a0a0a",
|
||||
"color-primary": "#22d3ee",
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
entry: "app/styles/main.css",
|
||||
},
|
||||
profiles: {
|
||||
production: {
|
||||
db: { driver: "postgres", url: process.env.DATABASE_URL! },
|
||||
},
|
||||
},
|
||||
} satisfies AppConfig;</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#config-loading">Config loading</a><a class="toc-level-4" href="#appconfig">AppConfig</a><a class="toc-level-4" href="#styles-pipeline">Styles pipeline</a><a class="toc-level-4" href="#theme-system">Theme system</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#wrnexus-config-ts">wrnexus.config.ts</a><a class="toc-level-4" href="#loading-config-producing-css">Loading config + producing CSS</a><a class="toc-level-4" href="#rendering-the-theme">Rendering the theme</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
page wrnexustest {
|
||||
seo {
|
||||
title = "@wrnexus/test"
|
||||
description = "WRNexusJS-aware component, route, and browser testing utilities."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Tooling</span><h1>@wrnexus/test</h1><p>WRNexusJS-aware component, route, and browser testing utilities.</p><code>bun add @wrnexus/test@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Tooling</span><h1>@wrnexus/test</h1><p>WRNexusJS-aware component, route, and browser testing utilities.</p><pre><code>bun add @wrnexus/test@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Testing utilities for WRNexusJS apps — component rendering, reactive-DOM mounting, route handler calls, and a full in-process app harness, plus a one-import re-export of <code>bun:test</code>.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/test</code> is the server-side test toolkit you reach for when writing tests for a WRNexusJS app. It runs under <code>bun test</code> (invoked via <code>wrnexus test</code>) and gives you a single import surface: the <code>bun:test</code> primitives (<code>test</code>, <code>expect</code>, <code>mock</code>, …) re-exported alongside WRNexusJS-aware helpers that compile <code>.wrn</code> components, hydrate server HTML in a DOM, invoke API route handlers, and boot the real app on an ephemeral port for integration tests.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/test</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="re-exported-test-primitives">Re-exported test primitives</h4>
|
||||
<p>For one-import DX, the following are re-exported straight from <code>bun:test</code>:</p>
|
||||
<p><code>test</code>, <code>expect</code>, <code>describe</code>, <code>it</code>, <code>beforeEach</code>, <code>afterEach</code>, <code>beforeAll</code>, <code>afterAll</code>, <code>mock</code>, <code>spyOn</code>.</p>
|
||||
<p><code>createContext</code> is also re-exported from <code>@wrnexus/core</code>.</p>
|
||||
<h4 id="rendercomponent-source-props"><code>renderComponent(source, props?)</code></h4>
|
||||
<pre data-language="ts"><code>function renderComponent(source: string, props?: Record<string, unknown>): Promise<string>;</code></pre>
|
||||
<p>Compiles a <code>.wrn</code> component <code>source</code> string (via <code>@wrnexus/compiler</code>) and renders it to an HTML string with the given <code>props</code>. Throws if the compiled module has no <code>render</code> export.</p>
|
||||
<h4 id="mounthtml-html"><code>mountHtml(html)</code></h4>
|
||||
<pre data-language="ts"><code>function mountHtml(html: string): {
|
||||
document: Document;
|
||||
window: unknown;
|
||||
querySelector: (sel: string) => Element | null;
|
||||
querySelectorAll: (sel: string) => Element[];
|
||||
};</code></pre>
|
||||
<p>Mounts server-rendered <code>html</code> in a <code>happy-dom</code> window with the reactive runtime hydrated, so you can test <code>data-scope</code> / <code>data-text</code> / <code>data-for</code> / <code>data-show</code> behaviour. Returns the window plus <code>document</code> and query helpers; assert on those.</p>
|
||||
<blockquote><code>happy-dom</code> is loaded lazily (via <code>require</code>), so importing this package never</blockquote>
|
||||
<blockquote>requires it unless you actually call <code>mountHtml</code>.</blockquote>
|
||||
<h4 id="callroute-handler-request"><code>callRoute(handler, request)</code></h4>
|
||||
<pre data-language="ts"><code>function callRoute(
|
||||
handler: (ctx: Context) => Response | Promise<Response>,
|
||||
request: Request,
|
||||
): Promise<Response>;</code></pre>
|
||||
<p>Calls an API route <code>handler</code> with a <code>Context</code> built from a <code>Request</code> (using <code>createContext</code>). Returns the handler's <code>Response</code>.</p>
|
||||
<h4 id="createharness-projectroot-options"><code>createHarness(projectRoot, options?)</code></h4>
|
||||
<pre data-language="ts"><code>function createHarness(projectRoot: string, options?: HarnessOptions): Promise<Harness>;
|
||||
|
||||
interface HarnessOptions {
|
||||
/** Config/env profile. Default "test". */
|
||||
profile?: string;
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
/** Base URL of the ephemeral test server. */
|
||||
url: string;
|
||||
/** Fetch a path on the app (relative to `url`). */
|
||||
fetch(path: string, init?: RequestInit): Promise<Response>;
|
||||
/** The scanned router (pages/api/realtime/components). */
|
||||
router: unknown;
|
||||
/** Stop the server. */
|
||||
close(): void;
|
||||
}</code></pre>
|
||||
<p>Boots the app at <code>projectRoot</code> on an ephemeral port (<code>port: 0</code>) for integration tests covering pages, API routes, middleware, and the full request pipeline. Loads env and app config for the given <code>profile</code> (default <code>"test"</code>) so it picks up your test database/env. The server runs in <code>development</code> mode with HMR disabled. Remember to <code>await app.close()</code> when done.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre data-language="ts"><code>import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";
|
||||
|
||||
test("counter renders its label", async () => {
|
||||
const html = await renderComponent(SRC, { start: 3, label: "Hits" });
|
||||
expect(html).toContain("Hits");
|
||||
});
|
||||
|
||||
test("reactive scope hydrates", () => {
|
||||
const { querySelector } = mountHtml(serverHtml);
|
||||
expect(querySelector("[data-text]")?.textContent).toBe("3");
|
||||
});
|
||||
|
||||
test("home page responds", async () => {
|
||||
const app = await createHarness("examples/basic-app");
|
||||
const res = await app.fetch("/");
|
||||
expect(res.status).toBe(200);
|
||||
await app.close();
|
||||
});</code></pre>
|
||||
<p>Calling an API route handler directly:</p>
|
||||
<pre data-language="ts"><code>import { test, expect, callRoute } from "@wrnexus/test";
|
||||
import { GET } from "../app/api/health.ts";
|
||||
|
||||
test("health endpoint", async () => {
|
||||
const res = await callRoute(GET, new Request("http://test/api/health"));
|
||||
expect(res.status).toBe(200);
|
||||
});</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> Runs under <code>bun test</code> (via <code>wrnexus test</code>); uses Bun's module</li>
|
||||
<p>loading and the <code>bun:test</code> runtime.</p>
|
||||
<li><code>mountHtml</code> requires <strong><code>happy-dom</code></strong> to be available in the workspace (loaded</li>
|
||||
<p>lazily; it's a dev dependency, not a runtime dependency of this package).</p>
|
||||
<li>Works with the rest of the WRNexusJS toolchain:</li>
|
||||
<p>[<code>@wrnexus/compiler</code>](../compiler) (compiles <code>.wrn</code> sources), [<code>@wrnexus/core</code>](../core) (<code>Context</code> / <code>createContext</code>), [<code>@wrnexus/csr</code>](../csr) (reactive runtime for <code>mountHtml</code>), [<code>@wrnexus/dev-server</code>](../dev-server) (<code>startServer</code> behind <code>createHarness</code>), and [<code>@wrnexus/styles</code>](../styles) (config/env/profile loading for the harness).</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { Context } from '@wrnexus/core';
|
||||
export { createContext } from '@wrnexus/core';
|
||||
export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn, test } from 'bun:test';
|
||||
|
||||
/**
|
||||
* @wrnexus/test — testing utilities for WRNexusJS apps. Runs on `bun test` (via
|
||||
* `wrnexus test`). Import everything from one place:
|
||||
*
|
||||
* import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";
|
||||
*
|
||||
* test("counter renders its label", async () => {
|
||||
* const html = await renderComponent(SRC, { start: 3, label: "Hits" });
|
||||
* expect(html).toContain("Hits");
|
||||
* });
|
||||
*
|
||||
* test("home page responds", async () => {
|
||||
* const app = await createHarness("examples/basic-app");
|
||||
* const res = await app.fetch("/");
|
||||
* expect(res.status).toBe(200);
|
||||
* await app.close();
|
||||
* });
|
||||
*/
|
||||
|
||||
/** Compile a `.wrn` component source + render it to HTML with the given props. */
|
||||
declare function renderComponent(source: string, props?: Record<string, unknown>): Promise<string>;
|
||||
/**
|
||||
* Mount server-rendered HTML in a happy-dom window with the reactive runtime
|
||||
* hydrated, so you can test `data-scope`/`data-text`/`data-for`/`data-show`
|
||||
* behaviour. Returns the window; assert on `win.document`.
|
||||
*/
|
||||
declare function mountHtml(html: string): {
|
||||
document: Document;
|
||||
window: unknown;
|
||||
querySelector: (sel: string) => Element | null;
|
||||
querySelectorAll: (sel: string) => Element[];
|
||||
};
|
||||
/** Call an API route handler with a `Context` built from a Request. */
|
||||
declare function callRoute(handler: (ctx: Context) => Response | Promise<Response>, request: Request): Promise<Response>;
|
||||
interface Harness {
|
||||
/** Base URL of the ephemeral test server. */
|
||||
url: string;
|
||||
/** Fetch a path on the app (relative to `url`). */
|
||||
fetch(path: string, init?: RequestInit): Promise<Response>;
|
||||
/** The scanned router (pages/api/realtime/components). */
|
||||
router: unknown;
|
||||
/** Stop the server. */
|
||||
close(): void;
|
||||
}
|
||||
interface HarnessOptions {
|
||||
/** Config/env profile. Default "test". */
|
||||
profile?: string;
|
||||
}
|
||||
/**
|
||||
* Boot the app on an ephemeral port for integration tests (pages, API routes,
|
||||
* middleware, the full pipeline). Uses the "test" profile by default so it picks
|
||||
* up your test database/env. Remember to `await app.close()`.
|
||||
*/
|
||||
declare function createHarness(projectRoot: string, options?: HarnessOptions): Promise<Harness>;
|
||||
|
||||
export { type Harness, type HarnessOptions, callRoute, createHarness, mountHtml, renderComponent };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/test</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>function renderComponent(source: string, props?: Record<string, unknown>): Promise<string>;</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>function mountHtml(html: string): {
|
||||
document: Document;
|
||||
window: unknown;
|
||||
querySelector: (sel: string) => Element | null;
|
||||
querySelectorAll: (sel: string) => Element[];
|
||||
};</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>function callRoute(
|
||||
handler: (ctx: Context) => Response | Promise<Response>,
|
||||
request: Request,
|
||||
): Promise<Response>;</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#re-exported-test-primitives">Re-exported test primitives</a><a class="toc-level-4" href="#rendercomponent-source-props">renderComponent(source, props?)</a><a class="toc-level-4" href="#mounthtml-html">mountHtml(html)</a><a class="toc-level-4" href="#callroute-handler-request">callRoute(handler, request)</a><a class="toc-level-4" href="#createharness-projectroot-options">createHarness(projectRoot, options?)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
page wrnexustracking {
|
||||
seo {
|
||||
title = "@wrnexus/tracking"
|
||||
description = "Error/event capture, middleware, filtering, and sinks."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Runtime</span><h1>@wrnexus/tracking</h1><p>Error/event capture, middleware, filtering, and sinks.</p><code>bun add @wrnexus/tracking@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Runtime</span><h1>@wrnexus/tracking</h1><p>Error/event capture, middleware, filtering, and sinks.</p><pre><code>bun add @wrnexus/tracking@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Error tracking for WRNexusJS apps: capture exceptions manually or via middleware and fan them out to pluggable sinks.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/tracking</code> is a small, server-side error-capture layer. You create a tracker with one or more <strong>sinks</strong>, then feed it errors — either manually with <code>tracker.capture(err, context)</code> or automatically by mounting <code>tracker.middleware()</code> in your request pipeline. A <code>consoleSink</code> is included; forwarding to Sentry, Datadog, or any other backend is just a matter of writing a tiny sink. Reach for it when you want a single, sink-agnostic place to route application errors. Sinks run best-effort — a throwing sink never breaks the request.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/tracking</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="createtracker-options-tracker"><code>createTracker(options?): Tracker</code></h4>
|
||||
<p>Creates a tracker. <code>TrackerOptions</code>:</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>sinks</code></td><td><code>ErrorSink[]</code></td><td>Initial sinks to fan events out to. Defaults to <code>[]</code>.</td></tr><tr><td><code>now</code></td><td><code>() => number</code></td><td>Clock used for <code>event.timestamp</code> (epoch ms). Defaults to <code>Date.now</code>.</td></tr><tr><td><code>beforeSend</code></td><td>`(event: ErrorEvent) => ErrorEvent \</td><td>null`</td><td>Scrub/enrich an event before it reaches any sink. Return <code>null</code> to drop it.</td></tr></tbody></table></div>
|
||||
<p>The returned <code>Tracker</code>:</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Member</th><th>Signature</th><th>Description</th></tr></thead>
|
||||
<tbody><tr><td><code>capture</code></td><td><code>(error: unknown, context?: Record<string, unknown>) => Promise<void></code></td><td>Normalizes any thrown value into an <code>Error</code>, builds an <code>ErrorEvent</code>, runs <code>beforeSend</code>, then dispatches to all sinks. Non-<code>Error</code> values are wrapped in an <code>Error</code> named <code>NonError</code>.</td></tr><tr><td><code>addSink</code></td><td><code>(sink: ErrorSink) => void</code></td><td>Registers an additional sink at runtime.</td></tr><tr><td><code>middleware</code></td><td><code>() => Middleware</code></td><td>Returns a WRNexusJS <code>Middleware</code> that captures any error thrown downstream, then re-throws it so the framework's error handler still produces the response.</td></tr></tbody></table></div>
|
||||
<p>The middleware attaches this context to captured events:</p>
|
||||
<pre data-language="ts"><code>{ method: ctx.req.method, path: ctx.url.pathname, requestId: ctx.locals.requestId }</code></pre>
|
||||
<h4 id="consolesink-errorsink"><code>consoleSink: ErrorSink</code></h4>
|
||||
<p>A built-in sink that logs a compact one-line message via <code>console.error</code>, e.g. <code>[error] TypeError: cannot read x {"userId":42}</code>.</p>
|
||||
<h4 id="types">Types</h4>
|
||||
<pre data-language="ts"><code>interface ErrorEvent {
|
||||
error: Error;
|
||||
context: Record<string, unknown>; // request info, user id, tags…
|
||||
timestamp: number; // epoch ms
|
||||
}
|
||||
|
||||
interface ErrorSink {
|
||||
name?: string;
|
||||
capture(event: ErrorEvent): void | Promise<void>;
|
||||
}</code></pre>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>Manual capture:</p>
|
||||
<pre data-language="ts"><code>import { createTracker, consoleSink } from "@wrnexus/tracking";
|
||||
|
||||
const tracker = createTracker({ sinks: [consoleSink] });
|
||||
|
||||
try {
|
||||
await doWork();
|
||||
} catch (err) {
|
||||
await tracker.capture(err, { userId: 42, op: "doWork" });
|
||||
throw err;
|
||||
}</code></pre>
|
||||
<p>As request middleware:</p>
|
||||
<pre data-language="ts"><code>import { createTracker, consoleSink } from "@wrnexus/tracking";
|
||||
|
||||
const tracker = createTracker({ sinks: [consoleSink] });
|
||||
|
||||
app.use(tracker.middleware()); // captures + re-throws downstream errors</code></pre>
|
||||
<p>A custom sink with <code>beforeSend</code> scrubbing:</p>
|
||||
<pre data-language="ts"><code>import { createTracker, type ErrorSink } from "@wrnexus/tracking";
|
||||
|
||||
const sentrySink: ErrorSink = {
|
||||
name: "sentry",
|
||||
async capture(event) {
|
||||
await Sentry.captureException(event.error, { extra: event.context });
|
||||
},
|
||||
};
|
||||
|
||||
const tracker = createTracker({
|
||||
sinks: [sentrySink],
|
||||
beforeSend(event) {
|
||||
delete event.context.password; // scrub secrets
|
||||
return event; // return null to drop the event entirely
|
||||
},
|
||||
});
|
||||
|
||||
tracker.addSink(anotherSink); // add more sinks later</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li>Runs on <strong>Bun</strong> only (Node is not supported).</li>
|
||||
<li>Peer package: [<code>@wrnexus/core</code>](../core) — the <code>Context</code> and <code>Middleware</code> types</li>
|
||||
<p>used by <code>tracker.middleware()</code> come from there.</p>
|
||||
<li>Sink dispatch is fire-and-forget-safe: all sinks run via <code>Promise.all</code>, and a</li>
|
||||
<p>sink that throws is swallowed so it can never break the app.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { Middleware } from '@wrnexus/core';
|
||||
|
||||
/**
|
||||
* @wrnexus/tracking — error tracking with pluggable sinks. Capture exceptions
|
||||
* manually or via middleware, and fan them out to any sink (console by default;
|
||||
* write a small sink to forward to Sentry/Datadog/etc.).
|
||||
*
|
||||
* const tracker = createTracker({ sinks: [consoleSink] });
|
||||
* app-middleware: tracker.middleware() // captures + re-throws request errors
|
||||
* tracker.capture(err, { userId }); // manual
|
||||
*/
|
||||
|
||||
interface ErrorEvent {
|
||||
error: Error;
|
||||
/** Arbitrary structured context (request info, user id, tags…). */
|
||||
context: Record<string, unknown>;
|
||||
/** Epoch ms. */
|
||||
timestamp: number;
|
||||
}
|
||||
interface ErrorSink {
|
||||
name?: string;
|
||||
capture(event: ErrorEvent): void | Promise<void>;
|
||||
}
|
||||
interface Tracker {
|
||||
capture(error: unknown, context?: Record<string, unknown>): Promise<void>;
|
||||
addSink(sink: ErrorSink): void;
|
||||
/** Middleware that captures errors thrown downstream, then re-throws them. */
|
||||
middleware(): Middleware;
|
||||
}
|
||||
interface TrackerOptions {
|
||||
sinks?: ErrorSink[];
|
||||
now?: () => number;
|
||||
/** Scrub/enrich an event before it hits sinks (return null to drop it). */
|
||||
beforeSend?: (event: ErrorEvent) => ErrorEvent | null;
|
||||
}
|
||||
/** A sink that logs a compact one-line error to the console. */
|
||||
declare const consoleSink: ErrorSink;
|
||||
declare function createTracker(options?: TrackerOptions): Tracker;
|
||||
|
||||
export { type ErrorEvent, type ErrorSink, type Tracker, type TrackerOptions, consoleSink, createTracker };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/tracking</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>{ method: ctx.req.method, path: ctx.url.pathname, requestId: ctx.locals.requestId }</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>interface ErrorEvent {
|
||||
error: Error;
|
||||
context: Record<string, unknown>; // request info, user id, tags…
|
||||
timestamp: number; // epoch ms
|
||||
}
|
||||
|
||||
interface ErrorSink {
|
||||
name?: string;
|
||||
capture(event: ErrorEvent): void | Promise<void>;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import { createTracker, consoleSink } from "@wrnexus/tracking";
|
||||
|
||||
const tracker = createTracker({ sinks: [consoleSink] });
|
||||
|
||||
try {
|
||||
await doWork();
|
||||
} catch (err) {
|
||||
await tracker.capture(err, { userId: 42, op: "doWork" });
|
||||
throw err;
|
||||
}</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#createtracker-options-tracker">createTracker(options?): Tracker</a><a class="toc-level-4" href="#consolesink-errorsink">consoleSink: ErrorSink</a><a class="toc-level-4" href="#types">Types</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
page wrnexusui {
|
||||
seo {
|
||||
title = "@wrnexus/ui"
|
||||
description = "Themeable server-rendered UI components and CSS."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h1>@wrnexus/ui</h1><p>Themeable server-rendered UI components and CSS.</p><code>bun add @wrnexus/ui@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend</span><h1>@wrnexus/ui</h1><p>Themeable server-rendered UI components and CSS.</p><pre><code>bun add @wrnexus/ui@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>First-party Wire UI component library — a set of themeable <code>.wrn</code> components plus a single tokenized stylesheet.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p><code>@wrnexus/ui</code> ships a library of server-rendered <code>.wrn</code> components (layout, form controls, and feedback UI) together with one themeable stylesheet, <code>ui.css</code>. The components are <strong>auto-discovered</strong> by the framework router — you don't import them in code. Once the package's component directory is on the router's scan path, you mount any component in a page with <code>data-component="<name>"</code>. Every visual is driven by <code>var(--wire-*)</code> theme tokens, so components restyle instantly when the theme changes. The tiny JS surface (<code>src/index.ts</code>) exists only so the toolchain (CLI build + dev server) can locate the component directory and stylesheet.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/ui</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<p>In practice you rarely install this directly: <code>@wrnexus/cli</code> and <code>@wrnexus/dev-server</code> already depend on it and wire it into the router for you (see [Auto-discovery](#auto-discovery)).</p>
|
||||
<h3 id="components">Components</h3>
|
||||
<p>Components live as <code>.wrn</code> files under <code>packages/ui/components/</code>. The mount name is the <strong>lowercase file basename</strong> (e.g. <code>button.wrn</code> → <code>data-component="button"</code>). Each accepts a <code>class</code> prop (appended to its root element) and most render their body from either a named prop or the default slot.</p>
|
||||
<h4 id="layout">Layout</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Name</th><th>Purpose</th><th>Key props</th></tr></thead>
|
||||
<tbody><tr><td><code>container</code></td><td>Max-width centered content wrapper</td><td><code>class</code></td></tr><tr><td><code>stack</code></td><td>Vertical column with gap</td><td><code>gap</code> (0–8)</td></tr><tr><td><code>hstack</code></td><td>Horizontal row with gap</td><td><code>gap</code> (0–8)</td></tr><tr><td><code>grid</code></td><td>CSS grid container</td><td>see source</td></tr><tr><td><code>divider</code></td><td>Horizontal rule</td><td><code>class</code></td></tr><tr><td><code>spacer</code></td><td>Flexible/empty spacing element</td><td>see source</td></tr></tbody></table></div>
|
||||
<h4 id="core-feedback">Core / feedback</h4>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Name</th><th>Purpose</th><th>Key props</th></tr></thead>
|
||||
<tbody><tr><td><code>button</code></td><td>Button</td><td><code>label</code>, <code>variant</code> (<code>default</code>\</td><td><code>primary</code>\</td><td><code>danger</code>\</td><td><code>ghost</code>), <code>size</code> (<code>sm</code>\</td><td><code>md</code>\</td><td><code>lg</code>), <code>type</code></td></tr><tr><td><code>input</code></td><td>Text input</td><td>see source</td></tr><tr><td><code>textarea</code></td><td>Multi-line input</td><td>see source</td></tr><tr><td><code>checkbox</code></td><td>Checkbox</td><td>see source</td></tr><tr><td><code>badge</code></td><td>Small status badge</td><td><code>label</code>, <code>variant</code></td></tr><tr><td><code>alert</code></td><td>Callout box</td><td><code>variant</code> (<code>info</code>\</td><td><code>success</code>\</td><td><code>danger</code>\</td><td><code>warning</code>), <code>title</code>, <code>message</code></td></tr><tr><td><code>card</code></td><td>Padded, bordered surface</td><td><code>class</code></td></tr><tr><td><code>avatar</code></td><td>User avatar</td><td>see source</td></tr><tr><td><code>spinner</code></td><td>Loading indicator</td><td>see source</td></tr><tr><td><code>disclosure</code></td><td>Expandable details/summary</td><td>see source</td></tr><tr><td><code>theme-toggle</code></td><td>Theme switch button (binds <code>data-wire-theme-toggle</code>)</td><td><code>label</code></td></tr></tbody></table></div>
|
||||
<h4 id="additional-controls-data-display">Additional controls & data display</h4>
|
||||
<p>Also shipped: <code>select</code>, <code>radio</code>, <code>switch</code>, <code>progress</code>, <code>tag</code>, <code>skeleton</code>, <code>tooltip</code>, and <code>table</code>.</p>
|
||||
<p>The authoritative, always-current list is <code>uiComponentNames()</code> (below), which reads the component directory at runtime.</p>
|
||||
<h3 id="api">API</h3>
|
||||
<p>The JS module (<code>@wrnexus/ui</code>) exposes four helpers used by the build tooling to locate the component assets. There is no component code to import — the components are <code>.wrn</code> files rendered server-side.</p>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Export</th><th>Signature</th><th>Returns</th></tr></thead>
|
||||
<tbody><tr><td><code>uiComponentsDir</code></td><td><code>() => string</code></td><td>Absolute path to the <code>.wrn</code> component directory (feed to <code>buildRouter</code>'s <code>componentDirs</code>).</td></tr><tr><td><code>uiCssPath</code></td><td><code>() => string</code></td><td>Absolute path to <code>ui.css</code>.</td></tr><tr><td><code>uiCss</code></td><td><code>() => string</code></td><td>The <code>ui.css</code> file contents (all <code>.wire-*</code> classes, themed via tokens).</td></tr><tr><td><code>uiComponentNames</code></td><td><code>() => string[]</code></td><td>Sorted list of built-in component names (e.g. for <code>wrnexus eject</code> listing).</td></tr></tbody></table></div>
|
||||
<h4 id="ui-css-asset-export"><code>./ui.css</code> asset export</h4>
|
||||
<p><code>package.json</code> also exposes the raw stylesheet as a subpath asset:</p>
|
||||
<pre data-language="json"><code>"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./ui.css": "./ui.css"
|
||||
}</code></pre>
|
||||
<p>The framework serves this stylesheet once at <code>/__wrnexus/ui.css</code>, so pages get all component styles from a single request.</p>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<h4 id="auto-discovery">Auto-discovery</h4>
|
||||
<p>The router scans extra <code>componentDirs</code> (in addition to the app's own <code>app/components</code>) and keys components by name. Library dirs are scanned <strong>first</strong> and <code>app/components</code> <strong>last</strong>, so an app component of the same name shadows the library's. The CLI build (<code>@wrnexus/cli</code>) and dev server (<code>@wrnexus/dev-server</code>) both wire the UI directory in for you:</p>
|
||||
<pre data-language="ts"><code>import { buildRouter } from "@wrnexus/router";
|
||||
import { uiComponentsDir } from "@wrnexus/ui";
|
||||
|
||||
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });</code></pre>
|
||||
<h4 id="mounting-components-in-a-page">Mounting components in a page</h4>
|
||||
<p>Once discovered, mount any component by name via <code>data-component</code>. Quoted attributes (other than <code>data-component</code>) become string props:</p>
|
||||
<pre data-language="html"><code><div data-component="card">
|
||||
<div data-component="badge" label="New"></div>
|
||||
<button data-component="button" label="Save" variant="primary" size="lg"></button>
|
||||
<div data-component="alert" variant="success" title="Done" message="Saved."></div>
|
||||
</div></code></pre>
|
||||
<h3 id="overrides">Overrides</h3>
|
||||
<p>Ways to customize the components, in increasing order of power:</p>
|
||||
<p>1. <strong>Theme tokens</strong> — override CSS custom properties such as <code>--wire-color-primary</code>, <code>--wire-color-surface</code>, <code>--wire-radius-sm</code>, etc. Every component style resolves through <code>var(--wire-*)</code>, so changing a token restyles everything instantly (including across theme switches). 2. <strong>App CSS</strong> — redefine a <code>.wire-*</code> class in your own stylesheet, which is loaded after <code>ui.css</code> and therefore wins. 3. <strong><code>class</code> prop</strong> — pass a <code>class</code> prop to a component; it is appended to the component's root element, letting you add per-instance classes without touching the base styles. 4. <strong><code>wrnexus eject <name></code></strong> — copy the component's <code>.wrn</code> source into your <code>app/components</code>, where (because app components shadow library ones) you fully own and can edit it. Use <code>uiComponentNames()</code> for the list of ejectable names.</p>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only</strong> — the package uses standard fs/path/url APIs but is published and</li>
|
||||
<p>consumed within the Bun-native WRNexusJS toolchain (Node is not supported).</p>
|
||||
<li>Peer packages: components are discovered and rendered by</li>
|
||||
<p>[<code>@wrnexus/router</code>](../router) (via <code>componentDirs</code>) and served by [<code>@wrnexus/dev-server</code>](../dev-server) / built by [<code>@wrnexus/cli</code>](../cli).</p>
|
||||
<li>Depends on [<code>@wrnexus/core</code>](../core) (<code>dependencies</code>).</li>
|
||||
<li><code>theme-toggle</code> relies on the framework's theme runtime, which binds the</li>
|
||||
<p><code>data-wire-theme-toggle</code> attribute — no per-component JS is required.</p>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* @wrnexus/ui — the Wire UI component library.
|
||||
*
|
||||
* Components are `.wrn` files under `components/`, auto-discovered by the
|
||||
* framework (the router scans this directory in addition to the app's own
|
||||
* `app/components`). Mount them in any page with `data-component="<name>"`.
|
||||
* Their styles live in a single themeable stylesheet, `ui.css`, served once at
|
||||
* `/__wrnexus/ui.css` — every class uses `var(--wire-*)` theme tokens.
|
||||
*
|
||||
* Override, in increasing order of power:
|
||||
* 1. theme tokens (change `--wire-color-primary`, etc.)
|
||||
* 2. redefine a `.wire-*` class in your own CSS (loaded after ui.css)
|
||||
* 3. pass a `class` prop (appended to the component root)
|
||||
* 4. `wrnexus eject <name>` to copy the component into `app/components` and own it
|
||||
*/
|
||||
/** Absolute path to the directory of Wire UI component `.wrn` files. */
|
||||
declare function uiComponentsDir(): string;
|
||||
/** Absolute path to the Wire UI stylesheet. */
|
||||
declare function uiCssPath(): string;
|
||||
/** The Wire UI stylesheet contents (all `.wire-*` classes, themed via tokens). */
|
||||
declare function uiCss(): string;
|
||||
/** Names of the built-in components (e.g. for `wrnexus eject` listing). */
|
||||
declare function uiComponentNames(): string[];
|
||||
|
||||
export { uiComponentNames, uiComponentsDir, uiCss, uiCssPath };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/ui</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="json"><code>"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./ui.css": "./ui.css"
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>import { buildRouter } from "@wrnexus/router";
|
||||
import { uiComponentsDir } from "@wrnexus/ui";
|
||||
|
||||
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="html"><code><div data-component="card">
|
||||
<div data-component="badge" label="New"></div>
|
||||
<button data-component="button" label="Save" variant="primary" size="lg"></button>
|
||||
<div data-component="alert" variant="success" title="Done" message="Saved."></div>
|
||||
</div></code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#components">Components</a><a class="toc-level-4" href="#layout">Layout</a><a class="toc-level-4" href="#core-feedback">Core / feedback</a><a class="toc-level-4" href="#additional-controls-data-display">Additional controls & data display</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#ui-css-asset-export">./ui.css asset export</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#auto-discovery">Auto-discovery</a><a class="toc-level-4" href="#mounting-components-in-a-page">Mounting components in a page</a><a class="toc-level-3" href="#overrides">Overrides</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,316 @@
|
||||
page wrnexusvalidation {
|
||||
seo {
|
||||
title = "@wrnexus/validation"
|
||||
description = "Typed schemas, coercion, validation, and browser descriptors."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Security</span><h1>@wrnexus/validation</h1><p>Typed schemas, coercion, validation, and browser descriptors.</p><code>bun add @wrnexus/validation@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Security</span><h1>@wrnexus/validation</h1><p>Typed schemas, coercion, validation, and browser descriptors.</p><pre><code>bun add @wrnexus/validation@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>One fluent schema, validated on the server (API bodies, env vars) and mirrored to an eval-free browser validator for forms.</blockquote>
|
||||
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
||||
<h3 id="overview">Overview</h3>
|
||||
<p>Define a schema once with the fluent <code>v</code> builder, then reuse it in three places: <code>.parse()</code> runs server-side and returns coerced values plus per-field errors; <code>.describe()</code> emits a plain-JSON <code>SchemaDescriptor</code> that the browser runtime interprets (no <code>eval</code>, no bundled validator); and helpers like <code>parseBody</code> and <code>parseEnv</code> wire schemas straight into API routes and startup config. The server rule logic (<code>applyRule</code>/<code>checkField</code>) and the client runtime (<code>VALIDATE_RUNTIME</code>) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in <code>app/schemas/</code>.</p>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<pre data-language="bash"><code>bun add @wrnexus/validation</code></pre>
|
||||
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
||||
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
||||
<h3 id="api">API</h3>
|
||||
<h4 id="the-v-builder">The <code>v</code> builder</h4>
|
||||
<pre data-language="ts"><code>import { v } from "@wrnexus/validation";</code></pre>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Factory</th><th>Returns</th><th>Field methods</th></tr></thead>
|
||||
<tbody><tr><td><code>v.string()</code></td><td><code>StringSchema</code></td><td><code>email()</code>, <code>url()</code>, <code>uuid()</code>, <code>date()</code>, <code>length(n)</code>, <code>oneOf(string[])</code>, <code>pattern(re)</code>, <code>trim()</code>, <code>min(n)</code>, <code>max(n)</code></td></tr><tr><td><code>v.number()</code></td><td><code>NumberSchema</code></td><td><code>integer()</code>, <code>positive()</code>, <code>oneOf(number[])</code>, <code>min(n)</code>, <code>max(n)</code></td></tr><tr><td><code>v.boolean()</code></td><td><code>BooleanSchema</code></td><td>(base methods only)</td></tr><tr><td><code>v.object(fields)</code></td><td><code>ObjectSchema</code></td><td><code>parse(input)</code>, <code>describe()</code></td></tr></tbody></table></div>
|
||||
<p>Every field schema is chainable and shares these base methods:</p>
|
||||
<ul>
|
||||
<li><code>min(n, message?)</code> / <code>max(n, message?)</code> — for strings, bounds the length; for numbers, bounds the value.</li>
|
||||
<li><code>optional()</code> — an empty/missing value passes instead of erroring <code>"Required"</code>.</li>
|
||||
<li><code>label(text)</code> — human label carried into the descriptor.</li>
|
||||
<li><code>default(value)</code> — value substituted when the field is absent (implies <code>optional</code>).</li>
|
||||
<li><code>refine(fn, message?)</code> — <strong>server-only</strong> predicate. <code>fn</code> returns <code>true</code> (ok), <code>false</code> (use <code>message</code>), or a <code>string</code> (that error). Not serialized to the client.</li>
|
||||
</ul>
|
||||
<p>Each string rule accepts an optional trailing <code>message</code> to override the default error text.</p>
|
||||
<h4 id="objectschema"><code>ObjectSchema</code></h4>
|
||||
<pre data-language="ts"><code>schema.parse(input: unknown): ParseResult
|
||||
schema.describe(): SchemaDescriptor</code></pre>
|
||||
<p><code>parse</code> coerces each field (strings stay strings, <code>v.number()</code> runs <code>Number()</code>, <code>v.boolean()</code> treats <code>true</code> / <code>"true"</code> / <code>"on"</code> as true), applies its rules and refinements, fills in <code>default()</code> values, and returns:</p>
|
||||
<pre data-language="ts"><code>interface ParseResult<T = Record<string, unknown>> {
|
||||
ok: boolean; // true when errors is empty
|
||||
value: T; // coerced values (present pass or fail)
|
||||
errors: Record<string, string>; // field name → first failing message
|
||||
}</code></pre>
|
||||
<p><code>describe()</code> returns the JSON bridge for the client:</p>
|
||||
<pre data-language="ts"><code>interface SchemaDescriptor {
|
||||
type: "object";
|
||||
fields: Record<string, FieldDescriptor>;
|
||||
}
|
||||
interface FieldDescriptor {
|
||||
type: "string" | "number" | "boolean";
|
||||
optional?: boolean;
|
||||
label?: string;
|
||||
trim?: boolean; // strings only
|
||||
rules: RuleDescriptor[];
|
||||
}</code></pre>
|
||||
<h4 id="rules-and-coercion">Rules and coercion</h4>
|
||||
<p><code>RuleDescriptor</code> is a discriminated union of the serializable rules — <code>min</code>, <code>max</code>, <code>length</code>, <code>email</code>, <code>url</code>, <code>uuid</code>, <code>date</code>, <code>oneOf</code>, <code>pattern</code>, <code>integer</code>. Two exported functions apply them and are shared by the server (the client runtime reimplements the same logic):</p>
|
||||
<ul>
|
||||
<li><code>applyRule(type, rule, value): string | null</code> — validate one already-coerced value against one rule.</li>
|
||||
<li><code>checkField(desc, raw): { value, error }</code> — coerce and validate one field. Empty input (<code>undefined</code>/<code>null</code>/<code>""</code>) is <code>"Required"</code> unless <code>optional</code>. Strings with <code>trim</code> are trimmed first. Numbers that fail <code>Number()</code> yield <code>"Must be a number"</code>.</li>
|
||||
</ul>
|
||||
<p>Notes on specific rules: <code>email</code>/<code>url</code>/<code>uuid</code> test built-in regexes; <code>date</code> uses <code>Date.parse</code>; <code>pattern</code> reconstructs a <code>RegExp</code> from its <code>source</code>/<code>flags</code> and passes silently if the pattern is invalid; <code>integer</code> requires <code>Number.isInteger</code>; <code>positive()</code> is implemented as <code>min(Number.MIN_VALUE)</code>.</p>
|
||||
<h4 id="api-helpers">API helpers</h4>
|
||||
<pre data-language="ts"><code>invalid(errors: Record<string, string>): Response // ready 400 { ok:false, errors }
|
||||
|
||||
parseBody<T>(schema, req):
|
||||
Promise<{ ok: true; value: T } | { ok: false; response: Response }></code></pre>
|
||||
<p><code>parseBody</code> reads the request body from JSON, <code>application/x-www-form-urlencoded</code>, or <code>multipart/form-data</code>, validates it, and on failure hands back a ready 400 <code>Response</code>.</p>
|
||||
<h4 id="environment-config">Environment config</h4>
|
||||
<pre data-language="ts"><code>parseEnv<T>(schema: ObjectSchema, source?): T</code></pre>
|
||||
<p>Validates env vars (from <code>Bun.env</code>, falling back to <code>process.env</code>) against a schema and coerces them (<code>PORT</code> → number, <code>DEBUG</code> → boolean). On any problem it throws <strong>one</strong> error listing every offending variable, so misconfiguration fails fast at startup.</p>
|
||||
<h4 id="client-runtime-from-runtime-ts">Client runtime (from <code>runtime.ts</code>)</h4>
|
||||
<pre data-language="ts"><code>renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string
|
||||
VALIDATE_RUNTIME: string</code></pre>
|
||||
<ul>
|
||||
<li><code>renderSchemasScript</code> produces <code>window.__wireSchemas = { name: descriptor, … };</code> to inline in the page.</li>
|
||||
<li><code>VALIDATE_RUNTIME</code> is a self-contained, eval-free IIFE string. Injected as a <code><script></code>, it binds every <code>form[data-schema]</code> and validates on submit and blur, writing messages into <code>[data-error="<field>"]</code> elements and toggling <code>aria-invalid</code> / <code>.wire-invalid</code>. On a valid submit it <code>fetch</code>es the form <code>action</code> as JSON (attaching the <code>wire-csrf</code> cookie as an <code>x-csrf-token</code> header), then follows <code>data-redirect</code> / a <code>redirect</code> in the response, surfaces server-side field errors, and fires <code>wire:success</code> / <code>wire:error</code> events. It exposes <code>window.__wireValidate.init(root)</code> and self-initializes on <code>DOMContentLoaded</code>.</li>
|
||||
</ul>
|
||||
<h3 id="usage">Usage</h3>
|
||||
<p>Define a schema and validate an API body:</p>
|
||||
<pre data-language="ts"><code>import { v, parseBody } from "@wrnexus/validation";
|
||||
|
||||
export const signupSchema = v.object({
|
||||
email: v.string().trim().email(),
|
||||
password: v.string().min(8).max(200),
|
||||
age: v.number().integer().min(13).max(120).optional(),
|
||||
role: v.string().oneOf(["user", "admin"]).default("user"),
|
||||
agree: v.boolean(),
|
||||
});
|
||||
|
||||
// inside a route handler
|
||||
const result = await parseBody(signupSchema, req);
|
||||
if (!result.ok) return result.response; // ready 400 with field errors
|
||||
const { email, password, role } = result.value;</code></pre>
|
||||
<p>Server-only refinement:</p>
|
||||
<pre data-language="ts"><code>const schema = v.object({
|
||||
username: v
|
||||
.string()
|
||||
.min(3)
|
||||
.refine((name) => !RESERVED.has(String(name)), "That name is taken"),
|
||||
});</code></pre>
|
||||
<p>Validate environment at startup:</p>
|
||||
<pre data-language="ts"><code>import { v, parseEnv } from "@wrnexus/validation";
|
||||
|
||||
export const env = parseEnv(
|
||||
v.object({
|
||||
DATABASE_URL: v.string().min(1),
|
||||
PORT: v.number().integer().default(3000),
|
||||
DEBUG: v.boolean().optional(),
|
||||
}),
|
||||
);
|
||||
// throws one readable error listing every bad variable if misconfigured</code></pre>
|
||||
<p>Wire the same schema into the browser:</p>
|
||||
<pre data-language="ts"><code>import { renderSchemasScript, VALIDATE_RUNTIME } from "@wrnexus/validation";
|
||||
import { signupSchema } from "./app/schemas/signup.ts";
|
||||
|
||||
const head = `<script>${renderSchemasScript({ signup: signupSchema.describe() })}</script>
|
||||
<script>${VALIDATE_RUNTIME}</script>`;
|
||||
// render a <form data-schema="signup"> with [data-error="email"] etc.</code></pre>
|
||||
<h3 id="requirements-notes">Requirements / Notes</h3>
|
||||
<ul>
|
||||
<li><strong>Bun-only.</strong> <code>parseEnv</code> reads <code>Bun.env</code> (falling back to <code>process.env</code>); <code>parseBody</code> and <code>invalid</code> use the Web <code>Request</code>/<code>Response</code> APIs that back <code>Bun.serve</code>.</li>
|
||||
<li>Refinements (<code>refine</code>) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same <code>RuleDescriptor</code> list.</li>
|
||||
<li>No runtime dependencies. Ships as TypeScript source (<code>src/index.ts</code>) executed directly by Bun.</li>
|
||||
<li>Pairs with the WRNexusJS server (<code>@wrnexus/core</code>) for route handlers and the SSR layer that injects <code>renderSchemasScript</code> / <code>VALIDATE_RUNTIME</code>.</li>
|
||||
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
||||
* Client-side validation. `renderSchemasScript` bakes the discovered schema
|
||||
* descriptors into `window.__wireSchemas`; `VALIDATE_RUNTIME` is a generic,
|
||||
* eval-free validator that reads them and validates every `form[data-schema]`
|
||||
* on submit and blur, writing messages into `[data-error="<field>"]` elements.
|
||||
* The rule logic mirrors `checkField`/`applyRule` in index.ts.
|
||||
*/
|
||||
|
||||
/** `window.__wireSchemas = { name: descriptor, ... }` for the client validator. */
|
||||
declare function renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string;
|
||||
declare const VALIDATE_RUNTIME: string;
|
||||
|
||||
/**
|
||||
* @wrnexus/validation — one schema, validated on the server (API) and the browser
|
||||
* (forms). A schema is a fluent builder; `.parse()` runs server-side and returns
|
||||
* coerced values + field errors, while `.describe()` emits a JSON descriptor the
|
||||
* eval-free client validator interprets. Define schemas once in `app/schemas/`.
|
||||
*/
|
||||
type RuleDescriptor = {
|
||||
kind: "min";
|
||||
n: number;
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "max";
|
||||
n: number;
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "length";
|
||||
n: number;
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "email";
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "url";
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "uuid";
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "date";
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "oneOf";
|
||||
values: (string | number)[];
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "pattern";
|
||||
source: string;
|
||||
flags?: string;
|
||||
message?: string;
|
||||
} | {
|
||||
kind: "integer";
|
||||
message?: string;
|
||||
};
|
||||
interface FieldDescriptor {
|
||||
type: "string" | "number" | "boolean";
|
||||
optional?: boolean;
|
||||
label?: string;
|
||||
/** Trim string input before validating. */
|
||||
trim?: boolean;
|
||||
rules: RuleDescriptor[];
|
||||
}
|
||||
interface SchemaDescriptor {
|
||||
type: "object";
|
||||
fields: Record<string, FieldDescriptor>;
|
||||
}
|
||||
interface ParseResult<T = Record<string, unknown>> {
|
||||
ok: boolean;
|
||||
/** Coerced values (present whether or not validation passed). */
|
||||
value: T;
|
||||
/** Field name → message, only for fields that failed. */
|
||||
errors: Record<string, string>;
|
||||
}
|
||||
/**
|
||||
* Apply one rule to an already-coerced value. Shared by the server; the client
|
||||
* runtime (runtime.ts) mirrors this exactly. Returns an error message or null.
|
||||
*/
|
||||
declare function applyRule(type: string, rule: RuleDescriptor, value: unknown): string | null;
|
||||
/** Coerce + validate one field against its descriptor. */
|
||||
declare function checkField(desc: FieldDescriptor, raw: unknown): {
|
||||
value: unknown;
|
||||
error: string | null;
|
||||
};
|
||||
/** A server-only refinement (a predicate that can't be serialized to the client). */
|
||||
type Refinement = {
|
||||
fn: (value: unknown) => boolean | string;
|
||||
message?: string;
|
||||
};
|
||||
declare abstract class FieldSchema {
|
||||
abstract readonly type: "string" | "number" | "boolean";
|
||||
protected _optional: boolean;
|
||||
protected _label?: string;
|
||||
protected _default?: unknown;
|
||||
protected rules: RuleDescriptor[];
|
||||
protected refinements: Refinement[];
|
||||
optional(): this;
|
||||
label(label: string): this;
|
||||
/** Value used when the field is absent (implies optional). */
|
||||
default(value: unknown): this;
|
||||
min(n: number, message?: string): this;
|
||||
max(n: number, message?: string): this;
|
||||
/**
|
||||
* Custom SERVER-side validation. `fn` returns true (ok), false (use `message`),
|
||||
* or a string (that error). Not mirrored to the client validator.
|
||||
*/
|
||||
refine(fn: (value: unknown) => boolean | string, message?: string): this;
|
||||
getDefault(): unknown;
|
||||
runRefinements(value: unknown): string | null;
|
||||
describe(): FieldDescriptor;
|
||||
}
|
||||
declare class StringSchema extends FieldSchema {
|
||||
readonly type: "string";
|
||||
private _trim;
|
||||
email(message?: string): this;
|
||||
url(message?: string): this;
|
||||
uuid(message?: string): this;
|
||||
date(message?: string): this;
|
||||
length(n: number, message?: string): this;
|
||||
oneOf(values: string[], message?: string): this;
|
||||
trim(): this;
|
||||
pattern(re: RegExp, message?: string): this;
|
||||
describe(): FieldDescriptor;
|
||||
}
|
||||
declare class NumberSchema extends FieldSchema {
|
||||
readonly type: "number";
|
||||
integer(message?: string): this;
|
||||
positive(message?: string): this;
|
||||
oneOf(values: number[], message?: string): this;
|
||||
}
|
||||
declare class BooleanSchema extends FieldSchema {
|
||||
readonly type: "boolean";
|
||||
}
|
||||
declare class ObjectSchema {
|
||||
private readonly fields;
|
||||
constructor(fields: Record<string, FieldSchema>);
|
||||
/** Validate an input object; returns coerced values + per-field errors. */
|
||||
parse(input: unknown): ParseResult;
|
||||
describe(): SchemaDescriptor;
|
||||
}
|
||||
/** The fluent schema builder. */
|
||||
declare const v: {
|
||||
string: () => StringSchema;
|
||||
number: () => NumberSchema;
|
||||
boolean: () => BooleanSchema;
|
||||
object: (fields: Record<string, FieldSchema>) => ObjectSchema;
|
||||
};
|
||||
/**
|
||||
* Validate environment variables against a schema at startup. Values are read
|
||||
* from `Bun.env` / `process.env` by default and coerced by the schema (so
|
||||
* `PORT` becomes a number, `DEBUG` a boolean). On any problem it throws ONE
|
||||
* readable error listing every offending variable, so misconfiguration fails
|
||||
* fast with an actionable message instead of surfacing deep inside the app.
|
||||
*
|
||||
* export const env = parseEnv(v.object({
|
||||
* DATABASE_URL: v.string().min(1),
|
||||
* PORT: v.number(),
|
||||
* }));
|
||||
*/
|
||||
declare function parseEnv<T = Record<string, unknown>>(schema: ObjectSchema, source?: Record<string, string | undefined>): T;
|
||||
/** A 400 response carrying field errors, for API routes. */
|
||||
declare function invalid(errors: Record<string, string>): Response;
|
||||
/**
|
||||
* Parse a request's JSON body against a schema. On failure returns
|
||||
* `{ ok: false, response }` (a ready 400); on success `{ ok: true, value }`.
|
||||
*/
|
||||
declare function parseBody<T = Record<string, unknown>>(schema: ObjectSchema, req: Request): Promise<{
|
||||
ok: true;
|
||||
value: T;
|
||||
} | {
|
||||
ok: false;
|
||||
response: Response;
|
||||
}>;
|
||||
|
||||
export { type FieldDescriptor, ObjectSchema, type ParseResult, type RuleDescriptor, type SchemaDescriptor, VALIDATE_RUNTIME, applyRule, checkField, invalid, parseBody, parseEnv, renderSchemasScript, v };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/validation</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>import { v } from "@wrnexus/validation";</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>schema.parse(input: unknown): ParseResult
|
||||
schema.describe(): SchemaDescriptor</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>interface ParseResult<T = Record<string, unknown>> {
|
||||
ok: boolean; // true when errors is empty
|
||||
value: T; // coerced values (present pass or fail)
|
||||
errors: Record<string, string>; // field name → first failing message
|
||||
}</code></pre></article></div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#the-v-builder">The v builder</a><a class="toc-level-4" href="#objectschema">ObjectSchema</a><a class="toc-level-4" href="#rules-and-coercion">Rules and coercion</a><a class="toc-level-4" href="#api-helpers">API helpers</a><a class="toc-level-4" href="#environment-config">Environment config</a><a class="toc-level-4" href="#client-runtime-from-runtime-ts">Client runtime (from runtime.ts)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
||||
</main>
|
||||
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// AUTO-GENERATED by `wrnexus dev` — do not edit.
|
||||
// Typed routes: a compile-time map of every page path to its [param] types,
|
||||
// plus an href() builder that fills params and rejects unknown paths.
|
||||
|
||||
export interface Routes {
|
||||
"/": Record<string, never>;
|
||||
"/architecture": Record<string, never>;
|
||||
"/getting-started": Record<string, never>;
|
||||
"/language": Record<string, never>;
|
||||
"/packages": Record<string, never>;
|
||||
"/packages/ai": Record<string, never>;
|
||||
"/packages/authz": Record<string, never>;
|
||||
"/packages/cli": Record<string, never>;
|
||||
"/packages/compiler": Record<string, never>;
|
||||
"/packages/core": Record<string, never>;
|
||||
"/packages/csr": Record<string, never>;
|
||||
"/packages/db": Record<string, never>;
|
||||
"/packages/dev-server": Record<string, never>;
|
||||
"/packages/encryption": Record<string, never>;
|
||||
"/packages/i18n": Record<string, never>;
|
||||
"/packages/jwt": Record<string, never>;
|
||||
"/packages/mobile": Record<string, never>;
|
||||
"/packages/native": Record<string, never>;
|
||||
"/packages/oauth": Record<string, never>;
|
||||
"/packages/pubsub": Record<string, never>;
|
||||
"/packages/queue": Record<string, never>;
|
||||
"/packages/reactive": Record<string, never>;
|
||||
"/packages/router": Record<string, never>;
|
||||
"/packages/ssr": Record<string, never>;
|
||||
"/packages/styles": Record<string, never>;
|
||||
"/packages/test": Record<string, never>;
|
||||
"/packages/tracking": Record<string, never>;
|
||||
"/packages/ui": Record<string, never>;
|
||||
"/packages/uploader": Record<string, never>;
|
||||
"/packages/validation": Record<string, never>;
|
||||
}
|
||||
|
||||
export type RoutePath = keyof Routes;
|
||||
|
||||
export function href<P extends RoutePath>(
|
||||
path: P,
|
||||
...args: Routes[P] extends Record<string, never> ? [] : [params: Routes[P]]
|
||||
): string {
|
||||
const params = (args[0] ?? {}) as Record<string, string>;
|
||||
return String(path)
|
||||
.split("/")
|
||||
.map((seg) =>
|
||||
seg.startsWith("[") && seg.endsWith("]")
|
||||
? encodeURIComponent(params[seg.slice(1, -1)] ?? "")
|
||||
: seg,
|
||||
)
|
||||
.join("/");
|
||||
}
|
||||
@@ -0,0 +1,757 @@
|
||||
@import "tailwindcss";
|
||||
@source "../**/*.{wrn,ts,tsx}";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f8fafc;
|
||||
--surface: #ffffff;
|
||||
--surface-soft: #f1f5f9;
|
||||
--text: #0f172a;
|
||||
--muted: #526075;
|
||||
--border: #dbe3ee;
|
||||
--brand: #5b5cf0;
|
||||
--brand-strong: #4338ca;
|
||||
--code: #111827;
|
||||
--code-text: #dbeafe;
|
||||
--shadow: 0 18px 60px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bg: #080b14;
|
||||
--surface: #101522;
|
||||
--surface-soft: #161c2c;
|
||||
--text: #f4f7fb;
|
||||
--muted: #9ba8bc;
|
||||
--border: #263047;
|
||||
--brand: #8b8df8;
|
||||
--brand-strong: #a5b4fc;
|
||||
--code: #05070d;
|
||||
--code-text: #dbeafe;
|
||||
--shadow: 0 22px 70px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
.docs-shell {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.docs-shell::before,
|
||||
.docs-shell::after {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
width: 38rem;
|
||||
height: 38rem;
|
||||
border-radius: 999px;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
filter: blur(110px);
|
||||
opacity: 0.12;
|
||||
animation: ambient-float 16s ease-in-out infinite alternate;
|
||||
}
|
||||
.docs-shell::before {
|
||||
top: -16rem;
|
||||
left: -12rem;
|
||||
background: #6366f1;
|
||||
}
|
||||
.docs-shell::after {
|
||||
right: -15rem;
|
||||
bottom: -18rem;
|
||||
background: #a855f7;
|
||||
animation-delay: -7s;
|
||||
}
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
height: 74px;
|
||||
padding: 0 clamp(20px, 4vw, 72px);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||
background: color-mix(in srgb, var(--bg) 82%, transparent);
|
||||
backdrop-filter: blur(22px) saturate(145%);
|
||||
}
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
width: fit-content;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.brand span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 11px;
|
||||
color: white;
|
||||
background: linear-gradient(145deg, #6366f1, #8b5cf6);
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.3);
|
||||
animation: logo-pulse 4s ease-in-out infinite;
|
||||
}
|
||||
.topbar nav {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.topbar nav a {
|
||||
position: relative;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.topbar nav a::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -0.45rem;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--brand), #a855f7);
|
||||
content: "";
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.topbar nav a:hover::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
.topbar nav a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
.theme-button {
|
||||
justify-self: end;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
padding: 0.5rem 0.8rem;
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
.theme-button:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--brand);
|
||||
box-shadow: 0 10px 28px color-mix(in srgb, var(--brand) 18%, transparent);
|
||||
}
|
||||
.page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-inline: clamp(20px, 4vw, 72px);
|
||||
}
|
||||
.hero {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 670px;
|
||||
padding: 100px 0;
|
||||
text-align: center;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 50% 25%,
|
||||
color-mix(in srgb, var(--brand) 19%, transparent),
|
||||
transparent 34%
|
||||
),
|
||||
radial-gradient(circle at 75% 55%, color-mix(in srgb, #a855f7 9%, transparent), transparent 26%);
|
||||
}
|
||||
.hero.compact {
|
||||
min-height: auto;
|
||||
padding: 100px 0 60px;
|
||||
}
|
||||
.eyebrow,
|
||||
.category {
|
||||
color: var(--brand);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.hero h1 {
|
||||
max-width: 920px;
|
||||
margin: 1rem 0;
|
||||
font-size: clamp(3rem, 7vw, 6.6rem);
|
||||
line-height: 0.96;
|
||||
letter-spacing: -0.065em;
|
||||
animation: reveal-up 0.72s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.hero.compact h1 {
|
||||
font-size: clamp(2.8rem, 5vw, 4.8rem);
|
||||
}
|
||||
.hero h1 em {
|
||||
color: transparent;
|
||||
background: linear-gradient(105deg, var(--brand), #a855f7, #0ea5e9, var(--brand));
|
||||
background-size: 240% auto;
|
||||
background-clip: text;
|
||||
font-style: normal;
|
||||
animation: gradient-shift 7s linear infinite;
|
||||
}
|
||||
.hero > p {
|
||||
max-width: 720px;
|
||||
margin: 0.75rem auto 0;
|
||||
color: var(--muted);
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.75;
|
||||
animation: reveal-up 0.72s 0.1s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem;
|
||||
margin-top: 2rem;
|
||||
animation: reveal-up 0.72s 0.18s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.actions a {
|
||||
padding: 0.8rem 1.15rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
border-color 0.18s ease;
|
||||
}
|
||||
.actions a:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--brand);
|
||||
box-shadow: 0 14px 35px color-mix(in srgb, var(--brand) 18%, transparent);
|
||||
}
|
||||
.actions .primary {
|
||||
border-color: var(--brand);
|
||||
color: white;
|
||||
background: var(--brand);
|
||||
}
|
||||
.code-window {
|
||||
width: min(680px, 100%);
|
||||
margin-top: 3rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
text-align: left;
|
||||
background: var(--code);
|
||||
box-shadow: var(--shadow);
|
||||
animation: reveal-up 0.8s 0.28s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.code-window > span {
|
||||
display: block;
|
||||
padding: 0.7rem 1rem;
|
||||
border-bottom: 1px solid #283044;
|
||||
color: #94a3b8;
|
||||
font:
|
||||
0.75rem ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
pre {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
margin: 1.25rem 0;
|
||||
padding: 1.2rem 1.35rem;
|
||||
border: 1px solid #20283a;
|
||||
border-radius: 12px;
|
||||
color: var(--code-text);
|
||||
background: var(--code);
|
||||
font:
|
||||
0.82rem/1.7 ui-monospace,
|
||||
SFMono-Regular,
|
||||
Consolas,
|
||||
monospace;
|
||||
tab-size: 2;
|
||||
}
|
||||
.code-window pre {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
code {
|
||||
border-radius: 5px;
|
||||
padding: 0.12rem 0.35rem;
|
||||
color: var(--brand-strong);
|
||||
background: var(--surface-soft);
|
||||
font:
|
||||
0.88em ui-monospace,
|
||||
SFMono-Regular,
|
||||
Consolas,
|
||||
monospace;
|
||||
}
|
||||
pre code,
|
||||
.sidebar > code {
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 0 0 100px;
|
||||
}
|
||||
.feature-grid article,
|
||||
.package-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--surface),
|
||||
color-mix(in srgb, var(--surface-soft) 42%, var(--surface))
|
||||
);
|
||||
box-shadow: var(--shadow);
|
||||
animation: reveal-up 0.62s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.feature-grid article:nth-child(2),
|
||||
.package-card:nth-child(3n + 2) {
|
||||
animation-delay: 0.06s;
|
||||
}
|
||||
.feature-grid article:nth-child(3),
|
||||
.package-card:nth-child(3n) {
|
||||
animation-delay: 0.12s;
|
||||
}
|
||||
.feature-grid h2,
|
||||
.package-card h2 {
|
||||
margin: 0.7rem 0;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.feature-grid p,
|
||||
.package-card p {
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.search {
|
||||
width: min(620px, 100%);
|
||||
margin-top: 2rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 13px;
|
||||
padding: 0.9rem 1rem;
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
font: inherit;
|
||||
box-shadow: var(--shadow);
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
.search:focus {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--brand);
|
||||
outline: none;
|
||||
box-shadow: 0 18px 50px color-mix(in srgb, var(--brand) 18%, transparent);
|
||||
}
|
||||
.category-filter {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.category-row {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
overflow-x: auto;
|
||||
padding: 0.2rem 0 0.65rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.category-row button {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 0.55rem 0.8rem;
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
font-family: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.16s ease,
|
||||
color 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
}
|
||||
.category-row button:hover,
|
||||
.category-row button:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--brand);
|
||||
color: var(--brand);
|
||||
outline: none;
|
||||
}
|
||||
.category-row button span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
border-radius: 999px;
|
||||
color: var(--text);
|
||||
background: var(--surface-soft);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
.category-filter p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.category-filter strong {
|
||||
color: var(--text);
|
||||
}
|
||||
.package-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
.package-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
.package-card:hover {
|
||||
transform: translateY(-7px);
|
||||
border-color: var(--brand);
|
||||
box-shadow: 0 24px 60px color-mix(in srgb, var(--brand) 14%, transparent);
|
||||
}
|
||||
.card-link {
|
||||
color: var(--brand);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.package-page {
|
||||
display: grid;
|
||||
grid-template-columns: 250px minmax(0, 1fr) 200px;
|
||||
gap: 48px;
|
||||
align-items: start;
|
||||
padding-top: 56px;
|
||||
padding-bottom: 110px;
|
||||
}
|
||||
.on-this-page {
|
||||
position: sticky;
|
||||
top: 110px;
|
||||
min-width: 0;
|
||||
border-left: 1px solid var(--border);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.on-this-page h2 {
|
||||
margin: 0 0 0.8rem;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.on-this-page nav {
|
||||
display: grid;
|
||||
gap: 0.48rem;
|
||||
max-height: calc(100vh - 160px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.on-this-page a {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.on-this-page a:hover {
|
||||
color: var(--brand);
|
||||
}
|
||||
.on-this-page .toc-level-3,
|
||||
.on-this-page .toc-level-4 {
|
||||
padding-left: 0.7rem;
|
||||
}
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 110px;
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
.sidebar > a {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sidebar h1 {
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0;
|
||||
font-size: 1.55rem;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
.sidebar p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.sidebar > code {
|
||||
overflow-x: auto;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
color: var(--text);
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
.sidebar nav {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.sidebar nav a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.documentation {
|
||||
min-width: 0;
|
||||
animation: reveal-up 0.65s 0.08s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.documentation.standalone,
|
||||
.standalone {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 90px clamp(0px, 5vw, 90px) 120px;
|
||||
}
|
||||
.standalone > * {
|
||||
max-width: 1000px;
|
||||
}
|
||||
.doc-intro {
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.doc-intro h1,
|
||||
.standalone h1 {
|
||||
margin: 0.7rem 0;
|
||||
font-size: clamp(2.5rem, 5vw, 4.2rem);
|
||||
letter-spacing: -0.055em;
|
||||
}
|
||||
.doc-intro p {
|
||||
color: var(--muted);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.prose {
|
||||
color: var(--muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.prose > * {
|
||||
max-width: 850px;
|
||||
}
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4 {
|
||||
scroll-margin-top: 90px;
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
.prose h2 {
|
||||
margin: 3.5rem 0 1rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.prose h3 {
|
||||
margin: 2.5rem 0 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.prose ul {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
.prose li {
|
||||
margin: 0.45rem 0;
|
||||
}
|
||||
.prose blockquote {
|
||||
margin: 1.5rem 0;
|
||||
border-left: 3px solid var(--brand);
|
||||
padding: 0.5rem 1.2rem;
|
||||
color: var(--text);
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--surface);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.prose th,
|
||||
.prose td {
|
||||
min-width: 130px;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.prose th {
|
||||
color: var(--text);
|
||||
background: var(--surface-soft);
|
||||
font-weight: 700;
|
||||
}
|
||||
.prose tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.prose a {
|
||||
color: var(--brand);
|
||||
}
|
||||
.api {
|
||||
margin-top: 4rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.api pre {
|
||||
max-height: 760px;
|
||||
}
|
||||
#guide,
|
||||
#api,
|
||||
#examples {
|
||||
scroll-margin-top: 90px;
|
||||
}
|
||||
.examples {
|
||||
margin-top: 4rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.example-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
.example-card {
|
||||
min-width: 0;
|
||||
}
|
||||
.example-card h3 {
|
||||
margin-bottom: -0.5rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.language-reference {
|
||||
max-width: 920px;
|
||||
}
|
||||
footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 34px 20px;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes reveal-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(22px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes ambient-float {
|
||||
from {
|
||||
transform: translate3d(-3%, -2%, 0) scale(0.95);
|
||||
}
|
||||
to {
|
||||
transform: translate3d(8%, 7%, 0) scale(1.12);
|
||||
}
|
||||
}
|
||||
@keyframes gradient-shift {
|
||||
to {
|
||||
background-position: 240% center;
|
||||
}
|
||||
}
|
||||
@keyframes logo-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.28);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 10px 34px rgba(139, 92, 246, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.topbar {
|
||||
grid-template-columns: 1fr auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.topbar nav {
|
||||
display: none;
|
||||
}
|
||||
.feature-grid,
|
||||
.package-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.package-page {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 35px;
|
||||
}
|
||||
.sidebar {
|
||||
position: static;
|
||||
}
|
||||
.on-this-page {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 901px) and (max-width: 1120px) {
|
||||
.package-page {
|
||||
grid-template-columns: 230px minmax(0, 1fr);
|
||||
}
|
||||
.on-this-page {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page {
|
||||
width: 100%;
|
||||
padding-inline: 14px;
|
||||
}
|
||||
.hero {
|
||||
min-height: 580px;
|
||||
padding: 70px 0;
|
||||
}
|
||||
.feature-grid,
|
||||
.package-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.package-page {
|
||||
padding-top: 30px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "wrnexusjs",
|
||||
"dependencies": {
|
||||
"@wrnexus/ai": "0.2.12",
|
||||
"@wrnexus/authz": "0.2.12",
|
||||
"@wrnexus/compiler": "0.2.12",
|
||||
"@wrnexus/core": "0.2.12",
|
||||
"@wrnexus/csr": "0.2.12",
|
||||
"@wrnexus/db": "0.2.12",
|
||||
"@wrnexus/dev-server": "0.2.12",
|
||||
"@wrnexus/encryption": "0.2.12",
|
||||
"@wrnexus/i18n": "0.2.12",
|
||||
"@wrnexus/jwt": "0.2.12",
|
||||
"@wrnexus/mobile": "0.2.12",
|
||||
"@wrnexus/native": "0.2.12",
|
||||
"@wrnexus/oauth": "0.2.12",
|
||||
"@wrnexus/pubsub": "0.2.12",
|
||||
"@wrnexus/queue": "0.2.12",
|
||||
"@wrnexus/reactive": "0.2.12",
|
||||
"@wrnexus/router": "0.2.12",
|
||||
"@wrnexus/ssr": "0.2.12",
|
||||
"@wrnexus/styles": "0.2.12",
|
||||
"@wrnexus/test": "0.2.12",
|
||||
"@wrnexus/tracking": "0.2.12",
|
||||
"@wrnexus/ui": "0.2.12",
|
||||
"@wrnexus/uploader": "0.2.12",
|
||||
"@wrnexus/validation": "0.2.12",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"@types/bun": "latest",
|
||||
"@wrnexus/cli": "0.2.12",
|
||||
"eslint": "^9.0.0",
|
||||
"prettier": "latest",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||
|
||||
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
|
||||
|
||||
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.2", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "enhanced-resolve": "5.21.6", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.2" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-Fzt+HrIZHDlkRYKdLMBeufaroaPvwCBG70sMLdmurdeadNMO/LxbmT8Sbb+P83ep0iAlAImettb7Y+rO+37rXw=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.2" } }, "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.2", "@tailwindcss/oxide-darwin-arm64": "4.3.2", "@tailwindcss/oxide-darwin-x64": "4.3.2", "@tailwindcss/oxide-freebsd-x64": "4.3.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", "@tailwindcss/oxide-linux-x64-musl": "4.3.2", "@tailwindcss/oxide-wasm32-wasi": "4.3.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.2", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.63.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/type-utils": "8.63.0", "@typescript-eslint/utils": "8.63.0", "@typescript-eslint/visitor-keys": "8.63.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.63.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.63.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/types": "8.63.0", "@typescript-eslint/typescript-estree": "8.63.0", "@typescript-eslint/visitor-keys": "8.63.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.63.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.63.0", "@typescript-eslint/types": "^8.63.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.63.0", "", { "dependencies": { "@typescript-eslint/types": "8.63.0", "@typescript-eslint/visitor-keys": "8.63.0" } }, "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.63.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.63.0", "", { "dependencies": { "@typescript-eslint/types": "8.63.0", "@typescript-eslint/typescript-estree": "8.63.0", "@typescript-eslint/utils": "8.63.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.63.0", "", {}, "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.63.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.63.0", "@typescript-eslint/tsconfig-utils": "8.63.0", "@typescript-eslint/types": "8.63.0", "@typescript-eslint/visitor-keys": "8.63.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.63.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/types": "8.63.0", "@typescript-eslint/typescript-estree": "8.63.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.63.0", "", { "dependencies": { "@typescript-eslint/types": "8.63.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw=="],
|
||||
|
||||
"@wrnexus/ai": ["@wrnexus/ai@0.2.12", "", {}, "sha512-yk9l9iJUskiJaJou2nMhizVCq3WqAjQkF6eizEpM5UVsLYvPMZGolTM9/7TbkI+a7cuc/EKjeuBXT55Jo1rS8g=="],
|
||||
|
||||
"@wrnexus/authz": ["@wrnexus/authz@0.2.12", "", {}, "sha512-LEsqeutezOmbzUwmljWL9n/aoGXTizG0EvZENGwgIbyuOLCaWN9eDVgEcYAjAsKR4k00gdHdND0m2Xho3Asc3w=="],
|
||||
|
||||
"@wrnexus/cli": ["@wrnexus/cli@0.2.12", "", { "dependencies": { "@wrnexus/compiler": "^0.2.12", "@wrnexus/core": "^0.2.12", "@wrnexus/csr": "^0.2.12", "@wrnexus/db": "^0.2.12", "@wrnexus/dev-server": "^0.2.12", "@wrnexus/i18n": "^0.2.12", "@wrnexus/router": "^0.2.12", "@wrnexus/styles": "^0.2.12", "@wrnexus/ui": "^0.2.12", "@wrnexus/validation": "^0.2.12" }, "bin": { "wrnexus": "dist/index.js" } }, "sha512-qw6f8cxH7wS15MTNzSGEMmUTCWfCk1aPBhE8euQ1kd2keysK15mVrqrmANq5aui1kwuVKTZ+T3/aCuo8GL5spQ=="],
|
||||
|
||||
"@wrnexus/compiler": ["@wrnexus/compiler@0.2.12", "", {}, "sha512-TFLkz/J/SJgRKe2WJRA7Way5QAda8k6TT4/jF/HhSOLm1H88gzof1QDG6X+9ORdfSZdcbduq2GwQK3Ulp59d2Q=="],
|
||||
|
||||
"@wrnexus/core": ["@wrnexus/core@0.2.12", "", {}, "sha512-JkLQ27zUiNvUWLWFSnNY2pZPwio/PVOx27cQll4HEGlALMAWLoLZOKV/5eRYktBN+vW0hhyFXJQvvpeIRT+IaQ=="],
|
||||
|
||||
"@wrnexus/csr": ["@wrnexus/csr@0.2.12", "", { "dependencies": { "@wrnexus/core": "^0.2.12" } }, "sha512-aXK3EmvhXfKFJPrME/pDwepAwWucVBq+9dBc0OUipsDWmGQOmlqxeZBZ95RbBrDwRbLp8ZRaOHJC1dMcaOlJzA=="],
|
||||
|
||||
"@wrnexus/db": ["@wrnexus/db@0.2.12", "", {}, "sha512-+ToSWC+Kj+nY6nX/FZw2lVW+8OlzH+k0H6YPrH4tReFyGdbFztbSaaielTQ6E97IwkzCofAF8Y5JMSt7avDOeQ=="],
|
||||
|
||||
"@wrnexus/dev-server": ["@wrnexus/dev-server@0.2.12", "", { "dependencies": { "@wrnexus/compiler": "^0.2.12", "@wrnexus/core": "^0.2.12", "@wrnexus/csr": "^0.2.12", "@wrnexus/db": "^0.2.12", "@wrnexus/i18n": "^0.2.12", "@wrnexus/pubsub": "^0.2.12", "@wrnexus/router": "^0.2.12", "@wrnexus/ssr": "^0.2.12", "@wrnexus/styles": "^0.2.12", "@wrnexus/ui": "^0.2.12", "@wrnexus/uploader": "^0.2.12", "@wrnexus/validation": "^0.2.12" } }, "sha512-foN6tBqNaAkzCBe5HZXbyh6cua4Km3dAbaHaVCpVyVywgNzm717JFKNH8+CFu30XW8pUdMe0auOoV+wiqHY5lw=="],
|
||||
|
||||
"@wrnexus/encryption": ["@wrnexus/encryption@0.2.12", "", {}, "sha512-o6HxrcQG0X4CwJYHFCrVH1oxydk3k8/v621I6rQz/IVkrdN5axPTU65xDMLXK48b3pF1LkgsLTV/GwBve3+tZA=="],
|
||||
|
||||
"@wrnexus/i18n": ["@wrnexus/i18n@0.2.12", "", { "dependencies": { "@wrnexus/core": "^0.2.12" } }, "sha512-tLUEv3iHNLtGMPuNd15+N/YWWmDkBW4rqbtMeFa4oILRKZykUN+JRsl3WkBFD8xYso1hBGG1gubJVwrvcT5FHw=="],
|
||||
|
||||
"@wrnexus/jwt": ["@wrnexus/jwt@0.2.12", "", {}, "sha512-6m2/HAds3dfCh/kwC1zU4JJFtyXDLV0MvByqw8YNF2Tmvlx+3H4pwW+XcoJEj9ksWBKkjxcetaOySKb1pLq+hQ=="],
|
||||
|
||||
"@wrnexus/mobile": ["@wrnexus/mobile@0.2.12", "", { "dependencies": { "@wrnexus/native": "^0.2.12" } }, "sha512-D+y+MPz07hV8w75JYos4tSR3snSWHK3iaOdedJL7bl2wZ9Ap/pUeAKV3RtyoUHQ3yKgh+m0r8Mzdu8Mv0K3jgA=="],
|
||||
|
||||
"@wrnexus/native": ["@wrnexus/native@0.2.12", "", {}, "sha512-Zc1j1ZqTIlwWFDFrwkm/Cz7Y4hotIHXuSWDEPEn5BehCtTn4cVmh5psiNF4MYho4La752HRp+8qDQhDDg7kmTA=="],
|
||||
|
||||
"@wrnexus/oauth": ["@wrnexus/oauth@0.2.12", "", {}, "sha512-6hOFSFVhWp4Dn2ztWFEOo+XKB5BmXX8Hrrq3GMhiMxC8p7p8KiQhyofDZ8mG+WfkAjrG0QeifW0wecxd8fAoBg=="],
|
||||
|
||||
"@wrnexus/pubsub": ["@wrnexus/pubsub@0.2.12", "", {}, "sha512-ul4w+8gW06rqMx9wcJ+F/PsdAAd2p7YsWzelv0fNOgfINvyG5VPkBaozqtNl9sbckhJlRC3/wEvHhRQJQaG5cQ=="],
|
||||
|
||||
"@wrnexus/queue": ["@wrnexus/queue@0.2.12", "", {}, "sha512-onogt5wDDrokbGapKTHUgOeqlBGcX8faA5rg/Gqi/pbiv3SvA62vtkX7t3ZaUZKnkGF/gG5ReE9Pt8VLQ1E0Dg=="],
|
||||
|
||||
"@wrnexus/reactive": ["@wrnexus/reactive@0.2.12", "", {}, "sha512-7oykh0M8ig6/cGBwyQ+5rNn3nypw8ziVqNMkIHfvDr0opQd/ZrQ0NzwbVVrmv90MspfWRyKemRsw6zA6xlPuTA=="],
|
||||
|
||||
"@wrnexus/router": ["@wrnexus/router@0.2.12", "", { "dependencies": { "@wrnexus/compiler": "^0.2.12", "@wrnexus/core": "^0.2.12" } }, "sha512-v72kL6xjybgqAgfLzjgwMBaGJIuHh1fSI1JwKk7OK+jrWNAn7fqEN6mIOnzKgBtKerFa62KW+oPw1pGLvVnkEA=="],
|
||||
|
||||
"@wrnexus/ssr": ["@wrnexus/ssr@0.2.12", "", { "dependencies": { "@wrnexus/core": "^0.2.12" } }, "sha512-DB2VFqzMDmAJG/k80gJMQ1mU+sfr7QVCyr3HHgrpKd1aHmLoIlTg1ItChrSZc9OsnYgiRXSMx290GRkG2POi/g=="],
|
||||
|
||||
"@wrnexus/styles": ["@wrnexus/styles@0.2.12", "", { "dependencies": { "@wrnexus/uploader": "^0.2.12" } }, "sha512-SQtvuxCCbR3OMmPDeV9zZtCL3ohPeOQ9tHp77aqMLHQchXEmhxQ831nYZqufSJ7xj9WafMJGnyN5HUe8LqqaQg=="],
|
||||
|
||||
"@wrnexus/test": ["@wrnexus/test@0.2.12", "", {}, "sha512-XfovFKYRRPBmwjpa4xbD6vuRa4zs0YnSAEn++yiC/CfP2N6st567y3rK5PxbXL6mC6rQR35xPGlVlIr+WeZtKQ=="],
|
||||
|
||||
"@wrnexus/tracking": ["@wrnexus/tracking@0.2.12", "", {}, "sha512-2pd2/lmRkGt3fVKc6Nw2/HFs0bWqqaNr5i1mrvtc7yVLuVkf8I6gtikWyJrUCkJCxKhGXZWLMmGqRHvep5BuiA=="],
|
||||
|
||||
"@wrnexus/ui": ["@wrnexus/ui@0.2.12", "", { "dependencies": { "@wrnexus/core": "^0.2.12" } }, "sha512-jKz8qGNtCbW0BvzXbMYttgUHDr11MacT886siIbTA36TtlxrY+6a0JVw4h+XMLYhFk1CjD6YXWyA2ZIoy0qHxQ=="],
|
||||
|
||||
"@wrnexus/uploader": ["@wrnexus/uploader@0.2.12", "", { "dependencies": { "@wrnexus/core": "^0.2.12" } }, "sha512-olRQ+5spWTWplWBWEr89ZVw/CphhJgTQ/quxVwgXZmh0yznqqmvrGq3//bUAwF9WVTusK2EMtOOUSpfVSxn8cw=="],
|
||||
|
||||
"@wrnexus/validation": ["@wrnexus/validation@0.2.12", "", {}, "sha512-cN9g6EKcGIuyQx2AFu9Qy2v0FD3neXsCA1uORr+T/x7ta/IDe8WzTall6EpcKzGq2RsLp/mCPspoF3t0CY7V2w=="],
|
||||
|
||||
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.2", "", {}, "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.63.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.63.0", "@typescript-eslint/parser": "8.63.0", "@typescript-eslint/typescript-estree": "8.63.0", "@typescript-eslint/utils": "8.63.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,259 @@
|
||||
# WrNexus
|
||||
|
||||
> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
|
||||
> `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based.
|
||||
> This document teaches an AI how to write correct WrNexus code. It is private and
|
||||
> post-dates model training data, so rely on THIS document, not prior web-framework
|
||||
> assumptions.
|
||||
|
||||
## Golden rules
|
||||
|
||||
- **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React
|
||||
for UI. Do NOT use `useState`, hooks, JSX, or a client bundler.
|
||||
- **Routing is file-based** under `app/`. The filename is the route. No router config.
|
||||
- **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render
|
||||
on the server and hydrate automatically — you never write client-side JS islands.
|
||||
- **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported.
|
||||
- To add files, prefer the CLI: `wrnexus generate page <Name>` / `component <name>` / `api <path>` / `schema <name>`.
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
app/
|
||||
pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug"
|
||||
components/ *.wrn → reusable UI, mounted in a page/component via <div data-component="name" ...props>
|
||||
layouts/ *.wrn → named layouts; a page opts in with layout = "name"
|
||||
api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response
|
||||
middleware/ *.ts → export default async (ctx, next) => next()
|
||||
realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/<name>)
|
||||
schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody
|
||||
locales/ *.json → i18n messages per language
|
||||
db/ schema.ts, queries/*.sql, migrations/*.sql
|
||||
styles/ global.css → Tailwind (default) or plain CSS
|
||||
wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles")
|
||||
public/ → static assets served at /
|
||||
```
|
||||
|
||||
## `.wrn` page
|
||||
|
||||
```wrn
|
||||
page Home {
|
||||
layout = "public" // optional: a component in app/layouts/<name>.wrn ("none" to skip)
|
||||
|
||||
state count = 0 // optional: seeds client-reactive state (omit for pure SSR)
|
||||
|
||||
seo {
|
||||
title = "Home"
|
||||
description = "..."
|
||||
canonical = "/"
|
||||
}
|
||||
|
||||
view {
|
||||
<h1>Hello</h1>
|
||||
<p>Count is {count}, doubled is {count * 2}.</p>
|
||||
<button @click="count++">Increment</button>
|
||||
<div data-component="counter" start="5" label="Clicks"></div>
|
||||
}
|
||||
|
||||
style {
|
||||
h1 { color: var(--wire-color-text); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `.wrn` component
|
||||
|
||||
```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 {
|
||||
<button @click="count++">{label}: {count}</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Mount it from any page/component: `<div data-component="counter" start="0" label="Clicks"></div>`.
|
||||
Components render on the server with their props, then hydrate — no per-component JS.
|
||||
|
||||
## The `view { }` block (plain HTML + a few directives)
|
||||
|
||||
- `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`.
|
||||
- `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`.
|
||||
- `<div data-component="name" prop="v">` — mount a component (attrs become string props, coerced).
|
||||
- `<slot></slot>` / `<slot name="x"></slot>` — component/layout slots; fill with `<div data-slot="x">…</div>`.
|
||||
- **Server loop (DB/list/table):** `{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `<list>` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`.
|
||||
- **Server conditional:** `{#if <expr>} … {:else if <expr>} … {:else} … {/if}` — renders the first truthy branch on the server. `<expr>` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}<span>●</span>{:else}<span>○</span>{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead.
|
||||
- i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`.
|
||||
- Theme: any element with `data-wire-theme-toggle` toggles light/dark; `data-wire-theme-set="dark"` sets it.
|
||||
- Void/self-closing tags are fine: `<br />`, `<img src="..." />`.
|
||||
- Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text.
|
||||
|
||||
## Data-driven tables / lists (server-rendered `.wrn`)
|
||||
|
||||
Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them.
|
||||
This renders on the **server** (SSR-first) and is HTML-escaped by default.
|
||||
|
||||
```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 {
|
||||
<table>
|
||||
<tbody>
|
||||
{#each rows as r, i}
|
||||
<tr>
|
||||
<td>#{i}</td>
|
||||
<td>{r.name}</td>
|
||||
<td><a href="mailto:{r.email}">{r.email}</a></td>
|
||||
</tr>
|
||||
{:empty}
|
||||
<tr><td colspan="3">No submissions yet.</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The matching API returns the array under a key the `ssr` block reads:
|
||||
|
||||
```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<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`),
|
||||
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.
|
||||
|
||||
## 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: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (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
|
||||
wrnexus create <name> # scaffold a new app
|
||||
wrnexus generate page <Name> # scaffold a page (aliases: g p)
|
||||
wrnexus generate component <name> | api <path> | schema <name>
|
||||
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
|
||||
wrnexus eject <component> # copy a Wire UI component's .wrn into app/components to customize
|
||||
```
|
||||
|
||||
## When asked to "create a page/component/feature"
|
||||
|
||||
1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
|
||||
2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`.
|
||||
3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`.
|
||||
4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wire-*)`), or `style { }`.
|
||||
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.
|
||||
Generated
+2667
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "wrnexusjs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wrnexus dev .",
|
||||
"build": "wrnexus build .",
|
||||
"test": "bun test app",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier . --write",
|
||||
"format:check": "prettier . --check",
|
||||
"check": "bun run lint && bun run test && bun run format:check",
|
||||
"docs:generate": "bun run scripts/generate-docs.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/ai": "0.2.12",
|
||||
"@wrnexus/authz": "0.2.12",
|
||||
"@wrnexus/compiler": "0.2.12",
|
||||
"@wrnexus/core": "0.2.12",
|
||||
"@wrnexus/csr": "0.2.12",
|
||||
"@wrnexus/db": "0.2.12",
|
||||
"@wrnexus/dev-server": "0.2.12",
|
||||
"@wrnexus/encryption": "0.2.12",
|
||||
"@wrnexus/i18n": "0.2.12",
|
||||
"@wrnexus/jwt": "0.2.12",
|
||||
"@wrnexus/mobile": "0.2.12",
|
||||
"@wrnexus/native": "0.2.12",
|
||||
"@wrnexus/oauth": "0.2.12",
|
||||
"@wrnexus/pubsub": "0.2.12",
|
||||
"@wrnexus/queue": "0.2.12",
|
||||
"@wrnexus/reactive": "0.2.12",
|
||||
"@wrnexus/router": "0.2.12",
|
||||
"@wrnexus/ssr": "0.2.12",
|
||||
"@wrnexus/styles": "0.2.12",
|
||||
"@wrnexus/test": "0.2.12",
|
||||
"@wrnexus/tracking": "0.2.12",
|
||||
"@wrnexus/ui": "0.2.12",
|
||||
"@wrnexus/uploader": "0.2.12",
|
||||
"@wrnexus/validation": "0.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^26.1.1",
|
||||
"@wrnexus/cli": "0.2.12",
|
||||
"eslint": "^9.0.0",
|
||||
"prettier": "latest",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@@ -0,0 +1,9 @@
|
||||
/* global window, document */
|
||||
|
||||
// Re-bind theme controls after WRNexusJS swaps the page during client navigation.
|
||||
// bind() is idempotent, so this is safe alongside newer framework runtimes.
|
||||
window.addEventListener("wrnexus:navigated", function () {
|
||||
if (window.wireTheme && typeof window.wireTheme.bind === "function") {
|
||||
window.wireTheme.bind(document);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,397 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const root = resolve(import.meta.dir, "..");
|
||||
const pages = join(root, "app", "pages");
|
||||
const packagePages = join(pages, "packages");
|
||||
mkdirSync(packagePages, { recursive: true });
|
||||
|
||||
const catalog = [
|
||||
["ai", "AI", "Server-side Anthropic client with generation and streaming."],
|
||||
["authz", "Security", "Role, permission, policy, and authorization guards."],
|
||||
["cli", "Tooling", "Create, develop, build, generate, test, and maintain WrNexus apps."],
|
||||
["compiler", "Core", "Parser and code generators for the .wrn language."],
|
||||
["core", "Core", "Contexts, middleware, security, sessions, caching, JSX, and realtime."],
|
||||
["csr", "Frontend", "Reactive, navigation, and realtime browser runtimes."],
|
||||
["db", "Data", "Database adapters, typed queries, models, migrations, and sessions."],
|
||||
["dev-server", "Runtime", "Development and production servers, HMR, assets, and gateways."],
|
||||
["encryption", "Security", "Hashing, HMAC, authenticated encryption, and key derivation."],
|
||||
["i18n", "Frontend", "Translation loading, locale resolution, and Intl formatting."],
|
||||
["jwt", "Security", "HS256 JWT signing, verification, and bearer authentication."],
|
||||
["mobile", "Native", "SSR-safe compatibility access to Capacitor plugins."],
|
||||
["native", "Native", "Cross-platform browser and Capacitor capability registry."],
|
||||
["oauth", "Security", "OAuth 2.0, PKCE, provider presets, and profile mapping."],
|
||||
["pubsub", "Realtime", "In-process and Redis-backed publish/subscribe."],
|
||||
["queue", "Data", "Background jobs with delay, concurrency, retry, and repetition."],
|
||||
["reactive", "Frontend", "Small type-safe reactive signal primitives."],
|
||||
["router", "Core", "Filesystem discovery, route matching, and typed route generation."],
|
||||
["ssr", "Runtime", "Secure HTML document rendering and SEO metadata."],
|
||||
["styles", "Frontend", "CSS pipeline, themes, fonts, profiles, and application config."],
|
||||
["test", "Tooling", "WrNexus-aware component, route, and browser testing utilities."],
|
||||
["tracking", "Runtime", "Error/event capture, middleware, filtering, and sinks."],
|
||||
["ui", "Frontend", "Themeable server-rendered UI components and CSS."],
|
||||
["uploader", "Data", "Validated local/S3 uploads and secure file serving."],
|
||||
["validation", "Security", "Typed schemas, coercion, validation, and browser descriptors."],
|
||||
] as const;
|
||||
|
||||
const escape = (value: string) =>
|
||||
value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/\{/g, "{")
|
||||
.replace(/\}/g, "}");
|
||||
|
||||
function inline(value: string): string {
|
||||
return escape(value)
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" rel="noreferrer">$1</a>');
|
||||
}
|
||||
|
||||
interface DocHeading {
|
||||
id: string;
|
||||
title: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
function markdown(source: string): { html: string; headings: DocHeading[] } {
|
||||
const lines = source.replace(/\r/g, "").split("\n");
|
||||
const out: string[] = [];
|
||||
const headings: DocHeading[] = [];
|
||||
const usedIds = new Map<string, number>();
|
||||
let code: string[] | null = null;
|
||||
let language = "";
|
||||
let list = false;
|
||||
let paragraph: string[] = [];
|
||||
let table: string[][] = [];
|
||||
const flushParagraph = () => {
|
||||
if (paragraph.length) out.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
||||
paragraph = [];
|
||||
};
|
||||
const closeList = () => {
|
||||
if (list) out.push("</ul>");
|
||||
list = false;
|
||||
};
|
||||
const flushTable = () => {
|
||||
if (!table.length) return;
|
||||
const separator = table[1]?.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
||||
const header = separator ? table[0]! : undefined;
|
||||
const rows = separator ? table.slice(2) : table;
|
||||
out.push('<div class="table-wrap"><table>');
|
||||
if (header) {
|
||||
out.push(
|
||||
`<thead><tr>${header.map((cell) => `<th>${inline(cell.trim())}</th>`).join("")}</tr></thead>`,
|
||||
);
|
||||
}
|
||||
out.push(
|
||||
`<tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell.trim())}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`,
|
||||
);
|
||||
table = [];
|
||||
};
|
||||
for (const line of lines) {
|
||||
if (/^\|.*\|\s*$/.test(line)) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
table.push(line.slice(1, line.lastIndexOf("|")).split("|"));
|
||||
continue;
|
||||
}
|
||||
flushTable();
|
||||
const fence = /^```(.*)$/.exec(line);
|
||||
if (fence) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
if (code) {
|
||||
out.push(
|
||||
`<pre data-language="${escape(language)}"><code>${escape(code.join("\n"))}</code></pre>`,
|
||||
);
|
||||
code = null;
|
||||
} else {
|
||||
code = [];
|
||||
language = fence[1]!.trim();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (code) {
|
||||
code.push(line);
|
||||
continue;
|
||||
}
|
||||
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
|
||||
if (heading) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
const level = Math.min(4, heading[1]!.length + 1);
|
||||
const title = heading[2]!.replace(/[`*_]/g, "").trim();
|
||||
const base =
|
||||
title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "") || "section";
|
||||
const occurrence = usedIds.get(base) ?? 0;
|
||||
usedIds.set(base, occurrence + 1);
|
||||
const id = occurrence ? `${base}-${occurrence + 1}` : base;
|
||||
headings.push({ id, title, level });
|
||||
out.push(`<h${level} id="${id}">${inline(heading[2]!)}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
const item = /^[-*]\s+(.+)$/.exec(line);
|
||||
if (item) {
|
||||
flushParagraph();
|
||||
if (!list) out.push("<ul>");
|
||||
list = true;
|
||||
out.push(`<li>${inline(item[1]!)}</li>`);
|
||||
continue;
|
||||
}
|
||||
if (!line.trim()) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
continue;
|
||||
}
|
||||
if (/^>\s?/.test(line)) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
out.push(`<blockquote>${inline(line.replace(/^>\s?/, ""))}</blockquote>`);
|
||||
continue;
|
||||
}
|
||||
if (/^---+$/.test(line.trim())) continue;
|
||||
paragraph.push(line.trim());
|
||||
}
|
||||
flushParagraph();
|
||||
closeList();
|
||||
flushTable();
|
||||
if (code) out.push(`<pre><code>${escape(code.join("\n"))}</code></pre>`);
|
||||
return { html: out.join("\n"), headings };
|
||||
}
|
||||
|
||||
function examplesFrom(readme: string, name: string): string {
|
||||
const examples = [...readme.matchAll(/```([^\n]*)\n([\s\S]*?)```/g)]
|
||||
.map((match) => ({ language: match[1]!.trim(), code: match[2]!.trim() }))
|
||||
.filter((example) => example.code)
|
||||
.filter(
|
||||
(example, index, all) => all.findIndex((value) => value.code === example.code) === index,
|
||||
)
|
||||
.slice(0, 4);
|
||||
if (!examples.length) {
|
||||
examples.push({
|
||||
language: "ts",
|
||||
code: `import * as packageApi from "@wrnexus/${name}";\n\nconsole.log(packageApi);`,
|
||||
});
|
||||
}
|
||||
return examples
|
||||
.map(
|
||||
(example, index) =>
|
||||
`<article class="example-card"><h3>Example ${index + 1}</h3><pre data-language="${escape(example.language || "text")}"><code>${escape(example.code)}</code></pre></article>`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function shell(title: string, description: string, content: string, state = ""): string {
|
||||
const document = `page ${title.replace(/[^A-Za-z0-9]/g, "")} {
|
||||
seo {
|
||||
title = "${title.replace(/"/g, "'")}"
|
||||
description = "${description.replace(/"/g, "'")}"
|
||||
}
|
||||
${state}
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WrNexus</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
${content}
|
||||
<footer>WrNexus 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
`;
|
||||
return document.replace(/\bWrNexus\b/g, "WRNexusJS");
|
||||
}
|
||||
|
||||
const cards = catalog
|
||||
.map(
|
||||
([
|
||||
name,
|
||||
category,
|
||||
summary,
|
||||
]) => `<a class="package-card" href="/packages/${name}" data-show="(category === 'All' ? true : category === '${category}') ? (query === '' ? true : '${name} ${category.toLowerCase()} ${summary.toLowerCase().replace(/'/g, "")} '.includes(query.toLowerCase())) : false">
|
||||
<span class="category">${category}</span><h2>@wrnexus/${name}</h2><p>${summary}</p><span class="card-link">Open documentation →</span>
|
||||
</a>`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const categories = ["All", ...new Set(catalog.map(([, category]) => category))];
|
||||
const categoryButtons = categories
|
||||
.map((category) => {
|
||||
const count =
|
||||
category === "All"
|
||||
? catalog.length
|
||||
: catalog.filter(([, value]) => value === category).length;
|
||||
return `<button type="button" @click="category = '${category}'">${category}<span>${count}</span></button>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
writeFileSync(
|
||||
join(pages, "packages.wrn"),
|
||||
shell(
|
||||
"Packages",
|
||||
"Explore every WrNexus package, API, function, and copy-ready usage example.",
|
||||
`<main class="page"><section class="hero compact"><span class="eyebrow">25 focused packages</span><h1>Package reference</h1><p>Everything in the framework, organized by responsibility and documented from the published 0.2.12 APIs.</p><input class="search" type="search" placeholder="Search packages, features, or categories…" @input="query = event.target.value" /></section><section class="category-filter" aria-label="Filter packages by category"><div class="category-row">${categoryButtons}</div><p>Showing <strong>{category}</strong> packages</p></section><section class="package-grid">${cards}</section></main>`,
|
||||
' state query = ""\n state category = "All"\n',
|
||||
),
|
||||
);
|
||||
|
||||
for (const [name, category, summary] of catalog) {
|
||||
const packageRoot = join(root, "node_modules", "@wrnexus", name);
|
||||
const readmePath = join(packageRoot, "README.md");
|
||||
const typesPath = join(packageRoot, "dist", "index.d.ts");
|
||||
if (!existsSync(readmePath) || !existsSync(typesPath))
|
||||
throw new Error(`Install @wrnexus/${name} before generating docs`);
|
||||
const readme = readFileSync(readmePath, "utf8").replace(/^#\s+[^\n]+\n?/, "");
|
||||
const types = readFileSync(typesPath, "utf8");
|
||||
const guide = markdown(readme);
|
||||
const toc = [
|
||||
{ id: "guide", title: "Guide", level: 2 },
|
||||
...guide.headings,
|
||||
{ id: "api", title: "Complete API", level: 2 },
|
||||
{ id: "examples", title: "Examples", level: 2 },
|
||||
]
|
||||
.map(
|
||||
(heading) =>
|
||||
`<a class="toc-level-${heading.level}" href="#${heading.id}">${escape(heading.title)}</a>`,
|
||||
)
|
||||
.join("");
|
||||
const content = `<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">${category}</span><h1>@wrnexus/${name}</h1><p>${summary}</p><code>bun add @wrnexus/${name}@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">${category}</span><h1>@wrnexus/${name}</h1><p>${summary}</p><pre><code>bun add @wrnexus/${name}@0.2.12</code></pre></section><section id="guide" class="prose">${guide.html}</section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>${escape(types)}</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid">${examplesFrom(readme, name)}</div></section></article>
|
||||
<aside class="on-this-page"><h2>On this page</h2><nav>${toc}</nav></aside>
|
||||
</main>`;
|
||||
writeFileSync(join(packagePages, `${name}.wrn`), shell(`@wrnexus/${name}`, summary, content));
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(pages, "index.wrn"),
|
||||
shell(
|
||||
"Home",
|
||||
"WrNexus documentation: build secure, server-rendered, reactive applications with Bun.",
|
||||
`<main class="page"><section class="hero"><span class="eyebrow">WrNexus 0.2.12</span><h1>Build from the server.<br><em>Ship only what matters.</em></h1><p>An SSR-first, Bun-native framework with reactive .wrn components, typed data, realtime rooms, mobile capabilities, and production security built in.</p><div class="actions"><a class="primary" href="/getting-started">Start building</a><a href="/packages">Explore 25 packages</a></div><div class="code-window"><span>app/pages/counter.wrn</span><pre><code>page Counter {
|
||||
state count = 0
|
||||
view {
|
||||
<button @click="count++">
|
||||
Count {count}
|
||||
</button>
|
||||
}
|
||||
}</code></pre></div></section><section class="feature-grid"><article><h2>SSR by default</h2><p>Useful HTML reaches the browser immediately. Interactive pages hydrate only the runtime they use.</p></article><article><h2>Secure foundations</h2><p>CSP, Trusted Types, CSRF, sessions, validation, authorization, encryption, and safe rendering are integrated.</p></article><article><h2>Web to native</h2><p>Share markup through Capacitor or compile portable pages into Expo and React Native routes.</p></article></section></main>`,
|
||||
),
|
||||
);
|
||||
|
||||
const guides = {
|
||||
"getting-started": [
|
||||
"Getting started",
|
||||
"Getting started with WrNexus",
|
||||
`<main class="page"><article class="documentation prose standalone"><span class="eyebrow">Guide</span><h1>Getting started</h1><p>Create a production-ready WrNexus application with Bun.</p><h2>1. Create the project</h2><pre><code>bunx @wrnexus/cli create my-app
|
||||
cd my-app
|
||||
bun install
|
||||
bun run dev</code></pre><h2>2. Add a page</h2><pre><code>page Dashboard {
|
||||
state count = 0
|
||||
view {
|
||||
<main>
|
||||
<h1>Dashboard</h1>
|
||||
<button @click="count++">{count}</button>
|
||||
</main>
|
||||
}
|
||||
}</code></pre><h2>3. Verify and build</h2><pre><code>wrnexus doctor
|
||||
wrnexus test
|
||||
wrnexus build</code></pre><h2>Where things live</h2><ul><li><code>app/pages</code> contains routes.</li><li><code>app/components</code> contains reusable .wrn components.</li><li><code>app/api</code> contains server API handlers.</li><li><code>app/layouts</code> contains shared shells.</li><li><code>app/middleware</code> contains request middleware.</li><li><code>wrnexus.config.ts</code> configures security, styles, data, mobile, and deployment.</li></ul></article></main>`,
|
||||
],
|
||||
language: [
|
||||
"Language and directives",
|
||||
"Complete WrNexus language reference for events, directives, loops, conditionals, data, components, forms, realtime, and native behavior.",
|
||||
`<main class="page"><article class="documentation prose standalone language-reference"><span class="eyebrow">Complete reference</span><h1>Language and directives</h1><p>This page documents the .wrn language and declarative browser features that span multiple packages.</p>
|
||||
<h2 id="file-anatomy">File anatomy</h2><p>A file declares a <code>page</code> or <code>component</code> and can contain metadata, props, state, data, view, style, server functions, APIs, and realtime handlers.</p><pre><code>${escape(`page Dashboard {
|
||||
layout = "default"
|
||||
seo { title = "Dashboard" }
|
||||
state count = 0
|
||||
view { <button @click="count++">{count}</button> }
|
||||
style { button { padding: 12px; } }
|
||||
}`)}</code></pre>
|
||||
<h2 id="state">State and interpolation</h2><p>State is scoped to the nearest generated <code>data-scope</code>. Text expressions update reactively after hydration.</p><pre><code>${escape(`state count = 0
|
||||
state user = { name: "Ada" }
|
||||
|
||||
view {
|
||||
<p>Count: {count}</p>
|
||||
<p>{user.name}</p>
|
||||
}`)}</code></pre>
|
||||
<h2 id="events">Events</h2><p>Any DOM event can use <code>@event="statement"</code>. The compiler emits <code>data-on-event</code>. The expression receives <code>event</code> and can mutate state.</p><div class="table-wrap"><table><thead><tr><th>Syntax</th><th>Purpose</th></tr></thead><tbody><tr><td><code>@click</code></td><td>Pointer or keyboard activation.</td></tr><tr><td><code>@input</code></td><td>Read live field values.</td></tr><tr><td><code>@change</code></td><td>React to committed field changes.</td></tr><tr><td><code>@submit</code></td><td>Handle form submission behavior.</td></tr><tr><td><code>@browser-click</code></td><td>Run only in a browser target.</td></tr><tr><td><code>@mobile-click</code></td><td>Run only in a native/mobile target.</td></tr></tbody></table></div><pre><code>${escape(`<input @input="name = event.target.value">
|
||||
<button @click="count++">Add</button>
|
||||
<form @submit="submitted = true">...</form>`)}</code></pre>
|
||||
<h2 id="directives">Reactive data attributes</h2><div class="table-wrap"><table><thead><tr><th>Directive</th><th>Behavior</th></tr></thead><tbody><tr><td><code>data-scope</code></td><td>Declares reactive state for a subtree.</td></tr><tr><td><code>data-text</code></td><td>Synchronizes textContent with an expression.</td></tr><tr><td><code>data-show</code></td><td>Shows or hides an element by truthiness.</td></tr><tr><td><code>data-for</code></td><td>Repeats an element for a client-side list.</td></tr><tr><td><code>data-on-<event></code></td><td>Compiled form of an event binding.</td></tr><tr><td><code>data-component</code></td><td>Mounts a server-rendered component.</td></tr><tr><td><code>data-slot</code></td><td>Fills a named component or layout slot.</td></tr><tr><td><code>data-wrnexus-csr</code></td><td>Connects generated client data fetching.</td></tr></tbody></table></div>
|
||||
<h2 id="conditionals">Conditional rendering</h2><h3>Server conditionals</h3><p>Server blocks render only the selected branch into the response.</p><pre><code>${escape(`{#if user.isAdmin}
|
||||
<a href="/admin">Admin</a>
|
||||
{:else if user}
|
||||
<p>Welcome {user.name}</p>
|
||||
{:else}
|
||||
<a href="/login">Sign in</a>
|
||||
{/if}`)}</code></pre><h3>Client visibility</h3><pre><code>${escape(`<section data-show="open">Visible while open is true</section>`)}</code></pre>
|
||||
<h2 id="loops">Loops and lists</h2><h3>Server each block</h3><pre><code>${escape(`{#each users as user, i}
|
||||
<p>{i + 1}. {user.name}</p>
|
||||
{:empty}
|
||||
<p>No users</p>
|
||||
{/each}`)}</code></pre><h3>Reactive client loop</h3><pre><code>${escape(`<li data-for="item, i in items">
|
||||
<span data-text="item.name"></span>
|
||||
<button data-on-click="items = items.filter(x => x !== item)">Remove</button>
|
||||
</li>`)}</code></pre>
|
||||
<h2 id="components">Components, props, and slots</h2><pre><code>${escape(`component Card {
|
||||
props { title = "Card" }
|
||||
view {
|
||||
<article><h2>{title}</h2><slot></slot></article>
|
||||
}
|
||||
}
|
||||
|
||||
<div data-component="card" title="Profile">
|
||||
<p>Card content</p>
|
||||
</div>`)}</code></pre>
|
||||
<h2 id="data">Server and client data</h2><p>Use named data bindings for SSR data or client hydration. Secrets and database work stay on the server.</p><pre><code>${escape(`data users {
|
||||
ssr GET "/api/users"
|
||||
}
|
||||
|
||||
view {
|
||||
{#each users as user}<p>{user.name}</p>{/each}
|
||||
}`)}</code></pre>
|
||||
<h2 id="forms">Forms and validation</h2><p>Schema-backed forms validate in the browser and on the server with the same descriptor.</p><pre><code>${escape(`<form data-schema="login" method="post" action="/api/login" data-redirect="/dashboard">
|
||||
<input name="email" type="email">
|
||||
<span data-error="email"></span>
|
||||
<button>Sign in</button>
|
||||
<p data-success="Signed in" hidden></p>
|
||||
</form>`)}</code></pre>
|
||||
<h2 id="i18n">Internationalization and themes</h2><pre><code>${escape(`<h1>{t:home.title}</h1>
|
||||
<button data-wire-lang-set="fr">Français</button>
|
||||
<button data-wire-theme-toggle>Toggle theme</button>
|
||||
<button data-wire-theme-set="dark">Dark</button>`)}</code></pre>
|
||||
<h2 id="realtime">Realtime rooms</h2><pre><code>${escape(`<div data-room="chat" data-room-user="Ada">
|
||||
<span data-room-status></span>
|
||||
<div data-room-log></div>
|
||||
<template data-room-item="message"><p>%user%: %text%</p></template>
|
||||
<form data-room-send><input name="text" data-room-reset></form>
|
||||
</div>`)}</code></pre>
|
||||
<h2 id="native">Browser and native directives</h2><pre><code>${escape(`<button data-native-browser="share" data-native-mobile="share"
|
||||
data-native-options='{"title":"WrNexus"}'>Share</button>
|
||||
<nav data-native-only="mobile">Mobile navigation</nav>
|
||||
<p data-native-only="browser">Browser instructions</p>
|
||||
<button data-native-requires="haptics">Haptic action</button>`)}</code></pre>
|
||||
<h2 id="other">Other framework attributes</h2><div class="table-wrap"><table><thead><tr><th>Attribute</th><th>Purpose</th></tr></thead><tbody><tr><td><code>data-error</code></td><td>Field validation error destination.</td></tr><tr><td><code>data-success</code></td><td>Successful form message.</td></tr><tr><td><code>data-redirect</code></td><td>Navigation after form success.</td></tr><tr><td><code>data-room-*</code></td><td>Realtime status, templates, sending, and reset behavior.</td></tr><tr><td><code>data-uploader</code></td><td>Config-driven upload widget.</td></tr><tr><td><code>data-wire-theme-*</code></td><td>Theme selection and toggling.</td></tr><tr><td><code>data-wire-lang*</code></td><td>Language selection.</td></tr><tr><td><code>data-native-*</code></td><td>Cross-platform capability and visibility behavior.</td></tr></tbody></table></div></article></main>`,
|
||||
],
|
||||
architecture: [
|
||||
"Architecture",
|
||||
"Understand the WrNexus SSR, compiler, runtime, and package architecture.",
|
||||
`<main class="page"><article class="documentation prose standalone"><span class="eyebrow">Concepts</span><h1>Architecture</h1><p>WrNexus separates server work, generated markup, and browser behavior so applications stay understandable and efficient.</p><h2>Request path</h2><pre><code>Request → Router → Middleware → Page/API → SSR document → Browser runtime</code></pre><h2>Compiler</h2><p>The compiler parses .wrn files and lowers state, events, interpolation, loops, conditionals, data bindings, components, and styles into server modules and small declarative browser directives.</p><h2>Runtime</h2><p>The server owns routing, data, secrets, sessions, validation, uploads, and rendering. The browser owns reactive scopes, navigation, forms, realtime clients, and native capability dispatch.</p><h2>Package boundaries</h2><p>Each package is independently installable. Start with the CLI and core, then add database, security, realtime, native, UI, and operational packages as required.</p><div class="actions"><a class="primary" href="/packages/core">Read core API</a><a href="/packages/compiler">Read compiler API</a></div></article></main>`,
|
||||
],
|
||||
} as const;
|
||||
for (const [route, [title, description, content]] of Object.entries(guides)) {
|
||||
writeFileSync(join(pages, `${route}.wrn`), shell(title, description, content));
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Generated ${catalog.length} package pages and ${Object.keys(guides).length + 2} site pages.`,
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"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",
|
||||
"wrnexus.config.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/dist",
|
||||
"**/.wrnexus"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
const config: AppConfig = {
|
||||
// Compatibility bridge for the currently published runtime. It can be
|
||||
// removed after the navigation-aware theme runtime is released.
|
||||
head: '<script src="/theme-navigation.js" defer></script>',
|
||||
|
||||
mobile: {
|
||||
enabled: true,
|
||||
appId: "com.example.wrnexusjs",
|
||||
appName: "WRNexusJS",
|
||||
userAgent: "WRNexusJSMobile",
|
||||
backgroundColor: "#0f172a",
|
||||
// layout: "mobile", // app/layouts/mobile.wrn
|
||||
// icon: "resources/icon.png",
|
||||
},
|
||||
|
||||
// PWA support is enabled automatically. Override any install metadata here.
|
||||
pwa: {
|
||||
name: "WRNexusJS Documentation",
|
||||
shortName: "WRNexusJS Docs",
|
||||
display: "standalone",
|
||||
themeColor: "#6366f1",
|
||||
backgroundColor: "#0f172a",
|
||||
},
|
||||
|
||||
seo: {
|
||||
title: "WRNexusJS Documentation",
|
||||
titleTemplate: "%s | WRNexusJS",
|
||||
description: "Complete documentation for every WRNexusJS package, API, function, and workflow.",
|
||||
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, mode }) => {
|
||||
const args = ["@tailwindcss/cli", "-i", entryPath!];
|
||||
if (mode === "production") args.push("--minify");
|
||||
return await Bun.$`bunx ${args}`.text();
|
||||
},
|
||||
},
|
||||
|
||||
fonts: {
|
||||
sans: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif',
|
||||
google: [{ family: "Plus Jakarta Sans", weights: [400, 500, 600, 700, 800] }],
|
||||
},
|
||||
|
||||
// Fonts — the framework emits optimized <head> markup (preconnect, subsetted
|
||||
// Google Fonts with font-display, self-hosted @font-face with preload) and
|
||||
// auto-extends the CSP for Google Fonts. Uncomment to use a custom font:
|
||||
//
|
||||
// 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: "Plus Jakarta Sans", src: "/fonts/jakarta.woff2", weight: "100 900", preload: true }],
|
||||
// },
|
||||
|
||||
// security: {
|
||||
// cors: {
|
||||
// enabled: true,
|
||||
// origin: ["http://localhost:5173"],
|
||||
// },
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user