feat(compiler): support the three api render-binding forms
- Parse api="name", api="name()", and api="name({ ... })" render bindings
for apis {} (mode "any") blocks, mirroring @click="fn()" syntax.
- Render-bind by calling Task 4's generated server `api` object directly
(api.<name>(args)) rather than re-implementing the fetch/response
transport, spliced into the SSR template via the existing loop/expression
sentinel mechanism so the call runs inside the async render function with
await support.
- A block that is both render-bound and called from code runs twice by
design (no dedup); pinned with a test.
- Fix packages/syntax's attribute-value lexer (readQuoted) to honor
backslash-escaped quotes, needed so an api="..." call expression can
itself contain a quoted string/object literal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||
// WRN editor compiler source hash: 3b1bf12fc638343d15eb8502a2b10df4ad909529de8c94b612cb567959324f5d
|
||||
// WRN editor compiler source hash: 9203410b358f7f7f824f49e1e793d2281403faab7097d2664761d5dbe3b8e957
|
||||
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
|
||||
// Generated with TypeScript: 6.0.3
|
||||
const __nodeRequire = require;
|
||||
@@ -1505,10 +1505,14 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
|
||||
if (isComponentTag(node.tag)) {
|
||||
return renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive);
|
||||
}
|
||||
const apiName = attrValue(node.attrs, "api");
|
||||
const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
|
||||
if (apiName && !apiBinding) {
|
||||
throw new Error(`Unknown .wrn api binding "${apiName}"`);
|
||||
const apiAttr = attrValue(node.attrs, "api");
|
||||
const parsedApi = apiAttr ? parseApiBinding(apiAttr) : null;
|
||||
if (apiAttr && !parsedApi) {
|
||||
throw new Error(`Invalid .wrn api binding "${apiAttr}"`);
|
||||
}
|
||||
const apiBinding = parsedApi ? apiBindings.get(parsedApi.name) : undefined;
|
||||
if (parsedApi && !apiBinding) {
|
||||
throw new Error(`Unknown .wrn api binding "${parsedApi.name}"`);
|
||||
}
|
||||
const ssrGet = attrValue(node.attrs, "ssrGet");
|
||||
const ssrText = attrValue(node.attrs, "ssrText");
|
||||
@@ -1530,16 +1534,18 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
|
||||
}
|
||||
const inner = apiBinding?.mode === "ssr"
|
||||
? ssrMarker(ssrBindings, renderBinding(apiBinding))
|
||||
: 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("");
|
||||
: 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("");
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>${inner}</${node.tag}>`;
|
||||
}
|
||||
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
|
||||
@@ -1602,6 +1608,33 @@ function renderNestedComponentInvocation(node, ctx) {
|
||||
`${ctx.forwardRestAttrs ? "${__wrnSpreadAttrs(__attrs)}" : ""}` +
|
||||
`${attrs}>${inner}</div>`);
|
||||
}
|
||||
/**
|
||||
* Parses the three `api="…"` render-binding forms: a bare name, an empty call,
|
||||
* or a call carrying an argument expression. Mirrors the shape of `@click="fn()"`,
|
||||
* so no new escaping or attribute-naming rules are introduced. The argument
|
||||
* capture is greedy up to the outer parens, so nested braces/parens/quotes in
|
||||
* the argument expression (object literals, arrays, strings) are carried
|
||||
* through untouched rather than truncated at the first `)`.
|
||||
*/
|
||||
function parseApiBinding(value) {
|
||||
const match = /^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\(([\s\S]*)\))?\s*$/.exec(value);
|
||||
if (!match)
|
||||
return null;
|
||||
return { name: match[1], args: (match[2] ?? "").trim() };
|
||||
}
|
||||
/**
|
||||
* Emit a render-time call into the server `api` object (Task 4's generated
|
||||
* transport) for an `apis {}` (mode "any") binding, and return the sentinel
|
||||
* that the loop/expression-splicing mechanism swaps for the real `${…}` code.
|
||||
* This calls the same server `api.<name>()` member a `load`/action block would
|
||||
* call -- it does not reimplement fetch/response handling -- so a block that is
|
||||
* both render-bound and called from code runs its own call each time (no
|
||||
* dedup is attempted; see apis-render-binding.test.ts).
|
||||
*/
|
||||
function apiCallMarker(loops, name, args) {
|
||||
loops.push(`\${__wrnexusEscapeHtml(await api.${name}(${args}))}`);
|
||||
return `\x00WRNEACH${loops.length - 1}\x00`;
|
||||
}
|
||||
function ssrMarker(bindings, binding) {
|
||||
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
||||
bindings.push({ marker, ...binding });
|
||||
@@ -7141,12 +7174,21 @@ function parseHtmlView(src, pos) {
|
||||
if (quote !== '"' && quote !== "'")
|
||||
return fail("Expected a quoted attribute value");
|
||||
i++;
|
||||
const start = i;
|
||||
while (i < src.length && src[i] !== quote)
|
||||
// Backslash-escapes the delimiter (and anything else) so an attribute
|
||||
// value -- e.g. an `api="fn({ a: \"b\" })"` call expression -- can carry
|
||||
// the same quote character it's wrapped in.
|
||||
let value = "";
|
||||
while (i < src.length && src[i] !== quote) {
|
||||
if (src[i] === "\\" && i + 1 < src.length) {
|
||||
value += src[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
value += src[i];
|
||||
i++;
|
||||
}
|
||||
if (i >= src.length)
|
||||
return fail("Unterminated attribute value");
|
||||
const value = src.slice(start, i);
|
||||
i++; // closing quote
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// WRN editor extension source hash: 42ceb9645e98cbc79abd3104e8426da535affc52327fb9e42ce658888d1b9941
|
||||
// WRN editor extension source hash: 0da6296bd5dd1b84aece4ad5aed742c81deefefc4189ead29706be24c22d1cf9
|
||||
// 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: d42dc4f557c0cb990f1d164708ca5d9ed8ddeabca3270f1b99f175b1f342572e
|
||||
// WRN editor language server source hash: 9614834cb7909e77f707aa31a8cc87ec6193ad642a81ee52870fd6872785fa4e
|
||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||
// @bun @bun-cjs
|
||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||
@@ -172082,12 +172082,18 @@ function parseHtmlView(src, pos) {
|
||||
if (quote !== '"' && quote !== "'")
|
||||
return fail("Expected a quoted attribute value");
|
||||
i++;
|
||||
const start = i;
|
||||
while (i < src.length && src[i] !== quote)
|
||||
let value = "";
|
||||
while (i < src.length && src[i] !== quote) {
|
||||
if (src[i] === "\\" && i + 1 < src.length) {
|
||||
value += src[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
value += src[i];
|
||||
i++;
|
||||
}
|
||||
if (i >= src.length)
|
||||
return fail("Unterminated attribute value");
|
||||
const value = src.slice(start, i);
|
||||
i++;
|
||||
return value;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user