feat(compiler): compile client api blocks into the browser module

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 15:41:13 +05:30
co-authored by Claude Opus 5
parent b457ad1d54
commit bd2f6ac5e3
2 changed files with 109 additions and 0 deletions
+27
View File
@@ -308,6 +308,32 @@ function _functionEntry(
}`;
}
/**
* Client-mode api blocks become members of an `api` object in client scope.
*
* Only the response and error bodies are emitted; the declared field types are
* type-only and are consumed by the types generator instead. Anything
* TypeScript reaching this module would be a syntax error in the .mjs artifact.
*/
function apiBindings(ast: PageAst): string {
const members = ast.dataApis
.filter((block) => block.mode === "client" && block.sections)
.map((block) => {
const sections = block.sections!;
const response = sections.response.trim() || "return data;";
const error = sections.error.trim();
const failure = error
? `(error) => { const status = error.status; const message = error.message; const data = error.data; ${error} }`
: `(error) => { throw error; }`;
return ` ${JSON.stringify(block.name)}: async (input) => context.callApi(${JSON.stringify(
block.path,
)}, ${JSON.stringify(block.method)}, input).then((data) => { ${response} }).catch(${failure})`;
});
return members.length ? `const api = {\n${members.join(",\n")}\n };` : "";
}
export function generateBrowserModule(ast: PageAst): string {
const functions = ast.runtimeFunctions.filter((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
@@ -365,6 +391,7 @@ function __wrnexusCreateClientFunctions(context) {
const server = context.server;
const props = context.props;
const refs = context.refs;
${apiBindings(ast)}
const __wrnexusCommit = () => { ${sharedCommit} };
const __wrnexusRestore = () => { ${sharedRestore} };
${!hasAuthoredCommit ? "const commit = __wrnexusCommit;" : ""}
@@ -0,0 +1,82 @@
import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generateTargets } from "../src/targets.ts";
function browserModule(inner: string): string {
return generateTargets(
parse(`page Repro {
client {
${inner}
}
functions {
client async function run(): Promise<void> {
const users = await api.searchUsers({ name: "Ajay" })
console.log(users)
}
}
view { <main><button @click="run()">go</button></main> }
}
`),
).browser;
}
const BLOCK = ` api searchUsers POST /api/users {
request {
body {
name?: string
age?: number
}
}
response {
return data.users
}
error {
return []
}
}`;
test("emits an api member that calls the transport with the block's path and method", () => {
const generated = browserModule(BLOCK);
expect(generated).toContain("const api =");
expect(generated).toContain("searchUsers");
expect(generated).toContain('"/api/users"');
expect(generated).toContain('"POST"');
});
test("declared field types never reach the browser module", () => {
// The artifact is written as .mjs and parsed as JavaScript.
const generated = browserModule(BLOCK);
expect(generated).not.toContain("name?: string");
expect(generated).not.toContain("age?: number");
});
test("the emitted module is valid JavaScript", () => {
const generated = browserModule(BLOCK);
expect(() => {
new Function(generated.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, ""));
}).not.toThrow();
});
test("a block without an error section still emits its response body", () => {
const generated = browserModule(` api plainUsers GET /api/users {
request {
parameters {
team: string
}
}
response {
return data.users
}
}`);
expect(generated).toContain("plainUsers");
expect(generated).toContain("data.users");
});