import type { PageAst, ViewNode } from "@wrnexus/syntax"; export type RouteExecutionKind = | "static" | "static-interactive" | "request-ssr" | "authenticated-ssr" | "streaming-ssr" | "dynamic"; export interface RuntimeRequirements { kind: RouteExecutionKind; canPrerender: boolean; needsClientRuntime: boolean; needsServerRuntime: boolean; hydrationStrategy: string | null; reasons: string[]; optimization: OptimizationReport; cachePolicy: Record; requiredPermission: string | null; } export interface OptimizationReport { staticNodes: number; reactiveRegions: number; eliminatedBranches: number; unusedState: string[]; unusedHandlers: string[]; constantProps: string[]; unusedLocalCssClasses: string[]; batchableStateUpdates: number; memoizableComponents: string[]; preloadDependencies: string[]; serverOnlyModules: string[]; } function identifiers(value: string): Set { return new Set(value.match(/[A-Za-z_$][\w$]*/g) ?? []); } function literalBoolean(expression: string | null): boolean | undefined { if (expression === null) return true; const value = expression.trim(); if (value === "true") return true; if ( value === "false" || value === "null" || value === "undefined" || value === "0" || value === "''" || value === '""' ) return false; if (/^-?(?:[1-9]\d*|0?\.\d+)$/.test(value) || /^(['"]).+\1$/.test(value)) return true; return undefined; } function optimizeNodes(nodes: ViewNode[], report: { eliminated: number }): ViewNode[] { const output: ViewNode[] = []; for (const node of nodes) { if (node.type === "element") output.push({ ...node, attrs: node.attrs.map((attribute) => ({ ...attribute })), children: optimizeNodes(node.children, report), }); else if (node.type === "each") output.push({ ...node, body: optimizeNodes(node.body, report), empty: optimizeNodes(node.empty, report), }); else if (node.type === "if") { let selected: ViewNode[] | undefined; let dynamic = false; for (const branch of node.branches) { const value = literalBoolean(branch.cond); if (value === undefined) { dynamic = true; break; } report.eliminated++; if (value) { selected = branch.body; break; } } if (dynamic) output.push({ ...node, branches: node.branches.map((branch) => ({ ...branch, body: optimizeNodes(branch.body, report), })), }); else if (selected) output.push(...optimizeNodes(selected, report)); } else output.push({ ...node }); } return output; } /** Safe compile-time folding for literal conditional branches. */ export function optimizeAst(ast: PageAst): { ast: PageAst; eliminatedBranches: number } { const report = { eliminated: 0 }; return { ast: { ...ast, view: optimizeNodes(ast.view, report) }, eliminatedBranches: report.eliminated, }; } export function analyzeOptimizations(ast: PageAst): OptimizationReport { const used = new Set(); let staticNodes = 0; let reactiveRegions = 0; const componentNames = new Set(); const staticClasses = new Set(); const visit = (nodes: ViewNode[]) => { for (const node of nodes) { if (node.type === "text") { const refs = identifiers(node.value); refs.forEach((name) => used.add(name)); if (node.value.includes("{")) reactiveRegions++; else staticNodes++; } else if (node.type === "element") { if (/^[A-Z]/.test(node.tag)) componentNames.add(node.tag); let reactive = false; for (const attribute of node.attrs) { identifiers(attribute.value).forEach((name) => used.add(name)); reactive ||= attribute.event || attribute.value.includes("{"); if (attribute.name === "class" && !attribute.value.includes("{")) for (const name of attribute.value.split(/\s+/)) if (name) staticClasses.add(name); } if (reactive) reactiveRegions++; else staticNodes++; visit(node.children); } else if (node.type === "each") { identifiers(`${node.list} ${node.key ?? ""}`).forEach((name) => used.add(name)); reactiveRegions++; visit(node.body); visit(node.empty); } else { for (const branch of node.branches) { identifiers(branch.cond ?? "").forEach((name) => used.add(name)); visit(branch.body); } reactiveRegions++; } } }; visit(ast.view); const handlerReferences = new Set(used); const executable = [ ...ast.runtimeFunctions.map((fn) => fn.body), ...ast.functions, ...ast.effects.map((effect) => effect.body), ...ast.watches.map((watch) => watch.body), ...ast.actions.map((action) => action.body), ].join("\n"); identifiers(executable).forEach((name) => used.add(name)); const localCss = new Set( ast.styles.flatMap((style) => [...style.matchAll(/\.([_a-zA-Z][\w-]*)/g)].map((match) => match[1]!), ), ); const optimized = optimizeAst(ast); const assignmentCounts = ast.runtimeFunctions.map( (fn) => ast.states.filter((state) => new RegExp(`\\b${state.name}\\s*(?:[+*/-]?=|\\+\\+|--)`).test(fn.body), ).length, ); return { staticNodes, reactiveRegions, eliminatedBranches: optimized.eliminatedBranches, unusedState: ast.states.filter((state) => !used.has(state.name)).map((state) => state.name), unusedHandlers: ast.runtimeFunctions .filter((fn) => fn.runtime !== "server" && !handlerReferences.has(fn.name)) .map((fn) => fn.name), constantProps: ast.props .filter((prop) => /^(?:-?\d+(?:\.\d+)?|true|false|null|(['"]).*\1)$/.test(prop.default.trim()), ) .map((prop) => prop.name), unusedLocalCssClasses: [...localCss].filter((name) => !staticClasses.has(name)).sort(), batchableStateUpdates: assignmentCounts .filter((count) => count > 1) .reduce((sum, count) => sum + count - 1, 0), memoizableComponents: [...componentNames].sort(), preloadDependencies: ast.structuredImports .filter((entry) => !entry.typeOnly && !entry.source.startsWith("node:")) .map((entry) => entry.source), serverOnlyModules: ast.structuredImports .filter((entry) => entry.source.startsWith("node:") || ast.runtime === "server") .map((entry) => entry.source), }; } function hasEvent(nodes: ViewNode[]): boolean { for (const node of nodes) { if (node.type === "element") { if (node.attrs.some((attribute) => attribute.event)) return true; if (hasEvent(node.children)) return true; } else if (node.type === "each") { if (hasEvent(node.body) || hasEvent(node.empty)) return true; } else if (node.type === "if") { if (node.branches.some((branch) => hasEvent(branch.body))) return true; } } return false; } export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements { const reasons: string[] = []; const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server"); const clientState = ast.states.some((state) => state.runtime !== "server"); const interactive = clientFunctions || clientState || ast.effects.length > 0 || ast.watches.length > 0 || hasEvent(ast.view); if (interactive) reasons.push("client interactivity"); const requestData = ast.loads.length > 0 || ast.actions.length > 0 || ast.dataApis.length > 0 || ast.apis.length > 0 || ast.realtimes.length > 0 || ast.runtimeFunctions.some((fn) => fn.runtime === "server") || ast.states.some((state) => state.runtime === "server"); if (requestData) reasons.push("server/request data"); const authenticated = /^(?:required|true)$/i.test(ast.security.auth ?? ""); if (authenticated) reasons.push("authentication required"); const streaming = /^(?:true|required)$/i.test(ast.security.streaming ?? ""); if (streaming) reasons.push("streaming enabled"); let kind: RouteExecutionKind; if (streaming) kind = "streaming-ssr"; else if (authenticated) kind = "authenticated-ssr"; else if (requestData && interactive) kind = "dynamic"; else if (requestData) kind = "request-ssr"; else if (interactive) kind = "static-interactive"; else kind = "static"; if (ast.renderMode === "static") { kind = "static"; reasons.push("explicit static rendering"); } else if (ast.renderMode === "server") { kind = requestData ? "request-ssr" : "static"; reasons.push("explicit server rendering"); } else if (ast.renderMode === "client") { kind = "static-interactive"; reasons.push("explicit client rendering"); } else if (ast.renderMode === "partial-static") { kind = "streaming-ssr"; reasons.push("partial-static shell with streamed dynamic regions"); } const clientDisabled = ast.renderMode === "static" || ast.renderMode === "server"; const serverDisabled = ast.renderMode === "client"; return { kind, canPrerender: kind === "static" || kind === "static-interactive", needsClientRuntime: !clientDisabled && (interactive || ast.renderMode === "client") && ast.hydrate !== "none" && ast.runtime !== "server", needsServerRuntime: !serverDisabled && (requestData || authenticated || streaming || ast.renderMode === "server" || ["server", "edge", "worker", "service-worker"].includes(ast.runtime ?? "")), hydrationStrategy: clientDisabled ? null : interactive ? (ast.hydrate ?? "load") : null, reasons, optimization: analyzeOptimizations(ast), cachePolicy: { ...(ast.cache ?? {}) }, requiredPermission: ast.security.permission ?? null, }; }