release: WRNexusJS 0.8.0
This commit is contained in:
@@ -15,6 +15,185 @@ export interface RuntimeRequirements {
|
||||
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 {
|
||||
@@ -67,12 +246,42 @@ export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements {
|
||||
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: interactive && ast.hydrate !== "none" && ast.runtime !== "server",
|
||||
needsServerRuntime: requestData || authenticated || streaming || ast.runtime === "server",
|
||||
hydrationStrategy: interactive ? (ast.hydrate ?? "load") : null,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user