feat(compiler): run error section on ssr api call failure
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user