Skip to content

Migration Guide

ZNTC provides a similar bundling model to esbuild, but the CLI flag surface is still a subset. The table below reflects options actually exposed by the current zntc CLI.

esbuildZNTCNote
esbuild src/index.ts --bundlezntc --bundle src/index.tsSame
--outfile=dist/out.js-o dist/out.jsShort form supported
--outdir=dist--outdir distSame
--outbase=src--outbase=srcSame
--format=esm--format=esmSame (esm/cjs/iife/umd/amd)
--platform=node--platform=nodeSame (browser/node/neutral/react-native)
--target=es2020--target=es2020Same (engine targets: chrome80, node20)
--bundle--bundleSame
--splitting--splittingSame (--outdir required)
--packages=external--packages=externalTreat all bare package imports as external
--external:react--external reactSpace instead of :
--minify--minifySame (--minify-{whitespace,syntax,identifiers} granular)
--sourcemap--sourcemapSame
(config only: sourceRoot)--source-root=...ZNTC exposes as CLI flag
--sources-content=false--sources-content=falseSame
--define:X=Y--define:X=YSame
--alias:react=preact/compat--alias:react=preact/compatSame
--inject:./shim.js--inject:./shim.jsSame
--pure:Pure.*--pure:Pure.*Register a pure call pattern
--drop:console--drop=console= instead of : (console/debugger)
--drop-labels=DEV--drop-labels=DEVSame. Use commas for multiple labels
--keep-names--keep-namesSame
--banner:js=...--banner:js=...Same
--footer:js=...--footer:js=...Same
--intro=...--intro=...Insert text before bundle code inside the wrapper
--outro=...--outro=...Insert text after bundle code inside the wrapper
--global-name=foo--global-name=fooIIFE/UMD global name
--global:react=React--global:react=Reactexternal specifier → IIFE/UMD global
--public-path=/static/--public-path=/static/Same
--out-extension:.js=.mjs--out-extension:.js=.mjsSame
--entry-names=[name]-[hash]--entry-names=[name]-[hash]Same
--chunk-names=chunks/[hash]--chunk-names=chunks/[hash]Same
--asset-names=assets/[hash]--asset-names=assets/[hash]Same
--loader:.css=text--loader:.css=textSame (text/file/dataurl/json/copy)
--jsx=automatic--jsx=automaticSame (classic/automatic/automatic-dev)
--jsx-dev--jsx-devSame
--jsx-factory=h--jsx-factory=hSame
--jsx-fragment=Fragment--jsx-fragment=FragmentSame
--jsx-import-source=preact--jsx-import-source=preactSame
--jsx-side-effects--jsx-side-effectsPreserve unused JSX expressions
--tsconfig=tsconfig.json-p tsconfig.json or --tsconfig-path=...-p short form
--tsconfig-raw='{...}'--tsconfig-raw='{...}'Inline tsconfig JSON
--conditions=prod,foo--conditions=prod,fooKeep default conditions and add custom ones
--main-fields=browser,main--main-fields=browser,mainSame
--resolve-extensions=.ts,.js--resolve-extensions=.ts,.jsSame (RN .ios.ts etc.)
--preserve-symlinks--preserve-symlinksSame
--node-paths=...--node-paths=...Additional lookup paths for bare specifiers
--charset=utf8--charset=utf8Same (preserve UTF-8)
--charset=ascii--ascii-onlyZNTC uses dedicated flag; escapes non-ASCII to \uXXXX
--legal-comments=eof--legal-comments=eofSame (none/inline/eof/linked/external)
--metafile=meta.json--metafile=meta.jsonSame
--analyze--analyzeSame (JSON now, tree format planned)
--log-level=warning--log-level=warningSame (silent/error/warning/info/debug)
--log-limit=10--log-limit=10Same
--line-limit=80--line-limit=80Same (wraps long output lines at safe token boundaries)
--ignore-annotations--ignore-annotationsIgnore pure/sideEffects annotations
--allow-overwrite--allow-overwriteInput=output is blocked by default; this flag explicitly permits it
--watch--watch or -wSame
--serve--serveSame (--port supported)
// esbuild
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
format: 'esm',
minify: true,
});
// ZNTC — almost identical
import { build } from '@zntc/core';
await build({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
format: 'esm',
minify: true,
});

ZNTC native plugins use the esbuild-style setup(build) structure directly. Return value keys (path/contents) are identical.

// esbuild plugin
const myPlugin = {
name: 'my-plugin',
setup(build) {
build.onResolve({ filter: /^virtual:/ }, (args) => ({
path: args.path,
namespace: 'virtual',
}));
build.onLoad({ filter: /.*/, namespace: 'virtual' }, (args) => ({
contents: 'export default 42',
loader: 'js',
}));
},
};
// ZNTC plugin — same esbuild style (use path prefix instead of namespace)
import type { ZntcPlugin } from '@zntc/core';
const myPlugin: ZntcPlugin = {
name: 'my-plugin',
setup(build) {
build.onResolve({ filter: /^virtual:/ }, (args) => ({
path: '\0' + args.path,
}));
build.onLoad({ filter: /^\0virtual:/ }, (args) => ({
contents: 'export default 42',
}));
},
};

If you want to use Rollup/Vite-style plugins (resolveId/load/transform) as-is, wrap them with vitePlugin().

import { vitePlugin } from '@zntc/core';
export default defineConfig({
plugins: [
vitePlugin({
name: 'virtual-loader',
resolveId(source) {
if (source.startsWith('virtual:')) return '\0' + source;
return null;
},
load(id) {
if (id.startsWith('\0virtual:')) return { code: 'export default 42' };
return null;
},
}),
],
});
esbuild optionAlternative
--mangle-props=<regex>Not supported (mangling limited to --minify-identifiers on internal names)
--mangle-cache=<path>Not supported
--mangle-quotedNot supported
--analyze (tree format)--analyze JSON + /analyze/ visualization. CLI tree format planned
--servedir=<path>--serve <dir> (positional arg)
--bundle=false (off by default)Same default. ZNTC transpiles only without --bundle
--splitting=falseOff by default. No flag means default
--tree-shaking=falseNot supported. Per-package --external can reduce bundle scope
--color=true|falseNot supported. Auto-detected from terminal
--log-override:X=YNot supported. Only --log-level
--supported:bigint=falseNot supported. Use --target for global control
--reserve-props=<regex>Not supported

Vite is a combination of dev server + production bundler (Rollup/Rolldown). ZNTC is a standalone bundler and doesn’t replace all Vite features.

Terminal window
# Vite
vite build
# ZNTC
zntc --bundle src/main.ts --outdir dist --format=esm --splitting --minify --sourcemap
vite.config.ts
export default defineConfig({
build: {
outDir: 'dist',
minify: true,
sourcemap: true,
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: { react: 'React', 'react-dom': 'ReactDOM' },
},
},
},
});
// ZNTC CLI equivalent
// zntc --bundle src/main.ts --outdir dist --minify --sourcemap --external react --external react-dom \
// --global:react=React --global:react-dom=ReactDOM

Rollup output.globals maps to ZNTC CLI --global:<specifier>=<global> or defineConfig({ globals: { react: "React" } }). Used when IIFE/UMD output needs to substitute external specifiers with global variables.

Vite/Rollup plugin hooks (resolveId/load/transform) work when wrapped with vitePlugin(). Return value keys follow the Rollup convention ({ id, code }).

zntc.config.ts
import { defineConfig, vitePlugin } from '@zntc/core';
import fs from 'node:fs';
export default defineConfig({
plugins: [
vitePlugin({
name: 'svg-loader',
load(id) {
if (id.endsWith('.svg')) {
const svg = fs.readFileSync(id, 'utf8');
return { code: `export default ${JSON.stringify(svg)}` };
}
return null;
},
}),
],
});

To write native-style plugins, use setup(build) { build.onLoad(...) }.

Vite featureZNTC equivalent
vite (dev server)zntc dev (HTML/env/public prepare + HMR/Fast Refresh + CSS-only HMR)
vite buildzntc build, or zntc --bundle <entry> --outdir dist --splitting --minify --sourcemap for library builds
vite previewzntc preview dist
import.meta.env.MODEApp mode auto-loads .env*; CLI bundles can use --define:import.meta.env.MODE=\"production\"
import.meta.env.DEVApp mode injects dev/build mode automatically; CLI bundles can use --define:import.meta.env.DEV=true
.env / .env.production auto-loadSupported in app mode (--env-dir, --env-prefix)
import.meta.globSupported (Vite-compatible eager / import options)
import.meta.hotSupported (--serve --bundle)
import.meta.urlSupported (ESM standard)
@vitejs/plugin-react--jsx=automatic (automatic runtime built-in)
@vitejs/plugin-react Fast RefreshBuilt-in HMR (React Refresh)
@vitejs/plugin-vueNot supported (details + workarounds)
@vitejs/plugin-legacyPartial via --target=es5 etc.
CSS Modules (.module.css)Supported in app mode. Provides default exports and valid named exports
CSS @importBuilt-in Lightning CSS or --loader:.css=text
PostCSS (postcss.config.js)Supported in app mode. zntc dev watches PostCSS dependencies and sends CSS-only HMR
Sass/Less/StylusSass/SCSS is supported in app mode. Less/Stylus are not supported
public/ static directorySupported in app mode (--public-dir)
HTML entry (index.html)Supported in app mode (--entry-html)
SPA fallbackzntc preview --spa-fallback
resolve.alias (object)--alias:name=target or defineConfig({ alias: { ... } })
resolve.alias (array, RegExp)defineConfig({ alias: [{ find: /^@\//, replacement: "./src/" }] }) (build() only, not in buildSync)
resolve.conditionsconditions: ["prod", "foo"] or --conditions=prod,foo
optimizeDeps (pre-bundling)Not needed (handled during bundling)
ssr / SSR buildNot supported
worker.formatnew Worker(new URL("./w.ts", import.meta.url)) is auto-detected and bundled as a standalone IIFE. The Vite worker.format knob itself is not supported
Rollup plugin compatresolveId/load/transform hooks compatible

webpack configuration is complex, but ZNTC handles most of it via CLI options.

webpack.config.js
module.exports = {
entry: './src/index.ts',
output: { path: 'dist', filename: 'bundle.js' },
resolve: { extensions: ['.ts', '.tsx', '.js'] },
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.svg$/, type: 'asset/resource' },
],
},
optimization: { minimize: true },
};
// ZNTC equivalent
// zntc --bundle src/index.ts -o dist/bundle.js --minify --loader:.svg=file --loader:.css=text
webpack loaderZNTC equivalent
ts-loader / babel-loaderNot needed. ZNTC handles TS/JSX directly
@swc/swc-loader / esbuild-loaderNot needed. Replaced by ZNTC
css-loader + style-loader--loader:.css=text or built-in Lightning CSS post-processing
file-loader / asset/resource--loader:.png=file
url-loader / asset/inline--loader:.png=dataurl
raw-loader / asset/source--loader:.txt=text
svg-loader / @svgr/webpack--loader:.svg=text/file/dataurl or plugin
json-loader--loader:.json=json (built-in default)
sass-loader / less-loader / stylus-loaderSass/SCSS is supported in app mode. Pre-compile Less/Stylus
postcss-loaderSupported in app mode. Use a plugin or pre-processing for library bundling
html-loaderApp mode rewrites index.html; --loader:.html=text can stringify imports
worker-loaderNot needed. new Worker(new URL("./w.ts", import.meta.url)) auto-detected and bundled as a standalone IIFE
thread-loaderNot needed. ZNTC has built-in parallel pipeline (--jobs=N)
cache-loaderNot needed. Uses .zig-cache / module-level cache
webpack pluginZNTC equivalent
DefinePlugin--define:KEY=VALUE
ProvidePlugin--inject:./shim.js
IgnorePlugin--external <pkg> or --block-list=<pattern>
resolve.fallback: { fs: false, crypto: 'crypto-browserify' }fallback: { fs: false, crypto: 'crypto-browserify' } (applied only when normal resolution fails)
BannerPlugin--banner:js=...
SplitChunksPlugin--splitting (automatic)
MiniCssExtractPluginBuilt-in Lightning CSS post-processing (separate CSS chunks)
HtmlWebpackPluginApp mode handles index.html plus %ENV%/asset rewriting
CopyWebpackPluginNot supported. Per-asset copy via --loader:.svg=copy
TerserPlugin--minify built-in
CssMinimizerPluginHandled by Lightning CSS post-processing
CompressionPlugin (gzip/brotli)Not supported. Handle in post-build
webpack.ContextReplacementPluginNot supported
Module FederationNot supported
DllPlugin / DllReferencePluginNot supported
webpack featureAlternative
require.contextSupported (require.context(dir, deep, regex) — resolved via plugin onResolveContext hook)
Lazy chunk (import(/* webpackChunkName: "x" */ ...))Dynamic import itself supported. Magic comments are not
webpack.config.js function / multi-configNot supported. Single-export zntc.config.ts
devServer.proxySupported (--proxy /api=http://localhost:3000)
Dev server overlaySupported (build/runtime error overlay + source map remap)
Persistent cache (cache.type: 'filesystem')Not needed. Built-in cache
Stats JSON--metafile=meta.json provides similar info