lenec ru

← все посты

Vite Failed to resolve import — что не так и как починить

15K

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, а нестыковка между тем, что написано в коде, и тем, что есть на диске. Минута проверки — и движок снова едет.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.