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:
2026-08-20 07:38:32 +05:30
co-authored by Claude Opus 5
parent a08dfa5322
commit 442c058d0c
6 changed files with 189 additions and 42 deletions
+50 -16
View File
@@ -725,10 +725,14 @@ function renderNode(
);
}
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");
@@ -756,18 +760,20 @@ function renderNode(
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}>`;
}
@@ -866,6 +872,34 @@ function renderNestedComponentInvocation(
);
}
/**
* 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: string): { name: string; args: string } | null {
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: string[], name: string, args: string): string {
loops.push(`\${__wrnexusEscapeHtml(await api.${name}(${args}))}`);
return `\x00WRNEACH${loops.length - 1}\x00`;
}
function ssrMarker(bindings: SsrBinding[], binding: RenderBinding): string {
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
bindings.push({ marker, ...binding });