Vite Failed to resolve import — что не так и как починить
Vite швыряет в консоль ровно такое:
[plugin:vite:import-analysis] Failed to resolve import "./button" from "src/App.tsx". Does the file exist?Сообщение конкретное: модуль не нашёлся по указанному пути. И обычно это правда, как Vite и говорит. Но «не нашёлся» — слишком общо. У этой ошибки в моей практике четыре основных вкуса.
1. Регистр в имени файла
Самый коварный случай. На macOS файловая система по умолчанию case-insensitive: Button.tsx и button.tsx — одно и то же. На Linux и в Docker — разные файлы.
На своём ноутбуке всё работает, в CI или на сервере падает с Failed to resolve import "./button" — это оно.
Лечение: точно совпадать в импорте и в имени файла.
// файл src/components/Button.tsx
import { Button } from './components/Button'; // ок
import { Button } from './components/button'; // упадёт в LinuxЧтобы это ловилось локально, на macOS можно включить tsconfig.json опцию:
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true
}
}Тогда TypeScript будет ругаться на разный регистр ещё на этапе типизации, не дожидаясь Vite-стек-трейса в продакшене.
2. Расширение и индекс
Vite по умолчанию умеет дорезолвить .ts, .tsx, .js, .jsx без указания расширения. Но в некоторых конфигах люди добавляют свои расширения, и привычная логика ломается.
Также ./components/Button может разрезолвиться в:
./components/Button.ts./components/Button.tsx./components/Button/index.ts
Если у тебя одновременно лежит и Button.ts, и Button/index.ts, поведение зависит от конфигурации resolve.extensions. Часто лечится тем, что просто оставляешь один вариант.
В vite.config.ts можно расширить:
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mts'],
},
});3. Алиас не настроен
Любимая история монорепо. Импорт через алиас:
import { Header } from '@/components/Header';В tsconfig.json есть paths, IDE подсказывает, всё работает в редакторе. А Vite об этом не знает: он видит просто строку @/components/Header и ничего не находит.
В tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}В vite.config.ts:
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});Алиас должен совпадать с tsconfig.paths. У меня привычка — добавлять оба сразу, как только начинаю проект, чтобы не забыть.
Альтернатива — плагин vite-tsconfig-paths, он сам читает tsconfig.json и настраивает алиасы Vite:
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()],
});4. Пакет не установлен или установлен с другим именем
Failed to resolve import "@hello-pangea/dnd" from "src/Board.tsx".В node_modules такого пакета нет, а в package.json в зависимостях он либо тоже нет, либо имя слегка другое. Это бывает после того, как кто-то переименовал пакет (привет, react-beautiful-dnd → @hello-pangea/dnd) или после запуска npm install без локфайла.
Проверка:
cat package.json | grep dnd
ls node_modules/@hello-pangea/dnd 2>/dev/nullЕсли пакета нет — pnpm add @hello-pangea/dnd. Если есть, но Vite не видит — почисти кэш.
5. Кэш Vite на устаревшем состоянии
Бывает, обновил пакет или переменную окружения, влияющую на резолвинг, а Vite всё ещё пользуется старым кэшем в node_modules/.vite. Симптом — ошибка появляется только в одном из режимов и пропадает после перезапуска.
rm -rf node_modules/.vite
pnpm devЕсли ошибка пропала — это был кэш.
6. Динамические импорты с переменной
// плохо
const lang = 'ru';
const messages = await import(`./locales/${lang}`);Vite пытается заранее проанализировать, какие модули могут запрашиваться. Если путь полностью динамический и непонятный по структуре, сборщик не понимает, какие файлы включить в бандл.
Решение — давать Vite подсказку через статический префикс:
const messages = await import(`./locales/${lang}.json`);Vite понимает ./locales/*.json и включит все варианты. Главное, чтобы префикс был статическим, а переменная — частью имени.
7. Платформенный модуль (server-only)
Импорт fs, path или другого Node-API в коде, который Vite собирает под браузер. У плагинов SSR это иногда обходится через vite.ssr.noExternal, но в чистом клиентском бандле этих модулей просто нет.
Пример:
// в клиентском файле
import { readFileSync } from 'fs'; // упадётЛечение — вынести серверную логику в SSR-точку или API-роут. На клиент идёт только то, что должен видеть браузер.
Алгоритм
Когда вылетает Failed to resolve import, я иду по такому пути:
- Открываю стек, читаю, какой именно импорт не нашёлся.
- Смотрю на регистр — совпадает ли строка в импорте с именем файла.
- Если это алиас — есть ли он в
vite.configи вtsconfig. - Если это пакет — установлен ли в
node_modules. - Чищу
.vite-кэш для уверенности.
Превентивные меры
На каждом проекте я в первый день делаю три вещи:
- включаю
forceConsistentCasingInFileNamesв TypeScript; - прописываю алиасы и в
tsconfig, и вvite.config(или подключаюvite-tsconfig-paths); - добавляю в README команду «
rm -rf node_modules/.viteи попробуй снова», чтобы новые члены команды не страдали от кэша.
Failed to resolve import — почти всегда не бажный Vite, а нестыковка между тем, что написано в коде, и тем, что есть на диске. Минута проверки — и движок снова едет.