Build Pipeline
TypoKit’s build pipeline is a Rust-native transform that converts plain TypeScript types into optimized runtime artifacts. It’s the core of the framework’s “write the type once, generate everything” philosophy.
The Rust/TypeScript Split
Section titled “The Rust/TypeScript Split”TypoKit draws a hard line between build time and runtime:
| Phase | Language | Rationale |
|---|---|---|
| Build time — AST parsing, type extraction, codegen, route compilation, OpenAPI generation, schema diffing | Rust (via napi-rs) | CPU-intensive computation that benefits from native performance |
| Runtime — server adapters, middleware, handlers, error handling, logging, debug sidecar | TypeScript | AI agents must be able to read, trace, and modify runtime behavior |
This follows the pattern established by SWC, Turbopack, Biome, and oxc — Rust for heavy computation, TypeScript for developer interaction. No native code runs in production unless you explicitly opt into a Rust-based server adapter.
Why Not a TS Compiler Plugin?
Section titled “Why Not a TS Compiler Plugin?”Custom TypeScript transforms are notorious for:
- Breaking with TypeScript version upgrades
- Not integrating with bundlers (Webpack, Rspack, Vite, esbuild)
- Causing problems in monorepo tools (Nx, Turborepo)
- Confusing AI agents that don’t understand the transform step
TS compiler plugins (ttypescript, ts-patch) require patching the TypeScript installation. Instead, TypoKit owns the build step entirely — the transform is a pre-compilation code generation step that reads TypeScript source, generates plain TypeScript files, and lets standard tools compile them normally.
# TypoKit IS the build tool for the server packagetypokit build # Production buildtypokit dev # Dev mode with watch + debug sidecarTransform Pipeline Stages
Section titled “Transform Pipeline Stages”The Rust transform executes eight stages in sequence:
-
Parse — SWC’s
swc_ecma_parserparses TypeScript source files into ASTs. This is the same parser used by SWC/Turbopack, proven to be 20–70× faster than TypeScript’s own parser. -
Extract — Type metadata extraction walks the ASTs, pulling out interfaces, JSDoc tags (
@table,@format,@minLength, etc.), generic parameters, union types, and route contracts. -
Route Table — A compiled radix tree is constructed from the extracted route contracts. The tree structure is serialized to a portable TypeScript module.
-
OpenAPI — An OpenAPI 3.1 specification is generated from the type metadata, including request/response schemas, parameter definitions, and endpoint documentation.
-
Test Stubs — Contract test scaffolding is generated for each route, including valid and invalid fixture data derived from the type constraints.
-
Schema Diff — The current type metadata is compared against the previous build’s snapshot to produce a structured changeset for migration generation.
-
Typia Callback — For each type needing runtime validation, the Rust transform calls back into Typia’s JavaScript API via napi-rs to generate optimized validator functions (see Typia Integration below).
-
Write — All generated artifacts are collected and written to the
.typokit/output directory.
src/types.ts (pure TS) │ ▼┌─────────────────────────────────┐│ TypoKit Native Transform │ ← Rust via napi-rs│ 1. Parse TS ASTs ││ 2. Extract type metadata ││ 3. Generate route table ││ 4. Generate OpenAPI spec ││ 5. Generate test stubs ││ 6. Schema diff (migrations) ││ 7. Validator codegen ││ (Typia callback to JS) ││ 8. Write to .typokit/ │└─────────────────────────────────┘ │ ▼┌─────────────────────────────────┐│ Standard TS Compile │ ← user's existing toolchain│ (tsc / tsup / SWC) ││ Output: dist/ │└─────────────────────────────────┘Typia Integration via napi-rs
Section titled “Typia Integration via napi-rs”Typia is a TypeScript transformer that handles hundreds of edge cases in validation — union discrimination, template literals, mapped types, conditional types, recursive types. Rather than rewriting Typia’s logic in Rust, TypoKit uses a callback pattern across the napi-rs boundary:
Rust Transform JavaScript (Typia)───────────── ──────────────────1. Parse TS ASTs ← Rust2. Extract type metadata ← Rust3. Generate route table ← Rust4. Generate OpenAPI spec ← Rust5. Generate test stubs ← Rust6. Schema diff ← Rust7. For each type needing validation: ├── Pass type metadata ──────────→ Receive metadata │ Call Typia's API └── Receive validator code ←────── Return generated code8. Write all outputs ← RustSteps 1–6 and 8 are where the heavy computation lives (AST parsing and traversal), running at native speed. Step 7 crosses the JS boundary only for validation codegen — a fraction of the total work.
The napi-rs Boundary
Section titled “The napi-rs Boundary”The native transform is published as prebuilt binaries via napi-rs:
@typokit/transform-native ├── src/ # Rust source │ ├── parser.rs # TS AST parsing (swc_ecma_parser) │ ├── type_extractor.rs # JSDoc + type metadata extraction │ ├── route_compiler.rs # Radix tree construction + serialization │ ├── openapi_generator.rs # OpenAPI 3.1 spec generation │ ├── schema_differ.rs # Type diff for migration generation │ ├── test_stub_generator.rs # Contract test scaffolding │ └── typia_bridge.rs # napi-rs callback to Typia JS API │ ├── npm/ # Prebuilt platform packages │ ├── darwin-arm64/ # macOS Apple Silicon │ ├── darwin-x64/ # macOS Intel │ ├── linux-arm64-gnu/ # Linux ARM64 │ ├── linux-x64-gnu/ # Linux x64 │ ├── linux-x64-musl/ # Alpine / Docker │ └── win32-x64-msvc/ # Windows x64 │ └── index.js # Loads correct platform binarynpm install automatically selects the right binary for the current platform. The linux-x64-musl target ensures compatibility with Alpine-based Docker images common in CI.
The .typokit/ Output Directory
Section titled “The .typokit/ Output Directory”All generated artifacts land in .typokit/:
.typokit/ validators/ User.validator.ts # Generated validation function (via Typia) CreateUserInput.validator.ts routes/ route-table.ts # Compiled route registry compiled-router.ts # Compiled radix tree schemas/ openapi.json # Generated OpenAPI spec tests/ users.contract.test.ts # Generated contract tests client/ index.ts # Generated type-safe clientThis directory is:
- Gitignored — generated on build, never committed
- Inspectable — AI agents can read all outputs (everything is TypeScript/JSON, not Rust artifacts)
- Cacheable — only regenerated when source types change (content-hash based)
Tapable Hook System
Section titled “Tapable Hook System”The build pipeline is structured as a tapable hook pipeline inspired by Rspack/Webpack’s plugin architecture. Hooks are exposed as a TypeScript API — plugin authors write hooks in TypeScript, and the Rust transform calls them at the appropriate points via napi-rs.
Build Phase Hooks
Section titled “Build Phase Hooks”// @typokit/core/src/build/pipeline.ts
export interface BuildPipeline { hooks: { /** Runs before any transforms — register additional type sources */ beforeTransform: AsyncSeriesHook<[BuildContext]>;
/** Runs after types are parsed — inspect/modify the type map */ afterTypeParse: AsyncSeriesHook<[SchemaTypeMap, BuildContext]>;
/** Runs after validators are generated — add custom validators */ afterValidators: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
/** Runs after the route table is compiled */ afterRouteTable: AsyncSeriesHook<[CompiledRouteTable, BuildContext]>;
/** Runs after all generation — plugins emit their own artifacts */ emit: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
/** Override or extend the compilation step */ compile: AsyncSeriesHook<[CompileContext, BuildContext]>;
/** Runs after build completes — cleanup, reporting */ done: AsyncSeriesHook<[BuildResult]>; };}The seven phases execute in order:
beforeTransform— Pre-processing, setup, register additional type sourcesafterTypeParse— Type metadata extracted and available for inspection or modificationafterValidators— Compiled validators generated, plugins can add custom validatorsafterRouteTable— Radix tree route table compiledemit— All artifacts ready, plugins emit their own files to.typokit/compile— Compilation step. By default runstsc, but plugins can override this (e.g.,plugin-axumrunscargo buildinstead). TheCompileContext.handledflag prevents the default compiler from running.done— Build complete, cleanup and reporting
Plugin Hook Example
Section titled “Plugin Hook Example”Here’s how @typokit/plugin-ws uses hooks to generate WebSocket artifacts:
// @typokit/plugin-ws/src/build.ts
import type { TypoKitPlugin } from "@typokit/core";
export const wsPlugin: TypoKitPlugin = { name: "@typokit/plugin-ws",
onBuild(pipeline) { // After types are parsed, extract WebSocket channel contracts pipeline.hooks.afterTypeParse.tap("ws-plugin", (typeMap, ctx) => { const wsChannels = extractWsChannels(typeMap); ctx.set("wsChannels", wsChannels); });
// At emit phase, generate WS handler types and validators pipeline.hooks.emit.tap("ws-plugin", (outputs, ctx) => { const wsChannels = ctx.get("wsChannels"); if (wsChannels.length > 0) { outputs.push( generateWsValidators(wsChannels), generateWsRouteTable(wsChannels), ); } }); },};The CompileContext Interface
Section titled “The CompileContext Interface”The compile hook receives a CompileContext that plugins use to override or extend the default TypeScript compilation:
export interface CompileContext { /** Set to true if your plugin handled compilation — skips the default tsc step */ handled: boolean; /** Which compiler ran (e.g., "tsc", "cargo") */ compiler?: string; /** Compilation result */ result?: { success: boolean; errors: string[] };}For example, @typokit/plugin-axum uses the compile hook to run cargo build instead of tsc:
pipeline.hooks.compile.tap("plugin-axum", async (compileCtx, ctx) => { const result = spawnSync("cargo", ["build"], { cwd: ctx.rootDir });
compileCtx.handled = true; // Skip default tsc compileCtx.compiler = "cargo"; compileCtx.result = { success: result.status === 0, errors: result.status !== 0 ? [result.stderr] : [], };});Why Tapable Hooks?
Section titled “Why Tapable Hooks?”- Precise ordering — plugins run at exact points in the pipeline, no ambiguity
- Typed context — each hook receives typed data, so AI agents can reason about what’s available at each phase
- Deterministic execution — hooks are async-series by default, no race conditions
- Inspectable —
typokit inspect build-pipeline --jsonshows all registered hooks and their order
Content-Hash Caching
Section titled “Content-Hash Caching”TypoKit uses content-hash caching for incremental rebuilds. Each source file’s content is hashed, and the transform only regenerates artifacts when the hash changes.
In typokit dev mode, the native transform runs incrementally:
- File watcher detects changes to
types.tstypes - Rust transform re-parses only changed files (AST cache held in memory)
- Only affected validators and routes are regenerated (dependency graph tracked in Rust)
- Hot reload applies changes without full restart
- Debug sidecar stays running across reloads
Build Performance Targets
Section titled “Build Performance Targets”| Metric | Target | Comparison (pure TS approach) |
|---|---|---|
| Cold build (50 types, 20 routes) | < 500ms | ~2–4s with ts-morph |
| Cold build (200 types, 100 routes) | < 2s | ~10–15s with ts-morph |
| Incremental rebuild (1 type changed) | < 50ms | ~500ms–1s with ts-morph |
| Hot reload (dev mode, 1 file) | < 50ms | ~200ms with ts-morph |
| Memory usage (200 types) | < 100MB | ~300–500MB with ts-morph |
These targets are achievable because the Rust transform uses SWC’s parser (proven 20–70× faster than TypeScript’s own parser) and performs all AST traversal, metadata extraction, and code generation in native memory without JS garbage collection pressure.
Integration with Build Tools
Section titled “Integration with Build Tools”TypoKit integrates as a build command, not a plugin:
{ "scripts": { "build": "typokit build", "dev": "typokit dev" }}{ "targets": { "build": { "executor": "@typokit/nx:build" } }}{ "pipeline": { "build": { "dependsOn": ["^build"] } }}The Nx executor and Turborepo integration are thin wrappers that call typokit build with the correct working directory. No custom Webpack/Rspack loaders. No plugin configuration. One command that works everywhere.
Further Reading
Section titled “Further Reading”- Architecture Overview — High-level system architecture
- Compiled Router — Radix tree route matching internals
- Plugins — How to write plugins that tap into the build pipeline
- Custom Plugin Development — Step-by-step plugin creation guide
- Building a Rust/Axum Server — Generate a complete Rust server from TypeScript schemas