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
+35 -140
View File
@@ -1,6 +1,6 @@
"use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
// WRN editor compiler source hash: 9203410b358f7f7f824f49e1e793d2281403faab7097d2664761d5dbe3b8e957
// WRN editor compiler source hash: 653a88ea2a34e9c446fc55b268f3ba9c5d27f9e114fc6aa9228ef560e1fd616d
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
// Generated with TypeScript: 6.0.3
const __nodeRequire = require;
@@ -779,14 +779,13 @@ function clientCalledApiNames(ast) {
* never does either.
*/
function assertNoDynamicApiAccess(ast) {
// 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))
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);
@@ -799,14 +798,13 @@ function assertNoDynamicApiAccess(ast) {
}
}
/**
* 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, called) {
return (Boolean(block.sections) &&
(block.mode === "client" || (block.mode === "any" && called.has(block.name))));
return Boolean(block.sections) && called.has(block.name);
}
/**
* Client-mode and client-called any-mode api blocks become members of an
@@ -1385,32 +1383,6 @@ function compileIfExpr(node) {
}
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, out = []) {
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, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
if (node.type === "text")
return substituteReactiveText(node.value, reactive, loops); // {t:key} + state baking
@@ -1518,34 +1490,30 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
const ssrText = attrValue(node.attrs, "ssrText");
const csrGet = attrValue(node.attrs, "csrGet");
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;
const csrId = 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 (syntax_1.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}>`;
}
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
@@ -1645,15 +1613,6 @@ function csrMarker(bindings, binding) {
bindings.push({ id, ...binding });
return id;
}
function renderBinding(binding) {
return {
method: binding.method,
path: binding.path,
body: binding.body,
helpers: binding.helpers,
...(binding.errorBody ? { errorBody: binding.errorBody } : {}),
};
}
function hasClientBehavior(nodes) {
return nodes.some((node) => {
// `{t:key}` is i18n sugar resolved server-side — not client reactivity.
@@ -1691,17 +1650,6 @@ function dataBody(source) {
return "return undefined;";
return /\breturn\b/.test(trimmed) ? trimmed : expressionBody(trimmed);
}
function modeHelpers(ast, mode, sharedHelpers) {
return [
sharedHelpers,
...ast.modeFunctions
.filter((block) => block.mode === mode)
.map((block) => block.body.trim())
.filter(Boolean),
]
.filter(Boolean)
.join("\n\n");
}
function apiBindingMap(ast, sharedHelpers) {
const bindings = new Map();
for (const block of ast.dataApis) {
@@ -1723,7 +1671,7 @@ function apiBindingMap(ast, sharedHelpers) {
// legacy blocks and sectioned blocks without `error` keep failures
// propagating exactly as before.
...(errorSection ? { errorBody: errorSection } : {}),
helpers: modeHelpers(ast, block.mode, sharedHelpers),
helpers: sharedHelpers,
});
}
return bindings;
@@ -2303,22 +2251,7 @@ function generateInner(ast) {
staticShellBody = staticShellBody.replaceAll(`\x00WRNEACH${idx}\x00`, code);
}
}
// 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 = [];
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;
if (needsRuntimeHelpers) {
@@ -6813,7 +6746,6 @@ function parse(source) {
case "client":
case "server": {
const rawMode = kw.value;
const mode = rawMode === "client" ? "client" : "ssr";
lx.next();
if ((rawMode === "client" || rawMode === "server") &&
lx.peek().type === "ident" &&
@@ -6824,52 +6756,15 @@ function parse(source) {
states.push(...(0, v060_ts_1.parseStateDeclarations)(lx.readBalancedBraces(), rawMode));
break;
}
if (mode === "client" && lx.peek().type === "eq") {
if (rawMode === "client" && lx.peek().type === "eq") {
lx.next();
hydrate = expect("string").value;
break;
}
expect("lbrace");
while (lx.peek().type !== "rbrace") {
const member = lx.peek();
if (member.type === "eof") {
throw new ParseError(`Unexpected end of input inside ${mode} block`);
}
if (member.type !== "ident") {
throw new ParseError(`Expected a ${mode} member keyword at offset ${member.pos}`);
}
switch (member.value) {
case "api": {
lx.next();
const name = expect("ident").value;
const method = expect("ident").value.toUpperCase();
const path = lx.readPath();
const body = lx.readBalancedBraces();
const sections = (0, api_sections_ts_1.parseApiSections)(body);
if (sections && mode !== "client" && (0, api_sections_ts_1.hasRequestSection)(body)) {
throw new ParseError(`An ssr api block cannot declare "request": there is no caller at render time to supply it. Use a client block, or a server function.`);
}
dataApis.push({
mode,
name,
method,
path,
body: sections ? "" : body,
...(sections ? { sections } : {}),
});
break;
}
case "functions": {
lx.next();
modeFunctions.push({ mode, body: lx.readBalancedBraces() });
break;
}
default:
throw new ParseError(`Unknown ${mode} member '${member.value}' at offset ${member.pos}`);
}
if (lx.peek().type === "lbrace") {
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`);
}
expect("rbrace");
break;
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
}
case "shared": {
lx.next();
+1 -1
View File
@@ -1,4 +1,4 @@
// WRN editor extension source hash: 0da6296bd5dd1b84aece4ad5aed742c81deefefc4189ead29706be24c22d1cf9
// WRN editor extension source hash: feecaeb59a41771bd1a35be172bf89cec8451468a4bf817f176cc54de3d8267a
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
"use strict";
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
+5 -46
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// WRN editor language server source hash: 9614834cb7909e77f707aa31a8cc87ec6193ad642a81ee52870fd6872785fa4e
// WRN editor language server source hash: 613d98d8154714b7e5799b02a27e16cd707a50bc2b5ae7d9fbb3f0834b9251e6
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
@@ -170866,9 +170866,6 @@ function parseApiSections(source) {
error: top.get("error")?.text ?? ""
};
}
function hasRequestSection(source) {
return scanTopLevelBlocks(source, SECTION_NAMES).has("request");
}
function parseApiEntries(source) {
const entries = [];
const lx = new Lexer(source);
@@ -171749,7 +171746,6 @@ function parse(source) {
case "client":
case "server": {
const rawMode = kw.value;
const mode = rawMode === "client" ? "client" : "ssr";
lx.next();
if ((rawMode === "client" || rawMode === "server") && lx.peek().type === "ident" && lx.peek().value === "state") {
lx.next();
@@ -171758,52 +171754,15 @@ function parse(source) {
states.push(...parseStateDeclarations(lx.readBalancedBraces(), rawMode));
break;
}
if (mode === "client" && lx.peek().type === "eq") {
if (rawMode === "client" && lx.peek().type === "eq") {
lx.next();
hydrate = expect("string").value;
break;
}
expect("lbrace");
while (lx.peek().type !== "rbrace") {
const member = lx.peek();
if (member.type === "eof") {
throw new ParseError(`Unexpected end of input inside ${mode} block`);
}
if (member.type !== "ident") {
throw new ParseError(`Expected a ${mode} member keyword at offset ${member.pos}`);
}
switch (member.value) {
case "api": {
lx.next();
const name2 = expect("ident").value;
const method = expect("ident").value.toUpperCase();
const path = lx.readPath();
const body = lx.readBalancedBraces();
const sections = parseApiSections(body);
if (sections && mode !== "client" && hasRequestSection(body)) {
throw new ParseError(`An ssr api block cannot declare "request": there is no caller at render time to supply it. Use a client block, or a server function.`);
}
dataApis.push({
mode,
name: name2,
method,
path,
body: sections ? "" : body,
...sections ? { sections } : {}
});
break;
}
case "functions": {
lx.next();
modeFunctions.push({ mode, body: lx.readBalancedBraces() });
break;
}
default:
throw new ParseError(`Unknown ${mode} member '${member.value}' at offset ${member.pos}`);
}
if (lx.peek().type === "lbrace") {
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`);
}
expect("rbrace");
break;
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
}
case "shared": {
lx.next();