feat(cli): generate type assertions for api blocks

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 15:56:02 +05:30
co-authored by Claude Opus 5
parent 70777e4a45
commit 419614d9d1
2 changed files with 119 additions and 1 deletions
+46 -1
View File
@@ -4,7 +4,7 @@ import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
// Keep the CLI checker sourced from the package contract so typecheck fixes are
// included in each published CLI bundle.
import { checkWrnFile, type WrnTypeDiagnostic } from "@wrnexus/typecheck";
import { parse } from "@wrnexus/syntax";
import { parse, type PageAst } from "@wrnexus/syntax";
import { generate, generateTargets } from "@wrnexus/compiler";
import { regenerateRoutes } from "./routes.ts";
import { loadAppConfig } from "@wrnexus/styles";
@@ -99,6 +99,42 @@ function writePluginArtifacts(root: string, contributions?: PluginContributions)
);
}
/**
* Type assertions for sectioned api blocks.
*
* Enforcement lives here rather than in the compiler because this file is under
* `app/` and is therefore compiled by the project's own tsc, while generated
* build artifacts are not type-checked at all.
*/
function apiBlockAssertions(pages: { path: string; ast: PageAst }[], apiContracts: string): string {
const lines: string[] = [];
for (const page of pages) {
for (const block of page.ast.dataApis) {
if (!block.sections) continue;
if (!apiContracts.includes(JSON.stringify(block.path))) {
console.warn(
`[wrnexus] api block "${block.name}" targets ${block.path}, which has no defineEndpoint contract — its declared types are not checked.`,
);
}
const fields = [...block.sections.parameters, ...block.sections.body];
const shape = fields.length
? `{ ${fields.map((f) => `${f.name}${f.optional ? "?" : ""}: ${f.type}`).join("; ")} }`
: "Record<string, never>";
lines.push(
` type __wrn_api_check_${block.name} = __wrn_expect_true<AssertAssignable<${shape}, ApiInput<${JSON.stringify(
block.path,
)}, ${JSON.stringify(block.method)}>>>;`,
);
}
}
return lines.join("\n");
}
export function generateApplicationTypes(
appRoot: string,
pluginContributions?: PluginContributions,
@@ -145,6 +181,10 @@ export function generateApplicationTypes(
return ` ${JSON.stringify(component.name)}: { props: ${props ? `{ ${props} }` : "Record<string, never>"}; outputs: ${outputs ? `{ ${outputs} }` : "Record<string, never>"} };`;
})
.join("\n");
const pageAsts = files(app, (path) => extname(path) === ".wrn").map((file) => ({
path: file,
ast: parse(readFileSync(file, "utf8")),
}));
const typeDir = join(app, "types");
const apiContracts = router.api
.map((route) => {
@@ -218,6 +258,11 @@ declare namespace WRNexusGenerated {
${generatedContractMap("RealtimeMessages", realtimeContracts)}
${generatedContractMap("QueuePayloads", queueContracts)}
type ApplicationConfig = ${configFile ? `(typeof import(${typeImport(typeDir, configFile)}))["default"]` : "Record<string, never>"};
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"];
${apiBlockAssertions(pageAsts, apiContracts)}
}
`;
mkdirSync(typeDir, { recursive: true });