feat: replace the ssr/client data blocks with apis blocks

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 07:59:37 +05:30
co-authored by Claude Opus 5
parent de99a2c2e0
commit 890d6106b3
14 changed files with 135 additions and 1421 deletions
+9 -13
View File
@@ -378,14 +378,13 @@ function clientCalledApiNames(ast: PageAst): Set<string> {
* never does either.
*/
function assertNoDynamicApiAccess(ast: PageAst): void {
// Only pages with a mode "any" block have anything at stake here: those
// Only pages with a sectioned api block have anything at stake here: those
// blocks are emitted solely because usage detection saw `api.<name>`, so a
// dynamic reference this scan can't see is the one that silently drops a
// block from the bundle. Mode "client" blocks always ship regardless of
// usage, and a page with no api blocks at all may still declare an
// ordinary `state api` (see the B5 regression test) where a bare "api"
// block from the bundle. A page with no api blocks at all may still declare
// an ordinary `state api` (see the B5 regression test) where a bare "api"
// identifier is just that state, not a missed block reference.
if (!ast.dataApis.some((block) => block.mode === "any" && block.sections)) return;
if (!ast.dataApis.some((block) => block.sections)) return;
for (const fn of ast.runtimeFunctions.filter((fn) => ["client", "shared"].includes(fn.runtime))) {
const masked = maskStringsAndComments(fn.body);
@@ -403,16 +402,13 @@ function assertNoDynamicApiAccess(ast: PageAst): void {
}
/**
* A block is emitted into the browser module when it is authored as
* client-only, or when it is mode "any" and a client function actually calls
* it. `hasClientApi` below must use this exact predicate so the `api`
* reserved-binding exclusion and the emitted object can never disagree.
* A block is emitted into the browser module when it declares typed sections
* and a client function actually calls it. `hasClientApi` below must use this
* exact predicate so the `api` reserved-binding exclusion and the emitted
* object can never disagree.
*/
function isClientEmittedApiBlock(block: PageAst["dataApis"][number], called: Set<string>): boolean {
return (
Boolean(block.sections) &&
(block.mode === "client" || (block.mode === "any" && called.has(block.name)))
);
return Boolean(block.sections) && called.has(block.name);
}
/**
+21 -88
View File
@@ -567,29 +567,6 @@ function compileIfExpr(node: IfNode): string {
return "${" + expr + "}";
}
/**
* Collect every server-control expression in a view (recursively): `{#each}` list
* expressions and `{#if}` conditions. Used to wrn up raw SSR data consts.
*/
function collectControlExprs(nodes: ViewNode[], out: string[] = []): string[] {
for (const node of nodes) {
if (node.type === "text") continue;
if (node.type === "each") {
out.push(node.list);
collectControlExprs(node.body, out);
collectControlExprs(node.empty, out);
} else if (node.type === "if") {
for (const b of node.branches) {
if (b.cond) out.push(b.cond);
collectControlExprs(b.body, out);
}
} else if (node.type === "element") {
collectControlExprs(node.children, out);
}
}
return out;
}
function renderNode(
node: ViewNode,
ssrBindings: SsrBinding[],
@@ -741,39 +718,32 @@ function renderNode(
const csrText = attrValue(node.attrs, "csrText");
const csrId =
apiBinding?.mode === "client"
? csrMarker(csrBindings, renderBinding(apiBinding))
: csrGet && csrText
? csrMarker(csrBindings, {
method: "GET",
path: apiRoutePath(csrGet),
body: expressionBody(csrText),
helpers: "",
})
: undefined;
csrGet && csrText
? csrMarker(csrBindings, {
method: "GET",
path: apiRoutePath(csrGet),
body: expressionBody(csrText),
helpers: "",
})
: undefined;
// Void elements (<br>, <img>, …) have no closing tag and no children.
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>`;
}
const inner =
apiBinding?.mode === "ssr"
? ssrMarker(ssrBindings, renderBinding(apiBinding))
: apiBinding?.mode === "any"
? apiCallMarker(loops, parsedApi!.name, parsedApi!.args)
: ssrGet && ssrText
? ssrMarker(ssrBindings, {
method: "GET",
path: apiRoutePath(ssrGet),
body: expressionBody(ssrText),
helpers: "",
})
: node.children
.map((child) =>
renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive),
)
.join("");
const inner = apiBinding
? apiCallMarker(loops, parsedApi!.name, parsedApi!.args)
: ssrGet && ssrText
? ssrMarker(ssrBindings, {
method: "GET",
path: apiRoutePath(ssrGet),
body: expressionBody(ssrText),
helpers: "",
})
: node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>${inner}</${node.tag}>`;
}
@@ -912,16 +882,6 @@ function csrMarker(bindings: CsrBinding[], binding: RenderBinding): string {
return id;
}
function renderBinding(binding: NamedDataBinding): RenderBinding {
return {
method: binding.method,
path: binding.path,
body: binding.body,
helpers: binding.helpers,
...(binding.errorBody ? { errorBody: binding.errorBody } : {}),
};
}
function hasClientBehavior(nodes: ViewNode[]): boolean {
return nodes.some((node) => {
// `{t:key}` is i18n sugar resolved server-side — not client reactivity.
@@ -963,18 +923,6 @@ function dataBody(source: string): string {
return /\breturn\b/.test(trimmed) ? trimmed : expressionBody(trimmed);
}
function modeHelpers(ast: PageAst, mode: DataMode, sharedHelpers: string): string {
return [
sharedHelpers,
...ast.modeFunctions
.filter((block) => block.mode === mode)
.map((block) => block.body.trim())
.filter(Boolean),
]
.filter(Boolean)
.join("\n\n");
}
function apiBindingMap(ast: PageAst, sharedHelpers: string): Map<string, NamedDataBinding> {
const bindings = new Map<string, NamedDataBinding>();
@@ -997,7 +945,7 @@ function apiBindingMap(ast: PageAst, sharedHelpers: string): Map<string, NamedDa
// legacy blocks and sectioned blocks without `error` keep failures
// propagating exactly as before.
...(errorSection ? { errorBody: errorSection } : {}),
helpers: modeHelpers(ast, block.mode, sharedHelpers),
helpers: sharedHelpers,
});
}
@@ -1664,22 +1612,7 @@ function generateInner(ast: PageAst): string {
}
}
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
// binding a loop references, so `{#each <name> as …}` can iterate the real value.
const loopConsts: string[] = [];
if (loops.length > 0) {
const lists = collectControlExprs(ast.view);
for (const [name, binding] of apiBindings) {
if (binding.mode !== "ssr") continue;
if (!lists.some((expr) => new RegExp(`\\b${name}\\b`).test(expr))) continue;
const errorBodyProp = binding.errorBody
? `, errorBody: ${JSON.stringify(binding.errorBody)}`
: "";
loopConsts.push(
` const ${name} = await __wrnexusResolveApiBinding({ path: ${JSON.stringify(binding.path)}, method: ${JSON.stringify(binding.method)}, body: ${JSON.stringify(binding.body)}, helpers: ${JSON.stringify(binding.helpers)}${errorBodyProp} }, ctx);`,
);
}
}
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
const needsRuntimeHelpers = needsSsrRuntime || hasServerApis;