How to Configure ESM and CJS Interop in Vite

Vite’s ESM-first architecture frequently throws module-resolution errors when consuming a legacy CommonJS package, because the dev server and the production build resolve formats through two different bundlers. This guide gives the exact configuration to fix interop conflicts without sacrificing dev-server speed or production tree-shaking. For the underlying condition-matching model, read Understanding ESM vs CommonJS in Modern Bundlers before applying the overrides below.

Vite's dual-bundler path for a CommonJS dependency A CJS dependency flowing through esbuild pre-bundling in dev and the Rollup commonjs plugin in production, with the SSR externalization branch. CJS dependency module.exports = ... require("...") Dev: esbuild optimizeDeps pre-bundle Build: Rollup plugin-commonjs + interop SSR: Node ssr.noExternal bundles it Browser/server ESM import default + named
Figure: the same CJS dependency takes three paths — esbuild pre-bundling in dev, the Rollup commonjs plugin in build, and ssr.noExternal in SSR.

Problem scope and reproducible setup

The errors below all stem from Vite’s split pipeline: the dev server serves native browser ESM with esbuild pre-bundling, while vite build uses Rollup. A CJS dependency that lacks a clean exports map, or that uses dynamic require(), fails to transform somewhere along that path. Reproduce with a minimal project:

# Vite 5.x / 6.x, Node 20+
npm create vite@latest interop-repro -- --template vanilla-ts
cd interop-repro && npm i
npm i some-legacy-cjs-lib       # a package shipping only CommonJS
npm run dev                     # observe the resolution error in the browser console

Diagnosis workflow

  1. Run with debug output. npx vite dev --debug prints dependency-resolution paths; find the line where the CJS module is dropped or mis-converted.
  2. Inspect the pre-bundle. Look in node_modules/.vite/deps/. A missing or malformed file for the package means optimizeDeps skipped or failed to convert it.
  3. Confirm the format. grep -l "__esModule" node_modules/.vite/deps/*.js shows which deps were wrapped for interop; absence of a wrapper on a CJS dep is the smoking gun.
  4. Classify the error. Match the exact error string against the table below to pick the right override rather than guessing.

The complete solution config

// vite.config.ts — Vite 5.x / 6.x, Node 20+
import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    // Fix 1 — dev server "require is not defined" / "Cannot use import statement
    // outside a module": force esbuild to pre-bundle the CJS dep into ESM.
    include: ['legacy-cjs-lib'],
    esbuildOptions: {
      // Only needed when the dep ships non-standard syntax (e.g. JSX in .js)
      loader: { '.js': 'jsx' },
    },
  },
  build: {
    rollupOptions: {
      output: {
        // Fix 2 — production "'default' is not exported by ...": let Rollup emit
        // synthetic ESM wrappers around CJS module.exports. 'compat' is stricter.
        interop: 'auto',
      },
    },
  },
  ssr: {
    // Fix 3 — vite build --ssr: bundle the CJS dep instead of handing it to
    // Node's require(), which avoids ERR_REQUIRE_ESM at runtime.
    noExternal: ['node-cjs-dep'],
    target: 'node', // 'node' keeps require/__dirname; 'webworker' strips Node globals
  },
});

Verification

After applying the override, prove the fix instead of trusting the absence of an error:

# Dev: a clean cold start must rebuild the pre-bundle with the dep converted
rm -rf node_modules/.vite && npx vite dev --force
# Then confirm the dep was wrapped for interop:
grep -l "legacy-cjs-lib" node_modules/.vite/deps/_metadata.json

# Build: production output must contain no unresolved 'default' export error
npx vite build && npx vite preview

A successful vite build exits 0 with no 'default' is not exported warning, and the SSR bundle (dist/server) contains the dependency inline rather than a bare require('node-cjs-dep').

Exact error string Root cause Override
SyntaxError: Cannot use import statement outside a module CJS file loaded as ESM in the browser optimizeDeps.include: ['pkg']
ReferenceError: require is not defined Pre-bundle skipped for a CJS dep optimizeDeps.include: ['pkg']
'default' is not exported by node_modules/<pkg>/index.js Rollup strict ESM validation build.rollupOptions.output.interop: 'auto'
Failed to resolve entry for package '<pkg>' Missing or malformed exports field resolve.alias or optimizeDeps.include
ERR_REQUIRE_ESM during --ssr CJS externalized but depends on ESM ssr.noExternal: ['pkg']

Gotchas & edge cases

  • Stale pre-bundle masks the fix. Vite caches optimizeDeps output; always re-run with --force after editing include, or the old wrapper persists.
  • Named exports still fail after interop: 'auto'. cjs-module-lexer cannot detect dynamically assigned exports. Default-import then destructure, and see resolving “named export not found” errors for the full pattern.
  • Dual-package hazard from noExternal. Bundling a dep for SSR while a transitive copy stays externalized can fork its state; pin one copy with resolve.dedupe.
  • vite-plugin-commonjs is a last resort. Reach for native optimizeDeps and Rollup interop first; the plugin adds an extra transform pass that slows cold start.