/** * @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. Normal preview/deploy output has no live-reload client; the * supervised `dev --production-runtime` mode can explicitly enable it. */ import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core"; import { compileRoutePattern, matchRoute, sortRoutes, type Route, type Router, } from "@wrnexus/router"; import { getActionRuntime, getReactiveRuntime, getNavRuntime, getRealtimeRuntime, } from "@wrnexus/csr"; import { loadEnv, resolveProfile, type ResolvedTheme, type MobileConfig, type PwaConfig, type ObservabilityConfig, type TenancyConfig, type NavigationConfig, } from "@wrnexus/styles"; import { VALIDATE_RUNTIME } from "@wrnexus/validation"; import { I18N_RUNTIME, type ResolvedI18n } from "@wrnexus/i18n"; import { setDb, registerLazyDb, getDb, hasDb, migrate } from "@wrnexus/db"; import { connectFromConfig } from "@wrnexus/db/connect"; import { configureStorage, serveStoredFile, UPLOAD_RUNTIME, UPLOAD_JS_HREF, UPLOADS_PREFIX, type StorageConfig, } from "@wrnexus/uploader"; import { realtimeBusFromConfig } from "./realtime-bus.ts"; import { createHandlers, type AssetServer, type WsData } from "./runtime.ts"; import { servePublicAsset } from "./public.ts"; import type { ClientRuntimeDefinition } from "@wrnexus/plugin"; import { HmrHub } from "./hmr.ts"; type RouteModule = Record; export interface ManifestRoute { /** URL pattern, e.g. `/users/[id]`. */ raw: string; /** The statically-imported route module. */ mod: RouteModule; /** Body shell precomputed by `wrnexus build` for a partial-static page. */ staticShell?: string; } export 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 }[]; } export interface ProductionPluginAsset { path: string; contentType: string; immutable?: boolean; } export 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 `` + `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("")`. */ databases?: Record; /** * 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//migrations`). */ databaseMigrationDirs?: Record; /** * 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; /** 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; /** Enable only for the CLI's supervised exact-production development mode. */ developmentRuntime?: boolean; } const MODE: Mode = "production"; const JS_HEADERS = { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=31536000, immutable", }; const CSS_HEADERS = { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=31536000, immutable", }; function resolvePort(explicit?: number): number { if (typeof explicit === "number" && Number.isFinite(explicit)) return explicit; const envPort = process.env.PORT; if (!envPort) return 3000; const parsed = Number(envPort); return Number.isFinite(parsed) ? parsed : 3000; } /** Resolve the production bind address. Gateway-managed app servers override * this with loopback so only the gateway is externally reachable. */ export function resolveProductionHostname( explicit?: string, environmentHostname = process.env.WRNEXUS_HOSTNAME, ): string { return environmentHostname?.trim() || explicit || "0.0.0.0"; } /** Build the route-matching tables + a module map from the manifest. */ function buildProdRouter(manifest: ProdManifest): { router: Router; modules: Map; } { const modules = new Map(); const toRoutes = (entries: ManifestRoute[]): Route[] => { const routes = entries.map((e): Route => { const { regex, paramNames } = compileRoutePattern(e.raw); // Use the raw pattern as a stable module key. modules.set( e.raw, e.staticShell === undefined ? e.mod : { ...e.mod, __wrnexusStaticShell: e.staticShell }, ); return { raw: e.raw, file: e.raw, regex, paramNames }; }); return sortRoutes(routes); }; const pages = toRoutes(manifest.pages); const api = toRoutes(manifest.api); const realtime = toRoutes(manifest.realtime); // Components are keyed by name; the runtime resolves them via loadModule(name). for (const c of manifest.components) modules.set(c.name, c.mod); // Layouts share the module map under a `layout:` prefix (no name collisions). for (const l of manifest.layouts) modules.set(`layout:${l.name}`, l.mod); const router: Router = { pages, api, realtime, middlewareFiles: [], components: manifest.components.map((c) => ({ name: c.name, file: c.name })), layouts: manifest.layouts.map((l) => ({ name: l.name, file: `layout:${l.name}` })), stores: [], schemas: [], // descriptors are pre-baked into schemasJs; not needed at runtime matchPage: (p) => matchRoute(pages, p), matchApi: (p) => matchRoute(api, p), matchRealtime: (p) => matchRoute(realtime, p), }; return { router, modules }; } /** Serve a pre-built asset file from disk, or 404 if it is absent. */ async function serveFile(path: string | undefined, headers: Record) { if (!path) return new Response("Not Found", { status: 404 }); const file = Bun.file(path); if (!(await file.exists())) return new Response("Not Found", { status: 404 }); return new Response(file, { headers }); } /** Production asset server: pre-built files from disk, reactive runtime inlined. */ function createProdAssetServer(opts: ProdOptions): AssetServer { return { async serve(pathname: string): Promise { if (pathname === "/__wrnexus/reactive.js") { if (opts.reactivePath) { const file = Bun.file(opts.reactivePath); if (await file.exists()) return new Response(file, { headers: JS_HEADERS }); } return new Response(getReactiveRuntime(), { headers: JS_HEADERS }); } if (pathname === "/__wrnexus/nav.js") return new Response(getNavRuntime(), { headers: JS_HEADERS }); if (pathname === "/__wrnexus/realtime.js") return new Response(getRealtimeRuntime(), { headers: JS_HEADERS }); if (pathname === "/__wrnexus/actions.js") return new Response(getActionRuntime(), { headers: JS_HEADERS }); if (pathname === "/__wrnexus/validate.js") return new Response(VALIDATE_RUNTIME, { headers: JS_HEADERS }); if (pathname === "/__wrnexus/i18n.js") return new Response(I18N_RUNTIME, { headers: JS_HEADERS }); if (pathname === UPLOAD_JS_HREF) return new Response(UPLOAD_RUNTIME, { headers: JS_HEADERS }); if (pathname.startsWith(UPLOADS_PREFIX)) { return (await serveStoredFile(pathname)) ?? new Response("Not Found", { status: 404 }); } if (pathname === "/__wrnexus/schemas.js") { return new Response(opts.schemasJs ?? "window.__wireSchemas={};", { headers: JS_HEADERS }); } if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS); if (pathname === "/__wrnexus/theme.js") return serveFile(opts.themeJsPath, JS_HEADERS); if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS); if (pathname === "/__wrnexus/framework.css") return serveFile(opts.frameworkCssPath, CSS_HEADERS); if (pathname === "/__wrnexus/styles.css") return serveFile(opts.stylesPath, CSS_HEADERS); const pluginAsset = opts.pluginAssets?.[pathname]; if (pluginAsset) { return serveFile(pluginAsset.path, { "content-type": pluginAsset.contentType, "cache-control": pluginAsset.immutable === false ? "no-cache" : "public, max-age=31536000, immutable", "x-content-type-options": "nosniff", }); } return servePublicAsset(opts.publicDir, pathname, MODE); }, }; } /** * 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. */ export function createProductionHandlers( manifest: ProdManifest, opts: ProdOptions, ): ReturnType { const { router, modules } = buildProdRouter(manifest); const assets = createProdAssetServer(opts); // Configure the default + named databases. Migrations must already be applied // (`wrnexus db migrate [--db=]` against the production databases). if (opts.db) { try { setDb(connectFromConfig(opts.db)); } catch (err) { console.warn("[wrnexus] database setup failed:", err instanceof Error ? err.message : err); } } for (const [name, cfg] of Object.entries(opts.databases ?? {})) { registerLazyDb(name, () => connectFromConfig(cfg)); } // File-upload storage. Relative local dirs resolve against the deployment cwd // (NOT dist/, which is rebuilt) so uploads persist across deploys. configureStorage(opts.storage, process.cwd()); // Middleware is already an ordered array of functions. const getMiddleware = async (): Promise => manifest.middleware; // In prod, modules are pre-imported; "loading" is a map lookup. const loadModule = async (key: string): Promise => { const mod = modules.get(key); if (!mod) throw new Error(`No module registered for route ${key}`); return mod; }; const productionHmr = opts.developmentRuntime === true; const handlers = createHandlers({ mode: MODE, hmr: productionHmr, hub: productionHmr ? new HmrHub() : undefined, router, loadModule, getMiddleware, assets, hasStyles: !!opts.stylesPath, hasUi: !!opts.uiCssPath, hasFrameworkStyles: !!opts.frameworkCssPath, theme: opts.theme, i18n: opts.i18n, inlineStyles: opts.inlineStyles, stylesIncludeFramework: opts.stylesIncludeFramework, assetVersion: opts.assetVersion, clientRuntimes: opts.clientRuntimes, head: opts.head, seo: opts.seo, mobile: opts.mobile, pwa: opts.pwa, security: opts.security, observability: opts.observability, tenancy: opts.tenancy, navigation: opts.navigation, maxBodyBytes: opts.maxBodyBytes, realtimeBus: realtimeBusFromConfig(opts.realtime), }); return handlers; } /** * Apply migrations bundled into the build before the server accepts traffic, so * a fresh deploy always runs on the latest schema — exactly like the dev server * auto-migrates on startup. Applied migrations are tracked in `_wire_migrations`, * so this is idempotent and safe to run on every boot. Opt out with * `autoMigrate: false` (e.g. multi-instance deploys that migrate in a release * step). A failed migration is logged but does not crash the server: each * migration runs in a transaction, so the DB is left at the last good state. */ async function runStartupMigrations(opts: ProdOptions): Promise { if (opts.autoMigrate === false) return; const targets: { name?: string; dir: string }[] = []; if (opts.db && opts.migrationsDir) targets.push({ dir: opts.migrationsDir }); for (const [name, dir] of Object.entries(opts.databaseMigrationDirs ?? {})) { targets.push({ name, dir }); } for (const { name, dir } of targets) { const label = name ? ` (db: ${name})` : ""; if (!hasDb(name)) continue; try { const applied = await migrate(getDb(name), dir); if (applied.length) { console.log( `WrNexus: applied ${applied.length} migration(s)${label}: ${applied.join(", ")}`, ); } } catch (err) { console.error( `WrNexus: migration failed${label} —`, err instanceof Error ? err.message : err, ); } } } /** Start the production server on Bun from a precompiled manifest. */ export async function createProductionServer(manifest: ProdManifest, opts: ProdOptions) { // Load the deployment's .env cascade for the active profile (real env wins), // so runtime secrets are available even though config was baked at build time. loadEnv(process.cwd(), resolveProfile({ mode: "production" })); const handlers = createProductionHandlers(manifest, opts); // Bring the schema up to date before listening (opt out with autoMigrate:false). await runStartupMigrations(opts); const server = Bun.serve({ port: resolvePort(opts.port), hostname: resolveProductionHostname(opts.hostname), development: false, maxRequestBodySize: opts.maxBodyBytes ?? 10 * 1024 * 1024, fetch: handlers.fetch, websocket: handlers.websocket, }); // Graceful shutdown: stop accepting connections, then exit. let shuttingDown = false; const shutdown = () => { if (shuttingDown) return; shuttingDown = true; console.log("WrNexus: shutting down…"); server.stop(); process.exit(0); }; process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); console.log(`WrNexus (production) listening on http://${server.hostname}:${server.port}`); return server; }