Skip to content

Runtime Polyfills (core-js)

--target (or browserslist) lowers syntax — arrow functions → function expressions, async/await → state machines, class fields, etc. But Promise, Map, Set, Object.values, String.prototype.replaceAll, Array.prototype.at, Object.hasOwn, structuredClone and friends are runtime APIs — older engines simply don’t have those functions/objects, so syntax transforms alone can’t help. --runtime-polyfills fills that gap (the same job as @babel/preset-env’s useBuiltIns + core-js).

core-js and core-js-compat are optional dependencies — install them only in projects that turn polyfills on.

Terminal window
bun add core-js core-js-compat # or npm i core-js core-js-compat
Terminal window
zntc --bundle entry.ts -o bundle.js \
--target=es5 \
--runtime-polyfills=auto \
--runtime-target="ios_saf 12" \
--core-js=3.49

Of the APIs detected in the bundle graph, ZNTC picks the ones --runtime-target doesn’t support and injects the required core-js/modules/*.js as a prelude that runs before the user entry.

CLI flagDescription
--runtime-polyfills=off|auto|usage|entryPolyfill injection mode (default off)
--runtime-target=<query>Browserslist query passed to core-js-compat. Repeatable (--runtime-target="ios_saf 12" --runtime-target="safari 12")
--core-js=<version>core-js version used by core-js-compat. Defaults to the installed core-js/package.json version
ModeBehavior@babel/preset-env equivalent
offDefault. Loads neither core-js-compat nor the graph collectoruseBuiltIns: false
autoInject only the core-js modules for APIs actually used in the bundle graph that the target doesn’t supportuseBuiltIns: "usage"
usageAlias of autouseBuiltIns: "usage"
entryInject all core-js ES/Web modules the target needs, used or not, as an entry preludeuseBuiltIns: "entry" (but ZNTC needs no import "core-js" in your entry — the flag is enough)

auto/usage operate on the native graph AST — after resolve, package exports, alias, and plugin load/transform — not a Babel pre-scan in the JS wrapper. Code inside dependencies is in scope, and with code splitting enabled the runtime prelude is still pulled in as a graph root so it runs before the user entry.

Detection is static AST-based, so it (a) excludes globals shadowed by a local binding/import and (b) does not infer dynamic computed access like obj["replaceAll"](). Force-inject such cases via include, or use entry mode.

The config file / JS API gives you fine-grained control via an object.

import { defineConfig } from "@zntc/core";
export default defineConfig({
entryPoints: ["src/index.ts"],
bundle: true,
target: "es5",
runtimePolyfills: {
mode: "auto",
targets: ["safari 12", "ios_saf 12"],
coreJs: "3.49",
include: ["es.array.at"],
exclude: ["web.url"],
},
});
FieldTypeDescription
mode"auto" | "usage" | "entry"See the modes table above
provider"core-js"Only core-js for now
targetsstring | string[]Browserslist query for core-js-compat (same format as Rspack/SWC env.targets)
coreJsstringcore-js version hint. Defaults to the installed version
includestring[]Modules to always inject. es.array.at or core-js/modules/es.array.at.js form
excludestring[]Modules to drop after target/usage resolution
proposalsbooleanInclude proposals in the core-js-compat query

runtimePolyfills: "auto" (a string) is shorthand for { mode: "auto" }.

targets queries are plain Browserslist syntax — write ios_saf 12, safari 12, node 18 explicitly; don’t use compact shorthand like ios12 / node18 or physical device names like "iPhone 8". React Native’s default Hermes target is selected automatically by --platform=react-native, so --runtime-target is optional there.

runtimePolyfills: {
mode: "auto",
targets: ["chrome >= 87", "edge >= 88", "firefox >= 78", "safari >= 14"],
}

The runtime polyfill prelude slots between the existing manual polyfills / entry hook.

manual polyfills (`polyfills`) / inject roots → runtime core-js prelude → `runBeforeMain` → user entry
  • polyfills — modules that run immediately at bundle start (always before app code).
  • runBeforeMain — modules that run right before the entry (environment setup — e.g. RN’s InitializeCore). Included in the bundle graph and emitted as a prelude, after the runtime polyfill, before the user entry.
defineConfig({
runBeforeMain: ["./src/setup-env.ts"],
});
Terminal window
ZNTC_DEBUG=runtime_polyfills zntc --bundle entry.ts \
--runtime-polyfills=auto \
--runtime-target="safari 12" \
--profile=graph --profile-level=detailed --profile-format=json

The runtime_polyfills debug category prints the candidate computation / graph usage tally / final injection list, and --profile=graph shows graph-phase timing.