/** * Request pipeline helpers: middleware execution and safe module loading. * These are deliberately runtime-agnostic (no Bun APIs) so they could run on * Node too. */ import { pathToFileURL } from "node:url"; import { copyFileSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, existsSync, } from "node:fs"; import { dirname, join, basename, extname, relative, resolve } from "node:path"; import { compile, generate, generateTargets, buildIslands, islandNamesFrom, resolveWrnImports, type PageAst, type ViewNode, } from "@wrnexus/compiler"; import type { Context, Middleware } from "@wrnexus/core"; /** * Run an onion-style middleware chain, ending in `final` (the route handler). * Each middleware receives `next`; calling it advances the chain. A middleware * may short-circuit by returning a Response without calling `next`. */ export function runMiddleware( middlewares: Middleware[], ctx: Context, final: () => Promise | Response, ): Promise { let lastIndex = -1; const dispatch = (index: number): Promise => { if (index <= lastIndex) { return Promise.reject(new Error("next() called multiple times")); } lastIndex = index; const mw = middlewares[index]; if (!mw) return Promise.resolve(final()); return Promise.resolve(mw(ctx, () => dispatch(index + 1))); }; return dispatch(0); } /** * Cache of imported route modules. Modules are only ever loaded from absolute * paths discovered during the startup scan — never from request input. */ const moduleCache = new Map>>(); const moduleVersions = new Map(); const browserArtifactPaths = new Map(); const islandArtifactPaths = new Map(); type ImportMode = "legacy" | "compatible" | "explicit"; interface CompileImportOptions { mode: ImportMode; aliases: Record; autoImport: boolean; } const compileImportOptions = new Map(); const warnedImportDiagnostics = new Set(); interface DevCompilerPipeline { transformAst(ast: PageAst, file: string): Promise; transformCode(code: string, file: string): Promise; virtualModules: Map; } let devCompilerPipeline: DevCompilerPipeline | null = null; /** Install the configured plugin compiler pipeline for development compilation. */ export function setDevCompilerPipeline(pipeline: DevCompilerPipeline | null): void { devCompilerPipeline = pipeline; } export function setCompileImportOptions( appRoot: string, options: { mode?: ImportMode; aliases?: Record; autoImport?: boolean } = {}, ): void { compileImportOptions.set(resolve(appRoot), { mode: options.mode ?? "compatible", aliases: { "@": "./app", ...(options.aliases ?? {}) }, autoImport: options.autoImport ?? true, }); } const compileInProgress = new Map(); function isApplicationImportSource( source: string, aliases: Record | undefined, ): boolean { if (source.startsWith(".")) return true; return Object.keys(aliases ?? {}).some( (alias) => source === alias || source.startsWith(`${alias}/`), ); } function projectRootForFile(file: string): string { let current = dirname(resolve(file)); while (true) { if (existsSync(join(current, "app"))) return current; const parent = dirname(current); if (parent === current) return dirname(resolve(file)); current = parent; } } function importOptionsHash(file: string): string { const root = resolve(projectRootForFile(file)); const options = compileImportOptions.get(root) ?? { mode: "compatible" as const, aliases: { "@": "./app" }, autoImport: true, }; const aliases = Object.fromEntries( Object.entries(options.aliases).sort(([left], [right]) => left.localeCompare(right)), ); return hashPath(JSON.stringify({ ...options, aliases })); } /** Island bundles already built this dev session, keyed by resolved source. */ const builtIslands = new Map(); /** Island name -> resolved .tsx source, so the watcher can rebuild on edit. */ const islandSources = new Map(); let islandAppRoot: string | null = null; let islandRuntimeBuilt = false; /** * Builds island bundles on demand in dev and registers them under * /__wrnexus/island/. Without this the browser bootstrap 404s and no island * ever mounts. */ async function ensureIslandArtifacts( islands: Array<{ name: string; sourcePath: string }>, appRoot: string, ): Promise { if (islands.length === 0) return; const outDir = join(appRoot, ".wrnexus", "island"); islandAppRoot = appRoot; for (const island of islands) islandSources.set(island.name, island.sourcePath); // Keyed by source AND mtime: keying on the path alone serves a stale bundle // forever once an island .tsx is edited. const stamp = (island: { name: string; sourcePath: string }) => { let mtime: number; try { mtime = statSync(island.sourcePath).mtimeMs; } catch { mtime = 0; } return `${island.sourcePath}:${mtime}`; }; const pending = islands.filter((island) => builtIslands.get(island.name) !== stamp(island)); if (pending.length === 0 && islandRuntimeBuilt) return; // The runtime is built alongside the islands so they share one React copy. const result = await buildIslands({ islands: pending.length ? pending : islands, outDir, appRoot, }); registerIslandArtifact("/__wrnexus/island/runtime.js", join(outDir, "runtime.js")); islandRuntimeBuilt = true; for (const asset of result.assets) { registerIslandArtifact(`/__wrnexus/island/${asset.name}.js`, asset.path); const rebuilt = pending.find((island) => island.name === asset.name); if (rebuilt) builtIslands.set(asset.name, stamp(rebuilt)); } for (const chunk of result.sharedChunks) { registerIslandArtifact(`/__wrnexus/island/${basename(chunk)}`, chunk); } } /** * Island imports in this file: names so codegen emits placeholders instead of * component mounts, and sources so the bundles can be built. */ /** * Rebuilds islands whose .tsx source changed. * * Page modules are cached after the first request, so no compile runs on a * later request and nothing else would notice an island edit. */ export async function rebuildChangedIslands(changed: string[]): Promise { if (!islandAppRoot) return false; const touched = new Set(changed.map((file) => resolve(file))); const affected = [...islandSources] .filter(([, sourcePath]) => touched.has(resolve(sourcePath))) .map(([name, sourcePath]) => ({ name, sourcePath })); if (affected.length === 0) return false; await ensureIslandArtifacts(affected, islandAppRoot); return true; } function islandsForFile( ast: PageAst, importer: string, ): { names: Set; inputs: Array<{ name: string; sourcePath: string }> } { if (!ast.structuredImports.length) return { names: new Set(), inputs: [] }; const root = projectRootForFile(importer); const importOptions = compileImportOptions.get(resolve(root)) ?? { mode: "compatible" as const, aliases: { "@": "./app" }, autoImport: true, }; const resolved = resolveWrnImports(ast.structuredImports, importer, { appRoot: root, mode: importOptions.mode, aliases: importOptions.aliases, }); const inputs = resolved .filter((entry) => entry.kind === "island" && entry.resolved && entry.declaration.defaultImport) .map((entry) => ({ name: entry.declaration.defaultImport!, sourcePath: entry.resolved! })); return { names: islandNamesFrom(resolved), inputs }; } function rewriteArtifactImports( code: string, ast: PageAst, importer: string, target: "main" | "server" | "browser", ): string { if (!ast.structuredImports.length) return code; const root = projectRootForFile(importer); const importOptions = compileImportOptions.get(resolve(root)) ?? { mode: "compatible" as const, aliases: { "@": "./app" }, autoImport: true, }; const resolved = resolveWrnImports(ast.structuredImports, importer, { appRoot: root, mode: importOptions.mode, aliases: importOptions.aliases, }); let output = code; for (const entry of resolved) { if (entry.diagnostic) { const key = `${importer}:${entry.diagnostic.code}:${entry.declaration.source}`; if (entry.diagnostic.severity === "error") { throw new Error(`${entry.diagnostic.code}: ${entry.diagnostic.message}`); } if (!warnedImportDiagnostics.has(key)) { warnedImportDiagnostics.add(key); console.warn(`[wrnexus] ${entry.diagnostic.code}: ${entry.diagnostic.message}`); } } if (!entry.resolved || !entry.declaration.source) continue; if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue; let replacement = entry.resolved; if (entry.resolved.endsWith(".wrn")) { const dependencySource = readFileSync(entry.resolved, "utf8"); const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource); const dependency = compileWrnArtifacts( entry.resolved, moduleVersions.get(entry.resolved) ?? 0, ); if (target === "browser") { if (!isStore) { // Components and layouts are compile-time dependencies in browser modules. output = output.replace(entry.declaration.raw, ""); continue; } replacement = dependency.browser; } else { replacement = target === "server" ? dependency.server : dependency.main; } } const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const specifier = target === "browser" ? (() => { const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus"); const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/"); return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`; })() : replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href; output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier)); } return output; } async function rewriteArtifactImportsAsync( code: string, ast: PageAst, importer: string, target: "main" | "server" | "browser", ): Promise { let output = code; for (const [id, replacement] of devCompilerPipeline?.virtualModules ?? []) { const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const specifier = target === "browser" ? (() => { const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus"); const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/"); return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`; })() : pathToFileURL(replacement).href; output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier)); } if (!ast.structuredImports.length) return output; const root = projectRootForFile(importer); const importOptions = compileImportOptions.get(resolve(root)) ?? { mode: "compatible" as const, aliases: { "@": "./app" }, autoImport: true, }; const resolved = resolveWrnImports(ast.structuredImports, importer, { appRoot: root, mode: importOptions.mode, aliases: importOptions.aliases, }); for (const entry of resolved) { if (entry.diagnostic?.severity === "error") throw new Error(entry.diagnostic.message); if (!entry.resolved || !entry.declaration.source) continue; if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue; let replacement = entry.resolved; if (replacement.endsWith(".wrn")) { const dependencySource = readFileSync(replacement, "utf8"); const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource); const dependency = await compileWrnArtifactsAsync( replacement, moduleVersions.get(replacement) ?? 0, ); if (target === "browser") { if (!isStore) { output = output.replace(entry.declaration.raw, ""); continue; } replacement = dependency.browser; } else replacement = target === "server" ? dependency.server : dependency.main; } const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const specifier = target === "browser" ? (() => { const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus"); const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/"); return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`; })() : replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href; output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier)); } return output; } export async function loadModule(file: string): Promise> { file = resolve(file); let mod = moduleCache.get(file); if (!mod) { mod = (async () => { const version = moduleVersions.get(file) ?? 0; // `.wrn` files are compiled to TypeScript first, then imported. let target = file.endsWith(".wrn") ? (await compileWrnArtifactsAsync(file, version)).main : file; let temporary = false; // Bun intentionally caches local TS/JS modules by filesystem path and ignores // URL query strings. A short-lived versioned sibling keeps relative imports // correct while giving the changed module a genuinely new import identity. if (version && !file.endsWith(".wrn")) { const extension = extname(file); const stem = basename(file, extension); target = join(dirname(file), `${stem}.wrnexus-hmr-${version}${extension}`); copyFileSync(file, target); temporary = true; } // pathToFileURL handles Windows drive letters and spaces correctly. let imported = import(pathToFileURL(target).href) as Promise>; if (temporary) { imported = imported.finally(() => { try { unlinkSync(target); } catch { /* best-effort cleanup after Bun has loaded the module */ } }); } return imported; })(); moduleCache.set(file, mod); } return mod; } /** * A single cache dir for ALL `.wrn` compilation (set once at server start). * When unset, compilation falls back to a sibling `.wrnexus/` next to each file. */ let compileCacheDir: string | null = null; const WRN_COMPILE_CACHE_VERSION = "v3"; /** * Point all `.wrn` compilation at ONE cache dir (typically `/.wrnexus`) * instead of scattering a `.wrnexus/` folder next to every `.wrn` source. Called * once by the dev server at startup. */ export function setCompileCacheDir(dir: string | null): void { compileCacheDir = dir; } /** FNV-1a hash of a string → short base36, to make unique flat cache filenames. */ function hashPath(s: string): string { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return (h >>> 0).toString(36); } /** * Compile a `.wrn` file to a `.ts` file inside the shared `.wrnexus/` cache dir and * return the generated path. The cache dir is hidden, so the router never re-scans * it and the dev watcher ignores it. Output names are flat + hash-suffixed by the * absolute source path, so `.wrn` files from anywhere (the app AND node_modules UI * components) share one cache dir without colliding. Generated modules are * self-contained (no relative imports), so the cache location doesn't affect them. */ function importedValueBindings(ast: PageAst): Set { const names = new Set(); for (const entry of ast.structuredImports) { if (entry.typeOnly) continue; if (entry.defaultImport) names.add(entry.defaultImport); if (entry.namespaceImport) names.add(entry.namespaceImport); for (const item of entry.namedImports) if (!item.typeOnly) names.add(item.local); } return names; } function viewComponentNames(nodes: ViewNode[], names = new Set()): Set { for (const node of nodes) { if (node.type === "element") { if (/^[A-Z]/.test(node.tag)) names.add(node.tag); viewComponentNames(node.children, names); } else if (node.type === "each") { viewComponentNames(node.body, names); viewComponentNames(node.empty, names); } else if (node.type === "if") { for (const branch of node.branches) viewComponentNames(branch.body, names); } } return names; } function validateConfiguredImports(_source: string, ast: PageAst, file: string): void { const root = projectRootForFile(file); const options = compileImportOptions.get(resolve(root)); if (!options || options.mode === "legacy") return; const imported = importedValueBindings(ast); const usedComponents = viewComponentNames(ast.view); const compilerBuiltins = new Set([ "Async", "Component", "Error", "Loading", "Portal", "Success", "Transition", ]); const missing = [...usedComponents].filter( (name) => !compilerBuiltins.has(name) && !imported.has(name), ); if (ast.layoutIsSymbol && ast.layout && !imported.has(ast.layout)) missing.push(ast.layout); if (!missing.length) return; const unique = [...new Set(missing)]; const message = `WRN-IMPORT-IMPLICIT: ${file} uses ${unique.join(", ")} without explicit imports.`; if (options.mode === "explicit") throw new Error(message); const key = `${file}:WRN-IMPORT-IMPLICIT:${unique.join(",")}`; if (!warnedImportDiagnostics.has(key)) { warnedImportDiagnostics.add(key); console.warn(`[wrnexus] ${message}`); } } export interface WrnCompileArtifacts { main: string; browser: string; server: string; declarations: string; contract: string; rpc: string; /** Island inputs for this file, so a cache hit can still build islands. */ islands: string; } export interface WrnCompileMetrics { hits: number; misses: number; compilations: number; errors: number; totalDurationMs: number; lastDurationMs: number; } const compileMetrics: WrnCompileMetrics = { hits: 0, misses: 0, compilations: 0, errors: 0, totalDurationMs: 0, lastDurationMs: 0, }; const asyncCompileInProgress = new Map>(); async function bundleBrowserArtifact( code: string, file: string, cacheDir: string, stem: string, ): Promise { const hasModuleImport = /(?:^|\n)\s*import(?:\s|["'])|\bimport\s*\(/m.test(code); if (!hasModuleImport || code.includes("wrnexus-client-bundled")) return code; const bun = (globalThis as any).Bun; if (!bun?.build) { throw new Error( `WRN-CLIENT-BUNDLE: ${file} has browser imports, but the Bun bundler is unavailable.`, ); } const entry = join(cacheDir, `${stem}.browser-entry.mjs`); writeFileSync(entry, code, "utf8"); try { const result = await bun.build({ entrypoints: [entry], target: "browser", format: "esm", splitting: false, minify: false, sourcemap: "inline", }); if (!result.success || !result.outputs?.length) { const detail = (result.logs ?? []).map(String).join("\n"); throw new Error(`WRN-CLIENT-BUNDLE: failed to bundle ${file}${detail ? `\n${detail}` : ""}`); } return `// wrnexus-client-bundled\n${await result.outputs[0].text()}`; } finally { try { unlinkSync(entry); } catch { // Best-effort cleanup; cache pruning removes stale temporary entries. } } } export function compileWrnArtifactsAsync(file: string, version = 0): Promise { const key = `${file}:${version}`; const active = asyncCompileInProgress.get(key); if (active) return active; const task = (async () => { if (!devCompilerPipeline) { const artifacts = compileWrnArtifacts(file, version); const code = readFileSync(artifacts.browser, "utf8"); const bundled = await bundleBrowserArtifact( code, file, dirname(artifacts.browser), basename(artifacts.browser, ".client.mjs"), ); if (bundled !== code) writeFileSync(artifacts.browser, bundled, "utf8"); return artifacts; } const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus"); const name = basename(file).replace(/\.wrn$/, ""); const suffix = version ? `-hmr-${version}` : ""; const source = readFileSync(file, "utf8"); // Plugin output affects the artifact, so use a separate cache generation. const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-plugin-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`; const artifacts: WrnCompileArtifacts = { main: join(cacheDir, `${stem}.wrn.ts`), browser: join(cacheDir, `${stem}.client.mjs`), server: join(cacheDir, `${stem}.server.ts`), declarations: join(cacheDir, `${stem}.d.ts`), contract: join(cacheDir, `${stem}.contract.json`), rpc: join(cacheDir, `${stem}.rpc.json`), islands: join(cacheDir, `${stem}.islands.json`), }; const result = compile(source, file); validateConfiguredImports(source, result.ast, file); const ast = await devCompilerPipeline!.transformAst(result.ast, file); const { names: islands, inputs: islandInputs } = islandsForFile(ast, file); mkdirSync(cacheDir, { recursive: true }); writeFileSync(artifacts.islands, JSON.stringify(islandInputs), "utf8"); await ensureIslandArtifacts(islandInputs, projectRootForFile(file)); const targets = generateTargets(ast); mkdirSync(cacheDir, { recursive: true }); const browserPath = `/__wrnexus/client/${stem}.mjs`; const outputs = { main: `// compiled from .wrn\n${generate(ast, { islands })}`.replaceAll( "__WRNEXUS_CLIENT_MODULE__", browserPath, ), browser: targets.browser, server: targets.server, declarations: targets.declarations, }; for (const target of ["main", "browser", "server"] as const) { const rewritten = await rewriteArtifactImportsAsync(outputs[target], ast, file, target); let transformed = await devCompilerPipeline!.transformCode(rewritten, file); if (target === "browser") { transformed = await bundleBrowserArtifact(transformed, file, cacheDir, stem); } writeFileSync(artifacts[target], transformed, "utf8"); } writeFileSync( artifacts.declarations, await devCompilerPipeline!.transformCode(outputs.declarations, file), "utf8", ); writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8"); writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8"); browserArtifactPaths.set(browserPath, artifacts.browser); compileMetrics.compilations++; return artifacts; })().finally(() => asyncCompileInProgress.delete(key)); asyncCompileInProgress.set(key, task); return task; } export function getWrnCompileMetrics(): Readonly { return { ...compileMetrics }; } export function resetWrnCompileMetrics(): void { Object.assign(compileMetrics, { hits: 0, misses: 0, compilations: 0, errors: 0, totalDurationMs: 0, lastDurationMs: 0, }); } export function compileWrnArtifacts(file: string, version = 0): WrnCompileArtifacts { const active = compileInProgress.get(file); if (active) return active; const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus"); const name = basename(file).replace(/\.wrn$/, ""); const suffix = version ? `-hmr-${version}` : ""; const source = readFileSync(file, "utf8"); const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`; const artifacts: WrnCompileArtifacts = { main: join(cacheDir, `${stem}.wrn.ts`), browser: join(cacheDir, `${stem}.client.mjs`), server: join(cacheDir, `${stem}.server.ts`), declarations: join(cacheDir, `${stem}.d.ts`), contract: join(cacheDir, `${stem}.contract.json`), rpc: join(cacheDir, `${stem}.rpc.json`), islands: join(cacheDir, `${stem}.islands.json`), }; compileInProgress.set(file, artifacts); try { try { // The islands manifest is written only by the async compile path, so it // is not part of the completeness check — a missing manifest means "no // islands known for this file", not a stale cache. const requiredArtifacts = Object.entries(artifacts) .filter(([key]) => key !== "islands") .map(([, path]) => path); if (requiredArtifacts.every((path) => statSync(path).isFile())) { compileMetrics.hits++; browserArtifactPaths.set(`/__wrnexus/client/${stem}.mjs`, artifacts.browser); // A cached .wrn still needs its island bundles: the .tsx may have changed // since, and after a restart with a warm cache nothing else would build them. let cachedIslands: Array<{ name: string; sourcePath: string }> = []; try { cachedIslands = JSON.parse(readFileSync(artifacts.islands, "utf8")) as Array<{ name: string; sourcePath: string; }>; } catch { cachedIslands = []; } // This variant is synchronous, so the rebuild is kicked off rather than // awaited. The async compile path awaits it before serving a page. void ensureIslandArtifacts(cachedIslands, projectRootForFile(file)).catch((error) => { console.warn("[wrnexus] island rebuild failed", error); }); return artifacts; } } catch { // Compile missing artifact set below. } compileMetrics.misses++; const started = performance.now(); try { const result = compile(source, file); validateConfiguredImports(source, result.ast, file); const targets = generateTargets(result.ast); mkdirSync(cacheDir, { recursive: true }); const browserPath = `/__wrnexus/client/${stem}.mjs`; const mainCode = rewriteArtifactImports( result.code.replaceAll("__WRNEXUS_CLIENT_MODULE__", browserPath), result.ast, file, "main", ); writeFileSync(artifacts.main, mainCode, "utf8"); writeFileSync( artifacts.browser, rewriteArtifactImports(targets.browser, result.ast, file, "browser"), "utf8", ); browserArtifactPaths.set(browserPath, artifacts.browser); writeFileSync( artifacts.server, rewriteArtifactImports(targets.server, result.ast, file, "server"), "utf8", ); writeFileSync(artifacts.declarations, targets.declarations, "utf8"); writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8"); writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8"); compileMetrics.compilations++; return artifacts; } catch (error) { compileMetrics.errors++; throw error; } finally { const duration = performance.now() - started; compileMetrics.lastDurationMs = duration; compileMetrics.totalDurationMs += duration; } } finally { compileInProgress.delete(file); } } export async function loadWrnServerModule(file: string): Promise> { const version = moduleVersions.get(file) ?? 0; const artifact = compileWrnArtifacts(file, version).server; return import(pathToFileURL(artifact).href) as Promise>; } export function wrnBrowserArtifact(file: string): string { return compileWrnArtifacts(file, moduleVersions.get(file) ?? 0).browser; } export function wrnBrowserArtifactUrl(file: string): string { const artifact = compileWrnArtifacts(file, moduleVersions.get(file) ?? 0).browser; return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`; } /** Async browser artifact URL used by HMR so imported client modules are bundled before delivery. */ export async function wrnBrowserArtifactUrlAsync(file: string): Promise { const artifact = (await compileWrnArtifactsAsync(file, moduleVersions.get(file) ?? 0)).browser; return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`; } export function serveWrnBrowserArtifact(pathname: string): Response | null { const artifact = browserArtifactPaths.get(pathname); if (!artifact || !existsSync(artifact)) return null; return new Response(readFileSync(artifact, "utf8"), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store, max-age=0", pragma: "no-cache", expires: "0", }, }); } /** Registers a built island asset for serving under `/__wrnexus/island/`. */ export function registerIslandArtifact(pathname: string, artifact: string): void { islandArtifactPaths.set(pathname, artifact); } /** Serves a built island bundle, chunk, or the island mount runtime. */ export function serveIslandArtifact(pathname: string): Response | null { const artifact = islandArtifactPaths.get(pathname); if (!artifact || !existsSync(artifact)) return null; return new Response(readFileSync(artifact, "utf8"), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store, max-age=0", pragma: "no-cache", expires: "0", }, }); } /** Forget one module and force its next dynamic import to bypass Bun's import cache. */ export function invalidateModule(file: string): void { file = resolve(file); moduleCache.delete(file); moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1); } /** Forget cached modules (used by build/dev tooling if needed). */ export function clearModuleCache(): void { for (const file of moduleCache.keys()) { moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1); } moduleCache.clear(); }