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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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.
- 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`.
- 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.
- 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.
- 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