fix(cli): move api-block assertions into a real .ts file
skipLibCheck exempts .d.ts contents from being checked, so assertions written inside wrnexus.generated.d.ts were never evaluated by tsc. Emit them into wrnexus.generated.api-checks.ts instead, referencing the WRNexusGenerated namespace's helper types (which stay in the .d.ts). Track the new generated file in check:generated-types. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
|
||||||
|
//
|
||||||
|
// Type-only assertions for sectioned `api` blocks. Kept as a real .ts file (not
|
||||||
|
// wrnexus.generated.d.ts) because `skipLibCheck` exempts .d.ts contents from being
|
||||||
|
// checked; this file is compiled and checked normally by the project's own tsc.
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -57,4 +57,8 @@ declare namespace WRNexusGenerated {
|
|||||||
"welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>;
|
"welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>;
|
||||||
}
|
}
|
||||||
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
|
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
|
||||||
|
type AssertAssignable<Actual, Expected> = [Actual] extends [Expected] ? true : false;
|
||||||
|
type __wrn_expect_true<T extends true> = T;
|
||||||
|
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
|
||||||
|
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ function apiBlockAssertions(pages: { path: string; ast: PageAst }[], apiContract
|
|||||||
: "Record<string, never>";
|
: "Record<string, never>";
|
||||||
|
|
||||||
lines.push(
|
lines.push(
|
||||||
` type __wrn_api_check_${block.name} = __wrn_expect_true<AssertAssignable<${shape}, ApiInput<${JSON.stringify(
|
`type __wrn_api_check_${block.name} = WRNexusGenerated.__wrn_expect_true<WRNexusGenerated.AssertAssignable<${shape}, WRNexusGenerated.ApiInput<${JSON.stringify(
|
||||||
block.path,
|
block.path,
|
||||||
)}, ${JSON.stringify(block.method)}>>>;`,
|
)}, ${JSON.stringify(block.method)}>>>;`,
|
||||||
);
|
);
|
||||||
@@ -262,12 +262,23 @@ declare namespace WRNexusGenerated {
|
|||||||
type __wrn_expect_true<T extends true> = T;
|
type __wrn_expect_true<T extends true> = T;
|
||||||
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
|
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
|
||||||
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
|
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
|
||||||
${apiBlockAssertions(pageAsts, apiContracts)}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
mkdirSync(typeDir, { recursive: true });
|
mkdirSync(typeDir, { recursive: true });
|
||||||
const output = join(typeDir, "wrnexus.generated.d.ts");
|
const output = join(typeDir, "wrnexus.generated.d.ts");
|
||||||
writeFileSync(output, code, "utf8");
|
writeFileSync(output, code, "utf8");
|
||||||
|
// `.d.ts` contents are exempt from checking under `skipLibCheck: true` (set in the
|
||||||
|
// repo/app tsconfig), so the per-block assertions are written into a real `.ts` file
|
||||||
|
// instead — only genuine `.ts`/`.tsx` sources are compiled and checked.
|
||||||
|
const apiChecksCode = `// AUTO-GENERATED by \`wrnexus generate types\` - do not edit.
|
||||||
|
//
|
||||||
|
// Type-only assertions for sectioned \`api\` blocks. Kept as a real .ts file (not
|
||||||
|
// wrnexus.generated.d.ts) because \`skipLibCheck\` exempts .d.ts contents from being
|
||||||
|
// checked; this file is compiled and checked normally by the project's own tsc.
|
||||||
|
${apiBlockAssertions(pageAsts, apiContracts)}
|
||||||
|
export {};
|
||||||
|
`;
|
||||||
|
writeFileSync(join(typeDir, "wrnexus.generated.api-checks.ts"), apiChecksCode, "utf8");
|
||||||
writePluginArtifacts(root, pluginContributions);
|
writePluginArtifacts(root, pluginContributions);
|
||||||
return {
|
return {
|
||||||
file: relative(root, output).replace(/\\/g, "/"),
|
file: relative(root, output).replace(/\\/g, "/"),
|
||||||
|
|||||||
@@ -51,15 +51,19 @@ test("emits the ApiInput, ApiOutput and AssertAssignable helpers", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The per-block assertions live in a plain .ts file, not the .d.ts: `skipLibCheck: true`
|
||||||
|
// (set repo-wide) exempts .d.ts *contents* from being checked at all, so a `.d.ts` can
|
||||||
|
// never actually enforce anything here. A real .ts file under app/ is compiled and
|
||||||
|
// checked normally.
|
||||||
test("emits one assertion per sectioned block, naming its route and method", () => {
|
test("emits one assertion per sectioned block, naming its route and method", () => {
|
||||||
const root = fixture(BLOCK);
|
const root = fixture(BLOCK);
|
||||||
generateApplicationTypes(root);
|
generateApplicationTypes(root);
|
||||||
const generated = readFileSync(join(root, "app/types/wrnexus.generated.d.ts"), "utf8");
|
const checks = readFileSync(join(root, "app/types/wrnexus.generated.api-checks.ts"), "utf8");
|
||||||
|
|
||||||
expect(generated).toContain("__wrn_api_check_searchUsers");
|
expect(checks).toContain("__wrn_api_check_searchUsers");
|
||||||
expect(generated).toContain('ApiInput<"/api/users", "POST">');
|
expect(checks).toContain('WRNexusGenerated.ApiInput<"/api/users", "POST">');
|
||||||
expect(generated).toContain("name?: string");
|
expect(checks).toContain("name?: string");
|
||||||
expect(generated).toContain("age?: number");
|
expect(checks).toContain("age?: number");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a legacy bare-body block produces no assertion", () => {
|
test("a legacy bare-body block produces no assertion", () => {
|
||||||
@@ -67,7 +71,16 @@ test("a legacy bare-body block produces no assertion", () => {
|
|||||||
return users.length
|
return users.length
|
||||||
}`);
|
}`);
|
||||||
generateApplicationTypes(root);
|
generateApplicationTypes(root);
|
||||||
const generated = readFileSync(join(root, "app/types/wrnexus.generated.d.ts"), "utf8");
|
const checks = readFileSync(join(root, "app/types/wrnexus.generated.api-checks.ts"), "utf8");
|
||||||
|
|
||||||
expect(generated).not.toContain("__wrn_api_check_legacyUsers");
|
expect(checks).not.toContain("__wrn_api_check_legacyUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the api-checks file has no runtime code and is a module", () => {
|
||||||
|
const root = fixture(BLOCK);
|
||||||
|
generateApplicationTypes(root);
|
||||||
|
const checks = readFileSync(join(root, "app/types/wrnexus.generated.api-checks.ts"), "utf8");
|
||||||
|
|
||||||
|
expect(checks).toContain("AUTO-GENERATED");
|
||||||
|
expect(checks.trim().endsWith("export {};")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const scratchParent = join(root, ".wrnexus-type-check");
|
|||||||
const scratch = mkdtempSync(`${scratchParent}-`);
|
const scratch = mkdtempSync(`${scratchParent}-`);
|
||||||
const generated = [
|
const generated = [
|
||||||
join("app", "types", "wrnexus.generated.d.ts"),
|
join("app", "types", "wrnexus.generated.d.ts"),
|
||||||
|
join("app", "types", "wrnexus.generated.api-checks.ts"),
|
||||||
join("app", "types", "wrnexus.plugins.generated.d.ts"),
|
join("app", "types", "wrnexus.plugins.generated.d.ts"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user