Skip to content

NAPI / JavaScript API

@zntc/core is the NAPI binding that runs the ZNTC engine in-process from Node.js / Bun. The same options as the CLI are available programmatically.

TypeDoc-generated reference: detailed type signatures for every export live under the API Reference sidebar group. This page is a user-friendly summary.

@zntc/core is structured as 3 layers: JS surface (user API + plugin dispatcher) + NAPI bridge + Zig native engine.

LayerResponsibility
JS (packages/core/index.ts)User-facing API surface, option normalization, plugin lifecycle dispatch
Plugin Dispatcherhook registration; per-module call into JS callbacks via NAPI threadsafe-fn
NAPI bridge (packages/core/src/napi_entry.zigzntc.node)JS↔Zig boundary. JSON payload conversion, plugin dispatch
Zig engine (src/)Scanner → Parser → Semantic → Transformer → Codegen for transpile + Bundler → Linker → Tree-shaker → Emitter for bundle

Plugin hooks like onResolve / onLoad are awoken via NAPI threadsafe-fn whenever the native bundler needs them — the Zig worker waits synchronously for the JS callback’s response (including Promise resolution).

Terminal window
bun add @zntc/core
import { init, close, transpile, build, watch } from "@zntc/core";
init(); // load NAPI addon (call once)
// ... use ...
close(); // cleanup on shutdown

Single-file transpile. TypeScript / JSX / Flow / decorator transforms only — no bundling.

import { transpile } from "@zntc/core";
const r = transpile("const x: number = 42;", {
target: "es2020",
jsx: "automatic",
sourcemap: true,
});
console.log(r.code); // "const x = 42;"
console.log(r.map); // sourcemap JSON

Key options (full list: Transpile Options):

  • targetes5 / es2015es2025 / esnext
  • jsxclassic / automatic / automatic-dev
  • flow — Flow type stripping
  • experimentalDecorators / emitDecoratorMetadata
  • minifyWhitespace / minifyIdentifiers / minifySyntax
  • define{ key: value } static replace
  • sourcemap / sourcemapDebugIds
  • tsconfigPath / tsconfigRaw
  • cacheTsconfigCache instance (reuse autodiscover walk results)

Lex a single file into a flat token array — no parser, no AST.

import { tokenize } from "@zntc/core";
const tokens = tokenize("const x = 1;", { filename: "input.ts" });
console.log(tokens[0]); // { kind, text, start, end, line, column, hasNewlineBefore }

TokenizeToken fields: kind / text / start / end / line / column / hasNewlineBefore. TokenizeOptions: { filename?: string }.

In-process cache for tsconfig autodiscover walk results. Recommended for Vite / Rollup plugin authors that repeatedly transpile many files — saves 5–10 fs syscalls per file. Bypassed when tsconfigPath / tsconfigRaw is explicitly set.

import { TsconfigCache, transpile } from "@zntc/core";
using cache = new TsconfigCache(); // Symbol.dispose auto-clears
for (const file of files) {
transpile(read(file), { filename: file, cache });
}
console.log(cache.size); // cached entry count
cache.clear(); // explicit clear (instance reusable)

configureProfile(profile, level?) / profileReport(format?)

Section titled “configureProfile(profile, level?) / profileReport(format?)”

Enable profile categories and read the report. NAPI counterpart to CLI --profile / --profile-level.

import { configureProfile, profileReport, transpile } from "@zntc/core";
configureProfile(["parse", "transform"], "summary");
transpile(source, {});
console.log(profileReport("table")); // "table" | "tree" | "json" | "csv"

level: "summary" | "detailed" | "per-module" | "per-pass".

Bundling. Options compatible with esbuild / Rollup / rolldown.

import { build } from "@zntc/core";
const result = await build({
entryPoints: ["src/index.ts"],
outdir: "dist",
bundle: true,
splitting: true,
format: "esm",
platform: "browser",
minify: true,
sourcemap: true,
outputExports: "named", // CJS/UMD entry export shape
inlineDynamicImports: false, // inline dynamic imports (#2185)
manualChunks: (id, meta) => { // function or record form
if (id.includes("node_modules")) return "vendor";
return null;
},
});
console.log(`Built ${result.outputFiles.length} files`);

buildSync() is the sync variant — does not support JS plugins (threading model limitation). Suitable for simple single-file builds. Array-form alias is also unsupported (RegExp matching is delegated to the host — use build()).

Bundle-specific options:

  • entryPoints
  • outdir / outfile
  • splitting — code splitting
  • preserveModules / preserveModulesRoot
  • inlineDynamicImports
  • externalstring[] or RegExp
  • alias — two forms (see below)
  • loader — extension-keyed loader override (see below)
  • manualChunks — function or record form (#2186)
  • outputExports"auto" | "named" | "default" | "none". default mode with named exports mixed in emits a warning and empty output.
  • banner / footer / intro / outro
  • entryNames / chunkNames / assetNames
  • metafile / analyze
  • sourcemap / sourcemapMode ("linked" | "external" | "inline")
  • emitDiskSourcemap — when false, skip writing .map to disk so dev servers can lazy-serve via WatchHandle.getBundleSourceMap()
  • logLevel (silent | error | warning | info | debug | verbose) — does NOT throw; check result.errors length to detect failure
  • logOverride / logLimit
  • browsersliststring | string[]. Takes precedence over target
  • serverDevServerOptions (defaults for zntc dev / --serve. CLI flags override)
// (1) Object — esbuild form. exact + prefix match
build({ alias: { react: "preact/compat" } });
// "react" / "react/hooks" → "preact/compat" / "preact/compat/hooks"
// (2) Array — Vite form. RegExp `find` supported, build() only
build({ alias: [{ find: /^@\/(.*)$/, replacement: "./src/$1" }] });

Accepted loader values: js | jsx | ts | tsx | json | css | file | dataurl | base64 | text | binary | copy | empty.

build({ loader: { ".png": "file", ".svg": "text", ".wasm": "binary" } });
FieldMeaning
portListen port. CLI --port overrides
hostListen host. true means 0.0.0.0 (Vite-compatible)
strictPortExit if port is taken instead of trying the next one
openOpen the URL in a browser after startup

Auto-enabled when platform: "react-native". The Hermes compatibility matrix is enforced, so target and browserslist are ignored.

  • assetRegistry — Metro AssetRegistry module path (or false to disable)
  • silentConsoleErrorPatterns — silently swallow console.error calls matching any RegExp source pattern
  • entryErrorGuard — wrap entry trigger calls in try/catch + ErrorUtils.reportFatalError. Auto-enabled on RN
  • strictExecutionOrder — prevent function-declaration hoisting (rolldown parity). Auto-enabled on RN
  • workletPluginVersionreact-native-worklets package version string
  • workletTransform — Reanimated worklet transform. Auto-enabled on RN
  • codegenTransform — replace codegenNativeComponent calls with inline view configs. Auto-enabled on RN
  • configurableExports — add configurable: true on Object.defineProperty (RN/Hermes compatibility)
  • reactRefresh — enable React Fast Refresh
  • devMode — wrap modules with __zntc_register() factory + inject HMR runtime
  • rootDir — base path for dev-mode module IDs
  • collectModuleCodes — collect per-module code in dev mode (for HMR rebuild)
  • moduleSpecifierMapimport { x } from 'lodash'import x from 'lodash/x' cherry-pick rewrite
  • globals — Rollup output.globals. IIFE/UMD external → global variable map
  • intro / outro — Rollup output.intro/outro. Inject text inside the format wrapper
  • blockList — resolution block patterns ((RegExp | string)[], Metro blockList compatible)
  • fallback — applied only when normal resolution fails ({ crypto: "crypto-browserify", fs: false })
  • watchFolders — extra watch roots outside the graph (Metro compatible)
  • watchInclude / watchExcludewatchFolders glob include/exclude

Auto-inject runtime API polyfills via core-js. Value forms:

runtimePolyfills: "off" | "auto" | "usage" | "entry" | RuntimePolyfillOptions

RuntimePolyfillOptions fields: mode (auto | usage | entry) / provider ("core-js") / targets (browserslist query) / coreJs (version string) / include[] / exclude[] / proposals (boolean). The top-level coreJs option is equivalent to runtimePolyfills.coreJs.

Per-library 1st-party transform configuration (@next/swc-compatible surface). Drop-in replacement for babel-plugin-styled-components / @emotion/babel-plugin — toggle via options, no plugin registration.

build({
compiler: {
styledComponents: { displayName: true, ssr: true, minify: true },
emotion: { autoLabel: "dev-only", labelFormat: "[local]" },
},
});

StyledComponentsOptions: displayName / ssr / cssProp / fileName / meaninglessFileNames / transpileTemplateLiterals / pure / minify / topLevelImportPaths / namespace / meta. EmotionOptions: autoLabel ("always" | "dev-only" | "never" | boolean) / labelFormat / importMap / sourceMap. (jsxImportSource is a separate top-level BuildOptions field.)

Incremental watch-mode build. Auto rebuild on file change. onReady / onRebuild may return a Promise. Plugin lifecycle hooks run for the initial build and every rebuild in this order: buildStart → buildEnd → onReady/onRebuild → closeBundle.

import { watch } from "@zntc/core";
const handle = watch({
entryPoints: ["src/index.ts"],
outdir: "dist",
bundle: true,
onReady: () => console.log("first build ready"),
onRebuild: (event) => {
console.log("rebuild", event.changed); // changed file paths
console.log("emit", event.phaseDurations?.emit);
},
});
handle.stop();
  • success: boolean / error?: string — rebuild result
  • changed?: string[] — file paths that triggered the rebuild
  • graphChanged?: boolean — whether the module graph topology changed
  • updates?: Array<{ id, code, map? }> — per-module HMR payload (dev mode)
  • bytes?: number / reparsedModules?: number
  • phaseDurations?: { ... } — milliseconds per phase

Default phaseDurations keys: detect / graph / link / shake / emit / delta / total.

Sub-phase keys (when profile: ["..."] or ZNTC_PROFILE env is active): scan / parse / resolve / semantic / transform / codegen / metadata / graphBuild / graphWorker / graphDiscover / graphFinalize / emitPolyfill / emitRefresh / emitOutput / emitMetafile / emitCss / emitPrelude / emitModulePass / emitConcat / emitSourcemapFinalize.

Before 2026-04-22 phaseDurations.parse / semantic were legacy aliases for graph / link+shake and have been removed. parse now reflects real parser time only.

const handle = watch({ ..., sourcemap: true, emitDiskSourcemap: false });
// when the dev server receives /bundle.js.map
const bundleMap = handle.getBundleSourceMap(); // string | null
// when the dev server receives /hmr-map/:moduleId
const moduleMap = handle.getHmrSourceMap(absoluteModulePath); // string | null

VLQ encoding and sourcesContent attachment are deferred from the emit phase to request time, cutting HMR latency. Returns null when sourcemaps are disabled, before the first build, or after stop().

Adapter that converts a Rollup plugin into a ZNTC ZntcPlugin. Vite plugins are Rollup-compatible, so they also work directly.

import { build, vitePlugin } from "@zntc/core";
import someRollupPlugin from "rollup-plugin-something";
await build({
entryPoints: ["src/index.ts"],
plugins: [vitePlugin(someRollupPlugin())],
});

Forwarded hooks: resolveId / load / transform / renderChunk / generateBundle / buildStart / buildEnd / closeBundle (#2156). Lifecycle hooks are also forwarded in watch() for the initial build and every rebuild.

Rollup-style hook interface. See the Plugins guide for details.

import type { ZntcPlugin } from "@zntc/core";
const myPlugin: ZntcPlugin = {
name: "my-plugin",
setup(build) {
build.onResolve({ filter: /^@my\// }, (args) => ({
path: resolve(__dirname, args.path),
// disabled: true, // replace with empty module (Metro/webpack fallback escape)
}));
build.onLoad({ filter: /\.md$/ }, (args) => ({
contents: readFileSync(args.path, "utf-8"),
loader: "text", // (#2157) loader override
}));
build.onTransform({ filter: /\.ts$/ }, (args) => ({
code: args.code.replace(/__VERSION__/g, '"1.0.0"'),
}));
build.onBuildStart(() => console.log("started")); // (#2156)
build.onBuildEnd((err) => err && console.error(err)); // (#2156)
build.onCloseBundle(() => console.log("done")); // (#2156)
// Fill require.context matches via the host runtime's RegExp engine
build.onResolveContext({ filter: /./ }, (args) => {
const re = new RegExp(args.filter ?? ".*", args.flags ?? "");
return { context: scanDir(args.dir, args.recursive, re) };
});
// Function-level AST visit — strip directive / inject trailing code
build.onAstFunction({ filter: /\.tsx?$/ }, (info) => {
if (info.directives.includes("worklet")) return { stripDirective: "worklet" };
});
},
};
  • path — resolved absolute path
  • external — keep the import statement (resolved at runtime)
  • disabled — replace with an empty module (module.exports = {}). Maps Metro { type: 'empty' } and webpack resolve.fallback: false

AstFunctionInfo: name / directives / closureVars / params / sourcePath / bodyText / flags: { async, generator }. AstFunctionResult: stripDirective? / trailingCode?: string[].

import { buildAppSync, prepareAppDevSync } from "@zntc/core";
// Production: scan modules / CSS / assets from an HTML entry → outdir
const r = buildAppSync({ root: ".", outdir: "dist", entryHtml: "index.html" });
// Development: temporary prepare for the dev server (mode = "development")
const dev = prepareAppDevSync({ root: ".", outdir: ".zntc-dev" });

Additional AppBuildOptions / AppDevPrepareOptions fields: publicDir / base / mode / envDir / envPrefixes / define / minify / sourcemap / splitting / compiler.

import { loadConfig, defineConfig } from "@zntc/core";
const env = { mode: "production", command: "build" };
const config = await loadConfig({ cwd: process.cwd(), env });

Additional exports: findConfigPath, findModeConfigPath, loadModuleDefault, mergeUserConfigs, importAndResolveDefault, defaultConfigEnv. Types: UserConfig / UserConfigInput / UserConfigFn / ConfigEnv / ModuleKind.

import { defineWorkspace, loadWorkspace } from "@zntc/core";
export default defineWorkspace([
"packages/*",
{ path: "apps/web", name: "web" },
{ config: defineConfig({ bundle: { entryPoints: ["src/i.ts"] } }) },
]);

Additional exports: loadWorkspace, filterWorkspaces, findWorkspacePath, identifyWorkspaceEntries, loadIdentifiedConfig, WORKSPACE_EXT_PRIORITY. Types: Workspace / WorkspaceEntry / WorkspaceFn / IdentifiedWorkspace.

  • envToDefine(env, prefixes).env variables → define mapping
  • loadEnv(mode, dir, prefixes?).env[.mode][.local] priority loader
  • KNOWN_CONFIG_KEYS — set of recognized config keys
  • suggestKey(key) — typo suggestion (Levenshtein-based)
  • warnUnknownKeys(obj) — warn on unknown keys
  • isPlainObject(v) — plain-object predicate
  • validateTsConfigRaw(raw) — pre-flight tsconfigRaw JSON validation

Type-safe helper for zntc.config.ts. No runtime behavior — identity function only.

zntc.config.ts
import { defineConfig } from "@zntc/core";
export default defineConfig({
bundle: {
entryPoints: ["src/index.ts"],
outdir: "dist",
minify: true,
},
});

Run a phase N times and return statistics (mean / median / p95 / p99 / stddev / min / max). NAPI counterpart to zntc bench --phase=... — same engine. See Benchmarks for results.

import { benchmark } from "@zntc/core";
const r = benchmark({
file: "./src/App.tsx",
phases: ["parse", "transform"],
iterations: 100,
warmup: 10,
});
console.log(r.phases.parse.mean_ms);

BenchmarkOptions fields:

  • source? or file? — exactly one is required
  • filename? — paired with source (used for extension detection)
  • phases: string[] — profile categories to measure. all / none are not allowed
  • iterations? (default 100) / warmup? (default 10)

BenchmarkPhaseStats: samples / mean_ms / median_ms / p95_ms / p99_ms / min_ms / max_ms / stddev_ms. BenchmarkResult: phases: Record<string, BenchmarkPhaseStats>.