/** * `wrnexus build` — production build (Point 4). * * Emits into `/dist`: * islands/.js pre-built, minified island chunks * server.js a self-contained Bun server with a STATIC manifest of * every page/api/realtime/middleware module (no runtime * filesystem scan, no on-the-fly bundling) * * Run it with: bun dist/server.js (PORT env var optional) */ import { createHash } from "node:crypto"; import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { basename, dirname, extname, join, relative, resolve } from "node:path"; import { buildRouter, type Route } from "@wrnexus/router"; import { getReactiveRuntime } from "@wrnexus/csr"; import { analyzeRuntimeImports, analyzeRuntimeRequirements, assertValidAst, generate, parse, type RuntimeRequirements, type DeploymentRuntime, runtimeCapabilities, resolveWrnImports, } from "@wrnexus/compiler"; import { loadAppConfig, headToString, renderProductionFontHead, findStyleEntry, renderStyles, resolveThemeConfig, renderThemeCss, renderThemeRuntime, } from "@wrnexus/styles"; import { uiComponentsDir, uiCss } from "@wrnexus/ui"; import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation"; import { loadLocales, resolveI18n } from "@wrnexus/i18n"; import { pathToFileURL } from "node:url"; import { checkPerformanceBudgets } from "@wrnexus/core"; import { currentCliVersion } from "./update-notifier.ts"; import { createPluginRunner, discoverPlugins, contentTypeForPath, type ClientRuntimeDefinition, type PackageAssetDefinition, type PluginContributions, } from "@wrnexus/plugin"; // Import the production server from the package specifier (not a source path) so // the generated entry resolves whether @wrnexus/dev-server is a workspace or an // installed dependency. Bun.build bundles it into a self-contained server.js. const PROD_MODULE = "@wrnexus/dev-server"; const INLINE_CSS_LIMIT_BYTES = 4096; const fwd = (p: string) => p.replace(/\\/g, "/"); function deploymentRuntime(adapter: string | undefined): DeploymentRuntime | undefined { if (!adapter) return "bun"; if (["bun", "node", "edge", "worker", "service-worker", "browser"].includes(adapter)) return adapter as DeploymentRuntime; return undefined; } export function validateRuntimeCapabilities(appRoot: string, adapter?: string): void { const runtime = deploymentRuntime(adapter); if (!runtime || runtime === "bun" || runtime === "node") return; const appDir = join(resolve(appRoot), "app"); if (!existsSync(appDir)) return; const files: string[] = []; const walk = (directory: string) => { for (const name of readdirSync(directory)) { const file = join(directory, name); const stat = statSync(file); if (stat.isDirectory()) walk(file); else if (/\.(?:[cm]?[jt]s|wrn)$/.test(file)) files.push(file); } }; walk(appDir); const diagnostics = files.flatMap((file) => analyzeRuntimeImports(readFileSync(file, "utf8"), runtime).map( (diagnostic) => `${fwd(file.slice(resolve(appRoot).length + 1))}: ${diagnostic.code} ${diagnostic.message}`, ), ); if (diagnostics.length) throw new Error(`Runtime capability validation failed:\n${diagnostics.join("\n")}`); } export async function runBuild(appRoot: string): Promise { const root = resolve(appRoot); const appDir = join(root, "app"); const distDir = join(root, "dist"); const compiledDir = join(distDir, "compiled"); const reactivePath = join(distDir, "reactive.js"); const publicDir = join(root, "public"); const distPublicDir = join(distDir, "public"); const config = await loadAppConfig(root); validateRuntimeCapabilities(root, config.build?.adapter); console.log(`Building ${appDir} -> ${distDir}`); // Clean output. rmSync(distDir, { recursive: true, force: true }); mkdirSync(compiledDir, { recursive: true }); if (existsSync(publicDir)) { cpSync(publicDir, distPublicDir, { recursive: true }); console.log(`✓ Public: ${distPublicDir}`); } const discoveredPlugins = await discoverPlugins(root, config.plugins, { includeDevDependencies: true, strict: true, warn: (message) => console.warn(`[wrnexus:plugin] ${message}`), runtime: deploymentRuntime(config.build?.adapter), capabilities: [...runtimeCapabilities(deploymentRuntime(config.build?.adapter) ?? "bun")], enforcePermissions: config.pluginPermissions?.enforce, grantedPermissions: config.pluginPermissions?.grants, }); const pluginRunner = createPluginRunner(discoveredPlugins, { root, mode: "production", command: "build", profile: process.env.WRNEXUS_PROFILE, metadata: new Map(), warn: (message) => console.warn(`[wrnexus:plugin] ${message}`), }); await pluginRunner.configure(config as Record); await pluginRunner.configResolved(config as Readonly>); const pluginContributions = await pluginRunner.contributions(); const { generateApplicationTypes } = await import("./types.ts"); generateApplicationTypes(root, pluginContributions); const componentDirs = [uiComponentsDir(), ...pluginContributions.componentDirs]; await pluginRunner.hook("buildStart"); const virtualModules = new Map(); for (const [index, module] of pluginContributions.virtualModules.entries()) { const output = join(compiledDir, `virtual-${index}.ts`); writeFileSync( output, await module.load({ root, mode: "production", command: "build", profile: process.env.WRNEXUS_PROFILE, metadata: new Map(), warn: (message) => console.warn(`[wrnexus:plugin] ${message}`), }), "utf8", ); virtualModules.set(module.id, output); } // `.wrn` route files are compiled once into deterministic intermediate modules. // Plugin AST/code transforms run only when configured, so existing applications // keep the exact compiler path and output contract by default. let compiledCount = 0; const compiledFiles = new Map(); const runtimeAnalysis = new Map(); const partialStaticFiles = new Set(); const compileWrn = async (file: string): Promise => { if (!file.endsWith(".wrn") || compiledFiles.has(file)) return; const out = join(compiledDir, `route${compiledCount++}.ts`); // Reserve the artifact before resolving imports so cycles terminate and // mutually dependent generated modules can point at deterministic paths. compiledFiles.set(file, out); const source = readFileSync(file, "utf8"); let ast = parse(source); assertValidAst(ast, { file, accessibility: true }); ast = await pluginRunner.transformAst(ast, file); if (ast.renderMode === "partial-static") partialStaticFiles.add(file); runtimeAnalysis.set(file, analyzeRuntimeRequirements(ast)); const pluginDiagnostics = await pluginRunner.diagnostics(ast, file); const errors = pluginDiagnostics.filter((diagnostic) => diagnostic.severity === "error"); for (const diagnostic of pluginDiagnostics.filter((item) => item.severity !== "error")) { console.warn(`[${diagnostic.code}] ${file}: ${diagnostic.message}`); } if (errors.length) { throw new Error( errors.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("\n"), ); } try { let code = `// compiled from .wrn\n${generate(ast)}`; code = await pluginRunner.transformCode(code, file); for (const [id, target] of virtualModules) { const sourcePattern = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/"); const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`; code = code.replace( new RegExp(`(["'])${sourcePattern}\\1`, "g"), JSON.stringify(specifier), ); } const resolvedImports = resolveWrnImports(ast.structuredImports, file, { appRoot: root, mode: config.imports?.mode ?? "compatible", aliases: config.imports?.aliases, }); for (const imported of resolvedImports) { if (imported.diagnostic?.severity === "error") { throw new Error(`${imported.diagnostic.code}: ${imported.diagnostic.message}`); } if (!imported.resolved || !imported.declaration.source.startsWith(".")) continue; let target = imported.resolved; if (target.endsWith(".wrn")) { await compileWrn(target); target = compiledFiles.get(target)!; } const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/"); const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`; code = code.replace( new RegExp(`(["'])${sourcePattern}\\1`, "g"), JSON.stringify(specifier), ); } writeFileSync(out, code, "utf8"); } catch (error) { compiledFiles.delete(file); throw error; } }; const importPathFor = (file: string): string => fwd(compiledFiles.get(file) ?? file); // Regenerate typed DB queries (app/db/queries/*.sql → queries.gen.ts) first, so // any page/API importing them is built against the current SQL. const { regenerateQueries } = await import("./db.ts"); const generated = await regenerateQueries(appDir, config.db?.driver); if (generated >= 0) console.log(`✓ Queries: ${generated} (db/queries.gen.ts)`); for (const [name, cfg] of Object.entries(config.databases ?? {})) { const n = await regenerateQueries(appDir, cfg.driver, name); if (n >= 0) console.log(`✓ Queries: ${n} (db/${name}/queries.gen.ts)`); } // Bundle application and package-owned DB migrations into the build. Package // files are namespaced by contribution id so independently installed systems // cannot collide with application migration filenames. const safeMigrationName = (value: string): string => value.trim().replace(/[^a-zA-Z0-9_.-]+/g, "_") || "migration"; let hasDefaultMigrations = false; const namedMigrationDbs = new Set(); const migrationTarget = (database?: string): string => { const target = database?.trim() || "default"; if (target === "default") { if (!config.db) { throw new Error( "WRN-PLUGIN-MIGRATION-DATABASE: a package targets the default database, but config.db is not configured.", ); } hasDefaultMigrations = true; return join(distDir, "migrations"); } if (!config.databases?.[target]) { throw new Error( `WRN-PLUGIN-MIGRATION-DATABASE: package migration targets unknown database '${target}'.`, ); } namedMigrationDbs.add(target); return join(distDir, "db", target, "migrations"); }; const defaultMigrationsSrc = join(appDir, "db", "migrations"); if (config.db && existsSync(defaultMigrationsSrc)) { cpSync(defaultMigrationsSrc, join(distDir, "migrations"), { recursive: true }); hasDefaultMigrations = true; console.log("✓ Migrations: dist/migrations"); } for (const name of Object.keys(config.databases ?? {})) { const source = join(appDir, "db", name, "migrations"); if (!existsSync(source)) continue; cpSync(source, join(distDir, "db", name, "migrations"), { recursive: true }); namedMigrationDbs.add(name); console.log(`✓ Migrations: dist/db/${name}/migrations`); } for (const migration of pluginContributions.migrations) { const destination = migrationTarget(migration.database); mkdirSync(destination, { recursive: true }); const prefix = safeMigrationName(migration.id); if (migration.source !== undefined) { writeFileSync(join(destination, `${prefix}.sql`), migration.source, "utf8"); continue; } if (!migration.entry || !existsSync(migration.entry)) { throw new Error( `WRN-PLUGIN-MIGRATION-MISSING: ${migration.id} points to ${migration.entry ?? ""}.`, ); } const stat = statSync(migration.entry); if (stat.isDirectory()) { for (const file of readdirSync(migration.entry) .filter((name) => name.endsWith(".sql")) .sort()) { cpSync( join(migration.entry, file), join(destination, `${prefix}__${safeMigrationName(file)}`), ); } } else if (stat.isFile() && migration.entry.endsWith(".sql")) { cpSync( migration.entry, join(destination, `${prefix}__${safeMigrationName(basename(migration.entry))}`), ); } else { throw new Error( `WRN-PLUGIN-MIGRATION-ENTRY: ${migration.id} must be a .sql file or directory.`, ); } } if (pluginContributions.migrations.length) { console.log(`✓ Package migrations: ${pluginContributions.migrations.length}`); } const router = buildRouter(appDir, { componentDirs, externalRoutes: pluginContributions.routes, middlewareFiles: pluginContributions.middleware, }); const wrnFiles = new Set([ ...router.pages.map((route) => route.file), ...router.api.map((route) => route.file), ...router.realtime.map((route) => route.file), ...router.components.map((component) => component.file), ...router.layouts.map((layout) => layout.file), ]); for (const file of wrnFiles) await compileWrn(file); const partialShells = new Map(); const partialPages = router.pages.filter((route) => partialStaticFiles.has(route.file)); if (partialPages.length > 0) { const { precomputePartialStaticShell } = await import("@wrnexus/dev-server"); const moduleCache = new Map>(); const loadCompiled = async (file: string): Promise> => { const existing = moduleCache.get(file); if (existing) return existing; const output = compiledFiles.get(file); if (!output) throw new Error(`WRN-PARTIAL-STATIC-MODULE: ${file} was not compiled`); const loaded = (await import(pathToFileURL(output).href)) as Record; moduleCache.set(file, loaded); return loaded; }; const components = await Promise.all( router.components.map(async (component) => ({ name: component.name, mod: await loadCompiled(component.file), })), ); for (const route of partialPages) { const result = await precomputePartialStaticShell(await loadCompiled(route.file), components); partialShells.set(route.raw, result); } writeFileSync( join(distDir, "partial-shells.json"), JSON.stringify(Object.fromEntries(partialShells), null, 2) + "\n", "utf8", ); console.log(`✓ Partial shells: ${partialShells.size} build-time route shell(s)`); } const assetHash = createHash("sha256"); const emittedPluginAssets = await emitPluginAssets( pluginContributions, distDir, config.build?.sourceMaps === true, ); for (const asset of emittedPluginAssets.assets) assetHash.update(readFileSync(asset.file)); if (emittedPluginAssets.assets.length) { console.log( `✓ Plugin assets: ${emittedPluginAssets.assets.length} (${emittedPluginAssets.runtimes.length} runtimes)`, ); } // 1) Components are `.wrn` modules rendered server-side — no browser chunks. // They are compiled + statically imported into the manifest below. const reactiveCode = await buildBrowserRuntime( getReactiveRuntime(), reactivePath, join(compiledDir, "reactive.entry.js"), ); assetHash.update(reactiveCode); console.log(`✓ Runtime: ${reactivePath}`); // 1a) Theme tokens + client switcher (always emitted; built-in light/dark). const theme = resolveThemeConfig(config.theme); const themeCss = renderThemeCss(theme); const themeJs = renderThemeRuntime(theme); writeFileSync(join(distDir, "theme.css"), themeCss, "utf8"); writeFileSync(join(distDir, "theme.js"), themeJs, "utf8"); assetHash.update(themeCss); assetHash.update(themeJs); console.log(`✓ Theme: ${theme.names.length} themes (default: ${theme.default})`); // 1a2) Wire UI stylesheet (all component classes, themed via tokens). const uiStyles = uiCss(); writeFileSync(join(distDir, "ui.css"), uiStyles, "utf8"); const frameworkStyles = `${themeCss}\n${uiStyles}`; writeFileSync(join(distDir, "framework.css"), frameworkStyles, "utf8"); assetHash.update(uiStyles); console.log(`✓ UI: dist/ui.css + framework.css`); // 1a3) Validation: bake schema descriptors into the client script. const descriptors: Record = {}; for (const s of router.schemas) { const mod = (await import(pathToFileURL(s.file).href)) as { default?: ObjectSchema }; if (mod.default && typeof mod.default.describe === "function") { descriptors[s.name] = mod.default.describe(); } } const schemasJs = renderSchemasScript(descriptors); assetHash.update(schemasJs); if (router.schemas.length) console.log(`✓ Schemas: ${router.schemas.length}`); // 1a4) i18n: bake locale messages into the manifest (opt-in via app/locales). const localeMessages = loadLocales(join(appDir, "locales")); const i18n = Object.keys(localeMessages).length ? resolveI18n(localeMessages, config.i18n) : undefined; if (i18n) console.log(`✓ i18n: ${i18n.langs.length} locales (default: ${i18n.default})`); // 1b) Build the global stylesheet, if any. const styleEntry = findStyleEntry(appDir, root, config.styles?.entry); let hasStyles = false; let inlineStyles = ""; const hasPackageStyleEntries = pluginContributions.styles.some((style) => !!style.entry); if (styleEntry || hasPackageStyleEntries) { const css = await renderStyles( { entryPath: styleEntry, appDir, appRoot: root, mode: "production", sources: [ ...componentDirs, ...pluginContributions.styles.flatMap((style) => (style.source ? [style.source] : [])), ], entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])), }, config.styles, ); assetHash.update(css); const combinedCss = `${frameworkStyles}\n${css}`; // One blocking CSS request in production: tokens → UI → application CSS. // The standalone framework.css remains available for apps without global CSS. writeFileSync(join(distDir, "styles.css"), combinedCss, "utf8"); hasStyles = true; if (Buffer.byteLength(combinedCss, "utf8") <= INLINE_CSS_LIMIT_BYTES) { inlineStyles = combinedCss; } console.log(`✓ Styles: ${join(distDir, "styles.css")}`); } const assetVersion = assetHash.digest("hex").slice(0, 12); const headStr = [await renderProductionFontHead(config.fonts), headToString(config.head)] .filter(Boolean) .join("\n "); // 2) Generate a server entry with STATIC imports + a manifest. const imports: string[] = []; let counter = 0; const manifestRoutes = (routes: Route[]): string => { const parts = routes.map((r) => { const v = `m${counter++}`; imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(r.file))};`); const partial = partialShells.get(r.raw); return ` { raw: ${JSON.stringify(r.raw)}, mod: ${v}${partial ? `, staticShell: ${JSON.stringify(partial.shell)}` : ""} },`; }); return parts.length ? `\n${parts.join("\n")}\n ` : ""; }; const pagesLit = manifestRoutes(router.pages); const apiLit = manifestRoutes(router.api); const realtimeLit = manifestRoutes(router.realtime); const mwVars = router.middlewareFiles.map((file) => { const v = `mw${counter++}`; imports.push(`import ${v} from ${JSON.stringify(fwd(file))};`); return v; }); // Components: compile each `.wrn` to a module and statically import it, // keyed by name so the production runtime can render it on demand. const componentsLit = router.components .map((c) => { const v = `c${counter++}`; imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(c.file))};`); return `{ name: ${JSON.stringify(c.name)}, mod: ${v} }`; }) .join(", "); console.log(`✓ Components: ${router.components.length}`); // Named page layouts (app/layouts/*.wrn), compiled + imported like components. const layoutsLit = router.layouts .map((l) => { const v = `c${counter++}`; imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(l.file))};`); return `{ name: ${JSON.stringify(l.name)}, mod: ${v} }`; }) .join(", "); if (router.layouts.length) console.log(`✓ Layouts: ${router.layouts.length}`); const entry = `// AUTO-GENERATED production server entry — do not edit. import { join } from "node:path"; import { createProductionServer } from ${JSON.stringify(PROD_MODULE)}; ${imports.join("\n")} await createProductionServer( { pages: [${pagesLit}], api: [${apiLit}], realtime: [${realtimeLit}], middleware: [${mwVars.join(", ")}], components: [${componentsLit}], layouts: [${layoutsLit}], }, { reactivePath: join(import.meta.dir, "reactive.js"), themePath: join(import.meta.dir, "theme.css"), themeJsPath: join(import.meta.dir, "theme.js"), theme: ${JSON.stringify(theme)}, uiCssPath: join(import.meta.dir, "ui.css"), frameworkCssPath: join(import.meta.dir, "framework.css"), schemasJs: ${JSON.stringify(schemasJs)}, i18n: ${i18n ? JSON.stringify(i18n) : "undefined"}, db: ${config.db ? JSON.stringify(config.db) : "undefined"}, databases: ${config.databases ? JSON.stringify(config.databases) : "undefined"}, storage: ${config.storage ? JSON.stringify(config.storage) : "undefined"}, ${hasDefaultMigrations ? `migrationsDir: join(import.meta.dir, "migrations"),` : ""} ${ namedMigrationDbs.size ? `databaseMigrationDirs: { ${[...namedMigrationDbs] .map( (n) => `${JSON.stringify(n)}: join(import.meta.dir, "db", ${JSON.stringify(n)}, "migrations")`, ) .join(", ")} },` : "" } realtime: ${config.realtime ? JSON.stringify(config.realtime) : "undefined"}, publicDir: join(import.meta.dir, "public"), ${hasStyles ? `stylesPath: join(import.meta.dir, "styles.css"),` : ""} ${hasStyles ? `stylesIncludeFramework: true,` : ""} ${inlineStyles ? `inlineStyles: ${JSON.stringify(inlineStyles)},` : ""} assetVersion: ${JSON.stringify(assetVersion)}, clientRuntimes: ${JSON.stringify(emittedPluginAssets.runtimes)}, pluginAssets: ${renderProductionPluginAssets(emittedPluginAssets.assets)}, head: ${JSON.stringify(headStr)}, seo: ${JSON.stringify(config.seo ?? {})}, mobile: ${JSON.stringify(config.mobile ?? {})}, pwa: ${JSON.stringify(config.pwa ?? {})}, security: ${JSON.stringify(config.security ?? {})}, observability: ${JSON.stringify(config.observability ?? {})}, tenancy: ${JSON.stringify(config.tenancy ?? {})}, navigation: ${JSON.stringify(config.navigation ?? {})}, developmentRuntime: process.env.WRNEXUS_PRODUCTION_DEV === "1", }, ); `; const entryPath = join(distDir, ".server-entry.ts"); writeFileSync(entryPath, entry, "utf8"); // 3) Bundle the entry into a single self-contained, minified server.js // (target bun). This also minifies every bundled page/component/route module. const result = await Bun.build({ entrypoints: [entryPath], target: "bun", format: "esm", minify: true, sourcemap: config.build?.sourceMaps ? "inline" : "none", }); if (!result.success) { throw new Error("Server build failed:\n" + result.logs.map(String).join("\n")); } writeFileSync(join(distDir, "server.js"), await result.outputs[0]!.text(), "utf8"); const report = createBuildReport({ root, distDir, publicDir: distPublicDir, adapter: config.build?.adapter ?? "bun", routes: [ ...router.pages.map((route) => ({ kind: "page" as const, route })), ...router.api.map((route) => ({ kind: "api" as const, route })), ...router.realtime.map((route) => ({ kind: "realtime" as const, route })), ], runtimeFile: reactivePath, cssFile: hasStyles ? join(distDir, "styles.css") : join(distDir, "framework.css"), runtimeAnalysis, }); report.pluginAssets = emittedPluginAssets.assets.map((asset) => ({ id: asset.id, publicPath: asset.publicPath, bytes: statSync(asset.file).size, runtime: asset.runtime, })); report.plugins = pluginRunner.plugins.map((plugin) => ({ name: plugin.name, version: plugin.version, })); report.clientRuntimes = emittedPluginAssets.runtimes.map((runtime) => ({ id: runtime.id, publicPath: runtime.publicPath!, type: runtime.type ?? "module", load: runtime.load ?? "defer", })); report.migrations = pluginContributions.migrations.map((migration) => ({ id: migration.id, database: migration.database ?? "default", source: migration.entry ?? "inline", })); report.componentDirs = componentDirs.map(fwd); report.partialStaticShells = [...partialShells].map(([route, value]) => ({ route, regions: value.regions, bytes: Buffer.byteLength(value.shell, "utf8"), })); const violations = checkPerformanceBudgets( config.performance?.budgets ?? {}, report.measurements, ); report.budgetViolations = violations; writeFileSync(join(distDir, "build-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8"); const contributedAdapter = pluginContributions.deploymentAdapters.find( (adapter) => adapter.name === config.build?.adapter, ); if (contributedAdapter) await contributedAdapter.build( { distDir, report }, { root, mode: "production", command: "build", profile: process.env.WRNEXUS_PROFILE, metadata: new Map(), warn: (message) => console.warn(`[wrnexus:plugin] ${message}`), }, ); await pluginRunner.hook("buildEnd", report); console.log(`✓ Server: ${join(distDir, "server.js")}`); console.log( `✓ Routes: ${router.pages.length} pages, ${router.api.length} api, ${router.realtime.length} realtime, ${mwVars.length} middleware`, ); console.log(`✓ Report: ${join(distDir, "build-report.json")}`); if (violations.length) { for (const violation of violations) { console.warn( `⚠ Budget ${violation.metric}: ${violation.actual} > ${violation.budget} (+${violation.overBy})`, ); } if (config.performance?.enforcement === "error") { throw new Error( `WRN-PERFORMANCE-BUDGET: ${violations.length} production budget(s) exceeded.`, ); } } console.log(`\nRun it: bun ${fwd(join(distDir, "server.js"))}`); } interface EmittedPluginAsset { id: string; publicPath: string; file: string; contentType: string; runtime: boolean; immutable: boolean; } interface EmittedPluginAssets { assets: EmittedPluginAsset[]; runtimes: ClientRuntimeDefinition[]; } function safeAssetName(id: string): string { return ( id .replace(/^@/, "") .replace(/[^A-Za-z0-9_-]+/g, "-") .replace(/^-+|-+$/g, "") || "asset" ); } function sourceBytes(source: string | Uint8Array): Uint8Array { return typeof source === "string" ? new TextEncoder().encode(source) : source; } async function compileRuntimeSource( runtime: ClientRuntimeDefinition, sourceMaps: boolean, ): Promise { if (runtime.source !== undefined) return sourceBytes(runtime.source); if (!runtime.entry) throw new Error(`Runtime '${runtime.id}' has no entry.`); const shouldBundle = runtime.bundle ?? /\.[cm]?tsx?$/.test(runtime.entry); if (!shouldBundle) return readFileSync(runtime.entry); const result = await Bun.build({ entrypoints: [runtime.entry], target: "browser", format: runtime.type === "script" ? "iife" : "esm", minify: true, sourcemap: sourceMaps ? "inline" : "none", }); if (!result.success || !result.outputs[0]) { throw new Error( `Client runtime '${runtime.id}' failed to build:\n${result.logs.map(String).join("\n")}`, ); } return new Uint8Array(await result.outputs[0].arrayBuffer()); } async function rawAssetSource(asset: PackageAssetDefinition): Promise { if (asset.source !== undefined) return sourceBytes(asset.source); if (!asset.entry) throw new Error(`Asset '${asset.id}' has no entry.`); return readFileSync(asset.entry); } async function emitPluginAssets( contributions: PluginContributions, distDir: string, sourceMaps: boolean, ): Promise { const outputDir = join(distDir, "plugin-assets"); mkdirSync(outputDir, { recursive: true }); const assets: EmittedPluginAsset[] = []; const runtimes: ClientRuntimeDefinition[] = []; for (const runtime of contributions.clientRuntimes) { const bytes = await compileRuntimeSource(runtime, sourceMaps); const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 12); const filename = `${safeAssetName(runtime.id)}.${hash}.js`; const file = join(outputDir, filename); writeFileSync(file, bytes); const publicPath = `/__wrnexus/assets/${filename}`; assets.push({ id: runtime.id, publicPath, file, contentType: "text/javascript; charset=utf-8", runtime: true, immutable: true, }); runtimes.push({ ...runtime, entry: undefined, source: undefined, publicPath }); } for (const asset of contributions.assets) { const bytes = await rawAssetSource(asset); const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 12); const extension = extname(asset.entry ?? asset.publicPath ?? "") || ""; const filename = `${safeAssetName(asset.id)}.${hash}${extension}`; const file = join(outputDir, filename); writeFileSync(file, bytes); // Package assets keep their declared public URL so component markup, CSS, // and server responses do not need build-time string rewriting. The disk // filename is still content-addressed to make deployments atomic. assets.push({ id: asset.id, publicPath: asset.publicPath!, file, contentType: asset.contentType ?? contentTypeForPath(asset.entry ?? asset.publicPath ?? ""), runtime: false, immutable: asset.immutable ?? false, }); } return { assets, runtimes }; } function renderProductionPluginAssets(assets: readonly EmittedPluginAsset[]): string { if (!assets.length) return "{}"; const entries = assets.map( (asset) => `${JSON.stringify(asset.publicPath)}: { path: join(import.meta.dir, "plugin-assets", ${JSON.stringify(asset.file.split(/[\\/]/).pop())}), contentType: ${JSON.stringify(asset.contentType)}, immutable: ${asset.immutable} }`, ); return `{ ${entries.join(", ")} }`; } interface BuildReport { frameworkVersion: string; plugins?: Array<{ name: string; version?: string }>; pluginAssets?: Array<{ id: string; publicPath: string; bytes: number; runtime: boolean }>; clientRuntimes?: Array<{ id: string; publicPath: string; type: string; load: string }>; migrations?: Array<{ id: string; database: string; source: string }>; componentDirs?: string[]; partialStaticShells?: Array<{ route: string; regions: number; bytes: number }>; generatedAt: string; root: string; adapter: string; routes: Array<{ kind: "page" | "api" | "realtime"; path: string; source: string; sourceBytes: number; dynamicParams: string[]; execution: RuntimeRequirements["kind"]; canPrerender: boolean; needsClientRuntime: boolean; needsServerRuntime: boolean; hydrationStrategy: string | null; reasons: string[]; optimization: RuntimeRequirements["optimization"]; cachePolicy: Record; requiredPermission: string | null; }>; assets: Array<{ file: string; bytes: number }>; measurements: { routeJsBytes: number; routeCssBytes: number; imageBytes: number }; budgetViolations: ReturnType; } function fileBytes(file: string): number { return existsSync(file) && statSync(file).isFile() ? statSync(file).size : 0; } function walkFiles(dir: string): string[] { if (!existsSync(dir)) return []; const files: string[] = []; for (const entry of readdirSync(dir)) { const path = join(dir, entry); const stat = statSync(path); if (stat.isDirectory()) files.push(...walkFiles(path)); else if (stat.isFile()) files.push(path); } return files; } function createBuildReport(input: { root: string; distDir: string; publicDir: string; adapter: string; routes: Array<{ kind: "page" | "api" | "realtime"; route: Route }>; runtimeFile: string; cssFile: string; runtimeAnalysis: ReadonlyMap; }): BuildReport { const assets = walkFiles(input.distDir) .filter((file) => !file.endsWith("build-report.json") && !fwd(file).includes("/compiled/")) .map((file) => ({ file: fwd(file.slice(input.distDir.length + 1)), bytes: fileBytes(file) })) .sort((a, b) => b.bytes - a.bytes); const imageExtensions = /\.(?:avif|gif|jpe?g|png|svg|webp)$/i; const imageBytes = Math.max( 0, ...walkFiles(input.publicDir) .filter((file) => imageExtensions.test(file)) .map(fileBytes), ); return { frameworkVersion: currentCliVersion(), generatedAt: new Date().toISOString(), root: input.root, adapter: input.adapter, routes: input.routes.map(({ kind, route }) => ({ kind, path: route.raw, source: fwd(route.file.replace(input.root, "").replace(/^\//, "")), sourceBytes: fileBytes(route.file), dynamicParams: route.paramNames, execution: input.runtimeAnalysis.get(route.file)?.kind ?? "dynamic", canPrerender: input.runtimeAnalysis.get(route.file)?.canPrerender ?? false, needsClientRuntime: input.runtimeAnalysis.get(route.file)?.needsClientRuntime ?? true, needsServerRuntime: input.runtimeAnalysis.get(route.file)?.needsServerRuntime ?? true, hydrationStrategy: input.runtimeAnalysis.get(route.file)?.hydrationStrategy ?? null, reasons: input.runtimeAnalysis.get(route.file)?.reasons ?? [ "runtime requirements unavailable", ], optimization: input.runtimeAnalysis.get(route.file)?.optimization ?? { staticNodes: 0, reactiveRegions: 0, eliminatedBranches: 0, unusedState: [], unusedHandlers: [], constantProps: [], unusedLocalCssClasses: [], batchableStateUpdates: 0, memoizableComponents: [], preloadDependencies: [], serverOnlyModules: [], }, cachePolicy: input.runtimeAnalysis.get(route.file)?.cachePolicy ?? {}, requiredPermission: input.runtimeAnalysis.get(route.file)?.requiredPermission ?? null, })), assets, measurements: { routeJsBytes: fileBytes(input.runtimeFile), routeCssBytes: fileBytes(input.cssFile), imageBytes, }, budgetViolations: [], }; } async function buildBrowserRuntime( source: string, outFile: string, entryFile: string, ): Promise { writeFileSync(entryFile, source, "utf8"); const result = await Bun.build({ entrypoints: [entryFile], target: "browser", format: "esm", minify: true, }); if (!result.success) { throw new Error("Runtime build failed:\n" + result.logs.map(String).join("\n")); } const code = await result.outputs[0]!.text(); writeFileSync(outFile, code, "utf8"); return code; }