Files
WRNexusJS/packages/dev-server/README.md
T

15 KiB

@wrnexus/dev-server

The WrNexus HTTP + WebSocket server runtime — request dispatch, SSR document assembly, live-reload (HMR), and the portable production handler.

Part of the WrNexus framework — an SSR-first, Bun-native full-stack web framework.

Overview

This package is the server runtime that powers a WrNexus app in both development and production. A single request runtime (createHandlers) 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 gateway (route several apps by Host header behind one port) and a portable node:http adapter for WinterCG hosts. It is entirely server-side and Bun-native (Bun.serve, Bun.file, Bun.gzipSync).

Installation

bun add @wrnexus/dev-server

Private package — the machine must be authenticated to the wrnexus npm org (a read token in ~/.npmrc). Requires Bun (Node is not supported for the full server; the node:http adapter is for WinterCG embedding only).

API

Main entry (@wrnexus/dev-server)

Export Kind Purpose
startServer(opts: ServeOptions) Promise<RunningServer> Start the dev server on Bun.serve: builds the router, connects/migrates databases, wires assets + HMR, and starts the file watcher.
createHandlers(deps: RuntimeDeps) Handlers The shared request runtime (fetch + websocket handlers). Re-exported from runtime.ts.
createProductionServer(manifest, opts) Bun.Server Start the production server from a precompiled manifest.
createProductionHandlers(manifest, opts) Handlers Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam).
startGateway(opts: GatewayOptions) Promise<RunningGateway> Boot multiple apps as child processes and route by Host.
toRequest, writeResponse, nodeListener, serveNode functions node:http ↔ WinterCG Request/Response adapter.
RESTART_EXIT_CODE number (97) Exit code the dev child uses to ask the supervisor for a fresh process.
STYLES_HREF, HMR_CLIENT_JS constants The global stylesheet URL and the inline HMR client script.

Exported types: ServeOptions, RunningServer, RuntimeDeps, AssetServer, WsData, GatewayApp, GatewayOptions, GatewayAuth, GatewaySecurity, RunningGateway, FetchHandler.

startServer(opts)

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;
}

In development, startServer also connects app/db/migrations (and app/db/<name>/migrations) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live; any other server change triggers process.exit(RESTART_EXIT_CODE) so the dev supervisor (@wrnexus/cli) respawns the process with fresh modules.

createHandlers(deps)

The core runtime shared by dev and prod. It handles CORS preflight, /healthz and /__wrnexus/health, request-body size limits (413), HMR socket upgrades (/__wrnexus/hmr), realtime WebSocket upgrades (defineRoom default export or a raw websocket export), the middleware pipeline, API routes (/api/*), framework assets (/__wrnexus/*), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).

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 };
}

WsData is the per-connection socket tag — a discriminated union of { kind: "realtime"; handler }, { kind: "room"; meta }, or { kind: "hmr" }.

createProductionServer(manifest, opts) / createProductionHandlers(manifest, opts)

Production runs the same request runtime as dev, but with no filesystem scan and no runtime bundling. wrnexus build emits an entry that statically imports every route/component/layout module and passes them as a ProdManifest; the route-matching tables are rebuilt from the raw patterns.

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;
}

createProductionServer also loads the .env cascade for the production profile, installs SIGTERM/SIGINT graceful shutdown, and binds 0.0.0.0 (port from opts.port or $PORT, default 3000). Migrations are not run here — apply them first (wrnexus db migrate). createProductionHandlers returns the bare handlers for edge/serverless/node:http deployment.

startGateway(opts) — multi-app gateway

Serves several apps behind one port and routes each request to the right app by its Host 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 @wrnexus/pubsub (use the Redis driver so messages cross processes).

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;
}

Open the gateway URL (normally http://127.0.0.1:3000), not an app's internal port. The gateway exposes /__gateway/health (JSON list of routed apps) and returns a RunningGateway ({ port, url, stop() }). Use --host=0.0.0.0 when other devices need to reach a development gateway.

node:http adapter (from ./adapters/node.ts)

For embedding the WinterCG handler behind an existing Node server or a WinterCG host. Note the full app still needs Bun-compatible globals (Bun.file, bun:sqlite, etc.); only the Request/Response conversion is fully portable.

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>

Subpath export: @wrnexus/dev-server/serve-entry

The child process the dev supervisor launches:

bun run serve-entry.ts <appDir> <port> <mode>

It loads the optional wrnexus.config.ts, resolves the style entry, calls startServer, 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. startGateway resolves this entry via import.meta.resolve("@wrnexus/dev-server/serve-entry") to spawn each dev app.

Usage

Programmatic dev server

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();

Production server from a build manifest

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,
});

Embedding the handler on node:http

import { createProductionHandlers, serveNode } from "@wrnexus/dev-server";

const handlers = createProductionHandlers(manifest, opts);
await serveNode(handlers.fetch, { port: 8080 });

Multi-app gateway

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 } },
});

Framework asset routes

The runtime serves these framework-owned paths (dev builds them live; prod serves pre-built/immutable versions):

  • /__wrnexus/nav.js, /__wrnexus/reactive.js, /__wrnexus/realtime.js — client runtimes
  • /__wrnexus/validate.js, /__wrnexus/schemas.js, /__wrnexus/i18n.js — validation + i18n runtimes
  • /__wrnexus/theme.css, /__wrnexus/theme.js, /__wrnexus/ui.css, /__wrnexus/styles.css — styles
  • /__wrnexus/hmr — dev-only HMR WebSocket
  • /__wrnexus/csr — server-evaluated CSR bindings for browser-side API fetches

Pages get only the scripts they use: nav.js always, reactive.js when a page has a data-scope/CSR fetch, plus theme/validation/i18n/realtime runtimes when the relevant markup is present.

Requirements / Notes

  • Bun-only. Uses Bun.serve (HTTP + WebSocket), Bun.file, and Bun.gzipSync. The full app also relies on bun:sqlite / Bun.SQL via @wrnexus/db.
  • Orchestrates the whole framework: @wrnexus/core (context, security, realtime registry), @wrnexus/router, @wrnexus/ssr (renderDocument), @wrnexus/csr (client runtimes), @wrnexus/compiler (.wrn → TS), @wrnexus/styles, @wrnexus/ui, @wrnexus/validation, @wrnexus/i18n, @wrnexus/db, and @wrnexus/pubsub (Redis-backed cross-process realtime).
  • .wrn files are compiled to TypeScript into a hidden sibling .wrnexus/ cache dir and dynamically imported; the module cache means each edited server module needs a fresh process (dev) — hence the restart-on-change model.
  • Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via Cache-Control: no-transform.