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