Bundler Version Compatibility Reference

This reference pins the version relationships between the major JavaScript bundlers and the Node.js runtimes and ecosystem packages they depend on: Rollup 3.x and 4.x, Vite 4 through 6, esbuild 0.19 through 0.25, and Turbopack as shipped in Next.js 13 through 15, against Node.js 18, 20, and 22. It documents the breaking changes and version conflicts that surface as engine warnings, ERR_REQUIRE_ESM failures, and peer-dependency mismatches during installs and CI. For the architectural background on why these tools couple the way they do — Vite delegating production builds to Rollup and dependency pre-bundling to esbuild — see Core Concepts of Modern Bundling, then use the matrices below to choose a known-good combination.

This problem exists because a modern bundler is not one program but a layered dependency graph that the package manager assembles at install time. When you write npm install vite@6, the resolver walks the transitive tree, picks the Rollup and esbuild builds Vite declared, checks each package’s engines.node field against the running interpreter, and reconciles every plugin’s peerDependencies against what actually landed in node_modules. Any one of those steps can produce a tree that installs cleanly on your laptop and then fails on a CI runner whose Node image is one minor behind, because the constraints are evaluated against the concrete runtime and lockfile present at that moment, not against the version numbers you typed.

Getting the combination wrong breaks in three characteristic ways. A too-old Node prints an EBADENGINE warning and then behaves unpredictably — the resolver, the native Rollup binary, or a top-level await in a config file may fail long after the warning scrolled past. A forced Rollup or esbuild override throws at config load, usually as plugin X is not a function, because a framework plugin was compiled against a plugin-API shape that no longer exists. And a peer mismatch aborts the install itself with ERESOLVE, which teams reflexively paper over with --legacy-peer-deps and thereby ship a tree the plugin author never tested. The rest of this page is the map of where each of those lands, so you can pick a combination that survives the jump from a developer machine to a locked-down CI image.

Bundler and Node release timeline A timeline showing Node LTS lines alongside the major Vite, Rollup, esbuild, and Next/Turbopack versions aligned to roughly the same period. 2023 2024 2025 2026 Node 18 LTS 20 LTS 22 LTS Vite 4.x 5.x 6.x Rollup 3.x 4.x esbuild 0.19 - 0.21 0.23 - 0.25 Next 13 (TP alpha) 14 (TP beta) 15 (TP stable dev) Thin outline = older / EOL line; thick outline = current recommended line. TP = Turbopack.
Figure: approximate release timeline aligning Node LTS lines with the major bundler versions that target them.

What This Page Pins and Why

Bundler compatibility is not a single number; it is the intersection of three constraints. First, the Node.js engine range a bundler declares — installing Vite 6 on Node 16 produces an EBADENGINE warning and unpredictable resolver behavior. Second, the internal coupling between tools: Vite bundles a specific Rollup major and a specific esbuild range, so you do not pick those independently — Vite pins them, and overriding via overrides/resolutions is where conflicts originate. Third, ecosystem peers: framework plugins (@vitejs/plugin-react, @vitejs/plugin-vue), PostCSS, and TypeScript each declare peer ranges that must overlap the bundler you chose.

Pinning matters because mismatches fail loudly in some cases (ERR_REQUIRE_ESM when a CJS config loads an ESM-only plugin) and silently in others (a Rollup major bump changing manualChunks semantics so chunks split differently with no error). The tables below give the known-good combinations and the specific traps at each boundary.

The silent class is the dangerous one, because there is no red text to grep for. A Rollup major bump can keep every plugin loading and every build passing while quietly changing how modules are grouped into chunks, which changes the emitted filenames and their content hashes. That reshuffles your long-term browser cache, invalidates the source-map file set your error tracker uploaded, and can move a dependency out of the vendor chunk you deliberately sized for HTTP caching. None of that shows up as a failure — the build is green — so the only defense is to treat a bundler major as a change that requires re-checking the output manifest, not just re-running the test suite. The practical rule that falls out of this: pin the bundler major in package.json ("vite": "^6.0.0", not "vite": "*"), let patch and minor float, and gate every major bump behind an explicit review of the produced assets.

Compatibility is the intersection of three constraints Bundler compatibility is the intersection of the Node.js engine range the bundler declares, the internal coupling where Vite pins a specific Rollup and esbuild, and the ecosystem peers such as framework plugins, PostCSS and TypeScript whose ranges must overlap. Node engine rangeEBADENGINE if below internal couplingVite pins Rollup + esbuild ecosystem peersplugins · PostCSS · TS All three ranges must overlap — a mismatch fails loudly or silently
Figure: a known-good combination is where all three ranges overlap — not any single version number.

Bundler to Node.js Compatibility

Node floor by bundler line Vite 4 runs on Node 14.18 or newer but warns on Node 22; Vite 5 needs Node 18; Vite 6 needs Node 18.18 or 20.19; and Rollup 4 and current esbuild need Node 18 or newer. Vite 4Node 14.18+ · warns @22 Vite 5Node 18+ Vite 6Node 18.18 / 20.19+ Rollup 4 / esbuildNode 18+
Figure: the Node 18.0–18.17 exclusion at Vite 6 is the floor that most often trips a CI image.
Bundler Versions Node 18 Node 20 Node 22 Notes
Vite 4.x Supported Supported Warns Vite 4 predates Node 22; works but unsupported
Vite 5.x Supported (≥18.0) Supported Supported Requires Node 18+; drops Node 16
Vite 6.x Min 18.0 / 20.0 Supported Supported Node 18.0–18.17 excluded; needs 18.18+ or 20.19+
Rollup 3.x Supported (≥14.18) Supported Works Maintenance only; superseded by 4.x
Rollup 4.x Supported (≥18.0) Supported Supported Native bindings shipped per-platform; needs Node 18+
esbuild 0.19–0.21 Supported Supported Works Go binary; engine range is permissive
esbuild 0.23–0.25 Supported Supported Supported Recommended current line
Turbopack (Next) 13 Supported (≥16.14) Supported n/a next dev --turbo alpha; not for builds
Turbopack (Next) 14 Supported (≥18.17) Supported Works Turbopack dev beta; next build still Webpack
Turbopack (Next) 15 Min 18.18 Supported Supported next dev stable on Turbopack; build stabilizing

The Node floors above are enforced by the engines.node field each package publishes. At install time npm reads that field and, if the running interpreter falls outside the declared range, emits an EBADENGINE warning; it does not abort unless you have set engine-strict=true in .npmrc. That default is a trap: the warning scrolls past in a noisy CI log, the install “succeeds”, and the failure resurfaces later as a cryptic runtime error deep inside the bundler. The Vite 6 floor is the specific gap that bites most often, because it is not a clean “Node 18 or newer” boundary. Vite 6 requires 18.18+, so the entire 18.0 through 18.17 window is excluded even though it is the same major. A CI image labelled node:18 that resolves to an early 18 patch will fail this floor while a developer on node:18.19 sees nothing wrong.

// package.json — declare the runtime you actually support (Vite 6 line)
// verified against Vite 6.0, npm 10, Node 20.19
{
  "engines": {
    "node": ">=18.18.0 || >=20.19.0 || >=22.12.0"
  }
}

Setting engine-strict=true in a project .npmrc promotes the warning to a hard install failure, which is what you want in CI: better to fail at npm ci with a clear message than to fail three steps later inside vite build. Confirm the running version with node -v as the first line of the build job, not as an afterthought — the single most common “it works locally” incident in this space is a runner whose Node is a patch or minor behind the floor the bundler quietly requires.

Internal Coupling: What Vite Bundles

You do not select Rollup and esbuild versions for a Vite project — Vite pins them. Overriding the pinned versions is the most common self-inflicted breakage.

Vite pins Rollup and esbuild — do not override A Vite version bundles a specific Rollup major and esbuild range; forcing a different Rollup through overrides can mismatch the plugin API contracts the framework plugins compiled against, producing plugin-is-not-a-function at config load. Vite version pins Rollup major pins esbuild range override → plugin not a functionAPI contract mismatch
Figure: overriding the pinned Rollup with overrides/resolutions is the most common self-inflicted config-load failure.
Vite Bundled Rollup Bundled esbuild Min Node
4.x Rollup 3.x esbuild 0.18.x 14.18+ / 16+
5.x Rollup 4.x esbuild 0.19.x–0.21.x 18+
6.x Rollup 4.x esbuild 0.24.x–0.25.x 18.18+ / 20.19+

If you force a different Rollup major through overrides, plugin API contracts (this.emitFile, renderChunk signatures, manualChunks function arguments) may not match the version @vitejs/plugin-* packages compiled against, producing plugin X is not a function at config load.

Under the hood, Vite does not shell out to a globally installed Rollup — it imports the exact Rollup build nested in its own dependency tree and re-exports its plugin container, so at runtime there is one and only one Rollup instance driving the production build. That is why overriding the version is so destructive: an overrides block that forces "rollup": "4.x" when Vite expects a different patch line replaces the copy Vite imports, and every plugin that touches Rollup’s internal PluginContext is now talking to an object whose method table has shifted. The failure is not always immediate; a plugin that only calls this.emitFile inside generateBundle will load fine and then throw at the very end of the build, after minutes of work.

The legitimate reasons to override are narrow: a published security advisory against the pinned esbuild or Rollup patch, or a confirmed upstream bug fixed in a newer patch that Vite has not yet absorbed. In both cases override to the nearest patch within the same major Vite pinned, never across a major, and remove the override the moment Vite catches up. If you find yourself reaching for an override to get a feature, the correct move is almost always to bump Vite itself, because the feature you want usually shipped alongside the Rollup or esbuild bump inside a Vite release.

// package.json — a defensible override: same major, one patch forward
// only until Vite ships the fix; verified with Vite 6.0 / Rollup 4
{
  "overrides": {
    "rollup": "4.24.4"
  }
}

You can confirm what actually resolved — rather than what you asked for — by inspecting the installed tree. npm ls rollup esbuild prints the deduplicated versions and, crucially, whether more than one copy landed; two Rollup versions in the output is a near-certain sign that an override or a stray transitive dependency has split the tree and is a strong predictor of the plugin is not a function class of failure.

Known Breaking Changes and Conflicts

The load-bearing break at each boundary Vite 4 to 5 drops Node 14 and 16 and deprecates the CJS API; Vite 5 to 6 excludes Node 18.0 to 18.17 and removes the CJS API; Rollup 3 to 4 adds per-platform native binaries; and esbuild 0.x minors can break without semver. Vite 4→5 / 5→6Node floor rises; CJS API removed by 6 Rollup 3→4per-platform native binaries esbuild 0.x does NOT follow semverread the changelog before any minor bump in CI
Figure: the esbuild 0.x line is the one to watch — a minor bump can break intentionally.
  • Vite 4 to 5: Node 14/16 dropped; CJS Node API deprecated (importing vite from a CommonJS file warns and is slated for removal). The define of process.env behavior tightened. Rollup 3 to 4 underneath changed output.manualChunks to also accept the { getModuleInfo } helper consistently.
  • Vite 5 to 6: Node 18.0–18.17 excluded — the minimum is 18.18 (or 20.19/22.12 for newer sub-lines). Default build.target moved to baseline-widely-available. The legacy CJS API was removed entirely; ESM config or .mjs/.mts is mandatory.
  • Rollup 3 to 4: Rewritten parser (SWC-based) and per-platform native binaries (@rollup/rollup-linux-x64-gnu, etc.). The common CI failure Cannot find module @rollup/rollup-linux-x64-gnu stems from an npm optional-dependency bug when a lockfile is reused across platforms; fix by deleting package-lock.json + node_modules and reinstalling on the target OS, or pinning the optional dep.
  • esbuild 0.19 to 0.25: Each minor can introduce intentional breaking changes (esbuild does not follow semver in the 0.x range). Notable: --sources-content default handling, stricter tsconfig.json extends resolution, and keepNames interactions with class fields. Always read the esbuild changelog before bumping a minor in CI.
  • Turbopack / Next 13 to 15: Turbopack was dev-only and behind --turbo in 13/14; Next 15 makes next dev Turbopack the documented default, and next build --turbo is the stabilizing build path. Plugins written for the Webpack next.config.js webpack() hook do not run under Turbopack — they require Turbopack’s turbopack/turbo config surface instead.

The Rollup native-binary failure in depth

The Cannot find module @rollup/rollup-linux-x64-gnu error deserves its own treatment because it is the single most reported Rollup 4 CI break, and the usual reflex — reinstalling — does not always fix it. Rollup 4 replaced its JavaScript parser with a native one compiled per platform and CPU architecture, distributed as a family of optionalDependencies like @rollup/rollup-linux-x64-gnu, @rollup/rollup-darwin-arm64, and @rollup/rollup-win32-x64-msvc. Only the binary matching the current platform is meant to install; the rest are skipped as optional. The break comes from a long-standing npm bug (npm issue 4828) where, if a package-lock.json was generated on one platform — say a macOS laptop — and then npm ci runs against it on another — a Linux runner — the lockfile records the macOS optional binary and omits the Linux one, so the Linux binary is never fetched and Rollup cannot load its parser.

The symptom is a clean install followed by an immediate crash on the first vite build or rollup -c. The root cause is a lockfile that encodes the wrong platform’s optional dependency. The durable fix is to make the lockfile platform-agnostic or regenerate it in the same environment the build runs in; the fast fix in CI is to remove the stale artifacts before installing.

# .github/workflows/build.yml — regenerate the tree on the runner's own platform
# verified with Rollup 4 / Vite 6, npm 10, ubuntu-latest
- name: Clean install for correct native binary
  run: |
    node -v                       # confirm the engine floor first
    rm -rf node_modules package-lock.json
    npm install                   # resolves the linux-x64-gnu optional dep here
    npm run build

Confirm the fix by checking that the platform binary is present after install: ls node_modules/@rollup | grep linux on a Linux runner should list rollup-linux-x64-gnu. If you must keep a committed lockfile — the normal, correct choice for reproducibility — pin the matching optional dependency explicitly for the CI platform, or commit the lockfile from a build that ran on the same OS and architecture as the runner. Alpine-based images are a common variant of this trap, because they need the -musl binary (@rollup/rollup-linux-x64-musl) rather than -gnu; a lockfile from a glibc machine will fetch the wrong one.

Peer Dependency Alignment

Plugin peer ranges track the Vite line plugin-react moves from 4.x on Vite 5 to 4.3+ on Vite 6, plugin-vue from 5.x to 5.2+; a mismatch surfaces as ERESOLVE at install, which should be fixed by upgrading the plugin, not forcing legacy-peer-deps. Vite 5 peersplugin-react 4.x · plugin-vue 5.x Vite 6 peersplugin-react 4.3+ · plugin-vue 5.2+ Mismatch → ERESOLVE at install; fix by upgrading the plugin, not --legacy-peer-deps
Figure: upgrade the plugin to the line that matches your Vite — do not paper over it with --legacy-peer-deps.
Package Vite 5 peer Vite 6 peer
@vitejs/plugin-react 4.x 4.3+
@vitejs/plugin-vue 5.x 5.2+
vite-plugin-pwa 0.20+ 0.21+
@sentry/vite-plugin 2.x 2.x

TypeScript ≥5.0 is assumed for moduleResolution: "bundler", which all current Vite, Rollup, and esbuild lines expect. Mismatched plugin peers surface during install as npm ERR! ERESOLVE — resolve by upgrading the plugin to the line matching your bundler rather than forcing --legacy-peer-deps.

A peer dependency is a contract the plugin author writes: “I call into this version range of Vite’s internals, and I have not tested anything else.” When npm 7+ finds a plugin whose peerDependencies.vite range does not include the Vite you installed, it aborts with ERESOLVE rather than guessing. --legacy-peer-deps restores the pre-npm-7 behavior of ignoring peer constraints entirely, which does not make the incompatibility go away — it ships it. The plugin will load against internals it was never compiled for, and the failure moves from a clear install-time error to an obscure runtime one, typically a transform hook throwing on a shape it did not expect. The reason to upgrade the plugin instead is that the plugin author has already published a build whose peer range includes your Vite; taking it costs one version bump and removes the mismatch at the source.

The direction of the fix matters. When you bump Vite across a major, expect to bump its framework plugin in the same commit — @vitejs/plugin-react and @vitejs/plugin-vue track the Vite line closely and a lagging plugin is the usual ERESOLVE cause. When a third-party plugin has genuinely not shipped a compatible release yet, that is a signal to hold the bundler upgrade, not to force the tree; a plugin that has not been updated for the new Vite major is one you are about to run untested. Reserve --legacy-peer-deps for the rare case where you have read the plugin’s changelog, confirmed the internals it uses are unchanged, and have a test that exercises the path — and even then, prefer a scoped overrides entry over the global flag so the exception is documented in package.json rather than buried in a CI command.

Verifying a Known-Good Combination

Rather than trusting the tables from memory, encode the check. A short preflight script that runs before the build asserts the Node floor and prints the resolved bundler versions, turning a silent drift into an explicit failure with a readable message.

#!/usr/bin/env bash
# scripts/preflight.sh — assert the engine floor and surface resolved versions
# verified with Node 20.19, npm 10, Vite 6
set -euo pipefail

REQUIRED_MAJOR=20
CURRENT_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
if [ "$CURRENT_MAJOR" -lt "$REQUIRED_MAJOR" ]; then
  echo "Node $CURRENT_MAJOR is below the required floor $REQUIRED_MAJOR" >&2
  exit 1
fi

# fail if more than one Rollup resolved — a split tree predicts plugin errors
if [ "$(npm ls rollup --all --parseable 2>/dev/null | grep -c '/rollup$')" -gt 1 ]; then
  echo "Multiple Rollup copies resolved; check overrides/resolutions" >&2
  exit 1
fi

npx vite --version
npx esbuild --version

Wire this into the pretest or prebuild npm lifecycle hook so it runs automatically. The payoff is that the class of failures this page catalogues — an under-floor Node, a split Rollup tree, an unexpected esbuild minor — surfaces in one place with a message a reviewer can act on, instead of as a stack trace from deep inside a plugin.

Performance and Cache Considerations Across Versions

Version choice is not only a correctness question; it moves build times and cache behavior. esbuild’s Go binary is the fast path for dependency pre-bundling, and its startup and transform throughput improved measurably across the 0.19 to 0.25 range, so an old esbuild pinned by an old Vite is a hidden tax on cold dev-server start. Rollup 4’s native parser cut production build wall-clock time relative to Rollup 3 on large graphs, which is a reason to prefer the Vite 5/6 line for big applications even setting features aside. The counterweight is cache stability: because chunking and hashing can shift across a Rollup major, the first build after an upgrade will invalidate the browser cache for returning users and force a full re-download of the changed vendor chunks. That is a one-time cost, but schedule it deliberately — do not ship a Rollup major bump in the same release as a latency-sensitive campaign, and warm your CDN afterward so the first real user does not pay for the cache miss.

Upgrade and Migration Notes

Bump Node before the bundler Upgrade the CI image Node version first and confirm node -v, then bump the bundler; going to Vite 6 on Node 18.16 fails the EBADENGINE gate even though Vite 5 was fine. 1 bump CI Nodeto 20.19+ 2 confirm node -v 3 bump the bundlersame change set Most "upgrade" failures are the engine gate — raise Node first
Figure: raising the Node image before the bundler avoids the EBADENGINE gate that blocks a same-image bump.

Upgrade Node and the bundler in the same change set when crossing a major, because the engine range moves with it: going to Vite 6 on a CI image still running Node 18.16 fails the EBADENGINE gate even though Vite 5 was fine. Bump the CI image to Node 20.19+ first, confirm node -v, then bump Vite. When the source map or chunking behavior of a deployed app must stay stable across an upgrade, validate against the guidance in Source Maps and Production Debugging — a Rollup major bump can shift chunk boundaries and therefore the .map file set your error tracker expects.

Ordering is the whole trick here, and it is worth stating as a rule because reversing it produces a confusing failure. Node first, bundler second, plugins in the same commit as the bundler. If you bump the bundler before the runtime, the engine gate blocks you and the error text points at the bundler, sending you to debug the wrong layer. If you bump the bundler without its framework plugin, ERESOLVE blocks the install and again the message is about a package you did not touch. Sequencing the runtime bump as its own reviewable step — change the image tag, confirm node -v in the log, merge — means that by the time the bundler bump lands, the floor is already satisfied and the only thing left to reason about is the bundler’s own changes.

Treat the first green build after a major bump as unverified, not done. A passing test suite confirms behavior, not output shape. Diff the generated asset manifest — filenames, chunk sizes, and the set of emitted .map files — against the previous release before promoting the build, because the silent-failure class described earlier hides precisely in that gap between “tests pass” and “the bytes shipped are the bytes you expected”.

When Not to Upgrade

Not every project should chase the current bundler line, and pinning to a maintenance version is sometimes the correct engineering call rather than technical debt. A build on Vite 4 with Rollup 3 that ships reliably and whose plugins are all still maintained does not need to move for its own sake; the upgrade carries the cache-invalidation and re-verification cost described above for a benefit you may not need. Hold the current line when you are inside a release freeze, when a load-bearing plugin has no build compatible with the newer bundler major, or when your deployment target constrains Node below the newer bundler’s floor and you cannot raise it. The one exception that overrides all of these is a published security advisory against the pinned version — a CVE in esbuild or a transitive dependency is a reason to move even mid-freeze, though even then you move to the nearest patched version, not necessarily to the newest major.

Deprecations

Four deprecations and their forward paths The Vite CJS Node API is removed in 6 so migrate config to ESM; Rollup 2 and 3 are maintenance only so target the 4.x API; esbuild --bundle needs an explicit --format; and Next 15 makes Turbopack the forward path over Webpack build. Vite CJS API → ESMremoved in 6 — use .mjs/.mts config Rollup 2/3 → 4 APInew plugins target 4.x esbuild --bundle → +--formatset --format=esm explicitly Next Webpack build → Turbopackforward path in Next 15
Figure: four old→new migrations — the Vite CJS API is the only hard removal; the rest are directional.
  • Vite CJS Node API: deprecated in 5, removed in 6. Migrate config and programmatic usage to ESM.
  • Rollup 2.x and 3.x: maintenance only; new plugin development targets 4.x APIs.
  • esbuild --bundle without an explicit --format: still works but the inferred format changed across 0.x lines — set --format=esm explicitly.
  • Next.js Webpack next build (non-Turbopack): not deprecated, but Turbopack is the forward path in Next 15; new framework features land on Turbopack first.

The Vite CJS API removal is the only entry here that is a hard wall rather than a nudge, and it is worth understanding why it lands the way it does. When Vite’s Node entry became ESM-only, a vite.config.js loaded by CommonJS resolution — for example under a toolchain that still require()s it — throws ERR_REQUIRE_ESM on Vite 6 where Vite 5 only printed a deprecation warning. The fix is mechanical: rename the config to vite.config.mjs (or .mts), or add "type": "module" to package.json so .js is interpreted as ESM, and convert any require()/module.exports in the config to import/export default. Confirm it by loading the config with node --input-type=module semantics or simply running vite --version against the ESM config; if that succeeds, the programmatic path will too. The other three deprecations are directional — old code keeps working — so schedule them with normal upgrades rather than as emergencies, but do not start new work on the deprecated surface.