feat(compiler): compile client api blocks into the browser module
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
export function generateBrowserModule(ast: PageAst): string {
|
||||||
const functions = ast.runtimeFunctions.filter((fn) =>
|
const functions = ast.runtimeFunctions.filter((fn) =>
|
||||||
["legacy", "client", "shared"].includes(fn.runtime),
|
["legacy", "client", "shared"].includes(fn.runtime),
|
||||||
@@ -365,6 +391,7 @@ function __wrnexusCreateClientFunctions(context) {
|
|||||||
const server = context.server;
|
const server = context.server;
|
||||||
const props = context.props;
|
const props = context.props;
|
||||||
const refs = context.refs;
|
const refs = context.refs;
|
||||||
|
${apiBindings(ast)}
|
||||||
const __wrnexusCommit = () => { ${sharedCommit} };
|
const __wrnexusCommit = () => { ${sharedCommit} };
|
||||||
const __wrnexusRestore = () => { ${sharedRestore} };
|
const __wrnexusRestore = () => { ${sharedRestore} };
|
||||||
${!hasAuthoredCommit ? "const commit = __wrnexusCommit;" : ""}
|
${!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");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user