/** * @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 { join } from "node:path"; import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core"; import { compileRoutePattern, matchRoute, sortRoutes, type Route, type Router, } from "@wrnexus/router"; import { getActionRuntime, getComponentControllerRuntime, getReactiveRuntime, getNavRuntime, getRealtimeRuntime, } from "@wrnexus/csr"; import { getIslandRuntime } from "@wrnexus/react/runtime"; 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 { hasAuthzCatalog, mergeCatalogs, setAuthzCatalog, type AuthzModule } from "@wrnexus/authz"; 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"; import { createRpcHandler, type RpcManifestContract } from "@wrnexus/ssr/rpc"; import { validateRpcCsrf, withServerFnRequestContext } from "./rpc-shared.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 }[]; /** RPC service implementations (from app/services/*.ts). */ services?: { 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; /** `stylesPath` already contains the shared WrNexus UI stylesheet. */ stylesIncludeUi?: boolean; /** Absolute path to the pre-built reactive runtime. */ reactivePath?: string; /** Absolute path to the on-demand component controller runtime. */ controllersPath?: string; /** Absolute directory containing bundled per-WRN browser modules. */ clientModulesDir?: string; islandsDir?: string; /** Absolute path to the pre-built theme stylesheet (`theme.css`). */ themePath?: string; /** Pre-built active theme/accent stylesheets, loaded on demand. */ themeAssetsDir?: 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 WrNexus UI stylesheet (`ui.css`). */ uiCssPath?: string; /** Combined production theme + WrNexus UI stylesheet. */ frameworkCssPath?: string; /** Pre-built `window.__wrnSchemas = {...}` script for client validation. */ schemasJs?: string; /** * Authorization declarations discovered by `wrnexus build` from * `app/authz/*.ts`, statically imported into the generated entry (the * catalog holds policy FUNCTIONS, so — unlike `schemasJs` — it cannot be * JSON-serialised). `module` is `undefined` for a file with no default * export. In the NORMAL generated-entry build, the catalog is already set * by the generated `.authz-setup.ts` module before this ever runs (see * `applyAuthzManifestEarly` below); `createProductionHandlers` merges this * same list again as an idempotent second pass — with its warnings — so a * caller that bypasses the generated entry and calls it directly still gets * a correctly merged catalog. */ authz?: AuthzManifestEntry[]; /** 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; /** Keep-alive idle timeout in seconds. Defaults to 30. */ idleTimeout?: number; /** Allow multiple Bun workers to share the listening port. */ reusePort?: boolean; /** 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"; } /** One `app/authz/*.ts` declaration as passed through `ProdOptions.authz`. */ export interface AuthzManifestEntry { source: string; /** Undefined when the declaration file has no default export. */ module?: AuthzModule; } function resolveAuthzSources( entries: AuthzManifestEntry[], ): { source: string; module: AuthzModule }[] { return entries.flatMap((entry) => entry.module ? [{ source: entry.source, module: entry.module }] : [], ); } /** * Merge + `setAuthzCatalog` as EARLY as possible, deliberately silently (no * missing-default-export warnings). Called ONLY from the generated * `.authz-setup.ts` module that `wrnexus build` imports FIRST in the * production entry — before any other static import, including app * middleware — so that a middleware module reading `getAuthzCatalog()` at its * own module scope (the same eager shape `authzMiddleware({ catalog, ... })` * itself requires) sees a populated catalog. `createProductionHandlers` below * performs the exact same merge again, WITH its warnings, as the canonical, * always-warns second pass — this function stays silent specifically so the * normal boot path does not print the same "no default export" warning * twice. A genuine conflict still throws here (via `mergeCatalogs`), which * fails the boot at import time — before the entry body, and thus * `createProductionHandlers`, ever runs. */ export function applyAuthzManifestEarly(entries: AuthzManifestEntry[]): void { setAuthzCatalog(mergeCatalogs(resolveAuthzSources(entries))); } /** 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); const optimizedMatcher = (routes: Route[]) => { const exact = new Map(); const dynamic: Route[] = []; for (const route of routes) { if (route.paramNames.length === 0) exact.set(route.raw, route); else dynamic.push(route); } return (pathname: string) => { const route = exact.get(pathname); return route ? { route, params: {} } : matchRoute(dynamic, pathname); }; }; // 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); for (const service of manifest.services ?? []) modules.set(`service:${service.name}`, service.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 authz: [], // authz declarations are not needed at runtime in production services: (manifest.services ?? []).map((service) => ({ name: service.name, file: `service:${service.name}`, })), matchPage: optimizedMatcher(pages), matchApi: optimizedMatcher(api), matchRealtime: optimizedMatcher(realtime), }; return { router, modules }; } const productionFileCache = new Map>(); const missingProductionFiles = new Set(); /** Serve a pre-built asset file from disk, caching stable build file handles. */ async function serveFile(path: string | undefined, headers: Record) { if (!path) return new Response("Not Found", { status: 404 }); if (missingProductionFiles.has(path)) return new Response("Not Found", { status: 404 }); let file = productionFileCache.get(path); if (!file) { file = Bun.file(path); if (!(await file.exists())) { missingProductionFiles.add(path); return new Response("Not Found", { status: 404 }); } productionFileCache.set(path, file); } 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.startsWith("/__wrnexus/client/")) { const name = pathname.slice("/__wrnexus/client/".length); if (!opts.clientModulesDir || !/^[A-Za-z0-9._-]+\.mjs$/.test(name)) { return new Response("Not Found", { status: 404 }); } return serveFile(join(opts.clientModulesDir, name), JS_HEADERS); } if (pathname.startsWith("/__wrnexus/island/")) { const name = pathname.slice("/__wrnexus/island/".length); if (!opts.islandsDir || !/^[A-Za-z0-9._-]+\.js$/.test(name)) { return new Response("Not Found", { status: 404 }); } return serveFile(join(opts.islandsDir, name), JS_HEADERS); } if (pathname === "/__wrnexus/islands.js") return new Response(getIslandRuntime(), { headers: JS_HEADERS }); 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/controllers.js") { if (opts.controllersPath) return serveFile(opts.controllersPath, JS_HEADERS); return new Response(getComponentControllerRuntime(), { 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.__wrnSchemas={};", { headers: JS_HEADERS }); } if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS); const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname); if (activeThemeMatch && opts.theme && opts.themeAssetsDir) { try { const themeName = decodeURIComponent(activeThemeMatch[1]!); const accentPart = decodeURIComponent(activeThemeMatch[2]!); if (!opts.theme.names.includes(themeName)) return new Response("Not Found", { status: 404 }); if (accentPart !== "_" && !opts.theme.accentNames.some((name) => name === accentPart)) { return new Response("Not Found", { status: 404 }); } return serveFile( join( opts.themeAssetsDir, encodeURIComponent(themeName), `${accentPart === "_" ? "_" : encodeURIComponent(accentPart)}.css`, ), CSS_HEADERS, ); } catch { return new Response("Not Found", { status: 404 }); } } 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. */ /** * Resolve the `.wrn` server-function module owning `rawComponent`, from the * manifest's already-loaded static imports (production has no dynamic module * loading, so unlike dev's `resolve` this is a synchronous in-memory search). * Searches components (preferring an exact name match), then pages, then * layouts — mirroring the candidate order dev's `resolve` walks in * index.ts, minus `router.stores`, which the production router never * populates (see `buildProdRouter` above). */ function resolveProdServerFunctions( manifest: ProdManifest, rawComponent: string, ): { functions: Record any>; manifest: RpcManifestContract[]; } | null { const componentName = rawComponent.split(":", 1)[0] ?? rawComponent; const preferred = manifest.components.find( (entry) => entry.name.toLowerCase() === componentName.toLowerCase(), ); const candidates: RouteModule[] = [ ...(preferred ? [preferred.mod] : []), ...manifest.pages.map((entry) => entry.mod), ...manifest.layouts.map((entry) => entry.mod), ...manifest.components.filter((entry) => entry !== preferred).map((entry) => entry.mod), ]; for (const mod of new Set(candidates)) { const functions = mod.__wrnexusServerFunctions; const rpcManifest = mod.__wrnexusRpcManifest; if (!functions || typeof functions !== "object" || !Array.isArray(rpcManifest)) continue; const ownsComponent = rpcManifest.some( (entry: any) => String(entry?.component ?? "").toLowerCase() === componentName.toLowerCase(), ); if (!ownsComponent) continue; return { functions: functions as Record any>, manifest: rpcManifest as RpcManifestContract[], }; } return null; } 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()); // Authorization: merge the build's statically-imported app/authz/*.ts // declarations into the process-wide catalog BEFORE the handlers (and thus // any request) exist, so a conflicting pair of declarations fails the boot // loudly instead of surfacing on the first request. This runs for every // deployment adapter that calls createProductionHandlers, not only the // Bun.serve path in createProductionServer below. The app still registers // authzMiddleware itself with its own store; this only makes the merged // catalog reachable. No declarations -> an empty catalog, no error. // // In the NORMAL generated-entry build, this is a deliberately redundant // SECOND pass: the generated `.authz-setup.ts` module already ran this // exact merge (silently, via applyAuthzManifestEarly above) before this // function was ever called, specifically so a middleware module that reads // getAuthzCatalog() at its own module scope sees a populated catalog — this // function's body runs too late for that (it is reached only once every // OTHER static import, including middleware, has already evaluated). // // The merge+validation of opts.authz always runs (a genuine conflict must // still fail the boot loudly, no matter which pass discovers it). But // setAuthzCatalog is only called when this pass actually has something to // contribute, OR when nothing has been set yet: client.ts documents an // escape hatch where a direct caller of createProductionHandlers may call // setAuthzCatalog(catalog) itself before importing anything that reads it, // specifically for a custom entry that never ran the generated // `.authz-setup.ts` pass. Calling setAuthzCatalog unconditionally here would // clobber that caller's catalog with an empty one whenever opts.authz is // omitted — silently deleting every permission the app declared. for (const missing of (opts.authz ?? []).filter((entry) => !entry.module)) { console.warn(`[wrnexus] authz declaration ${missing.source} has no default export; skipping.`); } const mergedAuthzCatalog = mergeCatalogs(resolveAuthzSources(opts.authz ?? [])); if ((opts.authz?.length ?? 0) > 0 || !hasAuthzCatalog()) { setAuthzCatalog(mergedAuthzCatalog); } // 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, stylesIncludeUi: opts.stylesIncludeUi, 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), }); // `/__wrnexus/rpc` (server.fn() calls from the browser) must be intercepted // BEFORE handlers.fetch, exactly as dev does in index.ts — it is NOT an app // route and must never fall through to isRpcPath/handleRpcRequest (the // internal-caller-gated inter-app service RPC), which would 404 it. const rpcHandler = createRpcHandler({ resolve: async (rawComponent) => resolveProdServerFunctions(manifest, rawComponent), validateCsrf: validateRpcCsrf, }); const serverFnRpcHandler = withServerFnRequestContext(rpcHandler); const appFetch = handlers.fetch; const fetchWithServerFnRpc: typeof handlers.fetch = (request, server) => { if (new URL(request.url).pathname === "/__wrnexus/rpc") return serverFnRpcHandler(request); return appFetch(request, server); }; return { ...handlers, fetch: fetchWithServerFnRpc }; } /** * 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 `_wrn_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, idleTimeout: opts.idleTimeout ?? 30, reusePort: opts.reusePort ?? false, 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; }