Cannot find module в Node.js: MODULE_NOT_FOUND и ERR_MODULE_NOT_FOUND

JavaScript Автор: Среда и версия: Node.js: CommonJS и ECMAScript modules
содержание

Cannot find module означает, что Node.js не смог сопоставить спецификатор модуля с файлом или точкой входа пакета. Для CommonJS код ошибки обычно равен MODULE_NOT_FOUND, для загрузчика ECMAScript modules — ERR_MODULE_NOT_FOUND. Причина зависит не от формулировки сообщения, а от трёх вещей: какой загрузчик работал, что указано между кавычками и из какого файла начался поиск.

Не начинайте с переустановки всего проекта. Сначала отделите относительный путь вроде ./config.js от имени пакета вроде kleur, найдите импортирующий файл и проверьте структурированные поля ошибки. Так можно исправить одну неверную строку, отсутствующую зависимость или устаревшую сборку, не удаляя node_modules и lock-файл наугад.

JavaScript / 01

Сначала узнай, какой загрузчик искал модуль

Сначала узнай, какой загрузчик искал модуль01 CommonJS MODULE_NOT_FOUND Смотри requireStack. Относительный путь — от файла; пакет — через node_modules. 02 ESM ERR_MODULE_NOT_FOUND Смотри url. Проверь расширение локального файла, exports и установку пакета.01CommonJSMODULE_NOT_FOUNDСмотри requireStack. Относительный путь —от файла; пакет — через node_modules.02ESMERR_MODULE_NOT_FOUNDСмотри url. Проверь расширение локальногофайла, exports и установку пакета.
Сначала определите загрузчик, затем вид спецификатора. Относительный путь проверяют от расположения импортирующего файла, а пакет — по дереву зависимостей и его публичным точкам входа.

Прочитайте код ошибки и цепочку загрузки

Текст Cannot find module встречается в разных ситуациях, поэтому ориентируйтесь на error.code. Node.js считает код стабильнее текста сообщения, который может меняться между версиями.

CommonJS добавляет массив requireStack. Первый путь в нём ведёт к модулю, который непосредственно выполнил неудачный require(), следующие показывают цепочку до точки запуска:

try {
  require('./config.cjs')
} catch (error) {
  console.error({
    code: error.code,
    requireStack: error.requireStack,
    message: error.message,
  })
}

Если app.cjs загружает report.cjs, а тот запрашивает отсутствующий ./config.cjs, проверять нужно путь относительно report.cjs. Первая строка вашего исходного кода в обычном стеке показывает место сбоя, а requireStack — кто кого загружал.

У ESM-ошибки полезны code, полный URL искомого модуля и фраза imported from в сообщении:

try {
  await import('./config.js')
} catch (error) {
  console.error({
    code: error.code,
    url: error.url,
    message: error.message,
  })
}

Поле url, если оно есть в вашей версии Node.js, показывает уже разрешённый file: URL. Декодируйте его как URL, а не склеивайте строку вручную. Для статического import тот же адрес обычно напечатан в сообщении об ошибке. Не строьте обработку ошибок по полному тексту message: для ветвления подходит code.

Перед любыми изменениями зафиксируйте среду запуска:

node --version
pwd
node -p "process.cwd()"

Эти команды ничего не устанавливают и не удаляют. Они помогают заметить, что терминал, редактор, контейнер и CI запускают один и тот же файл из разных каталогов или на разных версиях Node.js.

Сначала классифицируйте спецификатор

Спецификатор — строка, переданная в require(), import или import(). От её формы зависит весь поиск.

СпецификаторЧто ищет Node.jsЧастая причина
./config.js, ../lib/logger.jsФайл относительно импортирующего модуляНеверный уровень ../, регистр, расширение или отсутствующий результат сборки
/srv/app/config.js, file:///srv/app/config.jsФайл по абсолютному пути или URLВ окружении нет файла по этому адресу
kleur, @scope/loggerПакет, встроенный модуль или разрешённая точка входаЗависимость не установлена в нужном workspace
kleur/colors, @scope/logger/nodeПодпуть пакетаПодпуть переименован или закрыт полем exports
node:pathВстроенный модуль Node.jsОбычно опечатка в имени; установка из npm не нужна

Префикс ./ значим. require('config') не ищет соседний config.js: это запрос пакета config. Для локального файла нужен require('./config') в CommonJS или, как правило, import './config.js' в ESM.

Как CommonJS разрешает require()

require('./config') разрешается относительно каталога файла, который вызвал require(). Для относительного пути CommonJS сначала проверяет файл как указан, затем варианты .js, .json и .node, а также правила каталога. Поэтому этот импорт может работать без расширения:

// src/report.cjs
const config = require('./config')

Для имени без ./, ../ или начального / CommonJS ищет пакет в node_modules рядом с импортирующим файлом, затем поднимается по родительским каталогам. Это не поиск от произвольного каталога терминала.

Проверьте решение без выполнения целевого модуля:

console.log(require.resolve('./config'))
console.log(require.resolve.paths('kleur'))

require.resolve() возвращает точный файл, который выбрал бы загрузчик, либо выбрасывает тот же MODULE_NOT_FOUND. require.resolve.paths() печатает каталоги поиска для пакета. Разместите такую временную диагностику в том модуле, где падает require(): запуск node -p "require.resolve('kleur')" из корня репозитория проверяет контекст [eval], который может не совпасть с контекстом проблемного workspace.

MODULE_NOT_FOUND может относиться не к пакету, который вы загрузили первым, а к его вложенной зависимости. Сравните имя в сообщении с первым элементом requireStack; не переустанавливайте верхний пакет только потому, что он стоит в вашей строке require().

Как ESM разрешает import

ESM разрешает относительный спецификатор как URL относительно import.meta.url импортирующего файла. Для таких импортов Node.js требует полное имя файла с расширением; автоматического подбора .js и index.js нет.

// src/report.js
import config from './config.js'

Если написать import config from './config', существующий config.js не будет найден обычным ESM-загрузчиком. Укажите расширение, которое существует во время запуска. В TypeScript-проекте исходник может называться config.ts, но скомпилированный report.js обычно должен импортировать ./config.js: Node.js выполняет выходные файлы, а не догадки о пути исходника.

Текущую базу и результат разрешения можно увидеть внутри ESM-модуля:

console.log(import.meta.url)
console.log(import.meta.resolve('./config.js'))
console.log(import.meta.resolve('kleur'))

import.meta.resolve() возвращает абсолютный URL в контексте текущего модуля и учитывает правила пакетов и exports. Для относительного file: URL результат сам по себе не доказывает, что файл существует: современные версии Node.js могут вернуть URL несуществующего файла. Сопоставьте его с error.url и файловой системой.

Если вместо ERR_MODULE_NOT_FOUND возникает ReferenceError: require is not defined, файл уже запущен как ESM, но код использует CommonJS API. Это другой класс ошибки; выбор между преобразованием импорта и явным CommonJS разобран в статье «require is not defined».

Рабочая директория и каталог импортирующего файла — не одно и то же

Команда node ./dist/app.js сначала находит входной файл относительно process.cwd(). После загрузки модуля его относительные импорты разрешаются уже от самого файла: CommonJS использует каталог __dirname, ESM — URL import.meta.url.

// /srv/app/dist/report.js
import './config.js' // /srv/app/dist/config.js при любом cwd

Рабочая директория всё же влияет на другие операции: относительный аргумент точки входа, выбор проекта некоторыми командами пакетного менеджера и обычные обращения к файлам вроде readFileSync('./config.json'). Последний путь файловая система разрешает не как импорт, а от process.cwd().

Поэтому смена каталога иногда «чинит» запуск, но не доказывает, что импорт разрешается от cwd. Проверьте отдельно:

console.log({ cwd: process.cwd(), moduleUrl: import.meta.url })

Если эти пути различаются, определите, какая операция упала: загрузчик модуля или чтение данных приложением.

Относительный путь: файл, регистр и результат сборки

Для локального импорта пройдите короткую проверку от адреса из ошибки:

  1. Откройте импортирующий файл из requireStack, imported from или стека.
  2. Разрешите ./ и ../ от его каталога, а не от корня репозитория.
  3. Сверьте каждую букву и расширение с реальным именем файла.
  4. Если стек указывает в dist, build или другой выходной каталог, проверяйте именно его, а не только src.

Регистр важен. import './UserService.js' и файл userService.js могут случайно совпасть на файловой системе без учёта регистра и сломаться в Linux или CI. Исправьте имя в импорте либо переименуйте файл так, чтобы написание совпадало буквально; не добавляйте второй файл с почти тем же именем.

Сборка может оставить устаревший граф. Например, dist/report.js всё ещё импортирует удалённый ./config.js, хотя исходники уже используют ./settings.js. Другой вариант: package.json направляет main или exports в ./dist/index.js, но пакет workspace ещё не собран.

{
  "exports": {
    ".": "./dist/index.js"
  }
}

Сначала посмотрите фактический выходной каталог и настройки пакета. Затем запустите предусмотренную проектом сборку пакета-зависимости и повторите исходную команду. Не редактируйте сгенерированный dist вручную: следующая сборка затрёт правку.

Имя пакета: зависимость и workspace

Для bare-спецификатора проверьте, объявлен ли пакет у того приложения или workspace, который его импортирует. Наличие зависимости в корневом package.json или соседнем workspace не гарантирует доступ из потребителя.

Начните с команды своего пакетного менеджера:

npm ls kleur --depth=0
pnpm why kleur

npm ls показывает логическое дерево и помечает отсутствующие или некорректные зависимости. pnpm why показывает, почему пакет присутствует. Запускайте проверку в контексте нужного workspace или с фильтром проекта, иначе вы исследуете корень монорепозитория.

Проверьте также тип зависимости. Пакет, импортируемый при обычном запуске приложения, должен попадать в производственную установку. Если он объявлен только в devDependencies, среда с исключёнными dev-зависимостями закономерно его не найдёт.

Если зависимость уже объявлена в правильном манифесте, но установки нет, восстановите её по зафиксированному дереву:

pnpm install --frozen-lockfile
npm ci

Выберите одну команду по lock-файлу проекта; не запускайте обе. Если пакет ещё не объявлен, добавьте его штатной командой менеджера именно в workspace-потребитель. Не устанавливайте приложение глобально: глобальная установка обычно не участвует в локальном разрешении require('package') и import 'package'.

Не удаляйте node_modules, package-lock.json, pnpm-lock.yaml или другой lock-файл как первый шаг. Удаление lock-файла разрешает пакетному менеджеру выбрать другое дерево версий и может скрыть исходную причину новой несовместимостью. Полная установка по существующему lock-файлу уместна после того, как проверки показали отсутствующее или повреждённое дерево зависимостей.

Пакет найден, но подпуть закрыт через exports

Поле exports в package.json определяет публичные точки входа пакета. Если оно задано, внутренний файл нельзя импортировать только потому, что он физически лежит в node_modules.

{
  "name": "@scope/logger",
  "exports": {
    ".": "./dist/index.js",
    "./node": "./dist/node.js"
  }
}

Здесь разрешены @scope/logger и @scope/logger/node, но не @scope/logger/dist/private.js. Закрытый подпуть обычно даёт отдельный код ERR_PACKAGE_PATH_NOT_EXPORTED, а не ERR_MODULE_NOT_FOUND. Переустановка пакета это не исправит: используйте документированную публичную точку входа или совместимую версию API.

Если разрешённая запись exports ведёт в файл, которого нет в опубликованном пакете или локальной сборке, загрузчик уже может сообщить MODULE_NOT_FOUND либо ERR_MODULE_NOT_FOUND. Тогда проверьте установленную версию, содержимое пакета и результат его сборки. Не обходите контракт абсолютным путём внутрь node_modules: такой импорт зависит от внутренней раскладки и ломается при обновлении.

Что исправлять по результату диагностики

НаблюдениеИсправление
В ошибке относительный путь, итоговый адрес неверенИсправить ./ или ../ относительно импортирующего файла
ESM ищет ./config, а рядом лежит config.jsУказать полное ./config.js
Ошибка появляется только в Linux или CIСвести регистр импорта и имени файла к точному совпадению
npm ls или pnpm why не показывает пакет у workspace-потребителяОбъявить зависимость у потребителя и запустить установку из корня монорепозитория
Пакет объявлен, но npm ls или pnpm why не видит установкуВосстановить зависимости штатной командой по lock-файлу
Код равен ERR_PACKAGE_PATH_NOT_EXPORTEDПерейти на публичный подпуть из exports, не искать скрытый файл
Ошибка указывает на отсутствующий файл в distСобрать пакет, проверить main/exports и не править артефакт вручную
После нахождения файла возникает Cannot access … before initializationИскать порядок выполнения или цикл импортов, а не продолжать чинить разрешение

Циклический импорт и ошибка разрешения происходят на разных этапах. ERR_MODULE_NOT_FOUND означает, что загрузчик не построил ребро графа до модуля. При цикле оба файла уже найдены, но один может прочитать экспорт до его инициализации; это разобрано в статье «Cannot access before initialization».

Короткий порядок действий: прочитайте code, url или requireStack; классифицируйте спецификатор; разрешите относительный путь от импортирующего файла либо проверьте зависимость и exports; только затем меняйте импорт, устанавливайте зависимости или пересобирайте пакет. Такой порядок сохраняет воспроизводимое дерево пакетов и показывает настоящую причину ошибки.

Источники