Vite · beginner
Vite failed to resolve import: find the graph edge that broke
A focused playbook for resolving Vite's "failed to resolve import" errors by tracing the import graph edge that broke. It sequences triage steps from the literal error string to targeted file-system and configuration checks, with decision points that separate missing files, missing extensions, alias/path misconfiguration, package export problems, and dependency-installation issues. Read-only commands and named evidence gates let engineers act without making blind edits.
The symptoms
- •Terminal or browser output contains a "Failed to resolve import" message referencing a specifier such as "./Component" or "@/lib/foo" during `vite dev` or `vite build`.
- •HMR stops applying updates and the dev server logs the same resolve error after every file save that touches the unresolved module.
- •The error names a requesting file and a target specifier; clicking the file paths in dev overlays opens the requester but the target cannot be opened from the project tree.
- •Production `vite build` aborts with an import-resolution error even when `vite dev` appears to render the page, because the production dependency optimizer treats the same edge differently.
- •Error origin is inside `node_modules/.vite/deps` after pre-bundling, indicating the dependency optimizer could not reconcile the edge rather than a hand-written import.
Likely causes
- •The specifier points to a path that does not exist on disk because the file was renamed, deleted, or moved without updating its importing neighbor.
- •The file exists but lacks the extension that Vite's `resolve.extensions` list expects, and the import omits the extension or supplies the wrong one.
- •A path alias such as `@` or `~/` is referenced in code but is not declared in `vite.config` `resolve.alias`, or is declared with a trailing-slash mismatch relative to the project's `tsconfig.json`/`jsconfig.json` `paths`.
- •A bare specifier names a package whose `package.json` `exports` map does not expose the requested subpath, so Vite cannot resolve the conditional export the bundler is asking for.
- •A package is imported but is not installed, or was pruned by the lockfile change while `node_modules` was not reinstalled, so the import edge leaves the project graph.
- •A source file is outside the `root` directory Vite is serving, or a symlink crosses `root` and `fs.allow`/`server.fs.deny` policies block the read.
First ten minutes
- 01Copy the full error verbatim, including the "Failed to resolve import" line and any "Did you mean" suggestions, then identify the requesting file and the unresolved specifier it names.
- 02Open the requesting file at the path printed in the error and locate the exact import statement to confirm the specifier character-for-character against the error message.
- 03In a terminal, list the parent directory of the specifier and confirm whether the target file exists at the resolved path with the expected extension, using only read-only directory listings.
- 04Open `vite.config.js`/`vite.config.ts` and inspect the `resolve` block — alias map, `extensions` array, and any `dedupe` entries — to see what the resolver is configured to accept.
- 05If the specifier is a bare package name, read that package's `package.json` `exports` map and `main`/`module` fields to see which subpaths are exposed to a bundler.
- 06Compare the error's requesting file path against `vite.config.js` `root` and `server.fs.allow` so you can rule out files that Vite was never told to serve.
Evidence to collect
- •The exact specifier text from the error and the requesting file path Vite attributes it to, captured before any edits.
- •The on-disk result of listing the target directory so existence, casing, and extension of the file can be compared to the import string.
- •The active `resolve.alias`, `resolve.extensions`, and any `optimizeDeps` entries from the loaded `vite.config.*` file.
- •The target package's resolved `package.json` paths and its `exports` map for the subpath the import is requesting.
- •A confirmation of whether `node_modules` is present and synchronized with the lockfile by checking the lockfile's recorded integrity entries.
Where to look
- •The terminal pane running `vite` and the browser overlay that mirrors the same error at the same requesting file.
- •The boundary between source files and the resolver: the importing file's import statement, then the filesystem path the resolver attempts to stat.
- •`vite.config.js`/`vite.config.ts` `resolve` block and any `tsconfig.json`/`jsconfig.json` `paths` mappings that aliases are supposed to mirror.
- •The dependency boundary inside `node_modules/<pkg>/package.json` for any bare specifier, paying attention to `exports`, `main`, `module`, and `types`.
- •The optimizer cache at `node_modules/.vite/deps` when the error is logged from pre-bundling rather than from a hand-written import.
- •The `root`/`server.fs.allow` boundary when a requesting file lives outside what Vite was configured to serve, including symlinks that cross the boundary.
Diagnostic steps
- 01Classify the specifier: relative path (`./`, `../`), alias (`@/`, `~/`), or bare package (`pkg` or `pkg/sub`). The class determines which resolver table applies and narrows the search.
- 02For a relative path, list the importing file's directory and confirm the target exists with the expected casing; case-sensitive filesystems reject otherwise-correct paths.
- 03For an alias, diff the `resolve.alias` map against the project's `tsconfig.json`/`jsconfig.json` `paths`; mismatched keys between the two are a frequent silent breakage after editor-only edits.
- 04For a bare specifier, open the target package's `package.json` and verify the requested subpath is listed in `exports`, then check `main`/`module` for legacy entry points the bundler may still consult.
- 05Inspect `resolve.extensions` ordering: an extension listed after another may mask a same-named file when both exist, and an omitted extension forces the import to keep its extension.
- 06Check `optimizeDeps.include`/`exclude` when the failing edge originates from `node_modules/.vite/deps`, because exclusion can prevent pre-bundling from resolving a subpath.
- 07Confirm `root` and `server.fs.allow` cover the requesting file; Vite refuses to resolve imports it cannot stat behind the configured boundary.
- 08If `node_modules` was recently rewritten, diff the lockfile entries that name the unresolved package against the lockfile from the last green build to detect drift.
Common mistakes
- •Editing only `tsconfig.json` `paths` while leaving `resolve.alias` empty, so the editor type-checks the alias but Vite cannot resolve it at build time.
- •Renaming or moving a file and updating only some importers, leaving a residual reference that surfaces only after the next save triggers HMR.
- •Assuming `node_modules` is current because `npm ls` returns no error, when the package in question was added to `package.json` after the last install and was never installed.
- •Adding a file with the same basename under multiple extensions and trusting Vite to pick the one your tooling expects, when `resolve.extensions` order makes the choice non-deterministic.
- •Bypassing the resolver by hard-coding a relative path that crosses the `root`/`server.fs.allow` boundary, producing a resolve failure even though the file is visible to the OS.
Safe fixes
- •If the relative path is wrong, update the importer to point at the new file location and re-run the failing Vite command to confirm the specifier resolves; keep changes scoped to the importing file.
- •If the alias is missing, add a matching `resolve.alias` entry whose key matches the `tsconfig.json` `paths` pattern and whose target path resolves from `root`; verify by re-running the same failing command.
- •If the file exists but lacks the expected extension, either append the correct extension to the import or add the extension to `resolve.extensions`; verify with a fresh build to confirm no other importer regressed.
- •If a bare specifier points at a subpath not covered by `exports`, change the import to a subpath the package exposes or pin to a package version whose `exports` map supports it.
- •If the package is missing from `node_modules`, reinstall from the lockfile so resolved versions match previously known-good integrity records; do not hand-edit `package.json` without a corresponding lockfile update.
- •If the requesting file sits outside `root` or `server.fs.allow`, either move the file under `root` or widen the allow list deliberately, then re-run the command to confirm the edge resolves.
Prove the fix
- 01Re-running `vite dev` or `vite build` produces no "Failed to resolve import" lines that name the previously broken specifier, either in the terminal or in the dev overlay.
- 02A grep of the dev/build output for the previously broken specifier returns zero matches after the change, confirming the edge is no longer being attempted.
- 03HMR continues to apply updates to files neighboring the importer without re-logging the resolve error after each save.
- 04A production `vite build` reaches the dependency optimization stage with no entries for the previously broken dependency, indicating the optimizer no longer needs to reconcile the edge.
- 05If the fix touched a config file, restarting `vite dev` with the updated config loads without warnings referencing the previous alias, extension, or allow-list setting.
Prevention and next steps
- •Keep `resolve.alias` and `tsconfig.json`/`jsconfig.json` `paths` synchronized in a single source of truth, such as a shared alias map imported by both configs.
- •Configure the editor to rename files together with all importers, or use a move-aware tool that updates relative imports atomically.
- •Track `node_modules` alongside the lockfile in version control workflows: install from the lockfile after every dependency change so the project graph never drifts.
- •Add `resolve.extensions` deliberately and document the canonical extensions so importers do not silently rely on resolver inference.
- •Prefer package subpaths that appear in the dependency's `exports` map over internal paths, and document that policy in the project's import conventions.
Safe commands and checks
pwd
ls -la <project-root>
ls -la <directory-of-unresolved-specifier>
cat vite.config.* | sed -n '1,200p'
node -e "console.log(require.resolve('<unresolved-pkg>'))"
cat node_modules/<pkg>/package.json
grep -RIn "<unresolved-specifier>" src
npm ls <pkg> --depth=0