feat(compiler): emit browser api bindings only where the client calls them
This commit is contained in:
@@ -308,15 +308,51 @@ function _functionEntry(
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-mode api blocks become members of an `api` object in client scope.
|
||||
* Block names the page's client functions actually call.
|
||||
*
|
||||
* A block's response and error bodies are page code. Emitting one the browser
|
||||
* never calls would ship a server-only transform to every visitor and grow the
|
||||
* bundle for nothing.
|
||||
*/
|
||||
function clientCalledApiNames(ast: PageAst): Set<string> {
|
||||
const called = new Set<string>();
|
||||
const bodies = ast.runtimeFunctions
|
||||
.filter((fn) => ["client", "shared"].includes(fn.runtime))
|
||||
.map((fn) => fn.body)
|
||||
.join("\n");
|
||||
|
||||
for (const match of bodies.matchAll(/\bapi\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
|
||||
called.add(match[1]!);
|
||||
}
|
||||
|
||||
return called;
|
||||
}
|
||||
|
||||
/**
|
||||
* A block is emitted into the browser module when it is authored as
|
||||
* client-only, or when it is mode "any" and a client function actually calls
|
||||
* it. `hasClientApi` below must use this exact predicate so the `api`
|
||||
* reserved-binding exclusion and the emitted object can never disagree.
|
||||
*/
|
||||
function isClientEmittedApiBlock(block: PageAst["dataApis"][number], called: Set<string>): boolean {
|
||||
return (
|
||||
Boolean(block.sections) &&
|
||||
(block.mode === "client" || (block.mode === "any" && called.has(block.name)))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-mode and client-called any-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 called = clientCalledApiNames(ast);
|
||||
const members = ast.dataApis
|
||||
.filter((block) => block.mode === "client" && block.sections)
|
||||
.filter((block) => isClientEmittedApiBlock(block, called))
|
||||
.map((block) => {
|
||||
const sections = block.sections!;
|
||||
const response = eraseFunctionTypes(sections.response).trim() || "return data;";
|
||||
@@ -345,7 +381,9 @@ export function generateBrowserModule(ast: PageAst): string {
|
||||
// `state api` without any client api blocks must keep reading/writing that
|
||||
// state as before, so only exclude the "api" name from destructuring when
|
||||
// there is a real `api` binding to shadow it.
|
||||
const hasClientApi = ast.dataApis.some((block) => block.mode === "client" && block.sections);
|
||||
const hasClientApi = ast.dataApis.some((block) =>
|
||||
isClientEmittedApiBlock(block, clientCalledApiNames(ast)),
|
||||
);
|
||||
const localRuntimeBindings = hasClientApi
|
||||
? RUNTIME_BINDINGS
|
||||
: new Set([...RUNTIME_BINDINGS].filter((name) => name !== "api"));
|
||||
|
||||
Reference in New Issue
Block a user