Vite Version Compatibility Reference

This page pins which Vite major works with which Node runtime, which Rollup version it vendors, and which @vitejs/plugin-react/@vitejs/plugin-vue releases are safe to pair with it — so an upgrade does not strand you on an EOL Node or an incompatible plugin. It belongs to the broader Vite Configuration & Ecosystem and is the version companion to that overview’s runtime and plugin guidance. The data below covers Vite 4.x, 5.x, and 6.x, the breaking changes each major introduced (Vite 5’s Node 18 floor and Rollup 4 swap, Vite 6’s Environment API), and the deprecations to clear before bumping.

The reason this problem exists at all is that Vite is not a single tool but a coordinator around three moving parts you do not version independently. The dev server transforms modules with esbuild; the production build drives Rollup; and the config file itself is loaded by Node’s own ESM machinery. A Vite major bump therefore drags a Node floor, a Rollup major, and an esbuild range along with it, and each of those has its own compatibility surface. When people say “the Vite upgrade broke the build,” the failure is almost never in Vite’s own code — it is a Node engine check that hard-exits, a Rollup 4 output-hash change that busts a CDN cache, or a framework plugin whose peerDependencies no longer overlap the installed vite. Treating these as one linked decision is the whole point of a compatibility reference.

Getting the pin wrong fails in two distinct places, and knowing which one tells you where to look. A runtime-floor mismatch fails immediately and loudly: the CLI prints You are using Node.js <x>. Vite requires Node.js version 18.18+ or 20+ and exits before reading your config, so nothing you change in vite.config.ts will help. A plugin peer mismatch fails later and quietly: the install succeeds (npm 7+ only warns on peer conflicts, it does not block), the dev server boots, and then Hot Module Replacement degrades to full-page reloads or a React Fast Refresh boundary silently never engages — a symptom that looks like an application bug but is a version bug. The rest of this page exists so you can distinguish these before you spend an afternoon on the wrong one.

One framing helps before the tables: a Vite major is a coordination contract, not a feature release. The user-visible feature set between two adjacent Vite majors is small; what actually changes is the set of versions Vite promises to interoperate with — the Node floor, the vendored Rollup, the esbuild range, and the plugin peer ranges that track it. That is why the interesting content of an upgrade lives in the compatibility matrix rather than the changelog’s feature list, and why a green npm install tells you almost nothing about whether the combination will actually build and hot-reload. The tables below are the contract; the prose around them is how to tell which clause you violated when it breaks. Read the version you are on and the version you are moving to as two rows, and treat every difference between those rows as a task, not a footnote.

Vite major-version timeline with Node and Rollup floors A timeline from Vite 4 through Vite 6 showing the bundled Rollup version and minimum Node version at each major. Vite 4 Rollup 3.x Node 14.18+ Vite 5 Rollup 4.x Node 18.18+ Vite 6 Rollup 4.x Node 18.18 / 20.19+ 2022 2023 2024 Vite 6: Environment API (experimental) replaces ad-hoc ssr branching
Figure: Vite majors with their vendored Rollup and minimum Node, ending at Vite 6's Environment API.

What This Page Pins and Why

Vite vendors Rollup and esbuild internally, so the Rollup version is not something you choose — it ships inside the vite package, and a Rollup major bump (3 → 4 in Vite 5) changes output hashing, manualChunks semantics, and plugin context APIs. Node compatibility matters because Vite’s config loader and import.meta handling rely on runtime ESM features that only stabilised in specific Node lines; running Vite 5 on Node 16 fails at startup with Vite requires Node.js version 18. The framework plugins (@vitejs/plugin-react, @vitejs/plugin-vue) declare a vite peer range, and mismatches surface as Failed to resolve entry for package or HMR that silently degrades to full reloads. Pin all three together.

The mechanism behind the vendoring is worth understanding because it explains why you cannot patch Rollup independently. Vite declares Rollup as a real dependency (not a peer), so your lockfile resolves exactly one Rollup that lives under node_modules/vite/node_modules/rollup or hoisted alongside it, and Vite imports it directly. There is no supported override: forcing a different Rollup via npm overrides or pnpm resolutions frequently produces Cannot read properties of undefined (reading 'parseAst') or a plugin-context method that no longer exists, because Vite calls Rollup 4 internals that Rollup 3 never exposed. esbuild is vendored the same way for dependency pre-bundling and for single-file TS/JSX transforms; a globally-installed esbuild of a different version can shadow the vendored one through PATH resolution and throw the version-mismatch error described below. The practical rule: the only lever you own is the vite version, and everything downstream of it is determined by that choice plus your Node runtime.

You can verify exactly what your installed Vite vendored without guessing from release notes:

# Vite 5/6 — print the Rollup and esbuild versions Vite actually resolved
npm ls rollup esbuild
# or inspect the exact files Vite loads
node -p "require('vite/package.json').dependencies.rollup"
node -p "require('rollup/package.json').version"

If npm ls reports two Rollup versions or marks one deduped against an unexpected major, another dependency has pulled a second copy in and your build may be linking against the wrong one — resolve that before trusting any hash diff.

Why the lockfile, not the range, is what ships

The ^6.0.0 you write in package.json is a request, not a guarantee; the version that actually loads is whatever your lockfile resolved, and the lockfile is where the vendored Rollup and esbuild are frozen. This matters because two developers with the same package.json range can resolve different Vite patches — and therefore different vendored Rollup patches — if one of them regenerated the lockfile at a different time. A Rollup 4 patch that adjusts chunk-boundary heuristics can move your dist/ output between two machines that both “have Vite 6,” and the only artifact that explains the difference is the lockfile. Commit it, and in CI run npm ci (which installs strictly from the lockfile and errors on drift) rather than npm install (which is free to update it). The compatibility guarantees on this page are guarantees about resolved versions; they are only as stable as your lockfile discipline. A reference table that says “Vite 6 vendors Rollup 4” is true at the granularity of majors, but the patch that actually shipped in your build is a fact about your lockfile, not about the table — which is why the verification commands above matter more than the row you read.

Three things pinned together Vite vendors Rollup and esbuild so those versions are not chosen; the runtime Node version and the framework plugin peer range must be pinned alongside Vite because a mismatch in any one fails at startup or degrades HMR. Vite majorvendors Rollup + esbuild Node runtimebelow floor → won't start framework pluginpeer range mismatch → HMR fails Pin all three — Rollup/esbuild come along for free inside Vite
Figure: you pin Vite, Node, and the plugin; Rollup and esbuild are decided for you inside the Vite package.

Vite ↔ Node ↔ Rollup Matrix

Support status of each Vite major As of mid 2026, Vite 4 is end of life, Vite 5 is in maintenance, and Vite 6 is the current release; each raises the minimum Node floor. Vite 4 — EOLRollup 3 · Node 14.18+ Vite 5 — maintenanceRollup 4 · Node 18.18+ Vite 6 — currentRollup 4 · Node 18.18 / 20.19+ Support status as of 2026-06
Figure: target the current or maintenance line — Vite 4 is EOL and its Node floor is already unsupported.
Vite major Status (2026-06) Bundled Rollup Bundled esbuild Minimum Node Recommended Node
4.x EOL 3.x 0.17–0.18 14.18 / 16+ 18 LTS
5.x Maintenance 4.x 0.19–0.21 18.18 / 20+ 20 LTS
6.x Current 4.x 0.24+ 18.18 / 20.19 / 22.12 22 LTS

Node lines listed as 18.18 / 20.19 mean Vite refuses to start on patch releases below those within each major line; Vite 6 dropped Node 21 (a non-LTS odd line) entirely. esbuild ranges are the versions Vite vendored across that major’s lifetime — you do not install esbuild directly, but a globally-installed mismatched esbuild can shadow it and throw Host version does not match binary version.

The patch-level floor inside a major line trips people who assume “Node 18 is Node 18.” Vite’s engine check reads the exact process.versions.node string and compares against a minimum that includes the minor and patch — Node 18.0 through 18.17 are rejected by Vite 6 even though they are Node 18, because Vite relies on ESM and import.meta behaviour that only settled at 18.18. The recommended-Node column is not just the minimum plus a margin; it is the LTS line the Vite team actually runs CI against, so it is the configuration with the fewest surprising interactions with native addons and the pre-bundler. Targeting the recommended LTS rather than scraping the floor is the difference between “builds on my machine” and “builds on the runner.” Pin the runtime explicitly in package.json so the floor is enforced at install time, not discovered at first vite dev:

// package.json — fail fast on an unsupported runtime instead of at startup
{
  "engines": { "node": ">=20.19" },
  "devDependencies": { "vite": "^6.0.0" }
}

With engine-strict=true in .npmrc, npm refuses to install under a too-old Node, converting a late startup crash into an immediate, obvious install error — the cheapest place to catch it.

How the engine check actually fires

The floor is enforced twice, at two different layers, and confusing them wastes time. The first layer is npm’s own engines check, driven by the engines.node field and gated by engine-strict; it runs at install and knows nothing about Vite’s internals — it only compares your runtime against the range you declared. The second layer is Vite’s own runtime guard, a check at the top of the CLI that reads process.versions.node, parses it, and calls process.exit with the Vite requires Node.js message before your config is ever imported. The two can disagree: if your engines field is looser than Vite’s real floor (or absent entirely), the install passes and Vite’s own guard is what stops you — later, and with a message that names Node rather than the config line you were editing. Keep the engines field at or above Vite’s floor so the two agree and the failure always surfaces at install time, where it is cheapest to read.

The 18.18 / 20.19 notation in the table is not an either/or you choose between; it means “at least 18.18 if you are on the 18 line, or at least 20.19 if you are on the 20 line.” Vite carries a separate minimum per Node major because the ESM loader and import.meta fixes it depends on landed at different patch levels in each line — the 20 line needed a later patch than the 18 line to reach the same behaviour. This is why a blanket engines: ">=18" is subtly wrong: it lets Node 20.0 through 20.18 install even though Vite 6 rejects them, so the install passes and the runtime guard fails. Express the floor per line if you support more than one — ">=18.18 <19 || >=20.19" — or, more simply, just pin the single recommended LTS you actually run and matrix the rest in CI.

Vite ↔ Framework Plugin Matrix

Framework plugin majors per Vite major Vite 4 pairs with plugin-react 3 and plugin-vue 4; Vite 5 needs plugin-react 4 and plugin-vue 5; Vite 6 needs plugin-react 4.3+ and plugin-vue 5.2+, and installing an older plugin major fails to resolve or breaks HMR. Vite 4react 3 · vue 4react-swc 3 Vite 5react 4 · vue 5react 4 drops Vite 3 Vite 6react 4.3+ · vue 5.2+peer: ^5 || ^6
Figure: install plugins unpinned and let npm resolve against your Vite version, then lock the result.
Vite major @vitejs/plugin-react @vitejs/plugin-react-swc @vitejs/plugin-vue Notes
4.x 3.x 3.x 4.x Fast Refresh requires component-only default exports
5.x 4.x 3.x 5.x plugin-react 4 drops Vite 3 support
6.x 4.3+ 3.7+ 5.2+ plugins declare `vite: ^5

Installing @vitejs/plugin-react@3 against Vite 5 produces The following dependencies are imported but could not be resolved or an HMR boundary that never engages. When in doubt, install the plugin without a pin (npm i -D @vitejs/plugin-react) and let npm resolve against your vite version, then lock the result.

The reason a plugin major is bound so tightly to a Vite major is that the framework plugins hook into Vite-internal APIs that are not part of the public config surface — the HMR runtime protocol, the module graph invalidation calls, and (for React) the Fast Refresh boundary registration. When Vite 4 became Vite 5 those internals shifted, so @vitejs/plugin-react@3 calls methods that either no longer exist or have changed shape. The failure mode depends on which method moved: if it is a resolution hook you get the loud could not be resolved error at boot; if it is the Fast Refresh registration you get the quiet degradation where saving a component triggers a full reload instead of a hot swap, because the plugin registered a boundary the newer runtime ignores. There is no runtime assertion that catches the second case, which is exactly why HMR regressions after an upgrade are so often misdiagnosed as application state bugs.

The two React plugins (@vitejs/plugin-react using Babel, @vitejs/plugin-react-swc using SWC) version on separate tracks, so do not assume their majors line up — plugin-react is on 4.x while plugin-react-swc is on 3.x for the same Vite 6, and swapping one for the other requires reading each one’s peer range, not copying a version number across. Choose Babel-based plugin-react when you depend on Babel macros or custom transforms; choose the SWC plugin for faster cold transforms on large component trees, at the cost of a smaller plugin ecosystem.

Confirming the plugin actually took

Because a plugin peer mismatch degrades silently, do not trust the absence of an error — verify Fast Refresh positively. The cheapest probe is to render a component that holds local state (a counter), increment it in the browser, edit that component’s JSX, save, and watch whether the counter survives. If the count resets to zero the module was reloaded, not hot-swapped, and the plugin is not engaging its Fast Refresh boundary for your Vite version; if the count persists while the edited markup updates, the boundary is live. This behavioural check catches the exact failure that npm ls can miss when a transitive dependency has quietly pulled a second, older plugin copy into the tree. Pair it with an explicit range check so you have both a symptom and a cause:

# Confirm the installed plugin's declared Vite peer range overlaps your Vite
# (@vitejs/plugin-react 4.3 declares: vite ^5 || ^6)
npm ls vite @vitejs/plugin-react
node -p "require('@vitejs/plugin-react/package.json').peerDependencies.vite"

If the printed peer range does not include your installed Vite major, npm only warned at install — it did not stop you — and this is your confirmation that the quiet HMR degradation is a version problem, not application state. The fix is never to force the peer with --legacy-peer-deps, which merely silences the warning while leaving the incompatible plugin in place; it is to install the plugin unpinned so npm resolves the major that actually declares your Vite, then lock the result in the lockfile.

Breaking Changes by Major

The two consequential major breaks Vite 5 raised the Node floor to 18.18, swapped Rollup 3 for 4 which changed output hashing, and removed polyfillModulePreload; Vite 6 introduced the Environment API, dropped Node 21, and changed resolve.conditions defaults. Vite 5 Node 18.18+ floor Rollup 3 → 4 (hashes shift) polyfillModulePreload removed Vite 6 Environment API (experimental) Node 21 dropped resolve.conditions defaults changed
Figure: Vite 5 is a runtime + engine break; Vite 6 is an API + resolution break.

Vite 5 (Node 18+, Rollup 4)

  • Node 18.18+ required. Node 14 and 16 dropped. The CLI hard-exits on older runtimes.
  • Rollup 3 → 4. Output filename hashing changed, so cache keys and Subresource Integrity hashes shift even with no source change — expect a one-time cache bust on CDNs. manualChunks returning a falsy value now reliably falls through to default chunking.
  • build.polyfillModulePreload removed, replaced by build.modulePreload. The old key is ignored silently.
  • CJS Node API deprecated. Importing Vite’s API via require('vite') warns; the build still works but the path is on notice for removal.
  • define no longer stringifies values that are already strings in some edge cases — audit any define that injected JSON.

The Rollup 3 → 4 swap is the change most likely to produce a confusing but benign diff. Rollup 4 rewrote its hashing to be deterministic across platforms and changed the default hash length and the input that feeds the content hash, so every emitted assets/name-[hash].js filename moves even though the code inside is byte-for-byte identical. This is not a regression — it is a one-time re-baseline. The operational consequence is that any consumer keyed to the old filenames (a long-lived CDN cache, a Subresource Integrity manifest, a service worker precache list) must be invalidated once on the upgrade deploy; after that, hashes are stable again. Confirm the change is hash-only and not a chunking change by diffing the sorted asset list before and after, described in the upgrade steps below — if a chunk appears or disappears rather than merely renaming, that is a manualChunks behaviour change to investigate, not the expected re-hash.

Vite 6 (Environment API)

  • Environment API (experimental). A first-class environments config replaces ad-hoc ssr/client branching inside plugins. The legacy ssr options still work, but plugin authors should read this.environment rather than the old options.ssr boolean. This affects the branching patterns in advanced Vite plugin configuration.
  • Node 21 dropped; 18.18 / 20.19 / 22.12 floors. Odd-numbered non-LTS lines are no longer supported targets.
  • resolve.conditions defaults changed. The default condition order now includes module ahead of browser in more cases, which can change which export a dual-package resolves to. Re-test ESM/CJS interop after upgrading.
  • Sass legacy API removed by default in favour of the modern sass-embedded API; projects using additionalData with legacy syntax must migrate.
  • build.cssMinify and worker config gained per-environment overrides.

The resolve.conditions change is the Vite 6 break most likely to alter behaviour without any error at all. Vite reads the exports map in each dependency’s package.json and walks a list of conditions in order to pick which file an import resolves to; Vite 6 reordered that default list so module is consulted earlier relative to browser. For a dual-published package that ships both an ESM module build and a browser-specific browser build, this can silently flip which file you bundle — usually harmless, occasionally the source of a “works in dev, breaks in a Node test” mismatch or a polyfill that stops being applied. There is no warning because both resolutions are valid; the only way to catch it is to re-run your ESM/CJS interop tests after upgrading, and, if a specific package regresses, pin its resolution by spelling out resolve.conditions explicitly rather than inheriting the new default. The mechanics of why condition order matters are covered in understanding ESM vs CommonJS in modern bundlers.

The Environment API is experimental, which has a precise meaning here: the shape can change in a Vite 6 minor, so adopt it in plugin code you control and can update, not in a published plugin you expect to stay compatible across the whole Vite 6 line. The upgrade-safe posture is to keep reading the legacy ssr boolean until the API stabilises, while writing any new branching against this.environment so you are not accumulating more code to migrate later.

One more Rollup 4 detail deserves its own note because it changes behaviour rather than just hashes: the output.manualChunks contract tightened. Under Rollup 3, returning undefined or a falsy value from a manualChunks function was tolerated inconsistently; under Rollup 4 (and therefore Vite 5+) a falsy return reliably means “no opinion, use default chunking,” and a returned string reliably names a chunk. Configs that leaned on the old fuzzy behaviour — for example returning an empty string expecting it to mean “skip this module” — will now emit a chunk literally named the empty string or misroute a module into a chunk you did not intend. Audit any manualChunks function that does not return either a plain non-empty string or a clean undefined before upgrading, and confirm the resulting chunk graph with the same sorted dist/assets diff you use for the hash re-baseline; a new base name appearing in that diff, as opposed to an existing name with a moved hash, is the tell that chunking changed rather than merely re-hashing.

There is a second silent Vite 6 resolution change worth pairing with resolve.conditions: the default value of resolve.mainFields and the interaction between them. When a dependency has no exports map at all — an older package that still relies on top-level main/module/browser fields — the condition list does not apply and Vite falls back to mainFields order instead. A package that regresses after upgrade but has no exports map is failing on mainFields, not resolve.conditions, and the fix is to spell out mainFields explicitly rather than the conditions. Distinguishing the two is a matter of checking whether the offending dependency ships an exports key: if it does, it is a conditions problem; if it does not, it is a mainFields problem. Reaching for the wrong lever leaves the behaviour unchanged and sends you back to the changelog.

Upgrade and Migration Notes

Five-step upgrade order Bump Node first, upgrade Vite and plugins together and clear the pre-bundle cache, rebuild and diff dist, audit plugin SSR branching for Vite 6, then re-validate library builds with publint. 1 · Node firstto target LTS 2 · Vite+pluginsclear .vite cache 3 · diff disthashes only 4 · audit SSRenvironment API 5 · publintlibrary Most "Vite upgrade" failures are actually Node-floor failures — step 1 first
Figure: bump Node before Vite; the pre-bundle cache is keyed to the old Vite and must be cleared.
  1. Bump Node first. Move to the recommended LTS for your target Vite major before touching vite itself; most upgrade failures are actually Node-floor failures.
  2. Upgrade Vite and framework plugins together, in one package.json change, then rm -rf node_modules/.vite to clear the pre-bundle cache, which is keyed to the old Vite version.
  3. Re-run the build and diff dist/. The Rollup 3 → 4 jump (Vite 4 → 5) changes content hashes; confirm only hashes moved, not chunk membership, with ls -1 dist/assets | sort.
  4. For Vite 6, audit plugin SSR branching. Replace transform(code, id, { ssr }) reads with this.environment.name where you adopt the Environment API; the old signature still functions during the deprecation window.
  5. Re-validate library builds. Output hashing and exports resolution interact — re-run publint after upgrading if you ship a package via Vite library mode.

The ordering here is not arbitrary. Node goes first because the engine check gates everything after it — there is no point debugging a config error under a runtime Vite will refuse to run on. Vite and its plugins go together in a single commit because a half-upgrade (new Vite, old plugin) is precisely the peer-mismatch state you want to avoid, and doing it atomically means a single git bisect step can attribute any regression cleanly. Clearing node_modules/.vite matters because the pre-bundle cache stores optimized dependency output keyed by a hash that includes the Vite version and your lockfile; a stale cache from the old Vite can serve pre-bundled deps that the new dev server then fails to reconcile, producing phantom “outdated optimize dep” reloads that vanish once the cache is cleared. Here is the whole sequence as a runnable script:

# One-shot Vite 5 → 6 upgrade, ordered so failures surface at the cheapest step
# 1. runtime first
nvm use 20.19 || nvm install 20.19
# 2. Vite + framework plugin in one change
npm i -D vite@^6 @vitejs/plugin-react@^4.3
# 3. clear the pre-bundle cache keyed to the old Vite
rm -rf node_modules/.vite
# 4. rebuild and capture the asset list for a diff
npm run build && ls -1 dist/assets | sort > /tmp/after.txt
# compare against a pre-upgrade /tmp/before.txt captured the same way
diff <(sort /tmp/before.txt) /tmp/after.txt

The before.txt in that script has to be captured on the old Vite before you touch anything — check out the pre-upgrade commit, run the build, save the sorted asset list, then upgrade. Skipping that leaves you with only an “after” to stare at and no baseline to prove the difference is hash-only. If the diff shows only lines where a filename’s [hash] segment changed while the surrounding name and extension are identical, the change is the expected Rollup re-baseline and you invalidate downstream caches exactly once, on the upgrade deploy. If the diff adds or removes a base name, or moves a file’s size class materially, chunking moved and you have a manualChunks or dependency-graph change to explain before you ship. Automating this comparison as a build step — fail the pipeline when a base name appears or disappears but pass when only hashes move — turns the manual eyeball check into a gate that survives the next upgrade too.

Diagnosing a version mismatch

When something breaks after a bump, the symptom tells you which of the three pins is wrong. If the process exits before printing the Vite banner with a message naming Node, the Node floor is the problem: the fix is to move to the recommended LTS, and you confirm it with node -v matching the engines field, not by editing config. If the dev server boots but the browser console shows Failed to resolve import for a dependency that clearly exists, suspect a stale pre-bundle cache first (rm -rf node_modules/.vite and restart) and a plugin peer mismatch second (npm ls @vitejs/plugin-react vite and check the ranges overlap). If saving a file triggers a full page reload where it used to hot-swap, the framework plugin is too old for the installed Vite even though nothing errored — reinstall the plugin unpinned so npm resolves the compatible major, then lock it. If the production build succeeds but assets 404 in production only, the Rollup hash re-baseline changed every filename and a downstream cache or manifest is still pointing at the old names — invalidate it once. And if a package resolves to the wrong file after a Vite 6 upgrade with no error anywhere, it is the resolve.conditions reorder; confirm by logging the resolved path in a Vite plugin resolveId hook, then pin the condition order for that package.

CI integration

Encode the floor in the pipeline so a contributor on an unsupported Node cannot merge a green build that only passed locally. Matrix the Node versions you actually support and let the engines check plus vite build do the enforcement:

# .github/workflows/ci.yml — gate the supported Node floor for a Vite 6 project
jobs:
  build:
    strategy:
      matrix:
        node: ['20.19', '22.12']   # the recommended lines, not the bare floor
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci                 # fails here if engines is violated with engine-strict
      - run: npx vite build
      - run: npx publint            # catches exports/hash interaction for library builds

Running the matrix on the recommended lines rather than the bare minimum keeps CI representative of what the Vite team tests against, while npm ci under engine-strict turns a runtime-floor violation into a red install step instead of a mysterious build failure three jobs later.

Pinning Across a Monorepo

A single-package project has one vite and the pins are unambiguous; a workspace with several apps and shared libraries is where version drift actually bites. The failure is subtle: package-manager hoisting can install one Vite at the workspace root and a different Vite deeper in a package that declared its own range, so two apps in the same repo build against two vendored Rollups. The dist/ output then differs between apps for reasons that have nothing to do with their source, and a bundle-size regression in one app becomes impossible to attribute. The discipline that avoids this is to declare vite and the framework plugin once, at a single version, and let every workspace package depend on that same range — then verify there is exactly one resolved copy:

# Fail the build if the workspace resolved more than one Vite (or Rollup) major
# npm/pnpm/yarn all print the dependency tree; grep the distinct majors
npm ls vite --all 2>/dev/null | grep -oE 'vite@[0-9]+' | sort -u
npm ls rollup --all 2>/dev/null | grep -oE 'rollup@[0-9]+' | sort -u

If either command prints more than one line, a package in the tree pinned a divergent range and you have two toolchains masquerading as one. Resolve it by aligning the ranges, or by hoisting an explicit single version through the workspace root (npm overrides, pnpm overrides, yarn resolutions) before you trust any cross-app comparison of bundle size or hashes. The Environment API in Vite 6 makes this more pressing, not less: as more build behaviour becomes configurable per environment, two divergent Vite minors in one repo can apply different defaults to what looks like the same config, so the config file stops being a reliable description of what each app actually built.

When not to chase the newest major

A library you publish for others to consume should track the oldest Vite its consumers still run, not the newest you can install, because your build-time Vite version leaks into your output through hashing and exports resolution — a consumer on Vite 5 who pulls a package built and pinned against Vite 6 internals can hit resolution surprises you never see locally. An application you deploy yourself should track the current line for the security and performance fixes and the shortest support tail. These two goals pull in opposite directions, which is exactly why a monorepo that ships both an app and a library often needs the library on a deliberately older, wider peer range (vite: ^5 || ^6) than the app it sits beside. Reference-checking the version is therefore not a single answer per repo; it is one answer per publishable surface, and the library’s answer is usually the more conservative one.

Deprecations to Clear Before Upgrading

Five deprecations mapped old to new polyfillModulePreload becomes modulePreload, the require('vite') CJS entry becomes an ESM import, the legacy Sass API becomes sass-embedded, options.ssr reads become this.environment, and import.meta.glob 'as' becomes query and import options. build.polyfillModulePreload build.modulePreload require('vite') CJS entry ESM import legacy Sass API sass-embedded options.ssr reads this.environment import.meta.glob 'as' query / import options
Figure: clear these five old→new migrations before the version bump, not during.
  • build.polyfillModulePreloadbuild.modulePreload (removed in 5).
  • require('vite') CJS entry → ESM import (deprecated in 5).
  • Legacy Sass API / sass package → sass-embedded (default flipped in 6).
  • Plugin reads of options.ssrthis.environment (soft-deprecated in 6).
  • import.meta.glob as option → query/import options (deprecated across 5.x).

Clear these before the version bump rather than during it, because the two categories fail differently and mixing them muddies the diagnosis. The removals (polyfillModulePreload in 5, the legacy Sass default in 6) fail hard or silently drop your setting — build.polyfillModulePreload is simply ignored once you are on Vite 5, so a value you thought was controlling module-preload behaviour quietly stops applying with no warning. The soft-deprecations (require('vite'), options.ssr, import.meta.glob’s as) still work through a deprecation window but emit console warnings that will become errors in a later major; treating the warning as a to-do rather than noise is what keeps the next upgrade cheap. The concrete migration for import.meta.glob is to replace import.meta.glob('./*.svg', { as: 'raw' }) with import.meta.glob('./*.svg', { query: '?raw', import: 'default' }), which expresses the same “load these as strings” intent through the query/import options that survive into Vite 6. Do each of these on the current major first, ship it, confirm the warnings are gone, and only then bump — so the upgrade commit contains nothing but the version change and its unavoidable consequences.

To find these before they find you, run the current-major build with warnings surfaced rather than swallowed. Vite prints deprecation notices to stderr during vite build and vite dev; capture them and treat a non-empty capture as a pre-upgrade checklist:

# Surface Vite deprecation warnings on the CURRENT major before bumping.
# Run this on your existing Vite, not the target — that is where the warnings fire.
npm run build 2>&1 | grep -iE 'deprecat|will be removed|no longer' || echo 'no deprecation warnings'

An empty result is a genuine green light for the version bump; a non-empty one is the exact list of call sites to migrate first, on the current major, in a separate commit. The reason to clear them on the old version rather than the new one is attribution: if you bump Vite and migrate deprecations in the same commit and something breaks, you cannot tell whether the break came from the new major’s behaviour or from your migration. Separating the two keeps git bisect meaningful and keeps each commit’s failure mode legible — the migration commit can only have broken migration-related behaviour, and the bump commit changes nothing but the version.