The codegen CLI
@rhi-zone/fractal-api-tree includes a CLI (packages/api-tree/src/cli.ts, bin name fractal-api-tree) that generates a standalone, AOT (ahead-of-time) validator module from a tree's leaf op input types — extracted via the TypeScript compiler API — so route validation doesn't depend on a runtime type-reflection library. It orchestrates api-tree's own extractor/tree-walker (extract.ts/tree.ts) into @rhi-zone/fractal-type-ir's validator codegen (compileValidatorModule).
entry file (op tree) --extract input types--> compile --> validator module sourceInvocation
Directly with Bun, against the source file:
bun packages/api-tree/src/cli.ts <command> [options]Or, once the package is installed/linked, via its bin entry:
npx fractal-api-tree <command> [options]Both forms accept the same subcommands and flags.
Subcommands
build <entry> -o <output>
Generates the validator module for <entry> and writes it to <output>. Skips the (potentially expensive) TypeScript-compiler extraction when the output is already newer than the entry file's mtime:
bun packages/api-tree/src/cli.ts build src/api.ts -o src/generated/validators.tsForce a rebuild even when the output looks up to date:
bun packages/api-tree/src/cli.ts build src/api.ts -o src/generated/validators.ts --forceOutput on a skip vs. a real build:
up to date: src/generated/validators.ts
built src/generated/validators.ts in 42.3mswatch <entry> -o <output>
Runs an initial build, then watches the entry file's directory for .ts changes and rebuilds on a 150ms debounce (so rapid-fire saves collapse into one rebuild). Each rebuild diffs the newly generated source against what's on disk and skips the write (no changes) when nothing actually changed:
bun packages/api-tree/src/cli.ts watch src/api.ts -o src/generated/validators.tsCtrl-C (SIGINT) shuts the watcher down cleanly.
stub -o <output>
Writes an empty validator module — a placeholder with zero registered validators — without reading any entry file:
bun packages/api-tree/src/cli.ts stub -o src/generated/validators.tsUse this to get a working validators.ts in place before real codegen has run (e.g. on first checkout of a repo, or scaffolding a new package) so downstream code that imports it compiles and runs — wrapValidators treats a module with no entries as a no-op passthrough for every leaf (see § below).
check <entry> -o <output>
Regenerates the validator module source in memory and compares it byte-for- byte against <output> on disk. Prints up to date and exits 0 when they match; prints stale: ... needs regeneration to stderr and exits 1 otherwise. Intended for CI — fails the build when someone changed an op's input type without re-running build:
bun packages/api-tree/src/cli.ts check src/api.ts -o src/generated/validators.tsThe @generated header
Every file the CLI writes (build, watch, stub) is prefixed with:
// @generated by @rhi-zone/fractal-api-tree — do not editThis is a plain marker, not machinery — it signals to both humans and editor tooling that the file is derived output, not source to hand-edit. check compares the header along with the rest of the source, so editing a generated file by hand will show up as stale on the next CI run.
Connecting to wrapValidators
The generated module exports a validators: Record<routePath, GeneratedEntry> map (per entry-file), where each GeneratedEntry carries a parse(value) function performing coercion + validation + narrowing in one pass. wrapValidators(node, validators) (in packages/api-tree/src/build.ts) walks a Node tree and, for every leaf whose tree position (path segments joined by /, a fallback segment rendered as :name, e.g. "books/:bookId") matches a path in validators, wraps that leaf's handler to run the generated parse() first — success calls the original handler with the parsed value; failure returns Result.err(validationErrors) without ever reaching it. A leaf with no matching entry passes through untouched, which is exactly the behavior a stub-generated module (empty validators) gives you. wrapValidators never mutates the input tree — it returns a fresh one.
This happens at the Node level, before any protocol-specific projection runs, so one generated module wires validation into HTTP, MCP, and CLI alike:
import { wrapValidators } from "@rhi-zone/fractal-api-tree/build"
import { validators } from "./generated/validators.ts"
const validated = wrapValidators(apiTree, validators)In practice you rarely call wrapValidators directly — each projector's OOTB preset takes a validators option and wires it in for you: createFetch(node, { validators }) (packages/http-api-projector/src/preset.ts), createMcpServer(node, { validators }), and runCli(node, { validators }) all wrap the tree with wrapValidators before their own projection/dispatch runs.
Workflow
- Dev / first checkout — run
stub -o <output>so the import resolves and routes work with validation as a no-op, before any real types exist to extract. - Local development — run
watch <entry> -o <output>alongside your dev server; the validator module regenerates as you change op input types. - One-shot regeneration — run
build <entry> -o <output>in a pre-commit hook, build script, or manual step;--forcewhen you need to bypass the mtime skip. - CI — run
check <entry> -o <output>to fail the build if generated output doesn't match whatbuildwould produce from the current source, catching a forgottenbuildafter an op input type changed.