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
@@ -0,0 +1,55 @@
import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generate } from "../src/codegen.ts";
const page = (attr: string) => `page Probe {
apis {
listTeams GET /api/teams {
request { parameters { team?: string } }
response { return data.teams }
}
}
view { <main><p ${attr}>loading</p></main> }
}
`;
test("a bare name binds", () => {
expect(generate(parse(page('api="listTeams"')))).toContain('"/api/teams"');
});
test("an empty call binds identically to a bare name", () => {
const bare = generate(parse(page('api="listTeams"')));
const called = generate(parse(page('api="listTeams()"')));
expect(called).toContain('"/api/teams"');
expect(called.length).toBeGreaterThan(0);
expect(bare).toContain('"/api/teams"');
});
test("an argument expression is carried into the binding", () => {
const generated = generate(parse(page('api="listTeams({ team: \\"platform\\" })"')));
expect(generated).toContain("platform");
});
test("a block that is both bound and called is invoked twice", () => {
// Deliberate: a render-time fetch and a user-triggered fetch are usually
// meant to be different requests. Collapsing them silently would be worse
// than the duplication.
const generated = generate(
parse(`page P {
apis { listTeams GET /api/teams { response { return data.teams } } }
load server x { return await api.listTeams() }
view { <main><p api="listTeams">loading</p></main> }
}
`),
);
// Both call paths are emitted: the load block's call and the binding's.
// The route path itself is declared once, inside the shared server `api`
// object (Task 4) that both call sites invoke -- so the route string alone
// isn't a reliable proxy for "called twice". The call expression is.
const occurrences = generated.split("api.listTeams()").length - 1;
expect(occurrences).toBeGreaterThanOrEqual(2);
});