Vite / Webpack — Module not found
Diagnose unresolved imports caused by case sensitivity, aliases, package exports, missing dependencies, or the wrong working directory, and fix resolution without disabling checks.
Symptoms
- Failed to resolve import "./Button" from "src/App.tsx"
- Module not found: Can't resolve 'lib/utils'
- The import works on macOS but fails on Linux CI
- The package is installed, but the bundler cannot resolve a subpath
Possible causes
- The file name case or extension differs from the import path
- A path alias is configured in tsconfig but not in the bundler, or the reverse
- The dependency is missing from package.json or installed at a different workspace level
- The package exports field does not expose the requested subpath
- The import is resolved relative to the wrong working directory or root
Diagnosis steps
Reproduce with the bundler's resolution debug output
npx vite --debug 2>&1 | grep -i 'resolve\|failed'Interpretation
Vite prints the importer and the attempted resolution. Webpack users can run with --stats detailed or inspect the resolve configuration.
Verify the dependency and its exported paths
npm ls PACKAGE && node -e "console.log(require.resolve('PACKAGE'))"Interpretation
A missing package or an exports map that blocks the subpath explains a resolution failure despite an installed dependency.
Compare the import path with the actual filename
find src -iname 'Button*'Interpretation
Case-only differences work on a case-insensitive filesystem and fail on Linux. The build must match the exact case.
Root cause
The bundler cannot map the import specifier to a file. The cause is a path mismatch, an alias that exists in only one configuration, a missing dependency, or a package exports restriction.
Fix
Correct the path and case, add the alias to both tsconfig and the bundler, install the dependency at the correct workspace level, or import a subpath the package actually exports. Do not disable resolution checks or add broad fallbacks.
Risk note
A catch-all alias or a custom resolver that ignores exports can load the wrong file or bypass package boundaries, producing runtime bugs that the build no longer catches.
Verify the fix
Run a clean install and build in a Linux-like environment, then confirm the bundle resolves the import and the page renders. Add a CI build on a case-sensitive filesystem so local macOS behavior cannot hide the issue.
The error names the importer, not always the cause
Failed to resolve import "./Button" usually means the file is not where the import says, but the mismatch can be subtle: case, extension, an alias resolved differently by TypeScript and the bundler, or a package subpath blocked by exports. The import works on macOS because the filesystem is case-insensitive; Linux CI fails on Button versus button.
src/components/button.tsx
import { Button } from './Button'; // works on macOS, fails on Linux
Run the bundler with resolution debugging and compare the attempted path with find src -iname. Fix the source path to the exact case, or rename the file consistently.
Aliases must agree
If @/components is configured in tsconfig.json but not in vite.config.ts, the editor passes and the build fails. Configure the alias in both places from one source, or use a relative import. The same applies to Jest, Vitest, and any server-side runtime that resolves modules separately.
// vite.config.ts
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }
Check package exports
A package may be installed but not expose the subpath you import. exports maps are authoritative for modern Node and bundlers. import 'pkg/internal/foo' can fail even when the file exists in node_modules. Import a documented entry point or add the subpath through the package’s public API. Do not bypass exports with a deep relative path into node_modules.
Fix and verify
Correct the path, alias, dependency, or import target. Run a clean install and build in a Linux-like environment or CI, then confirm the bundle contains the module and the page renders. A local macOS build alone is not evidence because it does not exercise case-sensitive resolution. Do not disable resolution checks or add broad fallbacks; they hide the real mismatch and can load the wrong implementation.