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:
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user