> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Module Not Found in Vite but Fine in Webpack
- URL: https://debugly.dev/module-not-found-vite-but-not-webpack/
- Published: 2026-06-09T09:00:00.000Z
- Updated: 2026-10-17T05:58:39.000Z
- Description: Same code, same package.json, different bundler, different result. The difference is resolution conditions and how strict each tool is.
- Author: Rohit Bhadani
- Tags: JavaScript, Node.js, Error Autopsy, Tooling

## The short version

```
[vite]: Rollup failed to resolve import "some-lib/utils" from "src/app.ts"

```

Vite and Webpack resolve modules differently in four ways:

1. **Vite respects the `exports` map strictly.** Webpack 4 ignores it, Webpack 5 is more permissive about fallbacks.
2. **Vite prefers the `browser` and `module` conditions.** Webpack's default order differs.
3. **Vite does not polyfill Node builtins.** Webpack 4 did automatically.
4. **Vite requires explicit extensions for some paths** where Webpack guesses.

Diagnose with:

```bash
node -e "console.log(require.resolve('some-lib/utils'))"
cat node_modules/some-lib/package.json | jq '{main, module, browser, exports}'

```

Tested on Vite 6.0 and Webpack 5.97.

## The exports map is the usual answer

Modern packages declare their public API:

```json
{
  "name": "some-lib",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./styles.css": "./dist/styles.css"
  }
}

```

This is an **allowlist**. Anything not listed is unreachable, even if the file exists on disk.

So `import { helper } from "some-lib/utils"` fails, because `./utils` is not in the map. Webpack 4 ignored `exports` entirely and resolved the file path directly, which is why the same import worked before.

The package author made a deliberate decision about their public surface. The correct fix is to use the public entry:

```javascript
import { helper } from "some-lib";

```

If the export genuinely should be public, open an issue. If you need it now, alias it:

```javascript
// vite.config.js
export default {
  resolve: {
    alias: {
      "some-lib/utils": path.resolve(__dirname, "node_modules/some-lib/dist/utils.js"),
    },
  },
};

```

That works and it reaches past a boundary the author drew, so it will break on their next release. Leave a comment saying why.

## Conditions and the wrong build

The `exports` map can have several conditions and the bundler picks by priority:

```json
{
  "exports": {
    ".": {
      "browser": "./dist/browser.js",
      "node": "./dist/node.js",
      "default": "./dist/index.js"
    }
  }
}

```

Vite in client mode resolves `browser`. In SSR mode it resolves `node`. Webpack's `target` setting controls the same thing.

The symptom when this goes wrong is not a resolution error, it is a runtime error about a missing API, because you got the Node build in the browser or the reverse. `process is not defined` in the browser is the classic.

Force it if you need to:

```javascript
// vite.config.js
export default {
  resolve: {
    conditions: ["browser", "import", "default"],
  },
  ssr: {
    resolve: {
      conditions: ["node", "import", "default"],
    },
  },
};

```

Order matters. First match wins.

## Node builtins are not polyfilled

Webpack 4 automatically shimmed `buffer`, `crypto`, `stream`, `path`, and others for the browser. Webpack 5 stopped, and Vite never did.

```
Module "buffer" has been externalized for browser compatibility.
Cannot access "buffer.Buffer" in client code.

```

Three options, in order of preference.

**Do not use the package in browser code.** Most of the time a dependency pulling in `crypto` in a browser bundle is a sign you are shipping server code to the client. Check why it is there.

**Use a web equivalent.** `crypto.randomUUID()` exists natively. `TextEncoder` replaces most `Buffer` usage. `fetch` replaces `http`.

**Polyfill explicitly** if you genuinely need it:

```javascript
import { nodePolyfills } from "vite-plugin-node-polyfills";

export default {
  plugins: [nodePolyfills({ include: ["buffer", "process"] })],
};

```

Include only what you need. Polyfilling everything adds a lot of bytes for something that is usually a mistake upstream.

## CommonJS dependencies

Vite pre-bundles dependencies with esbuild and serves your source as native ES modules. A CommonJS dependency has to be converted, and the conversion is not always clean.

```
SyntaxError: The requested module '/node_modules/.vite/deps/some-lib.js'
does not provide an export named 'default'

```

Usually a package using conditional exports or dynamic `module.exports` assignment that static analysis cannot follow.

```javascript
// vite.config.js
export default {
  optimizeDeps: {
    include: ["some-lib"],           // force pre-bundling
  },
  build: {
    commonjsOptions: {
      transformMixedEsModules: true, // handle files mixing require and import
    },
  },
};

```

`optimizeDeps.include` is the one to reach for when a dependency works in production build and fails in dev, or the reverse. Vite pre-bundles some things and not others, and forcing it either way frequently resolves the inconsistency.

Related to [the whole ESM and CommonJS boundary problem in Node](https://debugly.dev/err-require-esm-node-fix/), which is the same underlying tension appearing in a different tool.

## Path aliases have to be declared twice

TypeScript path mapping does not affect the bundler.

```json
// tsconfig.json
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }

```

That satisfies the type checker. Vite still needs its own:

```javascript
// vite.config.js
export default {
  resolve: {
    alias: { "@": path.resolve(__dirname, "./src") },
  },
};

```

Symptom: types resolve in your editor, build fails. Two sources of truth is the problem, and `vite-tsconfig-paths` removes it by reading the tsconfig:

```javascript
import tsconfigPaths from "vite-tsconfig-paths";
export default { plugins: [tsconfigPaths()] };

```

## Case sensitivity

Works on macOS, fails in CI on Linux.

```javascript
import Button from "./components/button";   // file is Button.tsx

```

macOS and Windows have case insensitive filesystems by default. Linux does not. This is a whole category of [works locally, fails in CI](https://debugly.dev/docker-build-works-locally-fails-ci/) that has nothing to do with the bundler and shows up as a resolution error.

Catch it with `eslint-plugin-import` and its `no-unresolved` rule, with `caseSensitive: true`.

## Debugging resolution

**Ask Node what it resolves:**

```bash
node -e "console.log(require.resolve('some-lib/utils'))"
node --experimental-import-meta-resolve -e "import.meta.resolve('some-lib/utils')"

```

If Node cannot resolve it either, the `exports` map is the answer and Webpack was being lenient.

**Read the package manifest:**

```bash
cat node_modules/some-lib/package.json | jq '{main, module, browser, exports, type}'

```

**Turn on Vite's debug output:**

```bash
DEBUG=vite:resolve vite build

```

Verbose, and it shows every resolution attempt and which condition matched. When the failure is subtle this is the fastest path to the answer.

**Check what actually got bundled:**

```bash
vite build --mode production
npx vite-bundle-visualizer

```

Sometimes the resolution succeeded and picked the wrong file, which is worse than failing, and the only way to see it is to look at the output.

## The underlying reason

Webpack grew up before `exports` maps existed and accumulated a lot of leniency for compatibility. Vite started after and implements the current specification more strictly.

So a Vite failure is frequently Vite being correct about something Webpack let you get away with. The instinct is to configure around it, and it is worth first checking whether the import was always reaching into a package's internals.

That framing has saved me time: **when a stricter tool rejects something a looser tool accepted, assume the stricter tool is right until proven otherwise.**

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)