/** * @wrnexus/dev-server — the development HTTP + WebSocket server. * * Thin Bun.serve wrapper around the shared runtime (runtime.ts). Dynamic module * loading and targeted cache invalidation keep page/component/API edits inside * the running process while the HMR socket morphs fresh HTML into the browser. */ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { resolve, dirname, isAbsolute, join } from "node:path"; import { type Middleware, type Mode, type SecurityConfig, type SeoConfig } from "@wrnexus/core"; import { buildRouter, type Router } from "@wrnexus/router"; import { resolveThemeConfig, resolveBrowserCookieOptions, type BrowserCookiesConfig, type StylesConfig, type ThemeConfig, type MobileConfig, type PwaConfig, type NavigationConfig, } from "@wrnexus/styles"; import { uiComponentsDir, uiCssPath } from "@wrnexus/ui/registry"; import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation"; import { loadLocales, resolveI18n, type I18nConfig } from "@wrnexus/i18n"; import { applyMigrations, loadMigrations, migrate, setDb, registerDb, registerLazyDb, getDbPerformanceSnapshot, } from "@wrnexus/db"; import { connectFromConfig } from "@wrnexus/db/connect"; import { configureStorage, type StorageConfig } from "@wrnexus/uploader"; import { setAuthzCatalog, type AuthzCatalog, type AuthzModule } from "@wrnexus/authz"; import { loadAppAuthzCatalog } from "./authz-boot.ts"; import { realtimeBusFromConfig } from "./realtime-bus.ts"; import { invalidateModule, loadModule, loadWrnServerModule, setCompileCacheDir, setCompileImportOptions, rebuildChangedIslands, setDevCompilerPipeline, wrnBrowserArtifactUrlAsync, } from "./pipeline.ts"; import { createRpcHandler } from "@wrnexus/ssr/rpc"; import { createRecycleMonitor } from "./recycle.ts"; import { RESTART_EXIT_CODE } from "./restart.ts"; /* * Recycle after this many hot rebuilds. Each retains roughly 0.66 MB that Bun * cannot release, so 300 caps the leak near 200 MB -- far more than a normal * session reaches, and far less than what makes the server crawl. Set * WRNEXUS_DEV_RECYCLE_AFTER to tune it, or to 0 to never recycle. */ const RECYCLE_REBUILD_THRESHOLD = (() => { const configured = Number(process.env.WRNEXUS_DEV_RECYCLE_AFTER); return Number.isFinite(configured) && configured >= 0 ? configured : 300; })(); /** Quiet period required first, so a recycle never interrupts a live request. */ const RECYCLE_IDLE_MS = 10_000; const RECYCLE_CHECK_MS = 5_000; import { createHandlers, type WsData } from "./runtime.ts"; import { createDevAssetServer } from "./assets.ts"; import { pluginAssetsFromContributions } from "./plugin-assets.ts"; import { resolvePackageMigrations } from "./plugin-migrations.ts"; import { HmrHub } from "./hmr.ts"; import { startWatcher } from "./watch.ts"; export { RESTART_EXIT_CODE } from "./restart.ts"; export { expandStaticComponents, precomputePartialStaticShell } from "./partial-build.ts"; export { getWrnCompileMetrics, resetWrnCompileMetrics } from "./pipeline.ts"; export type { WrnCompileMetrics } from "./pipeline.ts"; import { resetDevCache } from "./cache.ts"; // Shared with prod.ts — kept in their own module to avoid a circular import // (index.ts re-exports createProductionServer/createProductionHandlers from // prod.ts, so prod.ts cannot import these back from index.ts). import { validateRpcCsrf, withServerFnRequestContext } from "./rpc-shared.ts"; export { validateRpcCsrf, withServerFnRequestContext }; import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types"; import { createPluginRunner, discoverPlugins, type PluginInput } from "@wrnexus/plugin"; import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles"; import { builtinDevToolbarPanels, createDevToolbarCollector, type DevToolbarCollector, } from "@wrnexus/dev-toolbar/server"; export interface ServeOptions { appDir: string; port?: number; hostname?: string; /** Development TLS material. Production TLS is normally terminated by the deployment proxy. */ tls?: { cert: string; key: string }; mode?: Mode; /** Inject the live-reload client (defaults to true in development). */ hmr?: boolean; appConfig?: Record; /** 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; /** Shared browser-cookie defaults and preference-specific overrides. */ cookies?: BrowserCookiesConfig; /** i18n config (default language + supported locales). */ i18n?: I18nConfig; /** Default database connection (driver + url). Enables `getDb()` and dev auto-migrate. */ db?: { driver: string; url: string }; /** Named databases, reached with `getDb("")`; migrations under app/db//. */ databases?: Record; /** Realtime scaling: bridge room broadcasts over Redis across app processes. */ realtime?: { scale?: boolean; redisUrl?: string }; /** File-upload storage: named stores (local dir / S3), reached with `getStore()`. */ storage?: StorageConfig; mobile?: MobileConfig; pwa?: PwaConfig | false; devToolbar?: boolean | DevToolbarConfig; plugins?: PluginInput; observability?: ObservabilityConfig; tenancy?: TenancyConfig; navigation?: NavigationConfig; } export interface RunningServer { port: number; hostname: string; url: string; router: Router; stop(): void; } /** Build an invalidatable middleware loader for in-process hot updates. */ function middlewareLoader(router: Router): { load: () => Promise; invalidate: () => void; } { let cache: Middleware[] | null = null; return { async load() { if (cache) return cache; const out: Middleware[] = []; for (const file of router.middlewareFiles) { const mod = await loadModule(file); if (typeof mod.default === "function") out.push(mod.default as Middleware); else console.warn(`[wrnexus] middleware ${file} has no default export; skipped`); } cache = out; return out; }, invalidate() { cache = null; }, }; } async function schemaRuntime(router: Router): Promise { const descriptors: Record = {}; for (const schemaRef of router.schemas) { try { const mod = await loadModule(schemaRef.file); const schema = mod.default as ObjectSchema | undefined; if (schema && typeof schema.describe === "function") { descriptors[schemaRef.name] = schema.describe(); } } catch (error) { console.warn(`[wrnexus] schema '${schemaRef.name}' failed to load`, error); } } return renderSchemasScript(descriptors); } function resolveDevToolbarConfig( mode: string, value: ServeOptions["devToolbar"], ): DevToolbarConfig | null { if (mode !== "development" || value === false || value === undefined) { return null; } if (value === true) { return { enabled: true, position: "bottom-center", defaultOpen: false, scanOnNavigation: true, scanOnHmr: true, openEditor: true, }; } if (value.enabled === false) { return null; } return { enabled: true, position: "bottom-center", defaultOpen: false, scanOnNavigation: true, scanOnHmr: true, openEditor: true, ...value, }; } export async function startServer(opts: ServeOptions): Promise { const appDir = resolve(opts.appDir); const appRoot = dirname(appDir); const mode: Mode = opts.mode ?? "development"; const importConfig = (opts.appConfig?.imports ?? {}) as { mode?: "legacy" | "compatible" | "explicit"; autoImport?: boolean; aliases?: Record; }; setCompileImportOptions(appRoot, importConfig); const configuredPlugins = opts.plugins ?? (opts.appConfig?.plugins as PluginInput | undefined); const discoveredPlugins = await discoverPlugins(appRoot, configuredPlugins, { includeDevDependencies: true, strict: true, warn: (message) => console.warn(`[wrnexus:plugin] ${message}`), enforcePermissions: (opts.appConfig?.pluginPermissions as { enforce?: boolean } | undefined) ?.enforce, grantedPermissions: ( opts.appConfig?.pluginPermissions as { grants?: Record } | undefined )?.grants, }); const pluginRunner = createPluginRunner(discoveredPlugins, { root: appRoot, mode, command: "dev", metadata: new Map(), warn: (message) => console.warn(`[wrnexus:plugin] ${message}`), }); /** * Plugins must receive the complete * wrnexus.config.ts object. * * ServeOptions only contains framework-known * properties. Package-specific configuration, * such as `auth`, is stored in appConfig. */ const pluginConfig: Record = { ...(opts.appConfig ?? {}), // Resolved runtime values take priority. appDir, port: opts.port, hostname: opts.hostname, mode, hmr: opts.hmr, styleEntry: opts.styleEntry, styles: opts.stylesConfig ?? (opts.appConfig?.styles as StylesConfig | undefined), head: opts.head, seo: opts.seo, security: opts.security, theme: opts.theme, i18n: opts.i18n, db: opts.db, databases: opts.databases, realtime: opts.realtime, storage: opts.storage, mobile: opts.mobile, pwa: opts.pwa, devToolbar: opts.devToolbar, plugins: configuredPlugins, observability: opts.observability, tenancy: opts.tenancy, navigation: opts.navigation, }; await pluginRunner.configure(pluginConfig); await pluginRunner.configResolved( Object.freeze({ ...pluginConfig, }), ); const pluginContributions = await pluginRunner.contributions(); const virtualModules = new Map(); // Keep generated artifacts under the single framework state directory. // Startup clears this cache and removes legacy PID-suffixed cache folders. const cacheDir = join(appRoot, ".wrnexus", "cache"); // Clear the cache BEFORE anything writes into it. This used to run several // hundred lines below, which deleted the plugin virtual modules written just // after this point -- so an app with a plugin (auth, say) lost its generated // modules at every boot, while an app with none never noticed. resetDevCache({ rootDir: appRoot, cacheDir, enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1", }); const virtualDir = join(cacheDir, "virtual"); mkdirSync(virtualDir, { recursive: true }); for (const [index, module] of pluginContributions.virtualModules.entries()) { const output = join(virtualDir, `plugin-${index}.ts`); writeFileSync( output, await module.load({ root: appRoot, mode, command: "dev", profile: process.env.WRNEXUS_PROFILE, metadata: new Map(), warn: (message) => console.warn(`[wrnexus:plugin] ${message}`), }), "utf8", ); virtualModules.set(module.id, output); } setDevCompilerPipeline({ transformAst: (ast, file) => pluginRunner.transformAst(ast, file), transformCode: (code, file) => pluginRunner.transformCode(code, file), virtualModules, }); console.log( `[wrnexus:plugin] discovered: ${ pluginRunner.plugins.map((plugin) => plugin.name).join(", ") || "none" }`, ); console.log(`[wrnexus:plugin] contributed routes: ${pluginContributions.routes.length}`); const pluginToolbarPanels = await pluginRunner.devToolbarPanels(); const bundledUiDir = uiComponentsDir(); const componentDirs = [ bundledUiDir, join(bundledUiDir, "..", "styles"), ...pluginContributions.componentDirs, ]; const hmr = opts.hmr ?? mode === "development"; const port = opts.port ?? 3000; const hostname = opts.hostname ?? "::"; const displayHost = hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname; const router = buildRouter(appDir, { componentDirs, externalRoutes: pluginContributions.routes, middlewareFiles: pluginContributions.middleware, }); const styleEntry = opts.styleEntry ?? null; const devToolbarConfig = resolveDevToolbarConfig(mode, opts.devToolbar); const devToolbarCollector: DevToolbarCollector | undefined = devToolbarConfig ? createDevToolbarCollector() : undefined; // Compile every `.wrn` into ONE cache dir at the project root, instead of a // `.wrnexus/` next to each source file (and inside node_modules UI dirs). setCompileCacheDir(cacheDir); const theme = resolveThemeConfig(opts.theme, opts.cookies); const uiStylesPath = uiCssPath(); const schemasJs = await schemaRuntime(router); // Authorization: load and merge every app/authz/*.ts declaration, then stash // it in the process-wide registry BEFORE middleware is resolved. App // middleware (which registers authzMiddleware itself, with its own store — // the framework never installs one) runs at request time and needs // getAuthzCatalog() already populated by then. An app with no declarations // gets an empty catalog; a genuine conflict between declarations throws and // fails this boot loudly. Pass the already-built `router` (not `appDir`): // it was just built above with the full componentDirs/externalRoutes/ // middlewareFiles options, so this avoids a second, redundant filesystem // scan of the whole app/ tree on every dev boot. const authzCatalog: AuthzCatalog = await loadAppAuthzCatalog(router); setAuthzCatalog(authzCatalog); // i18n is opt-in by the presence of app/locales/*.json. const localeMessages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict }); const i18nConfig = opts.i18n ? { ...opts.i18n, cookie: { ...resolveBrowserCookieOptions(opts.cookies, "language"), ...(opts.i18n.cookie ?? {}), }, } : undefined; const i18n = Object.keys(localeMessages).length ? resolveI18n(localeMessages, i18nConfig) : undefined; // Databases: configure the default (getDb()) + each named one (getDb("")), // and auto-migrate in dev so schemas are ready. The default's migrations live in // app/db/migrations; a named db's in app/db//migrations. Prod runs // migrations explicitly (files aren't in the bundle). const connectAndMigrate = async (name: string | null, cfg: { driver: string; url: string }) => { try { const db = name ? registerDb(name, connectFromConfig(cfg, appRoot)) : setDb(connectFromConfig(cfg, appRoot)); const dir = name ? join(appDir, "db", name, "migrations") : join(appDir, "db", "migrations"); const appApplied = await migrate(db, dir); const packageApplied = await applyMigrations( db, resolvePackageMigrations(pluginContributions.migrations, name ?? undefined), ); const applied = [...appApplied, ...packageApplied]; if (applied.length) { console.log( `[wrnexus] applied ${applied.length} migration(s)${name ? ` to '${name}'` : ""}`, ); } } catch (err) { const label = name ? `database '${name}'` : "database"; console.warn(`[wrnexus] ${label} setup failed:`, err instanceof Error ? err.message : err); } }; if (opts.db) await connectAndMigrate(null, opts.db); for (const [name, cfg] of Object.entries(opts.databases ?? {})) { const migrationDir = join(appDir, "db", name, "migrations"); const hasMigrations = loadMigrations(migrationDir).length > 0 || resolvePackageMigrations(pluginContributions.migrations, name).length > 0; if (hasMigrations) await connectAndMigrate(name, cfg); else registerLazyDb(name, () => connectFromConfig(cfg, appRoot)); } // File-upload storage: build a driver per configured store (local dir / S3). // Relative local dirs resolve against the app root; served/served-back below. configureStorage(opts.storage, appRoot); const assets = createDevAssetServer( appDir, mode, { entry: styleEntry, config: opts.stylesConfig, appRoot, publicDir: join(appRoot, "public"), // Component discovery is separate from utility-source discovery. // Packages that need Tailwind scanning opt in via styles.source. sources: pluginContributions.styles.flatMap((style) => (style.source ? [style.source] : [])), entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])), }, theme, () => readFileSync(uiStylesPath, "utf8"), schemasJs, pluginAssetsFromContributions(pluginContributions), ); const hub = hmr ? new HmrHub() : undefined; const unsubscribeDevToolbar = devToolbarCollector && hub ? devToolbarCollector.subscribe((issues) => { hub.broadcastJson({ channel: "toolbar", type: "toolbar:issues", issues }); }) : undefined; const middleware = middlewareLoader(router); const toolbarPlatform = { wrnexus060: { clientFunctions: true, serverFunctions: true, sharedFunctions: true, typedOutputs: true, requestScopedStores: true, }, plugins: pluginRunner.plugins.map((plugin) => ({ name: plugin.name, version: plugin.version })), runtimes: pluginContributions.clientRuntimes.map((runtime) => ({ id: runtime.id, publicPath: runtime.publicPath, type: runtime.type, load: runtime.load, })), assets: pluginContributions.assets.map((asset) => ({ id: asset.id, publicPath: asset.publicPath, contentType: asset.contentType, })), componentDirs, styles: pluginContributions.styles, routes: { pages: router.pages.length, api: router.api.length, realtime: router.realtime.length, }, }; const runtimeDeps = { mode, hmr, router, loadModule, getMiddleware: middleware.load, assets, hasStyles: !!styleEntry || pluginContributions.styles.some((style) => !!style.entry), hasUi: true, theme, i18n, head: opts.head, seo: opts.seo, mobile: opts.mobile, pwa: opts.pwa, security: opts.security, observability: opts.observability, tenancy: opts.tenancy, navigation: opts.navigation, clientRuntimes: pluginContributions.clientRuntimes, hub, realtimeBus: realtimeBusFromConfig(opts.realtime), renderHtml: (html: string) => pluginRunner.render(html), devToolbar: devToolbarConfig && devToolbarCollector ? { config: devToolbarConfig, collector: devToolbarCollector, root: appRoot, panels: () => [ { id: "runtime", title: "Runtime", icon: "cpu", description: "Client, server, and shared functions, hydration modules, and runtime-boundary diagnostics.", order: 10, }, { id: "stores", title: "Stores", icon: "database", description: "Global/page stores, safe client state, computed values, actions, persistence, and hydration.", order: 20, }, { id: "cache", title: "Cache", icon: "layers", description: "Request, data, component, and page cache entries plus hit/miss history.", order: 30, }, ...builtinDevToolbarPanels({ root: appRoot, platform: toolbarPlatform, database: getDbPerformanceSnapshot(), version: { current: "0.8.0" }, }), ...pluginToolbarPanels, ], platform: toolbarPlatform, } : undefined, }; const handlers = createHandlers(runtimeDeps); const rpcHandler = createRpcHandler({ async resolve(rawComponent) { const componentName = rawComponent.split(":", 1)[0] ?? rawComponent; const preferred = router.components.find( (entry) => entry.name.toLowerCase() === componentName.toLowerCase(), ); const candidates = [ ...(preferred ? [preferred.file] : []), ...router.pages.filter((route) => route.file.endsWith(".wrn")).map((route) => route.file), ...router.layouts.map((entry) => entry.file), ...router.stores.map((entry) => entry.file), ...router.components.filter((entry) => entry !== preferred).map((entry) => entry.file), ]; for (const file of new Set(candidates)) { const module = await loadWrnServerModule(file); const functions = module.__wrnexusServerFunctions; const manifest = module.__wrnexusRpcManifest; if (!functions || typeof functions !== "object" || !Array.isArray(manifest)) continue; const ownsComponent = manifest.some( (entry: any) => String(entry?.component ?? "").toLowerCase() === componentName.toLowerCase(), ); if (!ownsComponent) continue; return { functions: functions as Record any>, manifest: manifest as any, }; } return null; }, validateCsrf: validateRpcCsrf, }); const serverFnRpcHandler = withServerFnRequestContext(rpcHandler); /* * Hot rebuilds retain their predecessors (see recycle.ts). Only dev reloads * modules, so only dev needs to recycle. */ const recycle = hmr && mode === "development" && RECYCLE_REBUILD_THRESHOLD > 0 ? createRecycleMonitor({ threshold: RECYCLE_REBUILD_THRESHOLD, idleMs: RECYCLE_IDLE_MS, onRecycle(reason) { console.log(`[wrnexus] ${reason}`); process.exit(RESTART_EXIT_CODE); }, }) : null; const server = Bun.serve({ port, hostname, development: mode === "development", maxRequestBodySize: 10 * 1024 * 1024, ...(opts.tls ? { tls: opts.tls } : {}), fetch(request, server) { recycle?.recordRequest(Date.now()); if (new URL(request.url).pathname === "/__wrnexus/rpc") return serverFnRpcHandler(request); return handlers.fetch(request, server); }, websocket: handlers.websocket, }); if (recycle) { // unref so a pending check never keeps the process alive on its own. const timer = setInterval(() => recycle.tick(Date.now()), RECYCLE_CHECK_MS); timer.unref?.(); } try { await pluginRunner.hook("configureServer", { server, router, handlers, assets, devToolbarCollector, pluginContributions, }); } catch (error) { server.stop(); throw error; } let watcher: ReturnType; // In-process HMR: keep the server and socket alive, invalidate only changed // modules, rescan file routes, and ask browsers to morph in fresh HTML. if (hmr && hub) { const hotUpdate = async (files: string[]): Promise => { // Allow VS Code/Bun to finish writing pasted content. await new Promise((resolvePromise) => setTimeout(resolvePromise, 50)); for (const file of files) { invalidateModule(isAbsolute(file) ? file : resolve(appDir, file)); recycle?.recordRebuild(); } if (files.some((file) => file.endsWith(".wrn"))) assets.invalidateCss(); Object.assign( router, buildRouter(appDir, { componentDirs, externalRoutes: pluginContributions.routes, middlewareFiles: pluginContributions.middleware, }), ); middleware.invalidate(); const appFiles = files.filter((file) => !isAbsolute(file)); // Without this branch, editing app/authz/*.ts reloaded the page (watch.ts // classifies any non-CSS change as "server") while the OLD catalog stayed // authoritative — a false security signal: tightening or removing a // permission LOOKS like it took effect but does not until a restart. A // raw `import()` here would silently no-op: Bun caches local TS/JS // modules by filesystem path and ignores query strings, so the edited // file must be re-imported through `loadModule` (pipeline.ts), which // copies it to a versioned sibling path specifically to defeat that // cache — the same mechanism every other hot-reloaded module already // uses. `router` was just rebuilt above, so this reuses it rather than // re-scanning the filesystem a third time. if (appFiles.some((file) => file === "authz" || file.startsWith("authz/"))) { try { const nextAuthzCatalog = await loadAppAuthzCatalog( router, (file) => loadModule(file) as Promise<{ default?: AuthzModule }>, ); setAuthzCatalog(nextAuthzCatalog); } catch (error) { console.error( "[wrnexus] authz hot update failed — the PREVIOUS catalog remains authoritative " + "until this is fixed and the file saved again", error, ); } } if (appFiles.some((file) => file === "schemas" || file.startsWith("schemas/"))) { assets.updateSchemas(await schemaRuntime(router)); } if (appFiles.some((file) => file === "locales" || file.startsWith("locales/"))) { try { const messages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict }); runtimeDeps.i18n = Object.keys(messages).length ? resolveI18n(messages, opts.i18n) : undefined; } catch (error) { console.warn( "[wrnexus] locale hot update was incomplete; keeping the last valid bundle", error, ); } } console.log(`[wrnexus] hot update — ${files.join(", ")}`); await pluginRunner.hook("hmrUpdate", files); // Island .tsx sources are not .wrn files, so nothing below would rebuild // them; page modules are cached, so no compile runs on the next request. const islandFiles = files .map((changed) => (isAbsolute(changed) ? changed : resolve(appDir, changed))) .filter((changed) => changed.endsWith(".tsx")); if (islandFiles.length > 0) { try { await rebuildChangedIslands(islandFiles); } catch (error) { console.warn("[wrnexus] island rebuild failed", error); } } const storeUpdates: Array<{ name: string; url: string; kind: string }> = []; for (const changed of files) { const absolute = isAbsolute(changed) ? changed : resolve(appDir, changed); if (!absolute.endsWith(".wrn")) continue; try { const source = readFileSync(absolute, "utf8"); const declaration = /\b(global|page)\s+store\s+([A-Za-z_$][\w$]*)\s*\{/.exec(source); if (!declaration) continue; storeUpdates.push({ name: declaration[2]!, kind: declaration[1]!, url: await wrnBrowserArtifactUrlAsync(absolute), }); } catch (error) { console.warn(`[wrnexus] failed to prepare store HMR for ${absolute}`, error); } } if (storeUpdates.length) { hub.broadcastJson({ type: "store-update", version: Date.now(), stores: storeUpdates }); } const onlyStores = storeUpdates.length > 0 && files.every((changed) => { const absolute = isAbsolute(changed) ? changed : resolve(appDir, changed); if (!absolute.endsWith(".wrn")) return false; try { return /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test( readFileSync(absolute, "utf8"), ); } catch { return false; } }); if (!onlyStores) hub.reload(); hub.broadcastJson({ channel: "toolbar", type: "toolbar:scan", reason: "source-change", files, }); }; const packageWatchDirs = [ dirname(uiStylesPath), ...componentDirs, ...pluginContributions.clientRuntimes.flatMap((runtime) => runtime.entry ? [dirname(runtime.entry)] : [], ), ...pluginContributions.assets.flatMap((asset) => (asset.entry ? [dirname(asset.entry)] : [])), ...pluginContributions.styles.flatMap((style) => [style.source, style.entry ? dirname(style.entry) : undefined].filter( (value): value is string => !!value, ), ), ...pluginContributions.routes.map((route) => dirname(route.entry)), ...pluginContributions.middleware.map((file) => dirname(file)), ]; watcher = startWatcher({ appDir, extraDirs: packageWatchDirs, hub, assets, devToolbarCollector, onHotChange: hotUpdate, }); } const boundPort = server.port ?? port; return { port: boundPort, hostname, url: `http://${displayHost}:${boundPort}`, router, stop: () => { watcher?.close(); unsubscribeDevToolbar?.(); server.stop(); void pluginRunner.hook("shutdown"); rmSync(cacheDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); }, }; } export { createHandlers } from "./runtime.ts"; export type { RuntimeDeps, AssetServer, WsData } from "./runtime.ts"; // Multi-app gateway: route multiple apps by domain behind one port. export { startGateway } from "./gateway.ts"; export type { GatewayApp, GatewayOptions, GatewayAuth, GatewaySecurity, RunningGateway, } from "./gateway.ts"; // Deployment: the portable production handler + the node:http adapter. export { createProductionServer, createProductionHandlers } from "./prod.ts"; // Internal: called only by the generated `.authz-setup.ts` module (see // packages/cli/src/build.ts) to populate the authorization catalog before any // other static import — including app middleware — evaluates. Not meant for // direct use by application code. export { applyAuthzManifestEarly } from "./prod.ts"; export type { AuthzManifestEntry } from "./prod.ts"; export { toRequest, writeResponse, nodeListener, serveNode } from "./adapters/node.ts"; export type { FetchHandler } from "./adapters/node.ts";