/** * Dev-mode asset server for `/__wrnexus/*`: * /__wrnexus/reactive.js the reactive runtime * /__wrnexus/theme.css design-token themes (per resolved theme config) * /__wrnexus/theme.js client theme switcher * /__wrnexus/styles.css bundled global stylesheet (cached) * * Components are `.wrn` files rendered on the server (see runtime.ts), so there * are no per-component browser chunks to build or serve here. The CSS cache is * invalidated in-process by the file watcher so edits show without a restart. */ import { getActionRuntime, getComponentControllerRuntime, getReactiveRuntime, getNavRuntime, getRealtimeRuntime, } from "@wrnexus/csr"; import { renderStyles, renderActiveThemeCss, renderThemeCss, renderThemeRuntime, type ResolvedTheme, type StylesConfig, } from "@wrnexus/styles"; import { getIslandRuntime } from "@wrnexus/react/runtime"; import { VALIDATE_RUNTIME } from "@wrnexus/validation"; import { I18N_RUNTIME } from "@wrnexus/i18n"; import { UPLOAD_RUNTIME, UPLOAD_JS_HREF, UPLOADS_PREFIX, serveStoredFile } from "@wrnexus/uploader"; import type { Mode } from "@wrnexus/core"; import type { AssetServer } from "./runtime.ts"; import { servePublicAsset } from "./public.ts"; import { servePluginAsset, type ServedPluginAsset } from "./plugin-assets.ts"; import { serveIslandArtifact, serveWrnBrowserArtifact } from "./pipeline.ts"; /** Style inputs the dev asset server needs to build `/__wrnexus/styles.css`. */ export interface DevStyles { entry: string | null; config?: StylesConfig; appRoot: string; publicDir?: string; sources?: string[]; entries?: string[]; } /** A dev asset server also supports invalidating its caches in-process. */ export interface DevAssetServer extends AssetServer { invalidateCss(): void; updateSchemas(code: string): void; } function jsResponse(code: string): Response { return new Response(code, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store, max-age=0", pragma: "no-cache", expires: "0", }, }); } function cssResponse(code: string): Response { return new Response(code, { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "no-cache", }, }); } export function createDevAssetServer( appDir: string, mode: Mode, styles?: DevStyles, theme?: ResolvedTheme, uiCss?: string | (() => string), schemasJs?: string, pluginAssets: readonly ServedPluginAsset[] = [], ): DevAssetServer { let cssCache: string | null = null; let schemasCode = schemasJs ?? "window.__wrnSchemas={};"; return { invalidateCss() { cssCache = null; }, updateSchemas(code) { schemasCode = code; }, async serve(pathname: string): Promise { if (pathname.startsWith("/__wrnexus/client/")) { return serveWrnBrowserArtifact(pathname) ?? new Response("Not Found", { status: 404 }); } if (pathname.startsWith("/__wrnexus/island/")) { return serveIslandArtifact(pathname) ?? new Response("Not Found", { status: 404 }); } if (pathname === "/__wrnexus/islands.js") return jsResponse(getIslandRuntime(true)); if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime(true)); if (pathname === "/__wrnexus/controllers.js") return jsResponse(getComponentControllerRuntime(true)); if (pathname === "/__wrnexus/nav.js") return jsResponse(getNavRuntime()); if (pathname === "/__wrnexus/realtime.js") return jsResponse(getRealtimeRuntime()); if (pathname === "/__wrnexus/actions.js") return jsResponse(getActionRuntime()); if (pathname === "/__wrnexus/validate.js") return jsResponse(VALIDATE_RUNTIME); if (pathname === "/__wrnexus/i18n.js") return jsResponse(I18N_RUNTIME); if (pathname === UPLOAD_JS_HREF) return jsResponse(UPLOAD_RUNTIME); // Public local uploads served at /__wrnexus/uploads//. if (pathname.startsWith(UPLOADS_PREFIX)) { return (await serveStoredFile(pathname)) ?? new Response("Not Found", { status: 404 }); } if (pathname === "/__wrnexus/schemas.js") return jsResponse(schemasCode); if (pathname === "/__wrnexus/ui.css") { const currentUiCss = typeof uiCss === "function" ? uiCss() : uiCss; return currentUiCss ? cssResponse(currentUiCss) : new Response("Not Found", { status: 404 }); } if (pathname === "/__wrnexus/theme.css") { return theme ? cssResponse(renderThemeCss(theme)) : new Response("Not Found", { status: 404 }); } const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname); if (activeThemeMatch && theme) { try { const themeName = decodeURIComponent(activeThemeMatch[1]!); const accentPart = decodeURIComponent(activeThemeMatch[2]!); return cssResponse( renderActiveThemeCss(theme, themeName, accentPart === "_" ? undefined : accentPart), ); } catch { return new Response("Not Found", { status: 404 }); } } if (pathname === "/__wrnexus/theme.js") { return theme ? jsResponse(renderThemeRuntime(theme)) : new Response("Not Found", { status: 404 }); } if (pathname === "/__wrnexus/styles.css") { if (!styles?.entry && !styles?.entries?.length) { return new Response("Not Found", { status: 404 }); } if (cssCache === null) { cssCache = await renderStyles( { entryPath: styles.entry, appDir, appRoot: styles.appRoot, mode, sources: styles.sources, entries: styles.entries, }, styles.config, ); } return cssResponse(cssCache); } const pluginAsset = await servePluginAsset(pluginAssets, pathname, mode); if (pluginAsset) return pluginAsset; return servePublicAsset(styles?.publicDir, pathname, mode); }, }; }