feat(compiler): run error section on ssr api call failure

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 16:24:20 +05:30
co-authored by Claude Opus 5
parent 847b6dbe59
commit 7601477f7d
2 changed files with 73 additions and 5 deletions
+45 -5
View File
@@ -35,6 +35,10 @@ interface RenderBinding {
path: string;
body: string;
helpers: string;
// Present only for a sectioned ssr block with a non-empty `error {}` section.
// When set, a failed API call runs this body (with `status`/`message`/`data`
// bound) instead of propagating. Absent -> failures propagate, unchanged.
errorBody?: string;
}
interface SsrBinding extends RenderBinding {
@@ -873,6 +877,7 @@ function renderBinding(binding: NamedDataBinding): RenderBinding {
path: binding.path,
body: binding.body,
helpers: binding.helpers,
...(binding.errorBody ? { errorBody: binding.errorBody } : {}),
};
}
@@ -937,6 +942,7 @@ function apiBindingMap(ast: PageAst, sharedHelpers: string): Map<string, NamedDa
throw new Error(`Duplicate .wrn api binding "${block.name}"`);
}
const sectioned = block.sections;
const errorSection = sectioned?.error.trim();
bindings.set(block.name, {
mode: block.mode,
method: block.method,
@@ -946,6 +952,10 @@ function apiBindingMap(ast: PageAst, sharedHelpers: string): Map<string, NamedDa
body: sectioned
? `const data = $data; ${sectioned.response.trim() || "return data;"}`
: dataBody(block.body),
// Only a sectioned block with a non-empty `error {}` gets a fallback —
// legacy blocks and sectioned blocks without `error` keep failures
// propagating exactly as before.
...(errorSection ? { errorBody: errorSection } : {}),
helpers: modeHelpers(ast, block.mode, sharedHelpers),
});
}
@@ -973,6 +983,18 @@ function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: __Wrn
return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
}
function __wrnexusEvalError(err: unknown, body: string, helpers = "", ctx: __WrnexusContext): unknown {
const adapters = {
cookies: ctx.cookies,
session: ctx.session,
localStorage: ctx.localStorage,
};
const status = (err as { status?: unknown } | null | undefined)?.status;
const data = (err as { data?: unknown } | null | undefined)?.data;
const message = err instanceof Error ? err.message : String(err);
return new Function("$status", "$message", "$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nconst status = $status;\\nconst message = $message;\\nconst data = $data;\\n" + helpers + "\\n" + body)(status, message, data, adapters);
}
function __wrnexusPropAttr(
value: unknown,
): string {
@@ -1002,18 +1024,34 @@ async function __wrnexusCallApi(path: string, method: string, ctx: __WrnexusCont
const url = new URL(path, ctx.req.url);
const res = await fetch(new Request(url, { method, headers: ctx.req.headers }));
const type = res.headers.get("content-type") || "";
if (!res.ok) {
throw new Error(".wrn data API request failed with status " + res.status);
const data = type.includes("application/json")
? await res.json().catch(() => undefined)
: await res.text().catch(() => undefined);
throw Object.assign(new Error(".wrn data API request failed with status " + res.status), {
status: res.status,
data,
});
}
const type = res.headers.get("content-type") || "";
return type.includes("application/json") ? await res.json() : await res.text();
}
async function __wrnexusRenderSsrBindings(html: string, ctx: __WrnexusContext): Promise<string> {
for (const binding of __wrnexusSsrBindings) {
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
let value: unknown;
if (binding.errorBody) {
try {
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
} catch (err) {
value = __wrnexusEvalError(err, binding.errorBody, binding.helpers, ctx);
}
} else {
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
}
html = html.replace(binding.marker, __wrnexusEscapeHtml(value));
}
return html;
@@ -1531,7 +1569,9 @@ function generateInner(ast: PageAst): string {
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
if (needsSsrRuntime) {
out.push(ssrRuntimeSource());
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
out.push(
`const __wrnexusSsrBindings: Array<{ method: string; path: string; body: string; helpers: string; errorBody?: string }> = ${JSON.stringify(ssrBindings, null, 2)};`,
);
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
out.push(
`export default async function ${ast.name}(ctx: __WrnexusContext) {
@@ -32,3 +32,31 @@ test("a legacy ssr block is unchanged", () => {
expect(generated).toContain("users.length");
});
test("an ssr block with an error section emits the error body and binds status/message/data", () => {
const generated = serverModule(` api ssrUsers GET /api/users {
response {
return data.users.length
}
error {
return message + status + data
}
}`);
expect(generated).toContain('"errorBody"');
expect(generated).toContain("return message + status + data");
expect(generated).toContain("const status = $status");
expect(generated).toContain("const message = $message");
expect(generated).toContain("const data = $data");
expect(generated).toContain("__wrnexusEvalError");
});
test("an ssr block without an error section emits no catch entry for that binding", () => {
const generated = serverModule(` api ssrUsers GET /api/users {
response {
return data.users.length
}
}`);
expect(generated).not.toContain('"errorBody"');
});