Skip to content

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 source

Invocation ​

Directly with Bun, against the source file:

sh
bun packages/api-tree/src/cli.ts <command> [options]

Or, once the package is installed/linked, via its bin entry:

sh
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:

sh
bun packages/api-tree/src/cli.ts build src/api.ts -o src/generated/validators.ts

Force a rebuild even when the output looks up to date:

sh
bun packages/api-tree/src/cli.ts build src/api.ts -o src/generated/validators.ts --force

Output on a skip vs. a real build:

up to date: src/generated/validators.ts
built src/generated/validators.ts in 42.3ms

watch <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:

sh
bun packages/api-tree/src/cli.ts watch src/api.ts -o src/generated/validators.ts

Ctrl-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:

sh
bun packages/api-tree/src/cli.ts stub -o src/generated/validators.ts

Use 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:

sh
bun packages/api-tree/src/cli.ts check src/api.ts -o src/generated/validators.ts

The @generated header ​

Every file the CLI writes (build, watch, stub) is prefixed with:

// @generated by @rhi-zone/fractal-api-tree — do not edit

This 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:

ts
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 ​

  1. 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.
  2. Local development — run watch <entry> -o <output> alongside your dev server; the validator module regenerates as you change op input types.
  3. One-shot regeneration — run build <entry> -o <output> in a pre-commit hook, build script, or manual step; --force when you need to bypass the mtime skip.
  4. CI — run check <entry> -o <output> to fail the build if generated output doesn't match what build would produce from the current source, catching a forgotten build after an op input type changed.