745 lines
45 KiB
Plaintext
745 lines
45 KiB
Plaintext
page wrnexusdevserver {
|
|
seo {
|
|
title = "@wrnexus/dev-server"
|
|
description = "Development and production servers, HMR, assets, and gateways."
|
|
}
|
|
|
|
view {
|
|
<div class="docs-shell">
|
|
<SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
|
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
|
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.6.0</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
|
|
</header>
|
|
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
|
|
<main class="portal-main docs-layout">
|
|
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/dev-server</span></nav><section class="doc-intro"><span class="eyebrow">Runtime · Package reference</span><h1>@wrnexus/dev-server</h1><p>Development and production servers, HMR, assets, and gateways.</p><div class="doc-meta"><span>v0.6.0</span><span>Private registry</span><span>Runtime</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/dev-server@0.6.0</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide"><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>
|
|
<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. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.</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. In development, normal application edits are applied inside the existing child and sent through its existing HMR connection. The child supervisor remains as crash recovery rather than the normal update path. 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; // dev: "127.0.0.1"; production: "0.0.0.0"
|
|
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>Forward auth is a verification hook, not a login page. Configure <code>forward.url</code> with a dedicated endpoint such as <code>http://sso.localhost:3000/api/verify</code>. The gateway forwards the request's <code>Cookie</code> and <code>Authorization</code> headers plus <code>X-Forwarded-Host</code>, <code>X-Forwarded-Proto</code>, <code>X-Original-Method</code>, and <code>X-Original-Uri</code> (including its query string). The verifier must return 2xx only for an authenticated session and 401/403 otherwise. Pointing forward auth at an SSO home page that always returns 200 allows every request and does not implement SSO.</p>
|
|
<p>For browser SSO, the verifier may return a <code>302</code>/<code>303</code>/<code>307</code>/<code>308</code> with a <code>Location</code> header pointing to its login page. The gateway passes that redirect to the browser. The login flow should validate a signed <code>returnTo</code> value before redirecting back; API clients should receive <code>401</code>/<code>403</code> instead of an HTML login redirect.</p>
|
|
<p>Open the gateway URL (normally <code>http://127.0.0.1:3000</code>), not an app's internal port. The gateway exposes <code>/__gateway/health</code> (JSON list of routed apps) and returns a <code>RunningGateway</code> (<code>{ port, url, stop() }</code>). Use <code>--host=0.0.0.0</code> when other devices need to reach a development gateway.</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="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</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, ObservabilityConfig, TenancyConfig, NavigationConfig, StylesConfig, ThemeConfig } from '@wrnexus/styles';
|
|
import { ResolvedI18n, I18nConfig } from '@wrnexus/i18n';
|
|
import { StorageConfig } from '@wrnexus/uploader';
|
|
import { DevToolbarConfig, DevToolbarPlatformSnapshot, DevToolbarPanel } from '@wrnexus/dev-toolbar/types';
|
|
import { ClientRuntimeDefinition, PluginInput } from '@wrnexus/plugin';
|
|
import { DevToolbarCollector } from '@wrnexus/dev-toolbar/server';
|
|
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
|
|
/** Exit code a dev-server child uses to request a clean supervisor restart. */
|
|
declare const RESTART_EXIT_CODE = 97;
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* Page/component/API/middleware/realtime changes invalidate their modules and
|
|
* broadcast `reload` without closing the server or WebSocket. The browser asks
|
|
* the same process for fresh HTML and performs a soft DOM morph.
|
|
*/
|
|
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;
|
|
broadcastJson(message: unknown): 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;
|
|
/** Production build combined theme + UI stylesheet. */
|
|
hasFrameworkStyles?: boolean;
|
|
/** App stylesheet already contains theme + UI CSS and is the only CSS request needed. */
|
|
stylesIncludeFramework?: 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;
|
|
/** Package browser runtimes resolved by the plugin system. */
|
|
clientRuntimes?: ClientRuntimeDefinition[];
|
|
/** Page navigation strategy. `document` disables same-origin link interception. */
|
|
navigation?: {
|
|
mode?: "client" | "document";
|
|
};
|
|
/** 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;
|
|
/** Built-in request tracing and Server-Timing policy. */
|
|
observability?: ObservabilityConfig;
|
|
/** Built-in tenant identity resolution. */
|
|
tenancy?: TenancyConfig;
|
|
/** 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;
|
|
devToolbar?: {
|
|
config: DevToolbarConfig;
|
|
collector: DevToolbarCollector;
|
|
root: string;
|
|
platform?: DevToolbarPlatformSnapshot;
|
|
panels?: DevToolbarPanel[];
|
|
};
|
|
}
|
|
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).
|
|
*/
|
|
type GatewayForwardAuth = ({
|
|
url: string;
|
|
app?: never;
|
|
path?: never;
|
|
} | {
|
|
app: string;
|
|
path?: string;
|
|
url?: never;
|
|
}) & {
|
|
headers?: string[];
|
|
};
|
|
interface GatewayAuth {
|
|
basic?: {
|
|
user: string;
|
|
pass: string;
|
|
} | Array<{
|
|
user: string;
|
|
pass: string;
|
|
}>;
|
|
allowIps?: string[];
|
|
forward?: GatewayForwardAuth;
|
|
}
|
|
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[];
|
|
publicOrigin?: 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";
|
|
environment?: string;
|
|
hmr?: boolean;
|
|
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 ProductionPluginAsset {
|
|
path: string;
|
|
contentType: string;
|
|
immutable?: boolean;
|
|
}
|
|
interface ProdOptions {
|
|
/** Absolute path to the pre-built global stylesheet, if any. */
|
|
stylesPath?: string;
|
|
/** Small production stylesheet inlined into the document head. */
|
|
inlineStyles?: string;
|
|
/** `stylesPath` contains theme + UI + app CSS in cascade order. */
|
|
stylesIncludeFramework?: boolean;
|
|
/** 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;
|
|
/** Combined production theme + Wire UI stylesheet. */
|
|
frameworkCssPath?: 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;
|
|
/** Package browser runtimes already emitted by the production build. */
|
|
clientRuntimes?: ClientRuntimeDefinition[];
|
|
/** Public URL to emitted package asset metadata. */
|
|
pluginAssets?: Record<string, ProductionPluginAsset>;
|
|
/** 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;
|
|
/** Built-in request tracing and Server-Timing policy. */
|
|
observability?: ObservabilityConfig;
|
|
/** Built-in tenant identity resolution. */
|
|
tenancy?: TenancyConfig;
|
|
/** Page navigation strategy. */
|
|
navigation?: NavigationConfig;
|
|
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 and targeted cache invalidation keep page/component/API edits inside
|
|
* the running process while the HMR socket morphs fresh HTML into the browser.
|
|
*/
|
|
|
|
interface ServeOptions {
|
|
appDir: string;
|
|
port?: number;
|
|
hostname?: string;
|
|
mode?: Mode;
|
|
/** Inject the live-reload client (defaults to true in development). */
|
|
hmr?: boolean;
|
|
appConfig?: Record<string, unknown>;
|
|
/** 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;
|
|
devToolbar?: boolean | DevToolbarConfig;
|
|
plugins?: PluginInput;
|
|
observability?: ObservabilityConfig;
|
|
tenancy?: TenancyConfig;
|
|
navigation?: NavigationConfig;
|
|
}
|
|
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="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid"><article class="example-card"><h3>Programmatic dev server</h3><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></article><article class="example-card"><h3>Production server from a build manifest</h3><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></article><article class="example-card"><h3>Embedding the handler on node:http</h3><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></article><article class="example-card"><h3>Multi-app gateway</h3><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></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="#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><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS 0.6.0</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
|
|
<BackToTop />
|
|
</div>
|
|
}
|
|
}
|