diff --git a/examples/basic-app/app/types/wrnexus.generated.api-checks.ts b/examples/basic-app/app/types/wrnexus.generated.api-checks.ts new file mode 100644 index 00000000..372018ab --- /dev/null +++ b/examples/basic-app/app/types/wrnexus.generated.api-checks.ts @@ -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 {}; diff --git a/examples/basic-app/app/types/wrnexus.generated.d.ts b/examples/basic-app/app/types/wrnexus.generated.d.ts index 537a1c99..d897d761 100644 --- a/examples/basic-app/app/types/wrnexus.generated.d.ts +++ b/examples/basic-app/app/types/wrnexus.generated.d.ts @@ -57,4 +57,8 @@ declare namespace WRNexusGenerated { "welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>; } type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"]; + type AssertAssignable = [Actual] extends [Expected] ? true : false; + type __wrn_expect_true = T; + type ApiInput

= ApiContracts[P][M]["input"]; + type ApiOutput

= ApiContracts[P][M]["output"]; } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 2456f924..dbd653f2 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -125,7 +125,7 @@ function apiBlockAssertions(pages: { path: string; ast: PageAst }[], apiContract : "Record"; lines.push( - ` type __wrn_api_check_${block.name} = __wrn_expect_true>>;`, ); @@ -262,12 +262,23 @@ declare namespace WRNexusGenerated { type __wrn_expect_true = T; type ApiInput

= ApiContracts[P][M]["input"]; type ApiOutput

= ApiContracts[P][M]["output"]; -${apiBlockAssertions(pageAsts, apiContracts)} } `; mkdirSync(typeDir, { recursive: true }); const output = join(typeDir, "wrnexus.generated.d.ts"); 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); return { file: relative(root, output).replace(/\\/g, "/"), diff --git a/packages/cli/test/api-block-types.test.ts b/packages/cli/test/api-block-types.test.ts index 5bb2d712..1cb2708e 100644 --- a/packages/cli/test/api-block-types.test.ts +++ b/packages/cli/test/api-block-types.test.ts @@ -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", () => { const root = fixture(BLOCK); 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(generated).toContain('ApiInput<"/api/users", "POST">'); - expect(generated).toContain("name?: string"); - expect(generated).toContain("age?: number"); + expect(checks).toContain("__wrn_api_check_searchUsers"); + expect(checks).toContain('WRNexusGenerated.ApiInput<"/api/users", "POST">'); + expect(checks).toContain("name?: string"); + expect(checks).toContain("age?: number"); }); 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 }`); 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); }); diff --git a/scripts/check-generated-types.ts b/scripts/check-generated-types.ts index eed217c6..e02e5320 100644 --- a/scripts/check-generated-types.ts +++ b/scripts/check-generated-types.ts @@ -8,6 +8,7 @@ const scratchParent = join(root, ".wrnexus-type-check"); const scratch = mkdtempSync(`${scratchParent}-`); const generated = [ join("app", "types", "wrnexus.generated.d.ts"), + join("app", "types", "wrnexus.generated.api-checks.ts"), join("app", "types", "wrnexus.plugins.generated.d.ts"), ];