feat(cli): assert types for every api block with declared fields
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+13
-12
@@ -138,19 +138,20 @@ function apiBlockAssertions(
|
|||||||
for (const block of page.ast.dataApis) {
|
for (const block of page.ast.dataApis) {
|
||||||
if (!block.sections) continue;
|
if (!block.sections) continue;
|
||||||
|
|
||||||
// ssr-mode sectioned blocks can never declare a `request` (they are
|
// A block with zero declared request fields would fall back to the
|
||||||
// render-time only), so they always fall back to the empty-shape
|
// empty-shape `Record<string, never>` below. `keyof Record<string,
|
||||||
// `Record<string, never>` below. `keyof Record<string, never>` is
|
// never>` is `string`, which makes the key-exactness arm of
|
||||||
// `string`, which makes the key-exactness arm of AssertAssignable
|
// AssertAssignable evaluate to `false` unconditionally and raises
|
||||||
// evaluate to `false` unconditionally and raises TS2344 on every such
|
// TS2344 on every such block regardless of whether the block author
|
||||||
// block regardless of whether the block author did anything wrong.
|
// did anything wrong. We skip emission for any block with zero
|
||||||
// We choose to skip emission for both (a) any non-client-mode block,
|
// declared request fields, since there is nothing to assert
|
||||||
// since it structurally can never have a request to check, and (b) any
|
// type-safety about. This applies regardless of mode: `ssr` blocks can
|
||||||
// block -- client included -- that has zero declared request fields,
|
// never declare a `request` and so are always caught by this same
|
||||||
// since there is nothing to assert type-safety about. This is more
|
// check; mode-less (`any`) and `client` blocks can declare fields and
|
||||||
// honest about intent than emitting a vacuous/always-failing check.
|
// get an assertion whenever they do. This is more honest about intent
|
||||||
|
// than emitting a vacuous/always-failing check.
|
||||||
const fields = [...block.sections.parameters, ...block.sections.body];
|
const fields = [...block.sections.parameters, ...block.sections.body];
|
||||||
if (block.mode !== "client" || fields.length === 0) continue;
|
if (fields.length === 0) continue;
|
||||||
|
|
||||||
if (!apiContracts.includes(JSON.stringify(block.path))) {
|
if (!apiContracts.includes(JSON.stringify(block.path))) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { afterEach, expect, test } from "bun:test";
|
||||||
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { generateApplicationTypes } from "../src/types.ts";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function fixture(apisBlock: string): string {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-apis-types-"));
|
||||||
|
roots.push(root);
|
||||||
|
mkdirSync(join(root, "app/pages"), { recursive: true });
|
||||||
|
mkdirSync(join(root, "app/api"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(root, "app/api/users.ts"),
|
||||||
|
`export const POST = async () => Response.json({ users: [] });\n`,
|
||||||
|
);
|
||||||
|
writeFileSync(
|
||||||
|
join(root, "app/pages/search.wrn"),
|
||||||
|
`page Search {\n apis {\n${apisBlock}\n }\n\n view { <main>x</main> }\n}\n`,
|
||||||
|
);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a mode-less block with declared fields gets an assertion", () => {
|
||||||
|
const root = fixture(` searchUsers POST /api/users {
|
||||||
|
request { body { name?: string } }
|
||||||
|
response { return data.users }
|
||||||
|
}`);
|
||||||
|
generateApplicationTypes(root);
|
||||||
|
const generated = readFileSync(join(root, "app/types/wrnexus.generated.api-checks.ts"), "utf8");
|
||||||
|
|
||||||
|
expect(generated).toContain("searchUsers");
|
||||||
|
expect(generated).toContain('ApiInput<"/api/users", "POST">');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a block with no declared fields gets no assertion", () => {
|
||||||
|
const root = fixture(` listAll GET /api/users {
|
||||||
|
response { return data.users }
|
||||||
|
}`);
|
||||||
|
generateApplicationTypes(root);
|
||||||
|
const generated = readFileSync(join(root, "app/types/wrnexus.generated.api-checks.ts"), "utf8");
|
||||||
|
|
||||||
|
expect(generated).not.toContain("listAll");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user