/** * Shared request runtime used by BOTH the dev server and the production server. * * It owns the HTTP/WebSocket dispatch and the SSR document assembly, but knows * nothing about *how* modules or assets are produced — those come in via * `RuntimeDeps`. Dev wires in dynamic module loading + on-the-fly bundling; * prod wires in a static manifest + pre-built chunks on disk. */ import { bridgeRealtime, createContext, createCorsPreflightResponse, createRealtimeRegistry, csrfToken, etag, isRoomDefinition, isWebSocketOriginAllowed, notModified, isSafeRequestPath, renderError, renderNotFound, withContextHeaders, withSecurityHeaders, resolveRequestUrl, tenantMiddleware, tracingMiddleware, type Context, type Middleware, type Mode, type PageMeta, type RealtimeBus, type RealtimeConnectMeta, type SecurityConfig, type SeoConfig, type TFunction, } from "@wrnexus/core"; import type { Router } from "@wrnexus/router"; import { renderDocument } from "@wrnexus/ssr"; import { THEME_COOKIE, THEME_CSS_HREF, THEME_JS_HREF, resolveThemeName, type ResolvedTheme, type MobileConfig, type PwaConfig, type ObservabilityConfig, type TenancyConfig, } from "@wrnexus/styles"; import { LANG_COOKIE, I18N_JS_HREF, makeT, resolveLang, translateHtml, type ResolvedI18n, } from "@wrnexus/i18n"; import { runMiddleware } from "./pipeline.ts"; import type { HmrHub } from "./hmr.ts"; import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types"; import { createServerIssue, handleDevToolbarRoute, issueFromError, type DevToolbarCollector, } from "@wrnexus/dev-toolbar/server"; /** HTTP methods we recognize as API handler exports. */ const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const; /** A realtime module's `websocket` export: a bag of optional lifecycle hooks. */ export type WsHandler = Record unknown>; /** * Per-connection socket data. A socket is either an app realtime connection or * an internal HMR connection — discriminated by `kind`. */ export type WsData = | { kind: "realtime"; handler: WsHandler } // legacy raw `websocket` export | { kind: "room"; meta: RealtimeConnectMeta } // `defineRoom` default export | { kind: "hmr"; baseUrl: string; headers: [string, string][] }; type RouteModule = Record; type ApiRegistry = Record; interface CsrBinding { id: string; method?: string; path: string; body?: string; expr?: string; helpers?: string; } /** Serves framework-owned assets under `/__wrnexus/*` (islands, reactive, hmr). */ export interface AssetServer { serve(pathname: string): Promise; } export interface RuntimeDeps { mode: Mode; /** When true, inject the live-reload client into rendered pages. */ hmr: boolean; router: Router; /** Load a route module by absolute path (dev: dynamic import; prod: manifest). */ loadModule(file: string): Promise; /** Resolve the ordered middleware chain. */ getMiddleware(): Promise; /** Serve `/__wrnexus/*` assets. */ assets: AssetServer; /** When true, inject the global stylesheet link into every page head. */ hasStyles?: boolean; /** When true, inject the Wire UI stylesheet link (`/__wrnexus/ui.css`). */ hasUi?: boolean; /** Production build combined theme + UI stylesheet. */ hasFrameworkStyles?: boolean; /** App stylesheet already contains theme + UI CSS and is the only CSS request needed. */ stylesIncludeFramework?: boolean; /** Resolved theme config: enables `/__wrnexus/theme.css` + ``. */ theme?: ResolvedTheme; /** Resolved i18n bundle: enables `ctx.t`, ``, and `{t:key}` markers. */ i18n?: ResolvedI18n; /** Small production stylesheets can be inlined to avoid a render-blocking request. */ inlineStyles?: string; /** Production cache-busting version appended to framework asset URLs. */ assetVersion?: string; /** Raw HTML appended to every page head (e.g. CDN framework links). */ 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; /** Max request body size in bytes (413 above this). Default 10 MB. */ maxBodyBytes?: number; /** HMR hub for browser live-update sockets (dev only). */ hub?: HmrHub; /** * Cross-process realtime bus. When provided, room broadcasts/`toUser` sends are * bridged to it so they reach clients on every app process/instance sharing the * bus (use the Redis pub/sub driver). Enables realtime across multiple apps. */ realtimeBus?: RealtimeBus; devToolbar?: { config: DevToolbarConfig; collector: DevToolbarCollector; root: string; }; } const DEV_TOOLBAR_SCRIPT = ''; function shouldEnableDevToolbar(mode: string, deps: RuntimeDeps): boolean { return ( mode === "development" && deps.devToolbar !== undefined && deps.devToolbar.config.enabled !== false ); } function tenantIdentityFromConfig( config: TenancyConfig, ): (ctx: Context) => Promise<{ id: string; slug?: string } | null> { return async (ctx) => { const host = ctx.url.hostname.toLowerCase(); if (config.mode === "domain") return host ? { id: host, slug: host } : null; if (config.mode === "path") { const segments = ctx.url.pathname.split("/").filter(Boolean); const prefix = config.pathPrefix?.replace(/^\/+|\/+$/g, ""); const slug = prefix ? (segments[0] === prefix ? segments[1] : undefined) : segments[0]; return slug ? { id: slug, slug } : null; } if (config.mode === "subdomain" || config.mode === undefined) { const roots = config.rootDomains?.map((domain) => domain.toLowerCase()) ?? []; const root = roots.find((domain) => host === domain || host.endsWith(`.${domain}`)); const slug = root ? host.slice(0, -(root.length + 1)) : host.split(".")[0]; if (!slug || slug === host || slug === "www" || slug === "localhost") return null; return { id: slug, slug }; } return null; }; } function frameworkMiddleware(deps: RuntimeDeps): Middleware[] { const middleware: Middleware[] = []; if (deps.observability && deps.observability.enabled !== false) { middleware.push( tracingMiddleware(undefined, { sampleRate: deps.observability.sampleRate, serverTiming: deps.observability.serverTiming, onComplete: deps.observability.exporter === "console" ? (ctx, records) => { const total = records.find((record) => record.name === "http.request")?.durationMs; console.log( `[wrnexus:trace] ${ctx.req.method} ${ctx.url.pathname} ${total?.toFixed(2) ?? "0.00"}ms`, ); } : undefined, }), ); } if (deps.tenancy && deps.tenancy.mode !== "custom") { middleware.push( tenantMiddleware(tenantIdentityFromConfig(deps.tenancy), { required: deps.tenancy.required, }), ); } return middleware; } function shouldReportNotFound(pathname: string): boolean { return !( pathname.startsWith("/__wrnexus/") || pathname.startsWith("/.well-known/") || pathname === "/favicon.ico" || pathname.endsWith(".map") ); } export const PWA_CLIENT = `if ("serviceWorker" in navigator) { var swUrl = "/sw.js"; if (window.trustedTypes) { try { swUrl = window.trustedTypes.createPolicy("wrnexus-pwa", { createScriptURL: function(value) { return value; } }).createScriptURL(swUrl); } catch (_) {} } addEventListener("load", () => navigator.serviceWorker.register(swUrl).catch(() => {})); }`; function renderPwaServiceWorker(pwa: PwaConfig): string { const offlineUrl = pwa.offlineUrl ?? pwa.startUrl ?? "/"; const cacheUrls = [...new Set([offlineUrl, ...(pwa.cacheUrls ?? [])])]; return `const CACHE = ${JSON.stringify(pwa.cacheName ?? "wrnexus-pwa-v1")}; const OFFLINE_URL = ${JSON.stringify(offlineUrl)}; const PRECACHE_URLS = ${JSON.stringify(cacheUrls)}; self.addEventListener("install", event => { event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(PRECACHE_URLS)).catch(() => {})); self.skipWaiting(); }); self.addEventListener("activate", event => event.waitUntil( caches.keys().then(keys => Promise.all(keys.filter(key => key.startsWith("wrnexus-pwa-") && key !== CACHE).map(key => caches.delete(key)))) .then(() => self.clients.claim()) )); self.addEventListener("fetch", event => { if (event.request.method !== "GET" || event.request.mode !== "navigate") return; event.respondWith(fetch(event.request).then(response => { if (response.ok) { const copy = response.clone(); caches.open(CACHE).then(cache => cache.put(event.request, copy)); } return response; }).catch(() => caches.match(event.request).then(hit => hit || caches.match(OFFLINE_URL)))); });`; } const DEFAULT_PWA_ICON = ` `; export const MOBILE_CLIENT = `const isNativeApp = window.Capacitor?.isNativePlatform?.() === true || ["android", "ios"].includes(window.Capacitor?.getPlatform?.()); const nativeTarget = isNativeApp ? "mobile" : "browser"; const nativeCapabilities = new Map(); const registerNative = (name, adapters) => nativeCapabilities.set(name, adapters); const unregisterNative = name => nativeCapabilities.delete(name); const nativeAdapter = name => nativeCapabilities.get(name)?.[nativeTarget]; const nativeSupports = name => { const adapter = nativeAdapter(name); return !!adapter && (adapter.supported?.() ?? true); }; const nativeRun = async (name, options) => { const adapter = nativeAdapter(name); if (!adapter || !(adapter.supported?.() ?? true)) throw new Error('Native capability "' + name + '" is unavailable on ' + nativeTarget); return adapter.run(options); }; const cap = (plugin, method) => ({ supported: () => typeof window.Capacitor?.Plugins?.[plugin]?.[method] === "function", run: options => window.Capacitor.Plugins[plugin][method](options) }); const capWith = (plugin, method, transform) => ({ supported: () => typeof window.Capacitor?.Plugins?.[plugin]?.[method] === "function", run: options => window.Capacitor.Plugins[plugin][method](transform(options)) }); registerNative("clipboard.write", { browser: { supported: () => !!navigator.clipboard?.writeText, run: o => navigator.clipboard.writeText(String(o?.text ?? "")) }, mobile: capWith("Clipboard", "write", o => ({ string: String(o?.text ?? o?.string ?? "") })) }); registerNative("share", { browser: { supported: () => typeof navigator.share === "function", run: o => navigator.share(o) }, mobile: cap("Share", "share") }); registerNative("geolocation", { browser: { supported: () => !!navigator.geolocation, run: o => new Promise((resolve, reject) => navigator.geolocation.getCurrentPosition(resolve, reject, o)) }, mobile: cap("Geolocation", "getCurrentPosition") }); registerNative("network", { browser: { run: () => ({ connected: navigator.onLine }) }, mobile: cap("Network", "getStatus") }); registerNative("camera", { browser: { supported: () => !!navigator.mediaDevices?.getUserMedia, run: o => navigator.mediaDevices.getUserMedia({ video: o ?? true, audio: false }) }, mobile: cap("Camera", "getPhoto") }); registerNative("haptics", { mobile: cap("Haptics", "impact") }); nativeCapabilities.get("haptics").browser = { supported: () => typeof navigator.vibrate === "function", run: o => navigator.vibrate(o?.duration ?? 20) }; registerNative("storage.get", { browser: { run: o => ({ value: localStorage.getItem(String(o?.key ?? "")) }) }, mobile: cap("Preferences", "get") }); registerNative("storage.set", { browser: { run: o => localStorage.setItem(String(o?.key ?? ""), String(o?.value ?? "")) }, mobile: cap("Preferences", "set") }); registerNative("filesystem.read", { browser: { supported: () => "showOpenFilePicker" in window, run: async () => { const [handle] = await window.showOpenFilePicker(); return handle?.getFile(); } }, mobile: cap("Filesystem", "readFile") }); registerNative("filesystem.write", { browser: { supported: () => "showSaveFilePicker" in window, run: async o => { const handle = await window.showSaveFilePicker(); const writable = await handle.createWritable(); await writable.write(o?.data ?? ""); await writable.close(); } }, mobile: cap("Filesystem", "writeFile") }); registerNative("notifications.schedule", { browser: { supported: () => typeof Notification !== "undefined", run: async o => { if (Notification.permission === "default") await Notification.requestPermission(); if (Notification.permission !== "granted") throw new Error("Notification permission was not granted"); return new Notification(o?.title ?? "Notification", { body: o?.body }); } }, mobile: capWith("LocalNotifications", "schedule", o => o?.notifications ? o : ({ notifications: [{ id: o?.id ?? Date.now() % 2147483647, title: o?.title ?? "Notification", body: o?.body ?? "", schedule: o?.schedule }] })) }); registerNative("device.info", { browser: { run: () => ({ platform: "web", userAgent: navigator.userAgent, language: navigator.language }) }, mobile: cap("Device", "getInfo") }); window.__WrNexusNativePending?.forEach((adapters, name) => registerNative(name, adapters)); delete window.__WrNexusNativePending; window.WrNexusNative = { target: nativeTarget, isMobile: isNativeApp, register: registerNative, unregister: unregisterNative, supports: nativeSupports, run: nativeRun }; const revealMobileOnly = root => { const elements = []; if (root instanceof Element) elements.push(root); root.querySelectorAll?.("[data-mobile-only],[data-native-only],[data-native-requires],[data-native-unsupported]").forEach(element => elements.push(element)); elements.forEach(element => { if (element.matches("[data-mobile-only]")) element.hidden = !isNativeApp; const only = element.getAttribute("data-native-only"); if (only) element.hidden = !((only === "mobile" && isNativeApp) || (["browser", "web"].includes(only) && !isNativeApp) || only === window.Capacitor?.getPlatform?.()); const required = element.getAttribute("data-native-requires"); if (required) element.hidden = !nativeSupports(required); const unsupported = element.getAttribute("data-native-unsupported"); if (unsupported) element.hidden = nativeSupports(unsupported); }); }; revealMobileOnly(document); new MutationObserver(records => { records.forEach(record => record.addedNodes.forEach(node => { revealMobileOnly(node); installNativeEvents(node); })); }).observe(document.documentElement, { childList: true, subtree: true }); const handleNativeEvent = async event => { const eventSelector = "[data-on-wrnexus-browser-" + event.type + "],[data-on-wrnexus-mobile-" + event.type + "]"; const actionSelector = event.type === "click" ? ",[data-native-browser],[data-native-mobile]" : ""; const element = event.target.closest?.(eventSelector + actionSelector); if (!element) return; const platformEvent = "wrnexus-" + nativeTarget + "-" + event.type; if (element.hasAttribute("data-on-" + platformEvent)) element.dispatchEvent(new CustomEvent(platformEvent)); if (event.type !== "click") return; const capability = element.getAttribute("data-native-" + nativeTarget); if (!capability) return; let options = {}; const raw = element.getAttribute("data-native-options"); if (raw) try { options = JSON.parse(raw); } catch (error) { element.dispatchEvent(new CustomEvent("wrnexus:native-error", { detail: error, bubbles: true })); return; } try { const result = await nativeRun(capability, options); element.dispatchEvent(new CustomEvent("wrnexus:native-success", { detail: { capability, result }, bubbles: true })); } catch (error) { element.dispatchEvent(new CustomEvent("wrnexus:native-error", { detail: { capability, error }, bubbles: true })); } }; const nativeEventTypes = new Set(); const listenNativeEvent = type => { if (nativeEventTypes.has(type)) return; nativeEventTypes.add(type); document.addEventListener(type, handleNativeEvent, true); }; const installNativeEvents = root => { const elements = []; if (root instanceof Element) elements.push(root); root.querySelectorAll?.("*").forEach(element => elements.push(element)); elements.forEach(element => Array.from(element.attributes).forEach(attribute => { const match = /^data-on-wrnexus-(?:browser|mobile)-(.+)$/.exec(attribute.name); if (match) listenNativeEvent(match[1]); })); }; listenNativeEvent("click"); installNativeEvents(document); const openWebCamera = async image => { if (!navigator.mediaDevices?.getUserMedia) return false; let stream; try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } }, audio: false }); const overlay = document.createElement("div"); Object.assign(overlay.style, { position: "fixed", inset: "0", zIndex: "2147483647", display: "grid", placeItems: "center", padding: "24px", background: "rgba(2,6,23,.92)" }); const panel = document.createElement("div"); Object.assign(panel.style, { width: "min(720px,100%)", color: "white" }); const video = document.createElement("video"); video.autoplay = true; video.muted = true; video.playsInline = true; video.srcObject = stream; Object.assign(video.style, { display: "block", width: "100%", maxHeight: "70vh", objectFit: "contain", borderRadius: "16px", background: "black" }); const controls = document.createElement("div"); Object.assign(controls.style, { display: "flex", gap: "12px", marginTop: "16px" }); const capture = document.createElement("button"); capture.type = "button"; capture.textContent = "Capture photo"; const cancel = document.createElement("button"); cancel.type = "button"; cancel.textContent = "Cancel"; [capture, cancel].forEach(control => Object.assign(control.style, { flex: "1", padding: "14px", border: "0", borderRadius: "12px", fontWeight: "700", cursor: "pointer" })); capture.style.background = "#6366f1"; capture.style.color = "white"; const close = () => { stream.getTracks().forEach(track => track.stop()); overlay.remove(); }; capture.onclick = () => { const canvas = document.createElement("canvas"); canvas.width = video.videoWidth || 1280; canvas.height = video.videoHeight || 720; canvas.getContext("2d")?.drawImage(video, 0, 0, canvas.width, canvas.height); if (image) image.src = canvas.toDataURL("image/jpeg", 0.92); close(); }; cancel.onclick = close; controls.append(capture, cancel); panel.append(video, controls); overlay.append(panel); document.body.append(overlay); await video.play(); return true; } catch (error) { stream?.getTracks().forEach(track => track.stop()); return false; } }; document.addEventListener("click", async event => { const button = event.target.closest?.("[data-mobile-camera]"); if (!button) return; const selector = button.getAttribute("data-mobile-camera") || "[data-mobile-photo]"; const image = document.querySelector(selector); try { const camera = window.Capacitor?.Plugins?.Camera; if (isNativeApp && camera) { const photo = await camera.getPhoto({ quality: 90, resultType: "uri", source: "camera" }); if (image) image.src = photo.webPath || photo.path; return; } if (await openWebCamera(image)) return; const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.setAttribute("capture", "environment"); input.hidden = true; input.onchange = () => { if (image && input.files?.[0]) image.src = URL.createObjectURL(input.files[0]); input.remove(); }; document.body.append(input); input.click(); } catch (error) { window.dispatchEvent(new CustomEvent("wrnexus:camera-error", { detail: error })); } }); const nativeApp = window.Capacitor?.Plugins?.App; nativeApp?.addListener?.("backButton", ({ canGoBack }) => { if (canGoBack || history.length > 1) history.back(); // At the root screen intentionally do nothing instead of closing the app. });`; function revealMobileOnlyHtml(html: string): string { return html.replace(/<[^>]*\bdata-mobile-only\b[^>]*>/gi, (tag) => tag.replace(/\s+hidden(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/i, ""), ); } /** The global stylesheet URL (served by the asset server when styles exist). */ export const STYLES_HREF = "/__wrnexus/styles.css"; /** * Inline HMR client (WebSocket). Goals: * - CSS change -> hot-swap the stylesheet, zero reload, zero flash. * - markup/page -> ask over the existing HMR WebSocket for fresh HTML and * MORPH the live DOM in place, without restarting the server. * - island code -> same WebSocket sync path; no location.reload(). */ export const HMR_CLIENT_JS = ` (function () { if (!("WebSocket" in window)) return; var proto = location.protocol === "https:" ? "wss" : "ws"; var url = proto + "://" + location.host + "/__wrnexus/hmr"; var ws, openedBefore = false, timer, pendingSync = false; function connect() { ws = new WebSocket(url); ws.onopen = function () { if (openedBefore) requestSync(); // reconnect == server restarted openedBefore = true; }; ws.onmessage = function (e) { var msg; try { msg = JSON.parse(e.data); } catch (_) { return; } if (msg.channel === "toolbar") { window.dispatchEvent(new CustomEvent("wrnexus:toolbar-message", { detail: msg })); } else if (msg.type === "css") { swapCss(msg.version); window.dispatchEvent(new CustomEvent("wrnexus:hmr", { detail: msg })); } else if (msg.type === "reload") requestSync(); else if (msg.type === "html") applyHtml(msg.html); else if (msg.type === "error") console.error("[wrnexus] HMR update failed:", msg.message); }; ws.onclose = function () { clearTimeout(timer); timer = setTimeout(connect, 400); }; ws.onerror = function () { try { ws.close(); } catch (_) {} }; } function send(message) { if (!ws || ws.readyState !== WebSocket.OPEN) return false; ws.send(JSON.stringify(message)); return true; } function requestSync() { if (pendingSync) return; pendingSync = true; if (!send({ type: "sync", path: location.pathname + location.search })) { pendingSync = false; } } function swapCss(version) { var links = document.querySelectorAll('link[rel="stylesheet"]'); for (var i = 0; i < links.length; i++) (function (link) { var href = link.getAttribute("href"); if (!href || href.charAt(0) !== "/") return; // skip CDN links var next = href.split("?")[0] + "?hmr=" + encodeURIComponent(String(version || "css")); var copy = link.cloneNode(false); copy.setAttribute("href", next); copy.addEventListener("load", function () { if (link.parentNode) link.parentNode.removeChild(link); }); link.parentNode.insertBefore(copy, link.nextSibling); })(links[i]); } function applyHtml(html) { pendingSync = false; var doc = new DOMParser().parseFromString(html, "text/html"); if (doc.title) document.title = doc.title; var from = document.getElementById("app"); var to = doc.getElementById("app"); if (from && to) morph(from, to); swapCss("html"); // Components are re-rendered server-side into the new HTML; re-hydrate // their reactive scopes (and any browser-side API fetches) in place. if (window.__wrnexusHydrateScopes) window.__wrnexusHydrateScopes(document); if (window.__wrnexusHydrateCsrFetches) window.__wrnexusHydrateCsrFetches(document); window.dispatchEvent(new CustomEvent("wrnexus:hmr", { detail: { type: "html" } })); } // Minimal index-based DOM morph: preserve matching nodes (keeps state/focus), // patch text and attributes, clone genuinely new nodes, drop removed ones. // Hydrated subtrees (reactive scopes) are CLIENT-OWNED and left untouched, // so live state (e.g. a counter at 5) is never reset to the SSR 0. function morph(from, to) { if (from.__wrnexusHydrated) return; syncAttrs(from, to); var fc = from.childNodes, tc = to.childNodes, i; for (i = 0; i < tc.length; i++) { var t = tc[i], f = fc[i]; if (!f) { from.appendChild(t.cloneNode(true)); continue; } if (f.nodeType !== t.nodeType || (f.nodeType === 1 && f.nodeName !== t.nodeName)) { from.replaceChild(t.cloneNode(true), f); continue; } if (f.nodeType === 3 || f.nodeType === 8) { if (f.nodeValue !== t.nodeValue) f.nodeValue = t.nodeValue; continue; } if (f.nodeType === 1) morph(f, t); } while (from.childNodes.length > tc.length) from.removeChild(from.lastChild); } function syncAttrs(from, to) { var ta = to.attributes, fa = from.attributes, i; for (i = 0; i < ta.length; i++) if (from.getAttribute(ta[i].name) !== ta[i].value) from.setAttribute(ta[i].name, ta[i].value); for (i = fa.length - 1; i >= 0; i--) if (!to.hasAttribute(fa[i].name)) from.removeAttribute(fa[i].name); } // Exposed for tests; harmless (the client is injected only in dev). window.__wrnexusHmr = { morph: morph, swapCss: swapCss, requestSync: requestSync, applyHtml: applyHtml }; connect(); })(); `; /** A base64 CSP nonce for this request's inline scripts. */ function randomNonce(): string { const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); let bin = ""; for (const b of bytes) bin += String.fromCharCode(b); return btoa(bin); } /** The dev HMR client as a nonce-tagged inline script (strict-CSP friendly). */ function hmrClientTag(nonce: string): string { return ``; } /** 403 for a rejected cross-site WebSocket handshake. */ function forbiddenOrigin(): Response { return new Response("Forbidden WebSocket origin", { status: 403 }); } export interface UpgradeServer { upgrade(req: Request, opts: { data: WsData }): boolean; /** Bun's per-request socket peer address (used for the non-spoofable client IP). */ requestIP?(req: Request): { address: string } | null; } /** The subset of Bun's ServerWebSocket the runtime touches. */ export interface Ws { data: WsData; send(data: string | Uint8Array): unknown; close(code?: number, reason?: string): void; } export interface Handlers { fetch(req: Request, server: UpgradeServer): Promise; websocket: { open(ws: Ws): void; message(ws: Ws, message: string | Uint8Array): void; close(ws: Ws, code?: number, reason?: string): void; drain(ws: Ws): void; }; } /** Build the fetch + websocket handlers from a set of dependencies. */ export function createHandlers(deps: RuntimeDeps): Handlers { const { mode, hmr, router, loadModule, getMiddleware, assets } = deps; const builtInMiddleware = frameworkMiddleware(deps); const resolveMiddleware = async (): Promise => [ ...builtInMiddleware, ...(await getMiddleware()), ]; const maxBodyBytes = deps.maxBodyBytes ?? 10 * 1024 * 1024; // 10 MB default // Server-side realtime room manager (shared by every `defineRoom` connection). const realtime = createRealtimeRegistry(); // Bridge across processes/instances when a pub/sub bus is provided, so room // broadcasts reach clients on every app run sharing the bus (Redis in prod). if (deps.realtimeBus) bridgeRealtime(realtime, deps.realtimeBus); // Global additions, identical on every page (styles are global). // Theme tokens load first so global.css and component styles can override them. // Order: theme tokens, then Wire UI, then the app stylesheet — so the app's // own CSS (loaded last) can override both the tokens and the UI classes. const headParts: string[] = []; if (deps.hasFrameworkStyles && !deps.stylesIncludeFramework) { headParts.push( ``, ); } else if (deps.theme && !deps.stylesIncludeFramework) { const themeHref = versionAssetUrl(THEME_CSS_HREF, deps.assetVersion); headParts.push(``); } if (deps.hasUi && !deps.hasFrameworkStyles && !deps.stylesIncludeFramework) { headParts.push( ``, ); } if (deps.inlineStyles) { headParts.push(``); } else if (deps.hasStyles) { const stylesHref = versionAssetUrl(STYLES_HREF, deps.assetVersion); headParts.push(``); headParts.push(``); } if (deps.head) headParts.push(deps.head); const extraHead = headParts.join("\n ") || undefined; const pwaEnabled = deps.pwa !== false && deps.pwa?.enabled !== false; const pwaConfig: PwaConfig = deps.pwa && typeof deps.pwa === "object" ? deps.pwa : {}; const pwaServiceWorkerEnabled = pwaEnabled && pwaConfig.serviceWorker !== false; const configuredPermissions = deps.security?.permissionsPolicy; const runtimeSecurity: SecurityConfig | undefined = deps.mobile?.enabled === false ? deps.security : { ...deps.security, permissionsPolicy: configuredPermissions === false ? false : { camera: ["self"], ...(configuredPermissions ?? {}) }, }; async function fetchHandler(req: Request, server: UpgradeServer): Promise { // Behind a trusted proxy (nginx / gateway), honor X-Forwarded-Proto/Host so // ctx.url reflects the external HTTPS scheme — makes CSRF/session cookies Secure. const url = resolveRequestUrl(req, deps.security?.trustProxy); const nonce = randomNonce(); const secure = (res: Response): Response => withSecurityHeaders(req, res, mode, runtimeSecurity, nonce); const preflight = createCorsPreflightResponse(req, deps.security); if (preflight) return secure(preflight); // Health check — unauthenticated, skips the middleware pipeline. if (url.pathname === "/healthz" || url.pathname === "/__wrnexus/health") { return secure(Response.json({ status: "ok" })); } if (url.pathname === "/site.webmanifest" && pwaEnabled) { const pwa = pwaConfig; return secure( Response.json( { id: pwa.id ?? pwa.startUrl ?? "/", name: pwa.name ?? deps.seo?.title ?? "WrNexus App", short_name: pwa.shortName ?? pwa.name ?? deps.seo?.title ?? "WrNexus", description: pwa.description ?? deps.seo?.description, start_url: pwa.startUrl ?? "/", scope: pwa.scope ?? "/", lang: pwa.lang ?? "en", display: pwa.display ?? "standalone", orientation: pwa.orientation ?? "any", theme_color: pwa.themeColor ?? deps.seo?.themeColor ?? "#6c8cff", background_color: pwa.backgroundColor ?? "#0f172a", icons: pwa.icons ?? [ { src: "/__wrnexus/pwa-icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any maskable", }, ], categories: pwa.categories, screenshots: pwa.screenshots?.map((screenshot) => ({ src: screenshot.src, sizes: screenshot.sizes, type: screenshot.type, form_factor: screenshot.formFactor, label: screenshot.label, })), shortcuts: pwa.shortcuts?.map((shortcut) => ({ name: shortcut.name, short_name: shortcut.shortName, description: shortcut.description, url: shortcut.url, icons: shortcut.icons, })), }, { headers: { "content-type": "application/manifest+json", "cache-control": "no-cache" } }, ), ); } if (url.pathname === "/__wrnexus/pwa-icon.svg" && pwaEnabled) { return secure( new Response(DEFAULT_PWA_ICON, { headers: { "content-type": "image/svg+xml", "cache-control": "public, max-age=86400" }, }), ); } if (url.pathname === "/sw.js" && pwaServiceWorkerEnabled) { return secure( new Response(renderPwaServiceWorker(pwaConfig), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-cache", }, }), ); } if (url.pathname === "/__wrnexus/pwa.js" && pwaServiceWorkerEnabled) { return secure( new Response(PWA_CLIENT, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": url.searchParams.has("v") ? "public, max-age=31536000, immutable" : "no-cache", }, }), ); } if (url.pathname === "/__wrnexus/mobile.js" && deps.mobile?.enabled !== false) { return secure( new Response(MOBILE_CLIENT, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": url.searchParams.has("v") ? "public, max-age=31536000, immutable" : "no-cache", }, }), ); } // Reject oversized request bodies early (DoS guard). const contentLength = Number(req.headers.get("content-length") ?? 0); if (contentLength > maxBodyBytes) { return secure(new Response("Payload Too Large", { status: 413 })); } if (deps.devToolbar) { const toolbarResponse = await handleDevToolbarRoute(req, { mode, root: deps.devToolbar.root, collector: deps.devToolbar.collector, editor: deps.devToolbar.config.editor, allowOpenEditor: deps.devToolbar.config.openEditor !== false, }); if (toolbarResponse) return secure(toolbarResponse); } // --- HMR socket (dev only): upgrade before anything else. --- if (hmr && url.pathname === "/__wrnexus/hmr") { if (!isWebSocketOriginAllowed(req, deps.security)) return secure(forbiddenOrigin()); const headers = Array.from(req.headers.entries()).filter( ([name]) => !/^sec-websocket-|^connection$|^upgrade$/i.test(name), ); if (server.upgrade(req, { data: { kind: "hmr", baseUrl: url.origin, headers } })) { return undefined; } return secure(new Response("Expected a WebSocket upgrade request", { status: 426 })); } // --- Realtime: try to upgrade BEFORE the normal HTTP pipeline. --- const rt = router.matchRealtime(url.pathname); if (rt) { // Reject cross-site WebSocket handshakes (CSWSH) before doing any work. if (!isWebSocketOriginAllowed(req, deps.security)) return secure(forbiddenOrigin()); const mod = await loadModule(rt.route.file); let data: WsData; if (isRoomDefinition(mod.default)) { // Identify the connection: an authenticated session user id, else ?user=. const wsCtx = createContext(req, url); const sessionUser = wsCtx.session.get<{ id?: unknown }>("user"); const user = sessionUser && typeof sessionUser === "object" && sessionUser.id != null ? String(sessionUser.id) : (url.searchParams.get("user") ?? undefined); const query = Object.fromEntries(url.searchParams); // Room-level gate (e.g. require auth) — reject before accepting the socket. const allowed = await mod.default.handlers.authorize?.({ user, query, headers: req.headers, }); if (allowed === false) { return secure(new Response("Forbidden", { status: 403 })); } data = { kind: "room", meta: { room: url.pathname, def: mod.default, query, user } }; } else if (mod.websocket && typeof mod.websocket === "object") { data = { kind: "realtime", handler: mod.websocket as WsHandler }; } else { return secure( new Response("Realtime route needs a default defineRoom() or a `websocket` export", { status: 500, }), ); } const upgraded = server.upgrade(req, { data }); if (upgraded) return undefined; // Bun takes over the socket. return secure(new Response("Expected a WebSocket upgrade request", { status: 426 })); } try { const ctx = createContext(req, url); ctx.ip = server.requestIP?.(req)?.address ?? undefined; ctx.locals.cspNonce = nonce; // available to pages for their own inline scripts // Resolve the request language so both pages and API can translate. if (deps.i18n) { ctx.lang = resolveLang( deps.i18n, ctx.cookies.get(LANG_COOKIE), req.headers.get("accept-language"), ); ctx.t = makeT(deps.i18n, ctx.lang); } const mws = await resolveMiddleware(); const res = secure( withContextHeaders(ctx, await runMiddleware(mws, ctx, () => dispatch(ctx))), ); return compressResponse(req, res); } catch (err) { const app = process.env.WRNEXUS_APP_NAME ?? "app"; const detail = err instanceof Error ? (err.stack ?? `${err.name}: ${err.message}`) : String(err); const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err); console.error( `[wrnexus] unhandled request error (${app}) ${req.method} ${url.pathname}\n${detail}`, ); deps.devToolbar?.collector.add( issueFromError(err, { ruleId: "server/request-error", category: "server", title: "Request processing failed", pathname: url.pathname, }), ); const response = secure(renderError(err, mode)); // Gateway-managed production apps bind to loopback. Carry a bounded, // encoded diagnostic to the parent gateway so centralized log collectors // can explain child failures; the gateway always strips this header. response.headers.set("x-wrnexus-internal-error", encodeURIComponent(message.slice(0, 500))); return compressResponse(req, response); } } async function dispatch(ctx: Context): Promise { const { pathname } = ctx.url; // Framework-owned assets (island chunks, reactive runtime, HMR stream). if (pathname === "/__wrnexus/csr") { return handleCsrBinding(ctx); } if (pathname.startsWith("/__wrnexus/")) { const res = await assets.serve(pathname); return res ?? new Response("Not Found", { status: 404 }); } if (pathname === "/api" || pathname.startsWith("/api/")) { return handleApi(ctx); } const publicAsset = await assets.serve(pathname); if (publicAsset) return publicAsset; return handlePage(ctx); } async function handleCsrBinding(ctx: Context): Promise { const routePath = ctx.url.searchParams.get("route") ?? ""; const id = ctx.url.searchParams.get("id") ?? ""; if ( !routePath.startsWith("/") || routePath.startsWith("/__wrnexus/") || !isSafeRequestPath(routePath) || !/^[A-Za-z0-9_-]+$/.test(id) ) { return new Response("Not Found", { status: 404 }); } const page = router.matchPage(routePath); if (!page) return new Response("Not Found", { status: 404 }); const pageModule = await loadModule(page.route.file); const bindings = pageModule.__wrnexusCsr; if (!Array.isArray(bindings)) return new Response("Not Found", { status: 404 }); const binding = bindings.find((item): item is CsrBinding => { if (!item || typeof item !== "object") return false; const candidate = item as Partial; return ( candidate.id === id && typeof candidate.path === "string" && (candidate.method === undefined || typeof candidate.method === "string") && (typeof candidate.body === "string" || typeof candidate.expr === "string") && (candidate.helpers === undefined || typeof candidate.helpers === "string") ); }); if (!binding || !isSafeApiPath(binding.path)) return new Response("Not Found", { status: 404 }); const data = await callApiFromContext(ctx, binding.path, binding.method ?? "GET"); const value = evalData(ctx, data, binding.body ?? binding.expr ?? "", binding.helpers ?? ""); return new Response(String(value), { headers: { "content-type": "text/plain; charset=utf-8" }, }); } async function callApiFromContext(ctx: Context, path: string, method = "GET"): Promise { if (!isSafeApiPath(path)) { throw new Error("Unsafe framework API path"); } const normalizedMethod = method.toUpperCase(); if (!HTTP_METHODS.includes(normalizedMethod as (typeof HTTP_METHODS)[number])) { throw new Error(`Unsupported framework API method: ${normalizedMethod}`); } const apiUrl = new URL(path, ctx.req.url); const apiReq = new Request(apiUrl, { method: normalizedMethod, headers: ctx.req.headers, }); const apiCtx = createContext(apiReq, apiUrl); apiCtx.locals = ctx.locals; apiCtx.cookies = ctx.cookies; apiCtx.session = ctx.session; apiCtx.localStorage = ctx.localStorage; const apiRes = await handleApi(apiCtx); if (!apiRes.ok) { throw new Error(`API route ${apiUrl.pathname} returned ${apiRes.status}`); } const contentType = apiRes.headers.get("content-type") ?? ""; return contentType.includes("application/json") ? await apiRes.json() : await apiRes.text(); } function isSafeApiPath(pathname: string): boolean { return ( (pathname === "/api" || pathname.startsWith("/api/")) && isSafeRequestPath(pathname) && !pathname.includes("?") && !pathname.includes("#") ); } function evalData(ctx: Context, data: unknown, body: string, helpers = ""): unknown { const adapters = { cookies: ctx.cookies, session: ctx.session, localStorage: ctx.localStorage, }; return new Function( "$data", "$adapters", "const cookies = $adapters.cookies;\nconst session = $adapters.session;\nconst localStorage = $adapters.localStorage;\nwith ($data ?? {}) {\n" + helpers + "\n" + body + "\n}", )(data, adapters); } async function handleApi(ctx: Context): Promise { const matched = router.matchApi(ctx.url.pathname); if (!matched) return Response.json({ error: "Not Found" }, { status: 404 }); const mod = await loadModule(matched.route.file); const method = ctx.req.method.toUpperCase(); const embeddedApi = mod.__wrnexusApi as ApiRegistry | undefined; const handler = embeddedApi?.[`${method} ${matched.route.raw}`] ?? mod[method]; if (typeof handler !== "function") { const allowed = HTTP_METHODS.filter( (m) => typeof mod[m] === "function" || typeof embeddedApi?.[`${m} ${matched.route.raw}`] === "function", ); return new Response("Method Not Allowed", { status: 405, headers: { Allow: allowed.join(", ") }, }); } ctx.params = matched.params; return (await handler(ctx)) as Response; } /** * Resolve `` mounts by rendering the * matching `.wrn` component on the server and splicing its HTML in place. * Runs before script collection so the injected `data-scope` is visible. * Recurses so components can mount other components. */ async function renderComponents( body: string, translate: TFunction = (key) => key, depth = 0, ): Promise { if (depth > 15 || router.components.length === 0 || !body.includes("data-component=")) { return body; } let result = ""; let i = 0; for (;;) { const m = MOUNT_OPEN_RE.exec(body.slice(i)); if (!m) { result += body.slice(i); break; } const tagStart = i + m.index; result += body.slice(i, tagStart); const [open, tag, attrStr, name, selfClose] = m; const openEnd = tagStart + open.length; const { inner, end } = selfClose === "/" ? { inner: "", end: openEnd } : readElementBody(body, tag!, openEnd); i = end; const component = router.components.find((c) => c.name === name); if (!component) { console.warn(`[wrnexus] no component registered for '${name}'`); result += body.slice(tagStart, end); continue; } try { const mod = await loadModule(component.file); const render = (mod as { render?: (props: Record) => string }).render; if (typeof render !== "function") { console.warn(`[wrnexus] component '${name}' has no render() export`); result += body.slice(tagStart, end); continue; } // Render, drop the mount's children into the component's , then // recurse so components-in-the-view and components-in-the-children resolve. // Props may carry `{t:key}` i18n markers — resolve them for the active // language before handing them to the component. const props = resolveTProps(parseComponentProps(attrStr!), translate); const rendered = fillSlots(String(render(props)), inner); result += await renderComponents(rendered, translate, depth + 1); } catch (err) { console.error(`[wrnexus] component '${name}' failed to render`, err); deps.devToolbar?.collector.add( issueFromError(err, { ruleId: "server/component-render-error", category: "server", title: `Component '${name}' failed to render`, source: { file: component.file, component: name }, }), ); result += body.slice(tagStart, end); } } return result; } async function handlePage(ctx: Context): Promise { const isMobileRequest = ctx.req.headers.get("x-wrnexus-mobile") === "1" || new RegExp(deps.mobile?.userAgent ?? "WrNexusMobile", "i").test( ctx.req.headers.get("user-agent") ?? "", ); const matched = router.matchPage(ctx.url.pathname); if (!matched) { if (deps.devToolbar && shouldReportNotFound(ctx.url.pathname)) { deps.devToolbar.collector.add( createServerIssue({ ruleId: "routing/not-found", category: "routing", severity: "warning", title: "Route not found", message: `No page route matched ${ctx.url.pathname}.`, pathname: ctx.url.pathname, recommendation: "Verify the URL, page filename, dynamic route parameters, and route casing.", }), ); } const response = renderNotFound(); if (!isMobileRequest) return response; const headers = new Headers(response.headers); headers.set("x-wrnexus-original-status", "404"); // Android WebView may replace a main-frame HTTP error with its own error // surface. Keep the WrNexus 404 document renderable inside Capacitor. return new Response(response.body, { status: 200, headers }); } // Issue the CSRF token cookie so forms on this page can echo it back. csrfToken(ctx); const mod = await loadModule(matched.route.file); const component = mod.default; if (typeof component !== "function") { throw new Error(`Page ${matched.route.file} has no default export`); } ctx.params = matched.params; const meta = (mod.meta ?? {}) as PageMeta; const pageCtx = ctx as Context & { __wrnexusCallApi?: (path: string, method?: string) => Promise; }; pageCtx.__wrnexusCallApi = (path, method = "GET") => callApiFromContext(ctx, path, method); let body = await renderComponents(String(await component(pageCtx)), ctx.t); const resolvedTheme = deps.theme ? resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme) : ""; const language = deps.i18n && ctx.lang ? ctx.lang : (meta.lang ?? deps.seo?.lang ?? "en"); let documentTemplate: string | undefined; // Page layout: a page selects one by exporting `layout = ""` // (app/layouts/.wrn), else falls back to a `default` layout if one // exists. `layout = "none"` opts out. The layout wraps the body via . const layoutName = (isMobileRequest && deps.mobile?.layout ? deps.mobile.layout : typeof mod.layout === "string" ? mod.layout : undefined) ?? "default"; const layout = layoutName === "none" ? undefined : router.layouts.find((l) => l.name === layoutName); if (layout) { try { const layoutMod = await loadModule(layout.file); const layoutRender = (layoutMod as { render?: (p: Record) => string }) .render; if (typeof layoutRender === "function") { body = await renderComponents(fillSlots(String(layoutRender({})), body), ctx.t); } } catch (err) { console.error(`[wrnexus] layout '${layoutName}' failed to render`, err); deps.devToolbar?.collector.add( issueFromError(err, { ruleId: "server/layout-render-error", category: "server", title: `Layout '${layoutName}' failed to render`, pathname: ctx.url.pathname, source: { file: layout.file }, }), ); } } // A conventional app/layouts/document.wrn is a request-aware, top-level // document shell. It wraps the selected page layout and may own html, head, // body, and #app. The SSR renderer still merges framework metadata/assets. const documentLayout = layoutName === "document" ? undefined : router.layouts.find((item) => item.name === "document"); if (documentLayout) { try { const documentMod = await loadModule(documentLayout.file); const documentRender = ( documentMod as { render?: (props: Record) => string } ).render; if (typeof documentRender === "function") { const rendered = String( documentRender({ cookies: ctx.cookies.getAll(), theme: resolvedTheme, language, url: ctx.url.toString(), pathname: ctx.url.pathname, }), ); documentTemplate = await renderComponents(fillSlots(rendered, body), ctx.t); body = documentTemplate; } } catch (err) { console.error("[wrnexus] document layout failed to render", err); deps.devToolbar?.collector.add( issueFromError(err, { ruleId: "server/document-layout-render-error", category: "server", title: "Document layout failed to render", pathname: ctx.url.pathname, source: { file: documentLayout.file }, }), ); } } if (isMobileRequest) body = revealMobileOnlyHtml(body); // i18n: resolve `{t:key}` / `t:attr` markers against the request language. if (deps.i18n) body = translateHtml(body, ctx.t); // Point 3: only ship the JS this page actually uses. const scripts = collectScripts(body).map((src) => versionAssetUrl(src, deps.assetVersion)); if (pwaServiceWorkerEnabled) scripts.push(versionAssetUrl("/__wrnexus/pwa.js", deps.assetVersion)); if (deps.mobile?.enabled !== false && usesMobileRuntime(body)) scripts.push(versionAssetUrl("/__wrnexus/mobile.js", deps.assetVersion)); // attributes: no-flash theme (from cookie, validated) + active lang. const attrs: string[] = []; if (deps.theme) attrs.push(`data-theme="${resolvedTheme}"`); attrs.push(`lang="${safeLanguageTag(language)}"`); const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined; const html = renderDocument({ meta, seo: deps.seo, url: ctx.url, body, scripts, extraHead: [ pwaEnabled ? `` : "", pwaEnabled ? `` : "", pwaEnabled ? `` : "", extraHead, ] .filter(Boolean) .join("\n "), extraBody: [ hmr ? hmrClientTag((ctx.locals.cspNonce as string) ?? "") : "", shouldEnableDevToolbar(mode, deps) ? DEV_TOOLBAR_SCRIPT : "", ] .filter(Boolean) .join("\n") || undefined, htmlAttrs, documentTemplate, }); // Conditional GET: hash the page CONTENT (`body`), not the assembled shell — // the shell carries a per-request CSP nonce in dev, which would otherwise make // the ETag change every request. Same content → same ETag → 304 on revalidate. const tag = etag(`${htmlAttrs ?? ""}\n${scripts.join(",")}\n${body}`); const method = ctx.req.method.toUpperCase(); if ((method === "GET" || method === "HEAD") && notModified(ctx.req, tag)) { return new Response(null, { status: 304, headers: { etag: tag, "cache-control": "private, no-cache" }, }); } return new Response(html, { headers: { "content-type": "text/html; charset=utf-8", etag: tag, "cache-control": "private, no-cache", ...(shouldEnableDevToolbar(mode, deps) ? { "x-wrnexus-dev-toolbar": "enabled", "x-wrnexus-route": matched.route.raw, } : {}), }, }); } const hub = deps.hub; async function handleHmrMessage(ws: Ws, message: string | Uint8Array): Promise { let msg: { type?: unknown; path?: unknown }; try { msg = JSON.parse(typeof message === "string" ? message : new TextDecoder().decode(message)); } catch { return; } if (msg.type !== "sync") return; const path = typeof msg.path === "string" ? msg.path : "/"; let url: URL; try { url = new URL(path, ws.data.kind === "hmr" ? ws.data.baseUrl : "http://localhost"); } catch { ws.send(JSON.stringify({ type: "error", message: "Invalid HMR sync path" })); return; } if ( url.origin !== (ws.data.kind === "hmr" ? ws.data.baseUrl : url.origin) || !url.pathname.startsWith("/") || url.pathname.startsWith("/__wrnexus/") || !isSafeRequestPath(url.pathname) ) { ws.send(JSON.stringify({ type: "error", message: "Unsafe HMR sync path" })); return; } try { const headers = new Headers(ws.data.kind === "hmr" ? ws.data.headers : undefined); headers.set("accept", "text/html"); headers.set("x-wrnexus-hmr", "1"); const req = new Request(url, { headers }); const ctx = createContext(req, url); ctx.locals.cspNonce = randomNonce(); if (deps.i18n) { ctx.lang = resolveLang( deps.i18n, ctx.cookies.get(LANG_COOKIE), req.headers.get("accept-language"), ); ctx.t = makeT(deps.i18n, ctx.lang); } const res = withContextHeaders( ctx, await runMiddleware(await resolveMiddleware(), ctx, () => dispatch(ctx)), ); const html = await res.text(); ws.send(JSON.stringify({ type: "html", html })); } catch (err) { if (mode === "development") console.error(err); ws.send( JSON.stringify({ type: "error", message: err instanceof Error ? err.message : "Unknown HMR sync error", }), ); } } return { fetch: fetchHandler, websocket: { open(ws) { if (ws.data?.kind === "hmr") hub?.add(ws); else if (ws.data?.kind === "room") void realtime.open(ws, ws.data.meta); else ws.data?.handler?.open?.(ws); }, message(ws, message) { if (ws.data?.kind === "hmr") { void handleHmrMessage(ws, message); return; } if (ws.data?.kind === "room") void realtime.message(ws, message); else ws.data?.handler?.message?.(ws, message); }, close(ws, code, reason) { if (ws.data?.kind === "hmr") hub?.remove(ws); else if (ws.data?.kind === "room") void realtime.close(ws); else ws.data?.handler?.close?.(ws, code, reason); }, drain(ws) { if (ws.data?.kind === "hmr" || ws.data?.kind === "room") return; ws.data?.handler?.drain?.(ws); }, }, }; } /** * Decode the handful of HTML entities the page compiler emits when escaping * attribute values, so component props arrive as the author wrote them. */ function decodeHtmlEntities(value: string): string { let decoded = value; // Static page attributes can be escaped by their author/generator and then // escaped again by the page compiler. Decode both layers so structured // component props arrive as valid JSON instead of `"` text. for (let pass = 0; pass < 2; pass += 1) { const next = decoded .replace(/"/g, '"') .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/&/g, "&"); if (next === decoded) break; decoded = next; } return decoded; } /** Content types worth gzipping (text + text-like application types). */ const COMPRESSIBLE_TYPE = /^(?:text\/|application\/(?:json|xml|javascript|manifest\+json)|image\/svg\+xml)/i; /** Below this size gzip's overhead isn't worth it. */ const COMPRESS_MIN_BYTES = 1024; /** * Gzip a response when the client accepts it and the body is a compressible, * buffered (non-streaming) payload. Streaming/SSE responses opt out via * `Cache-Control: no-transform`, so they are never buffered here. */ async function compressResponse(req: Request, res: Response): Promise { const accept = req.headers.get("accept-encoding") ?? ""; if (!accept.toLowerCase().includes("gzip")) return res; if (res.headers.get("content-encoding")) return res; if (res.status === 204 || res.status === 304) return res; if (!COMPRESSIBLE_TYPE.test(res.headers.get("content-type") ?? "")) return res; if ((res.headers.get("cache-control") ?? "").includes("no-transform")) return res; const body = new Uint8Array(await res.arrayBuffer()); if (body.length < COMPRESS_MIN_BYTES) { return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers, }); } const gzipped = Bun.gzipSync(body); const headers = new Headers(res.headers); headers.set("content-encoding", "gzip"); headers.set("content-length", String(gzipped.length)); const vary = headers.get("Vary"); if (!vary) headers.set("Vary", "Accept-Encoding"); else if (!/\baccept-encoding\b/i.test(vary)) headers.set("Vary", `${vary}, Accept-Encoding`); return new Response(gzipped, { status: res.status, statusText: res.statusText, headers }); } /** Opening tag of a component mount: captures tag, attrs, name, self-close. */ const MOUNT_OPEN_RE = /<([A-Za-z][A-Za-z0-9-]*)\b([^>]*?\bdata-component="([A-Za-z0-9_-]+)"[^>]*?)(\/?)>/; /** * From `from` (just past a mount's opening `>`), return the element's inner HTML * and the index just past its matching close tag. Counts same-tag nesting so a * `
` may contain other `
`s. */ /** Cache of per-tag "open tag" regexes so we don't recompile one per call. */ const OPEN_TAG_RE = new Map(); function openTagRe(tag: string): RegExp { let re = OPEN_TAG_RE.get(tag); if (!re) { re = new RegExp(`<${tag}\\b`, "g"); OPEN_TAG_RE.set(tag, re); } return re; } export function readElementBody( html: string, tag: string, from: number, ): { inner: string; end: number } { const openRe = openTagRe(tag); const closeTag = ``; let depth = 1; let i = from; while (i < html.length) { const nextClose = html.indexOf(closeTag, i); if (nextClose === -1) return { inner: html.slice(from), end: html.length }; openRe.lastIndex = i; const openMatch = openRe.exec(html); if (openMatch && openMatch.index < nextClose) { const gt = html.indexOf(">", openMatch.index); if (gt !== -1 && html[gt - 1] === "/") { i = gt + 1; // self-closing child: no depth change } else { depth++; i = gt === -1 ? html.length : gt + 1; } } else { depth--; if (depth === 0) return { inner: html.slice(from, nextClose), end: nextClose + closeTag.length }; i = nextClose + closeTag.length; } } return { inner: html.slice(from), end: html.length }; } /** * Split a mount's children into named slots and the default slot. Content inside * `` goes to that named slot (the wrapper element is * dropped); everything else is the default slot. */ function extractSlots(inner: string): { named: Record; def: string } { const named: Record = {}; if (!inner.includes("data-slot=")) return { named, def: inner }; const SLOT_MOUNT_RE = /<([A-Za-z][A-Za-z0-9-]*)\b([^>]*?\bdata-slot="([A-Za-z0-9_-]+)"[^>]*?)(\/?)>/; let def = ""; let i = 0; for (;;) { const m = SLOT_MOUNT_RE.exec(inner.slice(i)); if (!m) { def += inner.slice(i); break; } const tagStart = i + m.index; def += inner.slice(i, tagStart); const [open, tag, , name, selfClose] = m; const openEnd = tagStart + open.length; const { inner: content, end } = selfClose === "/" ? { inner: "", end: openEnd } : readElementBody(inner, tag!, openEnd); named[name!] = content; i = end; } return { named, def }; } /** * Replace `` elements in a component's output with the mount's children: * ← content from `data-slot="x"` on the mount * ← everything else (the default slot) * A `fallback` keeps its fallback when nothing is provided. */ export function fillSlots(html: string, inner: string): string { if (!/]*?\bname="([A-Za-z0-9_-]+)"[^>]*?\/>/g, (_m, name: string) => named[name] ?? "", ) .replace( /]*?\bname="([A-Za-z0-9_-]+)"[^>]*?>([\s\S]*?)<\/slot>/g, (_m, name: string, fallback: string) => named[name] != null && named[name]!.trim() ? named[name]! : fallback, ) .replace(//g, defTrimmed ? def : "") .replace(/]*>([\s\S]*?)<\/slot>/g, (_m, fallback: string) => defTrimmed ? def : fallback, ); } /** * Extract props from a `data-component` mount's attribute string. Every quoted * attribute other than `data-component` becomes a (string) prop; the component's * declared defaults coerce them to their final types. */ export function parseComponentProps(attrStr: string): Record { const props: Record = {}; for (const m of attrStr.matchAll(/([A-Za-z_][\w-]*)(?:="([^"]*)")?/g)) { const key = m[1]!; if (key === "data-component") continue; props[key] = decodeHtmlEntities(m[2] ?? ""); } return props; } /** * Resolve `{t:key}` i18n markers inside component prop values against the active * language, so authors can pass localized text into a component: * `
`. */ export function resolveTProps( props: Record, translate: TFunction, ): Record { for (const key of Object.keys(props)) { const value = props[key]!; if (value.includes("{t:")) { props[key] = value.replace(/\{t:\s*([^}]+?)\s*\}/g, (_m, tkey: string) => translate(tkey.trim()), ); } } return props; } /** * Decide which framework scripts a rendered page needs. Components are already * server-rendered into the HTML; the only script is the reactive runtime, and * only when the page actually contains a scope or a browser-side API fetch. */ export function collectScripts(body: string): string[] { // Client-side navigation is an app-wide progressive enhancement: it must load // on every page (you navigate *from* any page), and degrades to full loads. const scripts: string[] = ["/__wrnexus/nav.js"]; if (/\bdata-scope=/.test(body) || /\bdata-wrnexus-csr=/.test(body)) { scripts.push("/__wrnexus/reactive.js"); } // The theme runtime is only needed when the page can switch themes. if (/\bdata-wire-theme-(toggle|set)\b/.test(body)) { scripts.push(THEME_JS_HREF); } // Validation: schema descriptors + the generic validator, only for pages with a form. if (/\bdata-schema="[A-Za-z0-9_-]+"/.test(body)) { scripts.push("/__wrnexus/schemas.js", "/__wrnexus/validate.js"); } // Language switcher runtime, only when the page has one. if (/\bdata-wire-lang-set\b/.test(body) || /\bselect[^>]*\bdata-wire-lang\b/.test(body)) { scripts.push(I18N_JS_HREF); } // Realtime client, only when the page declares a room. if (/\bdata-room=/.test(body)) { scripts.push("/__wrnexus/realtime.js"); } // File-upload runtime (drag-drop + progress), only when a page has an uploader. if (/\bdata-uploader\b/.test(body)) { scripts.push("/__wrnexus/uploader.js"); } return scripts; } /** Whether rendered markup needs the Capacitor/native browser bridge. */ export function usesMobileRuntime(body: string): boolean { return /\b(?:data-mobile-[\w-]+|data-native-(?:browser|mobile|only|requires|unsupported|options)|data-on-wrnexus-(?:browser|mobile)-[\w-]+)\b/.test( body, ); } function safeLanguageTag(value: string): string { return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value) ? value : "en"; } function versionAssetUrl(src: string, version?: string): string { if (!version) return src; return `${src}${src.includes("?") ? "&" : "?"}v=${encodeURIComponent(version)}`; } function escapeStyleContent(css: string): string { return css.replace(/<\/style/gi, "<\\/style"); }