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.
Architecture
Section titled “Architecture”@zntc/core is structured as 3 layers: JS surface (user API + plugin dispatcher) + NAPI bridge + Zig native engine.
| Layer | Responsibility |
|---|---|
JS (packages/core/index.ts) | User-facing API surface, option normalization, plugin lifecycle dispatch |
| Plugin Dispatcher | hook registration; per-module call into JS callbacks via NAPI threadsafe-fn |
NAPI bridge (packages/core/src/napi_entry.zig → zntc.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).
Install + initialize
Section titled “Install + initialize”bun add @zntc/coreimport { init, close, transpile, build, watch } from "@zntc/core";
init(); // load NAPI addon (call once)// ... use ...close(); // cleanup on shutdowntranspile(source, options)
Section titled “transpile(source, options)”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 JSONKey options (full list: Transpile Options):
target—es5/es2015–es2025/esnextjsx—classic/automatic/automatic-devflow— Flow type strippingexperimentalDecorators/emitDecoratorMetadataminifyWhitespace/minifyIdentifiers/minifySyntaxdefine—{ key: value }static replacesourcemap/sourcemapDebugIdstsconfigPath/tsconfigRawcache—TsconfigCacheinstance (reuse autodiscover walk results)
tokenize(source, options)
Section titled “tokenize(source, options)”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 }.
TsconfigCache
Section titled “TsconfigCache”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-clearsfor (const file of files) { transpile(read(file), { filename: file, cache });}console.log(cache.size); // cached entry countcache.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".
build(options) / buildSync(options)
Section titled “build(options) / buildSync(options)”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:
entryPointsoutdir/outfilesplitting— code splittingpreserveModules/preserveModulesRootinlineDynamicImportsexternal—string[]or RegExpalias— two forms (see below)loader— extension-keyed loader override (see below)manualChunks— function or record form (#2186)outputExports—"auto" | "named" | "default" | "none".defaultmode with named exports mixed in emits a warning and empty output.banner/footer/intro/outroentryNames/chunkNames/assetNamesmetafile/analyzesourcemap/sourcemapMode("linked" | "external" | "inline")emitDiskSourcemap— whenfalse, skip writing.mapto disk so dev servers can lazy-serve viaWatchHandle.getBundleSourceMap()logLevel(silent | error | warning | info | debug | verbose) — does NOT throw; checkresult.errorslength to detect failurelogOverride/logLimitbrowserslist—string | string[]. Takes precedence overtargetserver—DevServerOptions(defaults forzntc dev/--serve. CLI flags override)
alias forms
Section titled “alias forms”// (1) Object — esbuild form. exact + prefix matchbuild({ alias: { react: "preact/compat" } });// "react" / "react/hooks" → "preact/compat" / "preact/compat/hooks"
// (2) Array — Vite form. RegExp `find` supported, build() onlybuild({ alias: [{ find: /^@\/(.*)$/, replacement: "./src/$1" }] });loader vocabulary
Section titled “loader vocabulary”Accepted loader values: js | jsx | ts | tsx | json | css | file | dataurl | base64 | text | binary | copy | empty.
build({ loader: { ".png": "file", ".svg": "text", ".wasm": "binary" } });server (DevServerOptions)
Section titled “server (DevServerOptions)”| Field | Meaning |
|---|---|
port | Listen port. CLI --port overrides |
host | Listen host. true means 0.0.0.0 (Vite-compatible) |
strictPort | Exit if port is taken instead of trying the next one |
open | Open the URL in a browser after startup |
React Native related options
Section titled “React Native related options”Auto-enabled when platform: "react-native". The Hermes compatibility matrix is enforced, so target
and browserslist are ignored.
assetRegistry— Metro AssetRegistry module path (orfalseto disable)silentConsoleErrorPatterns— silently swallowconsole.errorcalls matching any RegExp source patternentryErrorGuard— wrap entry trigger calls intry/catch + ErrorUtils.reportFatalError. Auto-enabled on RNstrictExecutionOrder— prevent function-declaration hoisting (rolldown parity). Auto-enabled on RNworkletPluginVersion—react-native-workletspackage version stringworkletTransform— Reanimated worklet transform. Auto-enabled on RNcodegenTransform— replacecodegenNativeComponentcalls with inline view configs. Auto-enabled on RNconfigurableExports— addconfigurable: trueonObject.defineProperty(RN/Hermes compatibility)reactRefresh— enable React Fast RefreshdevMode— wrap modules with__zntc_register()factory + inject HMR runtimerootDir— base path for dev-mode module IDscollectModuleCodes— collect per-module code in dev mode (for HMR rebuild)moduleSpecifierMap—import { x } from 'lodash'→import x from 'lodash/x'cherry-pick rewriteglobals— Rollupoutput.globals. IIFE/UMD external → global variable mapintro/outro— Rollupoutput.intro/outro. Inject text inside the format wrapperblockList— resolution block patterns ((RegExp | string)[], MetroblockListcompatible)fallback— applied only when normal resolution fails ({ crypto: "crypto-browserify", fs: false })watchFolders— extra watch roots outside the graph (Metro compatible)watchInclude/watchExclude—watchFoldersglob include/exclude
runtimePolyfills / runtimeTarget / coreJs
Section titled “runtimePolyfills / runtimeTarget / coreJs”Auto-inject runtime API polyfills via core-js. Value forms:
runtimePolyfills: "off" | "auto" | "usage" | "entry" | RuntimePolyfillOptionsRuntimePolyfillOptions 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.
compiler namespace
Section titled “compiler namespace”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.)
watch(options)
Section titled “watch(options)”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();WatchRebuildEvent fields
Section titled “WatchRebuildEvent fields”success: boolean/error?: string— rebuild resultchanged?: string[]— file paths that triggered the rebuildgraphChanged?: boolean— whether the module graph topology changedupdates?: Array<{ id, code, map? }>— per-module HMR payload (dev mode)bytes?: number/reparsedModules?: numberphaseDurations?: { ... }— 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/semanticwere legacy aliases forgraph/link+shakeand have been removed.parsenow reflects real parser time only.
WatchHandle lazy sourcemap
Section titled “WatchHandle lazy sourcemap”const handle = watch({ ..., sourcemap: true, emitDiskSourcemap: false });
// when the dev server receives /bundle.js.mapconst bundleMap = handle.getBundleSourceMap(); // string | null
// when the dev server receives /hmr-map/:moduleIdconst moduleMap = handle.getHmrSourceMap(absoluteModulePath); // string | nullVLQ 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().
vitePlugin(rollupPlugin)
Section titled “vitePlugin(rollupPlugin)”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.
Authoring a ZNTC plugin
Section titled “Authoring a ZNTC plugin”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" }; }); },};Additional onResolve return fields
Section titled “Additional onResolve return fields”path— resolved absolute pathexternal— keep the import statement (resolved at runtime)disabled— replace with an empty module (module.exports = {}). Maps Metro{ type: 'empty' }and webpackresolve.fallback: false
onAstFunction
Section titled “onAstFunction”AstFunctionInfo: name / directives / closureVars / params / sourcePath / bodyText /
flags: { async, generator }.
AstFunctionResult: stripDirective? / trailingCode?: string[].
App build (Vite-style HTML entry)
Section titled “App build (Vite-style HTML entry)”import { buildAppSync, prepareAppDevSync } from "@zntc/core";
// Production: scan modules / CSS / assets from an HTML entry → outdirconst 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.
Config / Workspace API
Section titled “Config / Workspace API”Unified config loader
Section titled “Unified config loader”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.
Workspace
Section titled “Workspace”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.
Utilities
Section titled “Utilities”envToDefine(env, prefixes)—.envvariables →definemappingloadEnv(mode, dir, prefixes?)—.env[.mode][.local]priority loaderKNOWN_CONFIG_KEYS— set of recognized config keyssuggestKey(key)— typo suggestion (Levenshtein-based)warnUnknownKeys(obj)— warn on unknown keysisPlainObject(v)— plain-object predicatevalidateTsConfigRaw(raw)— pre-flighttsconfigRawJSON validation
defineConfig(config)
Section titled “defineConfig(config)”Type-safe helper for zntc.config.ts. No runtime behavior — identity function only.
import { defineConfig } from "@zntc/core";
export default defineConfig({ bundle: { entryPoints: ["src/index.ts"], outdir: "dist", minify: true, },});benchmark(options)
Section titled “benchmark(options)”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?orfile?— exactly one is requiredfilename?— paired withsource(used for extension detection)phases: string[]— profile categories to measure.all/noneare not allowediterations?(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>.
See also
Section titled “See also”- Detailed signatures of every export: API Reference (sidebar) (TypeDoc auto-generated)
- CLI — same options on the command line
- Transpile Options — full options table from JSON Schema
- Plugins guide — how to author plugin hooks
- Vite plugin —
@zntc/vite-plugin - Rspack / Webpack integration —
@zntc/rspack-loader