Files
WRNexusJS/packages/dev-server
Clintchiz dd9b7a289a
Quality / quality (ubuntu-latest) (push) Failing after 9m48s
Quality / quality (windows-latest) (push) Canceled after 0s
fix(theme): persist switcher accent for SSR
2026-08-12 21:17:20 +05:30
..

@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 wrn 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, connects 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. 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.

getWrnCompileMetrics() exposes cumulative content-addressed compiler cache hits, misses, successful compilations, errors, totalDurationMs, and lastDurationMs for the DevToolbar or custom diagnostics. Tests and embedded servers can call resetWrnCompileMetrics() to establish a fresh measurement window.

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 WrNexus 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. 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 @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;
}

Forward auth is a verification hook, not a login page. Configure forward.url with a dedicated endpoint such as http://sso.localhost:3000/api/verify. The gateway forwards the request's Cookie and Authorization headers plus X-Forwarded-Host, X-Forwarded-Proto, X-Original-Method, and X-Original-Uri (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.

For browser SSO, the verifier may return a 302/303/307/308 with a Location header pointing to its login page. The gateway passes that redirect to the browser. The login flow should validate a signed returnTo value before redirecting back; API clients should receive 401/403 instead of an HTML login redirect.

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 compile into a content-addressed hidden .wrnexus/ cache. Targeted invalidation gives changed modules a fresh import identity without restarting the development server.
  • 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.