Files
WRNexusJSDoc/app/pages/packages/dev-server.wrn
T
2026-07-12 16:14:06 +05:30

728 lines
42 KiB
Plaintext

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&lt;RunningServer&gt;</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&lt;RunningGateway&gt;</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 &#123;
appDir: string; // absolute/relative path to the app/ dir
port?: number; // default 3000
hostname?: string; // default &quot;localhost&quot;
mode?: Mode; // &quot;development&quot; | &quot;production&quot;; default &quot;development&quot;
hmr?: boolean; // inject live-reload client; default (mode === &quot;development&quot;)
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 &lt;head&gt;
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?: &#123; driver: string; url: string &#125;; // default db → getDb(); dev auto-migrates
databases?: Record&lt;string, &#123; driver: string; url: string &#125;&gt;; // named dbs → getDb(&quot;&lt;name&gt;&quot;)
realtime?: &#123; scale?: boolean; redisUrl?: string &#125;; // bridge rooms over Redis across processes
&#125;
interface RunningServer &#123;
port: number;
hostname: string;
url: string;
router: Router;
stop(): void;
&#125;</code></pre>
<p>In development, <code>startServer</code> also connects <code>app/db/migrations</code> (and <code>app/db/&lt;name&gt;/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 &#123;
mode: Mode;
hmr: boolean; // inject the live-reload client into pages
router: Router;
loadModule(file: string): Promise&lt;Record&lt;string, unknown&gt;&gt;;
getMiddleware(): Promise&lt;Middleware[]&gt;;
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 + &lt;html data-theme&gt;
i18n?: ResolvedI18n; // enables ctx.t, &lt;html lang&gt;, &#123;t:key&#125; markers
inlineStyles?: string; // inline small prod stylesheets into &lt;head&gt;
assetVersion?: string; // cache-busting ?v= on framework asset URLs
head?: string; // raw HTML appended to every page &lt;head&gt;
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)
&#125;
interface Handlers &#123;
fetch(req: Request, server: UpgradeServer): Promise&lt;Response | undefined&gt;;
websocket: &#123; open; message; close; drain &#125;;
&#125;</code></pre>
<p><code>WsData</code> is the per-connection socket tag — a discriminated union of <code>&#123; kind: &quot;realtime&quot;; handler &#125;</code>, <code>&#123; kind: &quot;room&quot;; meta &#125;</code>, or <code>&#123; kind: &quot;hmr&quot; &#125;</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 &#123;
pages: &#123; raw: string; mod: RouteModule &#125;[];
api: &#123; raw: string; mod: RouteModule &#125;[];
realtime: &#123; raw: string; mod: RouteModule &#125;[];
middleware: Middleware[];
components: &#123; name: string; mod: RouteModule &#125;[];
layouts: &#123; name: string; mod: RouteModule &#125;[];
&#125;
interface ProdOptions &#123;
stylesPath?: string;
inlineStyles?: string;
reactivePath?: string;
themePath?: string;
themeJsPath?: string;
theme?: ResolvedTheme;
uiCssPath?: string;
schemasJs?: string;
i18n?: ResolvedI18n;
db?: &#123; driver: string; url: string &#125;;
databases?: Record&lt;string, &#123; driver: string; url: string &#125;&gt;;
realtime?: &#123; scale?: boolean; redisUrl?: string &#125;;
assetVersion?: string;
publicDir?: string;
head?: string;
seo?: SeoConfig;
security?: SecurityConfig;
port?: number;
hostname?: string;
maxBodyBytes?: number;
&#125;</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 &#123;
port?: number; // default 3000
hostname?: string; // default &quot;localhost&quot;
mode?: &quot;development&quot; | &quot;production&quot;;
apps: GatewayApp[];
security?: GatewaySecurity;
&#125;
interface GatewayApp &#123;
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
&#125;
interface GatewayAuth &#123;
basic?: &#123; user: string; pass: string &#125; | Array&lt;&#123; user: string; pass: string &#125;&gt;;
allowIps?: string[]; // exact-match IP allowlist
forward?: &#123; url: string &#125;; // forward-auth (SSO): 2xx allows
&#125;
interface GatewaySecurity &#123;
trustedHostsOnly?: boolean; // 404 unknown hosts instead of first app
rateLimit?: &#123; max: number; windowMs?: number &#125;; // global by client IP (429)
headers?: boolean; // add baseline edge security headers
forwardedHeaders?: boolean; // set X-Forwarded-* (default true)
accessLog?: boolean;
&#125;</code></pre>
<p>The gateway exposes <code>/__gateway/health</code> (JSON list of routed apps) and returns a <code>RunningGateway</code> (<code>&#123; port, url, stop() &#125;</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) =&gt; Response | undefined | Promise&lt;Response | undefined&gt;;
toRequest(req: IncomingMessage, opts?): Promise&lt;Request&gt;
writeResponse(res: ServerResponse, response: Response): Promise&lt;void&gt; // preserves multiple Set-Cookie
nodeListener(handler: FetchHandler, opts?): (req, res) =&gt; Promise&lt;void&gt;
serveNode(handler: FetchHandler, opts?): Promise&lt;Server&gt;</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 &lt;appDir&gt; &lt;port&gt; &lt;mode&gt;</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(&quot;@wrnexus/dev-server/serve-entry&quot;)</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 &#123; startServer &#125; from &quot;@wrnexus/dev-server&quot;;
const server = await startServer(&#123;
appDir: &quot;./app&quot;,
port: 3000,
mode: &quot;development&quot;,
theme: &#123;/* design tokens */&#125;,
db: &#123; driver: &quot;sqlite&quot;, url: &quot;file:./data/app.db&quot; &#125;,
&#125;);
console.log(`Running at $&#123;server.url&#125;`);
// 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 &#123; createProductionServer &#125; from &quot;@wrnexus/dev-server&quot;;
import &#123; manifest &#125; from &quot;./dist/manifest.js&quot;; // generated by `wrnexus build`
createProductionServer(manifest, &#123;
stylesPath: &quot;./dist/styles.css&quot;,
reactivePath: &quot;./dist/reactive.js&quot;,
assetVersion: process.env.BUILD_ID,
db: &#123; driver: &quot;postgres&quot;, url: process.env.DATABASE_URL! &#125;,
port: Number(process.env.PORT) || 3000,
&#125;);</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 &#123; createProductionHandlers, serveNode &#125; from &quot;@wrnexus/dev-server&quot;;
const handlers = createProductionHandlers(manifest, opts);
await serveNode(handlers.fetch, &#123; port: 8080 &#125;);</code></pre>
<h4 id="multi-app-gateway">Multi-app gateway</h4>
<pre data-language="ts"><code>import &#123; startGateway &#125; from &quot;@wrnexus/dev-server&quot;;
await startGateway(&#123;
port: 3000,
apps: [
&#123; name: &quot;web&quot;, dir: &quot;./apps/web&quot;, domains: [&quot;localhost&quot;, &quot;web.localhost&quot;] &#125;,
&#123;
name: &quot;admin&quot;,
dir: &quot;./apps/admin&quot;,
domains: [&quot;admin.localhost&quot;],
auth: &#123; basic: &#123; user: &quot;root&quot;, pass: &quot;s3cret&quot; &#125; &#125;,
&#125;,
],
security: &#123; trustedHostsOnly: true, rateLimit: &#123; max: 600 &#125; &#125;,
&#125;);</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>&lt;/content&gt;</p>
</ul>
<p>&lt;/invoke&gt;</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 &#123; Mode, Middleware, SeoConfig, SecurityConfig, RealtimeBus, RealtimeConnectMeta &#125; from '@wrnexus/core';
import &#123; Router &#125; from '@wrnexus/router';
import &#123; ResolvedTheme, MobileConfig, PwaConfig, StylesConfig, ThemeConfig &#125; from '@wrnexus/styles';
import &#123; ResolvedI18n, I18nConfig &#125; from '@wrnexus/i18n';
import &#123; StorageConfig &#125; from '@wrnexus/uploader';
import &#123; IncomingMessage, ServerResponse, Server &#125; 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:
*
* &#123; type: &quot;css&quot; &#125; -&gt; the browser hot-swaps the stylesheet (no reload)
* &#123; type: &quot;reload&quot; &#125; -&gt; 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 = &#123;
type: &quot;css&quot;;
version: number;
&#125; | &#123;
type: &quot;reload&quot;;
version: number;
&#125;;
/** Minimal shape of a Bun ServerWebSocket we rely on. */
interface Socket &#123;
send(data: string): unknown;
&#125;
declare class HmrHub &#123;
private sockets;
private version;
add(ws: Socket): void;
remove(ws: Socket): void;
broadcast(message: HmrMessage): void;
get size(): number;
css(): void;
reload(): void;
&#125;
/**
* 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&lt;string, (...args: any[]) =&gt; unknown&gt;;
/**
* Per-connection socket data. A socket is either an app realtime connection or
* an internal HMR connection — discriminated by `kind`.
*/
type WsData = &#123;
kind: &quot;realtime&quot;;
handler: WsHandler;
&#125; | &#123;
kind: &quot;room&quot;;
meta: RealtimeConnectMeta;
&#125; | &#123;
kind: &quot;hmr&quot;;
baseUrl: string;
headers: [string, string][];
&#125;;
type RouteModule$1 = Record&lt;string, unknown&gt;;
/** Serves framework-owned assets under `/__wrnexus/*` (islands, reactive, hmr). */
interface AssetServer &#123;
serve(pathname: string): Promise&lt;Response | null&gt;;
&#125;
interface RuntimeDeps &#123;
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&lt;RouteModule$1&gt;;
/** Resolve the ordered middleware chain. */
getMiddleware(): Promise&lt;Middleware[]&gt;;
/** 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` + `&lt;html data-theme&gt;`. */
theme?: ResolvedTheme;
/** Resolved i18n bundle: enables `ctx.t`, `&lt;html lang&gt;`, and `&#123;t:key&#125;` 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;
&#125;
interface UpgradeServer &#123;
upgrade(req: Request, opts: &#123;
data: WsData;
&#125;): boolean;
/** Bun's per-request socket peer address (used for the non-spoofable client IP). */
requestIP?(req: Request): &#123;
address: string;
&#125; | null;
&#125;
/** The subset of Bun's ServerWebSocket the runtime touches. */
interface Ws &#123;
data: WsData;
send(data: string | Uint8Array): unknown;
close(code?: number, reason?: string): void;
&#125;
interface Handlers &#123;
fetch(req: Request, server: UpgradeServer): Promise&lt;Response | undefined&gt;;
websocket: &#123;
open(ws: Ws): void;
message(ws: Ws, message: string | Uint8Array): void;
close(ws: Ws, code?: number, reason?: string): void;
drain(ws: Ws): void;
&#125;;
&#125;
/** 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 &#123;
/** HTTP Basic auth — one or more allowed user/password pairs. */
basic?: &#123;
user: string;
pass: string;
&#125; | Array&lt;&#123;
user: string;
pass: string;
&#125;&gt;;
/** 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?: &#123;
url: string;
&#125;;
&#125;
interface GatewayApp &#123;
/** 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. [&quot;localhost&quot;, &quot;web.localhost&quot;]). */
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;
&#125;
/** Gateway-wide security controls, enforced for every app. */
interface GatewaySecurity &#123;
/** 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?: &#123;
max: number;
windowMs?: number;
&#125;;
/** 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;
&#125;
interface GatewayOptions &#123;
port?: number;
hostname?: string;
mode?: &quot;development&quot; | &quot;production&quot;;
apps: GatewayApp[];
security?: GatewaySecurity;
&#125;
interface RunningGateway &#123;
port: number;
url: string;
stop(): void;
&#125;
/** Boot every app as a child process, then route by Host on one gateway port. */
declare function startGateway(opts: GatewayOptions): Promise&lt;RunningGateway&gt;;
/**
* @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&lt;string, unknown&gt;;
interface ManifestRoute &#123;
/** URL pattern, e.g. `/users/[id]`. */
raw: string;
/** The statically-imported route module. */
mod: RouteModule;
&#125;
interface ProdManifest &#123;
pages: ManifestRoute[];
api: ManifestRoute[];
realtime: ManifestRoute[];
middleware: Middleware[];
/** Server-rendered components, statically imported and keyed by name. */
components: &#123;
name: string;
mod: RouteModule;
&#125;[];
/** Named page layouts (from app/layouts/*.wrn). */
layouts: &#123;
name: string;
mod: RouteModule;
&#125;[];
&#125;
interface ProdOptions &#123;
/** 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 `&lt;html data-theme&gt;` + `theme.css` link. */
theme?: ResolvedTheme;
/** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
uiCssPath?: string;
/** Pre-built `window.__wireSchemas = &#123;...&#125;` script for client validation. */
schemasJs?: string;
/** Resolved i18n bundle (default lang + locale messages). */
i18n?: ResolvedI18n;
/** Default database connection (driver + url); enables `getDb()`. */
db?: &#123;
driver: string;
url: string;
&#125;;
/** Named databases, reached with `getDb(&quot;&lt;name&gt;&quot;)`. */
databases?: Record&lt;string, &#123;
driver: string;
url: string;
&#125;&gt;;
/**
* 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/&lt;name&gt;/migrations`). */
databaseMigrationDirs?: Record&lt;string, string&gt;;
/**
* 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?: &#123;
scale?: boolean;
redisUrl?: string;
&#125;;
/** 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;
&#125;
/**
* Build the portable request handler from a precompiled manifest — a
* WinterCG-style `fetch(request) =&gt; 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&lt;typeof createHandlers&gt;;
/** Start the production server on Bun from a precompiled manifest. */
declare function createProductionServer(manifest: ProdManifest, opts: ProdOptions): Promise&lt;Bun.Server&lt;WsData&gt;&gt;;
/**
* node:http adapter — bridge a WinterCG `fetch(request) =&gt; 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) =&gt; Response | undefined | Promise&lt;Response | undefined&gt;;
/** Convert a Node IncomingMessage into a web Request (buffers the body). */
declare function toRequest(req: IncomingMessage, opts?: &#123;
origin?: string;
&#125;): Promise&lt;Request&gt;;
/** Write a web Response into a Node ServerResponse. */
declare function writeResponse(res: ServerResponse, response: Response): Promise&lt;void&gt;;
/** A `node:http` request listener that dispatches to a fetch handler. */
declare function nodeListener(handler: FetchHandler, opts?: &#123;
origin?: string;
&#125;): (req: IncomingMessage, res: ServerResponse) =&gt; Promise&lt;void&gt;;
/** Create and start a `node:http` server for a fetch handler. */
declare function serveNode(handler: FetchHandler, opts?: &#123;
port?: number;
hostname?: string;
&#125;): Promise&lt;Server&gt;;
/**
* @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 &#123;
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?: &#123;
driver: string;
url: string;
&#125;;
/** Named databases, reached with `getDb(&quot;&lt;name&gt;&quot;)`; migrations under app/db/&lt;name&gt;/. */
databases?: Record&lt;string, &#123;
driver: string;
url: string;
&#125;&gt;;
/** Realtime scaling: bridge room broadcasts over Redis across app processes. */
realtime?: &#123;
scale?: boolean;
redisUrl?: string;
&#125;;
/** File-upload storage: named stores (local dir / S3), reached with `getStore()`. */
storage?: StorageConfig;
mobile?: MobileConfig;
pwa?: PwaConfig | false;
&#125;
interface RunningServer &#123;
port: number;
hostname: string;
url: string;
router: Router;
stop(): void;
&#125;
declare function startServer(opts: ServeOptions): Promise&lt;RunningServer&gt;;
export &#123; 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 &#125;;
</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 &#123;
appDir: string; // absolute/relative path to the app/ dir
port?: number; // default 3000
hostname?: string; // default &quot;localhost&quot;
mode?: Mode; // &quot;development&quot; | &quot;production&quot;; default &quot;development&quot;
hmr?: boolean; // inject live-reload client; default (mode === &quot;development&quot;)
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 &lt;head&gt;
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?: &#123; driver: string; url: string &#125;; // default db → getDb(); dev auto-migrates
databases?: Record&lt;string, &#123; driver: string; url: string &#125;&gt;; // named dbs → getDb(&quot;&lt;name&gt;&quot;)
realtime?: &#123; scale?: boolean; redisUrl?: string &#125;; // bridge rooms over Redis across processes
&#125;
interface RunningServer &#123;
port: number;
hostname: string;
url: string;
router: Router;
stop(): void;
&#125;</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>interface RuntimeDeps &#123;
mode: Mode;
hmr: boolean; // inject the live-reload client into pages
router: Router;
loadModule(file: string): Promise&lt;Record&lt;string, unknown&gt;&gt;;
getMiddleware(): Promise&lt;Middleware[]&gt;;
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 + &lt;html data-theme&gt;
i18n?: ResolvedI18n; // enables ctx.t, &lt;html lang&gt;, &#123;t:key&#125; markers
inlineStyles?: string; // inline small prod stylesheets into &lt;head&gt;
assetVersion?: string; // cache-busting ?v= on framework asset URLs
head?: string; // raw HTML appended to every page &lt;head&gt;
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)
&#125;
interface Handlers &#123;
fetch(req: Request, server: UpgradeServer): Promise&lt;Response | undefined&gt;;
websocket: &#123; open; message; close; drain &#125;;
&#125;</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>interface ProdManifest &#123;
pages: &#123; raw: string; mod: RouteModule &#125;[];
api: &#123; raw: string; mod: RouteModule &#125;[];
realtime: &#123; raw: string; mod: RouteModule &#125;[];
middleware: Middleware[];
components: &#123; name: string; mod: RouteModule &#125;[];
layouts: &#123; name: string; mod: RouteModule &#125;[];
&#125;
interface ProdOptions &#123;
stylesPath?: string;
inlineStyles?: string;
reactivePath?: string;
themePath?: string;
themeJsPath?: string;
theme?: ResolvedTheme;
uiCssPath?: string;
schemasJs?: string;
i18n?: ResolvedI18n;
db?: &#123; driver: string; url: string &#125;;
databases?: Record&lt;string, &#123; driver: string; url: string &#125;&gt;;
realtime?: &#123; scale?: boolean; redisUrl?: string &#125;;
assetVersion?: string;
publicDir?: string;
head?: string;
seo?: SeoConfig;
security?: SecurityConfig;
port?: number;
hostname?: string;
maxBodyBytes?: number;
&#125;</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>
}
}