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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. 01Classify the specifier: relative path (`./`, `../`), alias (`@/`, `~/`), or bare package (`pkg` or `pkg/sub`). The class determines which resolver table applies and narrows the search.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 06Check `optimizeDeps.include`/`exclude` when the failing edge originates from `node_modules/.vite/deps`, because exclusion can prevent pre-bundling from resolving a subpath.
  7. 07Confirm `root` and `server.fs.allow` cover the requesting file; Vite refuses to resolve imports it cannot stat behind the configured boundary.
  8. 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

  1. 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.
  2. 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.
  3. 03HMR continues to apply updates to files neighboring the importer without re-logging the resolve error after each save.
  4. 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.
  5. 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