Files
WRNexusJS/packages/compiler/src/analysis.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

288 lines
9.7 KiB
TypeScript

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<string, string>;
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<string> {
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<string>();
let staticNodes = 0;
let reactiveRegions = 0;
const componentNames = new Set<string>();
const staticClasses = new Set<string>();
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,
};
}