From bd2f6ac5e3087f1445ad1d66fb43aba1994c1d0f Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Wed, 19 Aug 2026 15:41:13 +0530 Subject: [PATCH] feat(compiler): compile client api blocks into the browser module Co-Authored-By: Claude Opus 5 --- packages/compiler/src/client-codegen.ts | 27 ++++++ .../compiler/test/api-block-codegen.test.ts | 82 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 packages/compiler/test/api-block-codegen.test.ts diff --git a/packages/compiler/src/client-codegen.ts b/packages/compiler/src/client-codegen.ts index 7292ba27..066b7b5d 100644 --- a/packages/compiler/src/client-codegen.ts +++ b/packages/compiler/src/client-codegen.ts @@ -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;" : ""} diff --git a/packages/compiler/test/api-block-codegen.test.ts b/packages/compiler/test/api-block-codegen.test.ts new file mode 100644 index 00000000..d1f07da2 --- /dev/null +++ b/packages/compiler/test/api-block-codegen.test.ts @@ -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 { + const users = await api.searchUsers({ name: "Ajay" }) + console.log(users) + } + } + + view {
} +} +`), + ).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"); +});