feat: replace the ssr/client data blocks with apis blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+35
-140
@@ -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,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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user