Why the require() call survives untouched
When a dependency is marked external (not bundled, common for SSR/Node-target builds and for libraries expecting the consumer to provide certain packages), the bundler still has to emit something at every place your code imports it. Vite’s migration guide documents exactly what changed here: “Require calls for externalized modules are now preserved as require calls and not converted to import statements.”
Vite 7 and earlier rewrote a bundled require("external-pkg") call into a real ESM import statement targeting that external, so the output stayed valid ESM regardless of how the original source code referenced the dependency. Vite 8 stops doing that rewrite. Whatever form the original call took, require() or import, survives into the output unchanged.
That’s a real problem specifically when the output itself is meant to run as ESM. A literal require() call in an .mjs file, or any module Node loads under "type": "module", throws at runtime, because nothing in that file (or Node’s own ESM loader) defines a require function by default.
Fix it: avoid depending on the old conversion
Option 1: don’t mark the dependency external
The most reliable fix is the one that doesn’t depend on Vite’s bundling behavior at all. If you don’t specifically need the dependency to stay external, bundle it instead:
export default defineConfig({
build: {
rollupOptions: {
external: ["some-node-only-pkg"], // BROKEN: require() to this survives raw in ESM output
},
},
});export default defineConfig({
build: {
rollupOptions: {
// FIXED: not external, Vite bundles it, no require()/import
// mismatch possible because nothing is left unresolved
},
},
});Option 2: interop explicitly with Node’s own createRequire
If the dependency genuinely needs to stay external (a peer dependency, or something environment-specific), don’t rely on the bundler to paper over the CJS/ESM boundary. Handle it explicitly with Node’s own supported interop:
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const somePkg = require("some-node-only-pkg"); // FIXED: real require, defined explicitlyThis is the same pattern Node itself documents for using require() inside an ESM module. It doesn’t depend on Vite 8, Rolldown, or any bundler-specific compatibility plugin, so it keeps working even if this exact behavior changes again in a future Vite release.
A third option exists, but verify it yourself before relying on it
Rolldown documents an esmExternalRequirePlugin specifically for converting
external require() calls back into import statements. Check Rolldown’s own
current plugin documentation for the exact import path and usage before adding
it. Plugin APIs for a tool this new can shift between releases faster than a
bundler’s stable config surface.
Confirmed version range
Documented in Vite’s own current migration guide as an intentional Vite 8 behavior change, under “External Module Requires.” This site deploys as a static build with no SSR externals, so this specific behavior change wasn’t independently reproducible against this repo’s own build. The mechanism applies to any Vite 8 SSR or Node-target build with externalized CommonJS-style dependencies. See also Fix Vite 8 CommonJS Default Import Breaking Change, a related but distinct change to how Vite 8 handles the CJS/ESM boundary, from the import side rather than the require side. Browse more posts like this in the Guides & Fixes archive.







