Vite · advanced

Vite import-resolution checklist

A diagnostic checklist for engineers when a Vite module resolves correctly in one mode (dev, build, SSR, or preview) but fails in another. The guide frames the failure as a boundary mismatch between request contexts, base path assumptions, and import-graph metadata, and provides a triage sequence keyed to Vite's resolve pipeline, environment-specific options, and dependency pre-bundling.

The symptoms

  • The same import path works under `vite dev` but throws at `vite build`, or vice versa, with no source code change.
  • Errors of the form "Failed to resolve import ... from ..." appear in the build log but not in the dev server console for the identical URL.
  • SSR or `vite preview` output returns 404 for an asset that served normally during development, particularly when the app is mounted under a subpath.
  • CSS or static asset imports break only after enabling build target options such as `assetsInlineLimit`, `build.cssCodeSplit`, or `legacy.proxy`/`build.ssr`.
  • A `?url`, `?raw`, or `?worker` import suffix resolves in dev but is rejected by Rollup during build, or returns a transformed URL in one mode and the raw module in another.

Likely causes

  • `resolve.alias` entries are applied during dev via the dev server middleware but are not picked up by Rollup in build because plugin order or `enforce` is inconsistent across modes.
  • `base` is set for production builds (e.g. `/app/`) but the dev server is reached at `/`, causing build-only asset 404s or `import.meta.env.BASE_URL` mismatches.
  • Conditional or dynamic imports use `import.meta.glob` patterns that match in dev (which scans at request time) but fail in build (which scans at bundle time and freezes the graph).
  • SSR vs client mode disagreement: `ssr.noExternal`, `ssr.external`, and `optimizeDeps.exclude` are tuned for one environment and silently change resolution for the other.
  • Pre-bundling (`optimizeDeps.entries` / `optimizeDeps.include`) rewrites bare specifiers for dev but the resulting cached `.vite/deps` entry conflicts with the package's `exports` field at build time.
  • Workspace or monorepo `node_modules` hoisting differs between the directory Vite is invoked from and the package a CI build runs in, changing which package satisfies a bare specifier.
  • Query-suffix semantics (`?url`, `?raw`, `?worker`) are not consistently handled by a custom plugin in both dev and build hooks.

First ten minutes

  1. 01Reproduce the failure in the exact mode that errors: run `vite build` and compare to `vite dev` for the same import path; record the precise error code and the file emitting it.
  2. 02Capture the Vite and Node versions in use, plus the active mode flags (e.g. `--mode`, `--ssr`, `vite preview` vs `vite build`), since `define` and `import.meta.env` values differ across these.
  3. 03Read `vite.config.*` for mode-conditional branches: confirm `resolve.alias`, `optimizeDeps`, `ssr.*`, and `build.*` are not split between a shared base and a per-mode override that disagrees.
  4. 04Check `process.cwd()` vs the directory of the entry file: a bare specifier may resolve from a different `node_modules` than expected, especially in monorepos.
  5. 05Diff the import path as written against the failing one in the error: trailing slashes, missing extensions on relative imports, and case differences on case-sensitive filesystems.
  6. 06List the request or build artifact: the failing module's URL in dev vs the chunk filename in build, to confirm whether it is being requested at all in the failing mode.

Evidence to collect

  • The exact error message text and stack frame, including which resolver emitted it (dev middleware, esbuild pre-bundle, or Rollup).
  • The contents of `vite.config.*` with mode-conditional sections evaluated for the failing mode.
  • The dependency graph snapshot: `node_modules/.vite/deps/_metadata.json` for dev pre-bundling, and the Rollup chunk list for build, to compare which specifier maps to which file.
  • The HTTP request that surfaces the error in dev (path, query string, headers) and the import record in the build manifest that fails to resolve.
  • `import.meta.env` values for the failing mode, particularly `MODE`, `BASE_URL`, `DEV`, and `SSR`, to detect base path or mode drift.
  • Filesystem layout of `node_modules` for the package in question, including whether it is hoisted, linked, or duplicated, to detect a hoisting boundary mismatch.

Where to look

  • The boundary between Vite's dev server plugin pipeline and the Rollup-driven build pipeline, since `resolve.alias` and `optimizeDeps` apply at different stages.
  • The package's `package.json` `exports` field and `main`/`module`/`types` entries, because pre-bundling can shadow these in dev but expose them in build.
  • The `build` and `ssr` sections of `vite.config.*`, where per-mode option overrides commonly diverge from the dev defaults.
  • The `node_modules/.vite` cache directory, which contains the pre-bundled dependency metadata that is authoritative for dev but irrelevant for build.
  • The CI build environment's `node_modules` tree, which may differ from the developer's local tree in hoisting and symlink behavior.

Diagnostic steps

  1. 01Run the same import through both modes and align the error origin: dev (esbuild pre-bundle) vs build (Rollup `resolveId`). Different origins indicate the bug is in pre-bundling, not in source.
  2. 02Disable pre-bundling for the suspect dependency via `optimizeDeps.exclude` and rebuild: if the failure changes form, the pre-bundle was masking or causing the resolution.
  3. 03Force a clean dependency cache and rebuild, then compare the new `_metadata.json` hash for the suspect package against the one in the failing build.
  4. 04Reduce `resolve.alias` to a single rule, or remove it temporarily, and observe whether the mode-dependent failure disappears; reintroduce rules one at a time to localize the conflict.
  5. 05For SSR vs client mismatches, set `ssr.noExternal` and `ssr.external` explicitly and re-run the SSR build, checking whether the failure moves from a missing export to a different resolver error.
  6. 06For dynamic or globbed imports, replace `import.meta.glob` with an explicit static `import` to determine whether the failure is in pattern matching or in module resolution itself.
  7. 07For base path errors, set `base: './'` temporarily and rebuild; if asset 404s disappear, the original `base` value did not match the deployment path, not the import graph.
  8. 08For query-suffix errors, write a minimal custom plugin that logs the `id` and `query` in both `resolveId` and `load`, and run the failing build to observe the lifecycle in which the suffix is dropped or rejected.

Common mistakes

  • Assuming the dev server and build share the same resolver. They do not: dev uses an esbuild-driven pre-bundle and on-the-fly transforms, while build uses Rollup's `resolveId` chain.
  • Adding to `optimizeDeps.include` to "fix" a build error. That option affects dev pre-bundling only and is ignored by the production build.
  • Editing `node_modules/.vite/deps/_metadata.json` or pre-bundled output. The cache is regenerated; persistent fixes must live in `vite.config.*` or the dependency's own `exports` field.
  • Treating a `base` path mismatch as an import-resolution bug. The module resolved correctly; the URL it was emitted under is wrong for the deployment target.
  • Reaching for a wildcard alias (`'*': '/'`) to mask the real path, which can hide the actual boundary that the production build exposes.
  • Mixing `import` with `require`-style resolution in the same file when `build.ssr` is enabled, since the two are resolved by different code paths.

Safe fixes

  • When pre-bundling is masking a package's `exports` field, pin resolution by adding the package to `optimizeDeps.exclude` for dev and to `build.rollup.external` only if the package is intentionally not bundled; verify with a clean build.
  • When `resolve.alias` applies in dev but not in build, ensure the alias plugin has a consistent `enforce` value across configurations, or move the alias to `build.rollup.options.resolve.alias` if it is build-specific.
  • When `import.meta.glob` patterns differ between modes, prefer explicit, statically analyzable patterns and verify that the pattern matches the files at bundle time, not just at request time.
  • When SSR mode disagrees with client mode, set `ssr.noExternal` to the smallest set of packages that must be bundled for SSR, and confirm with a separate SSR build before combining with the client build.
  • When `base` differs between dev and build, derive `base` from a single source (e.g. an environment variable) and apply it identically to both, so dev and build produce the same asset URLs.
  • When query suffixes behave inconsistently, write a plugin that branches on `ssr` or `command` in its hook and returns a result keyed to the current mode; do not assume one code path covers both.
  • For monorepo hoisting issues, ensure Vite is invoked from the workspace root that owns the dependency, and verify by running the failing build from a fresh `node_modules` in CI.

Prove the fix

  1. 01The build command that previously emitted a "Failed to resolve import" error now completes and emits a chunk for the previously failing module; verify by inspecting the Rollup output chunk list.
  2. 02The dev server serves the previously failing URL with a `200` status and a `Content-Type` matching the resolved module, in a fresh dev session after clearing `node_modules/.vite`.
  3. 03For SSR fixes, the SSR build emits a bundle that imports the previously failing module, and the runtime does not log a missing-export or missing-module error on startup.
  4. 04For base path fixes, the built `index.html` references assets via paths that resolve under the deployment base, verified by serving the build output and checking the response status of each referenced URL.
  5. 05The fix holds when the same commands are run from a clean checkout in CI, with `node_modules` reinstalled and the `.vite` cache absent, demonstrating that no local cache is masking the regression.

Prevention and next steps

  • Keep `resolve.alias`, `optimizeDeps`, `ssr.*`, and `build.*` in a single source of truth in `vite.config.*`, and branch only on documented mode flags rather than duplicating config files.
  • Run both `vite build` and `vite dev` (or the SSR build) in CI on every change to a configuration block that affects resolution, so a mode-dependent regression is caught before merge.
  • Pin dependency versions and prefer packages with explicit `exports` fields; avoid relying on `main` fallback behavior that pre-bundling may resolve differently than Rollup.
  • Document the intended `base` path for each environment in a single place and derive it from a shared constant, so dev and build cannot drift.
  • Treat `node_modules/.vite` as ephemeral: do not commit it, and include a cache-bust step in CI so pre-bundling decisions are reproducible.

Safe commands and checks

vite build 2>&1 | tee <build-log-path>
vite dev --mode <mode-name> --debug resolve 2>&1 | tee <dev-log-path>
vite build --mode <mode-name> --debug resolve 2>&1 | tee <build-log-path>
vite build --ssr <ssr-entry-path> 2>&1 | tee <ssr-build-log-path>
vite optimizeDeps --force 2>&1 | tee <optimize-log-path>
node -e "import('vite').then(v => console.log(v.version))"
cat <vite-config-path>
ls -la node_modules/.vite/deps 2>/dev/null | head -n 50