How to Compile TypeScript Cores to Native Zig Code at Build Time in the Native SDK

Use the native-core CLI to run transpileFile() on your TypeScript core, then add that generation step as a dependency in build.zig so the resulting Zig module is compiled with your project at build time.

The Vercel Labs Native SDK ships a TypeScript-to-Zig transpiler that converts application cores written in a supported TypeScript subset into native Zig modules during compilation. This build-time workflow lets you author business logic in TypeScript while producing efficient native binaries through Zig. Learning how to drive this pipeline from packages/core/src/cli.ts and integrate it into build.zig is the key to shipping hybrid TS/Zig projects.

How the TypeScript-to-Zig Transpiler Works

The entry point for the transpilation pipeline is transpileFile(entry: string, options?: TranspileOptions): TranspileResult in packages/core/src/transpile.ts at line 53. This function moves the source code through five discrete stages before emitting a single Zig file.

  1. Parse and type-check the entry file and all its imports using the TypeScript compiler API via typed_ast.ts.
  2. Resolve the module graph in modules.ts, ensuring only files within the app’s src/ tree are included.
  3. Run the subset checker from checker.ts to enforce Native SDK rules R1–R18.
  4. Perform integer inference in infer.ts to map JavaScript numbers to Zig i64 where required.
  5. Emit a single Zig module from emitter.ts, writing the runtime prelude, kernel capacity comptime parameters, JavaScript-semantics helpers, and exported core symbols.

Running the native-core CLI Tool

The command-line wrapper lives in packages/core/src/cli.ts and exposes the native-core binary. When invoked, it calls transpileFile(entry, { frameCap, heapCap }) and writes the generated Zig code to the path provided by -o <out.zig> or prints it to stdout.

You must supply two comptime constants that configure the runtime kernel (rt.zig):

  • --frame-cap sets the frame arena size.
  • --heap-cap sets the model heap size.

A typical manual invocation looks like this:

node packages/core/src/cli.ts src/core.ts -o src/core.zig \
    --frame-cap 131072 \
    --heap-cap 524288

Integrating Transpilation into build.zig

Because compilation happens entirely at build time, you can treat the transpiler as a standard system command inside build.zig. Add a custom step that runs the CLI before your main executable compilation, and make the executable depend on that step.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const nativeCore = b.addSystemCommand(&.{
        "node",
        "packages/core/src/cli.ts",
        "examples/system-monitor-ts/src/core.ts",
        "-o", "examples/system-monitor-ts/src/core.zig",
        "--frame-cap", "65536",
        "--heap-cap", "262144",
    });

    const exe = b.addExecutable(.{
        .name = "system-monitor",
        .root_source_file = b.path("examples/system-monitor-ts/src/main.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });
    exe.addCSourceFile(b.path("examples/system-monitor-ts/src/core.zig"), &.{});
    exe.step.dependOn(&nativeCore.step);
    b.installArtifact(exe);
}

Calling exe.step.dependOn(&nativeCore.step) guarantees that zig build regenerates core.zig whenever the TypeScript source changes, preventing stale artifacts from being compiled.

Importing and Using the Generated Zig Module

The output produced by packages/core/src/emitter.ts is a self-contained Zig module that includes three major components:

  • A runtime kernel (rt) parameterized by the frame_cap and heap_cap comptime values.
  • Helper functions that replicate JavaScript semantics, including jsAnd, jsOr, jsShl, and numEq.
  • Exported symbols matching the public API of the original TypeScript module.

You can import the generated file like any other Zig module and interact with its exports directly:

const std = @import("std");
const core = @import("core.zig");

pub fn main() void {
    const phase = core.model.phase;
    std.debug.print("App phase: {s}\n", .{ @tagName(phase) });

    const result = core.jsAnd(0x1234, 0x00FF);
    std.debug.print("jsAnd result: {x}\n", .{ result });
}

Troubleshooting Common Build-Time Errors

  • Missing imports (NS1034): The resolver in packages/core/src/modules.ts only follows imports inside the entry file’s graph. Absolute or out-of-tree paths trigger diagnostic NS1034. Keep all core files under the same src/ directory or define a recognized path alias.
  • TypeScript subset violations: The checker enforces rules R1–R18 from checker.ts. Unsupported features raise diagnostics defined in packages/core/src/diagnostics.ts. Rewrite the offending code to match the supported subset.
  • Insufficient kernel capacities: If your model exceeds default limits, the runtime kernel (rt.zig) may run out of frame or heap space. Pass explicit --frame-cap and --heap-cap values to the CLI.
  • Stale generated files: When the transpilation step is not a declared dependency, Zig can reuse a cached build that skips regeneration. Always link the executable step to the CLI step with exe.step.dependOn(&nativeCore.step).

Summary

  • transpileFile() in packages/core/src/transpile.ts drives the entire pipeline, from parsing TypeScript to emitting a single Zig module.
  • The native-core CLI in packages/core/src/cli.ts exposes this API for build scripts and requires --frame-cap and --heap-cap to size the runtime kernel.
  • In build.zig, declare the CLI as a b.addSystemCommand() step and make your executable depend on it to enforce correct build ordering.
  • The emitted file contains a runtime kernel, JS-semantics helpers such as jsAnd and numEq, and exported TypeScript symbols ready for use via @import.

Frequently Asked Questions

What TypeScript features are supported by the Native SDK transpiler?

The transpiler enforces a strict subset defined by rules R1–R18 in packages/core/src/checker.ts. Only features that pass this validation are emitted; everything else raises a diagnostic through packages/core/src/diagnostics.ts and halts compilation.

How do I configure memory limits for the generated Zig runtime?

Pass --frame-cap and --heap-cap flags to the native-core CLI. These values become comptime constants in rt.zig that size the frame arena and model heap for your transpiled core.

Why does my build fail with diagnostic NS1034?

Diagnostic NS1034 indicates that the module resolver in packages/core/src/modules.ts encountered an import outside the allowed source tree. Move the imported file into the core src/ directory or register a proper path alias to resolve it.

Can I transpile TypeScript to Zig without using the Zig build system?

Yes. You can invoke the CLI manually with node packages/core/src/cli.ts <entry.ts> -o <out.zig> and then import the resulting file into any Zig project using @import("out.zig"). Embedding the call inside build.zig is recommended to keep generated artifacts synchronized automatically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →