React Native
ZNTC ships a --platform=react-native preset that emits Metro-compatible RN bundles. No extra adapter is required — zntc dev / zntc --bundle plug straight into an RN CLI project. For Expo projects, see React Native + Expo.
Project layout
Section titled “Project layout”my-rn-app/├── index.js # entry — calls registerRootComponent├── App.tsx├── ios/ # native shell (RN CLI)├── android/ # native shell (RN CLI)├── zntc.config.ts # ZNTC config└── package.jsonAutomatic setup (RN CLI projects)
Section titled “Automatic setup (RN CLI projects)”The fastest way to add ZNTC to an existing RN CLI project is @zntc/init. It patches package.json scripts and writes zntc.config.ts — it does not generate a new native shell.
zntc-init takes a mode as its first argument; for RN it’s react-native (omitting the mode falls back to react-native for backward compatibility). Other modes — vite, rspack, web — are covered by their own guides.
npx @zntc/init react-nativenpx @zntc/init react-native --platform=androidnpx @zntc/init --helpWhat it does:
- Adds
@zntc/core,@zntc/react-nativeto dev dependencies. - Replaces
startwithzntc dev --platform=react-native --rn-platform=<ios|android> index.js. - Adds
bundle:ios,bundle:androidZNTC bundle commands. - Preserves existing Metro commands as
start:metro,bundle:metro:ios,bundle:metro:androidfallbacks. - Writes a default
zntc.config.tsif missing; existing files are not overwritten without--force.
Help output:
Usage: zntc-init <mode> [options]
Modes: react-native Overlay ZNTC onto an existing React Native CLI project vite Overlay ZNTC onto an existing Vite project (@zntc/vite-plugin) rspack Overlay ZNTC onto an existing Rspack/Webpack project (@zntc/rspack-loader) web Scaffold a standalone ZNTC web project (no Vite/Rspack)
Common options: --root <dir> Project root (default: cwd) --zntc-version <range> Version range for @zntc packages (default: latest) --package-manager <pm> Install command hint: bun, npm, pnpm, or yarn --force Overwrite existing files where the mode allows --dry-run Print planned changes without writing files --help, -h Show this help message
react-native options: --platform <ios|android> Default platform for the start script (default: ios) --no-metro-fallback Do not add Metro fallback scripts
rspack options: --bundler <rspack|webpack> Force bundler choice (default: auto-detect)
web options: --name <pkg-name> package.json name field (default: directory name) --framework <react|vanilla> Starter template (default: react)Common options
Section titled “Common options”| Option | Description |
|---|---|
--root <dir> | Project root. Defaults to cwd. |
--zntc-version <range> | Version range for the @zntc/* packages (default: latest). |
--package-manager <bun|npm|pnpm|yarn> | Install-command hint printed after init. |
--force | Overwrite existing files where the mode allows. |
--dry-run | Print planned changes without writing files. |
--help, -h | Show help. |
react-native mode options
Section titled “react-native mode options”| Option | Description |
|---|---|
--platform <ios|android> | Default RN platform for the start script. Defaults to ios. |
--no-metro-fallback | Skip start:metro / bundle:metro:* fallback scripts. |
Manual setup
Section titled “Manual setup”To author the same result yourself, write these two files.
package.json scripts
Section titled “package.json scripts”{ "scripts": { "start": "zntc dev --platform=react-native --rn-platform=ios index.js", "bundle:ios": "zntc --bundle index.js --platform=react-native --rn-platform=ios --minify -o ios/main.jsbundle", "bundle:android": "zntc --bundle index.js --platform=react-native --rn-platform=android --minify -o android/app/src/main/assets/index.android.bundle",
"start:metro": "react-native start", "bundle:metro:ios": "react-native bundle --platform ios --entry-file index.js --bundle-output ios/main.jsbundle", "bundle:metro:android": "react-native bundle --platform android --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle" }}zntc.config.ts
Section titled “zntc.config.ts”import { dirname } from "node:path";import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);
export default { root: __dirname, entry: "index.js", dev: true, minify: false, transformer: { babel: {}, }, serializer: { polyfills: [], prelude: [], }, server: { port: 8081, host: "localhost", useGlobalHotkey: true, forwardClientLogs: true, },};Basic build commands
Section titled “Basic build commands”# Default (no sub-platform — shared bundle)zntc --bundle index.js --platform=react-native -o bundle.js
# iOSzntc --bundle index.js --platform=react-native --rn-platform=ios -o bundle.js
# Androidzntc --bundle index.js --platform=react-native --rn-platform=android -o bundle.js.ios.* / .android.* extension resolution kicks in once a sub-platform is specified.
Extension resolution order
Section titled “Extension resolution order”With --rn-platform=ios:
.ios.tsx → .ios.ts → .ios.jsx → .ios.js →.native.tsx → .native.ts → .native.jsx → .native.js →.tsx → .ts → .jsx → .js → .jsonmain-fields
Section titled “main-fields”The RN platform sets package.json field order automatically:
react-native → browser → module → mainMetro / react-native bundle compatibility flags
Section titled “Metro / react-native bundle compatibility flags”zntc --bundle --platform=react-native accepts the standard react-native bundle (Metro CLI) flags — a dropin layer so you can swap a react-native bundle ... call in package.json for zntc --bundle ....
| Metro flag | Description |
|---|---|
--bundle-output <path> | Bundle output path (treated like -o; used as a fallback when -o is not given) |
--sourcemap-output <path> | Source map output path — implies sourcemap when set |
--source-map-url <url> | Value for the trailing //# sourceMappingURL (default: the source map file name) |
--sourcemap-sources-root <dir> | Source map sourceRoot (same meaning as --source-root) |
--sourcemap-use-absolute-path | Use absolute paths for sources in the source map |
--assets-dest <dir> | Destination for copied assets (images/fonts) — in production (not --dev) builds the asset loader copies there (iOS 1x/2x/3x, Android res/) |
--asset-catalog-dest <dir> | iOS asset catalog destination — currently ignored (accepted but no-op, stderr warning) |
--bundle-encoding <utf8|utf16le|ascii> | Bundle file encoding (default utf-8) |
--reset-cache | Invalidate the cache on startup |
--max-workers <n> | Parallel worker count — alias of --jobs |
--unstable-transform-profile <name> | Hermes transform profile — currently ignored (accepted but no-op, stderr warning) |
--no-interactive | Disable terminal interactive actions (Metro UI compat) |
--watchFolders <a,b> | Extra watch roots (Metro’s camelCase form, comma-separated) — forwarded to the RN preset’s watchFolders. Distinct from the native watcher’s --watch-folder |
--sourceExts <a,b> | Extra source extensions (Metro’s camelCase form, comma-separated) |
--rn-project-root <dir> | The RN preset’s projectRoot. Defaults to cwd; set it for monorepo roots |
--transform-option key=value | Metro transformer option (repeatable) — currently ignored (Metro graph-bundler only; emits an unsupported-stderr warning) |
--resolver-option key=value | Metro resolver option (repeatable) — currently ignored (same) |
--transform-option/--resolver-optionare accepted for compatibility but have no effect. Customize the Babel transform viatransformer.babelinzntc.config.ts.
Flow / Hermes / Watch
Section titled “Flow / Hermes / Watch”Flow support
Section titled “Flow support”Flow is enabled automatically under --platform=react-native. Types are stripped from files with the @flow pragma. See Flow Support for details.
Hermes compatibility
Section titled “Hermes compatibility”ZNTC’s ES5 downleveling produces Hermes-compatible output.
zntc --bundle index.js --platform=react-native --target=hermes0.70 -o bundle.jsWatch + NDJSON
Section titled “Watch + NDJSON”Stream NDJSON events for external tooling:
zntc --bundle index.js --platform=react-native -o bundle.js --watch-json{"type":"ready","files":2592,"bytes":123456}{"type":"rebuild","success":true,"changed":["/src/app.tsx"],"modules":["/src/app.tsx"],"bytes":123456}Common options
Section titled “Common options”blockList
Section titled “blockList”Compatible with Metro resolver.blockList. Absolute paths matching a pattern are dropped from the graph (the resolver fails them).
RegExp[]orstring[](regex strings). Both forms can be mixed.- Supported syntax: literals,
.*,^,$,\xescapes.|,[],(),+?,\w\dare not. - Under
platform: "react-native", Metro’s default patterns (__tests__, iOS/Android build folders, …) are prepended; user patterns are appended.
defineConfig({ platform: "react-native", blockList: [/\.web\.tsx?$/, "fixtures/.*"],});silentConsoleErrorPatterns
Section titled “silentConsoleErrorPatterns”Selectively swallow noise like RN/Expo native-immutable-global polyfill conflicts. Injects a console.error setter intercept into the prologue.
- Empty/unset → the wrapper isn’t emitted at all (vanilla RN CLI build pays zero dead code).
- The RN preset does not turn this on automatically (trigger is environment-specific).
- Orthogonal to
entryErrorGuard.
defineConfig({ platform: "react-native", silentConsoleErrorPatterns: ["^Failed to set polyfill\\.\\s+\\w+\\s+is not configurable\\.?$"],});assetRegistry
Section titled “assetRegistry”Path to Metro’s AssetRegistry module. Controls RN-style asset wrapping.
undefined: decided by the platform preset. Underplatform: "react-native", defaults toreact-native/Libraries/Image/AssetRegistry.string: wraps asmodule.exports = require(path).registerAsset({...}).false: disabled (asset exports become URL strings, like web).
watchFolders / watchInclude / watchExclude
Section titled “watchFolders / watchInclude / watchExclude”Metro watchFolders compatible. Adds watch roots that live outside the bundle graph.
defineConfig({ platform: "react-native", watchFolders: ["../shared", "../design-tokens"], watchInclude: ["**/*.ts", "**/*.tsx"], watchExclude: ["**/__tests__/**"],});moduleSpecifierMap
Section titled “moduleSpecifierMap”Cherry-pick rewrite for import { x } from 'mod' (equivalent to babel-plugin-lodash). Used to force tree-shaking on large RN packages. Only applies to: named specifiers, no alias, not type-only.
defineConfig({ platform: "react-native", moduleSpecifierMap: { lodash: "lodash/{name}" },});// import { map } from 'lodash' → import map from 'lodash/map'runBeforeMain / polyfills / globalIdentifiers
Section titled “runBeforeMain / polyfills / globalIdentifiers”Pre-main resources executed before the entry module.
polyfills: string[]— executed first thing in the bundle (e.g. RN’sInitializeCore).runBeforeMain: string[]— module paths run right before the entry.globalIdentifiers: string[]— globals reserved during scope hoisting (RN runtime:__DEV__,__r,__d,__c, …).
defineConfig({ platform: "react-native", polyfills: ["react-native/Libraries/Core/InitializeCore.js"], runBeforeMain: ["./bootstrap.js"], globalIdentifiers: ["__DEV__", "__r", "__d", "__c", "global"],});RN mode option reference
Section titled “RN mode option reference”One-line summary of the RN-specific options.
| Option | Description |
|---|---|
workletPluginVersion | Reanimated worklet’s __pluginVersion. Must match the installed react-native-worklets version, or you’ll get a runtime error. |
codegenTransform | Replaces codegenNativeComponent(...) in *NativeComponent.{js,ts} with an inline view config. Auto-enabled by the RN platform. |
entryErrorGuard | Wraps the entry trigger in try/catch + ErrorUtils.reportFatalError (Metro guardedLoadModule equivalent). Auto-enabled. |
strictExecutionOrder | Demotes function declarations to in-factory assignments to prevent hoisting (Rolldown equivalent). Auto-enabled. |
configurableExports | Adds configurable: true to Object.defineProperty (RN / Hermes compatibility). |
reactRefresh | Enables React Fast Refresh. |
devMode | Wraps modules in a __zntc_register() factory and injects the HMR runtime. |
rootDir | Base path for dev-mode module IDs. |
collectModuleCodes | Collects per-module code in dev mode (used by HMR rebuilds). |
workletTransform | Injects __workletHash / __closure / __initData into "worklet" directive functions. Auto-enabled. |
Dev server
Section titled “Dev server”zntc dev --platform=react-native starts a Metro-compatible dev server.
zntc dev --platform=react-native --rn-platform=ios index.js \ --port=8081 --host=localhostEndpoints (Metro-compatible):
GET /status— packager liveness check (packager-status:running).GET /index.bundle?platform=ios&dev=true— main bundle. Withmultipart/mixedAccept, returns progress + bundle chunks.GET /index.map?platform=ios— source map (lazy, per-build cache).GET /__zntc_hmr_map/<id>?platform=ios— per-module HMR source map.GET /assets/*,/node_modules/*— asset registry (iOS @2x/@3x scale variants + 7-strategy package resolve).WS /hot— HMR (hmr:update-start→hmr:update→hmr:update-done/hmr:reload/hmr:error).POST /symbolicate— reverse-map RN LogBox stack traces.POST /reload/POST /devmenu/POST /open-url— emits Metro-compatible messages.
Optional peer packages
Section titled “Optional peer packages”The dev server lazy-loads some features; missing ones degrade gracefully:
| Package | Feature |
|---|---|
@react-native-community/cli-server-api | messageSocketEndpoint.broadcast (/reload / /devmenu ws) + CLI websocket endpoints (/message, /events, /debugger-proxy). |
@react-native/dev-middleware | DevTools inspector / /json / /open-debugger / /launch-js-devtools / fusebox. Resolved per project — compatible with monkey-patchers like Rozenite. |
Install (recommended on RN 0.83+):
bun add -D @react-native-community/cli-server-api @react-native/dev-middlewareKeyboard shortcuts
Section titled “Keyboard shortcuts”In the dev server terminal (Metro-compatible):
r— Reloadd— Dev Menuj— DevTools (POST /open-debugger)i— iOS Simulator open (darwin only)a— Android Emulator open (requiresANDROID_HOME)c— Clear cache?— Help- Ctrl+C / Ctrl+D — graceful shutdown
Programmatic API
Section titled “Programmatic API”import { buildRnDevServerOptions, serveRn } from "@zntc/react-native";
const handle = await serveRn( buildRnDevServerOptions({ bundle: { entry: "index.js", projectRoot: process.cwd(), rnPlatform: "ios", dev: true, }, port: 8081, host: "localhost", enhanceMiddleware: (base, ctx) => (req, res, next) => { if (req.url?.startsWith("/rozenite/")) { // user-defined handling... return; } base(req, res, next); }, symbolicator: { customizeFrame: async (frame) => ({ collapse: frame.file?.includes("/node_modules/") ?? false, }), }, }),);
console.log(`Listening on ${handle.url}`);// ... handle.stop() for graceful shutdown.Examples
Section titled “Examples”Verification matrix (both use bun run start:zntc for the ZNTC dev server):
examples/react-native-bare/— RN 0.85 bare.examples/react-native-expo/— Expo 55 / RN 0.83 (Expo Router).
Compatibility
Section titled “Compatibility”- RN
>= 0.83peer-optional.@zntc/react-nativematches the Hermes / RN-runtime HMRClient interface and Metro’ssourceMappingURLroute conventions. - Runs on Bun + Node 22+. Dev-server lifecycle handles SIGINT / SIGTERM with a graceful shutdown.