12k
All articles

Команды npm на случай, когда что-то пошло не так

Используйте npm ls, npm explain, overrides и npm ci, чтобы найти неожиданные зависимости, исправить версии и избежать расхождений lockfile.

OpenReplay Team
OpenReplay Team
Команды npm на случай, когда что-то пошло не так

Когда в node_modules появляется пакет, который не запрашивал ни один из элементов package.json, или версия, которую вы не фиксировали, выполните npm ls <package>, чтобы понять, где он находится, и npm explain <package>, чтобы узнать, какая зависимость его подтянула, — прежде чем что-либо трогать.

У каждого разработчика был момент, когда он смотрел на номер версии в node_modules и думал: откуда ты вообще взялся? Рефлекс знакомый: в дереве что-то не так, значит rm -rf node_modules, переустановка и надежда на лучшее. Иногда проблема исчезает. Гораздо чаще она возвращается сразу же, потому что установщик пересобрал то же самое дерево из тех же входных данных, и теперь вы понятия не имеете, что изменилось.

Эта статья разбирает одно конкретное расследование: неожиданный пакет или версия, отслеженные до зависимости, которая их запросила, и затем исправленные на нужном уровне. Ошибки на этапе установки вроде ERESOLVE, EACCES и сбоев сборки нативных модулей разбираются в других материалах этого блога — в руководствах по устранению конфликтов ERESOLVE, ошибкам прав доступа EACCES и сбоям сборки node-gyp. Этот текст — для случаев, когда ошибка ещё нигде не возникла.

Ключевые выводы

  • npm ls <package> показывает каждое место, где пакет встречается в установленном дереве, и версию в каждой точке; npm explain <package> показывает цепочку зависимостей, которые его запросили.
  • Без --all команда npm ls выводит только ваши прямые зависимости; с --all она печатает полное дерево, а --depth=<n> задаёт явную границу между этими двумя крайностями.
  • npm why — это псевдоним для npm explain, поэтому одно и то же слово работает в npm, pnpm и yarn.
  • Поле overrides в package.json принудительно задаёт конкретную версию вложенной зависимости независимо от диапазона, запрошенного её родителем, — именно поэтому сначала стоит попробовать обновить родителя.
  • npm ci требует наличия package-lock.json, удаляет node_modules, устанавливает ровно то, что указано в lock-файле, и завершается с ошибкой, если lock-файл и package.json расходятся.

Почему удаление node_modules уничтожает улики?

Удаление node_modules с последующей переустановкой стирает единственную запись о том, как неожиданный пакет попал в проект. Установленное дерево и package-lock.json вместе кодируют каждое решение, принятое npm при разрешении зависимостей: какой родитель запросил какой диапазон, какая версия его удовлетворила и где результат оказался на диске.

Переустановка просто повторяет эти решения на основе package.json и lock-файла. Если входные данные не изменились, вы получите то же дерево и тот же сюрприз. Если изменились (флаг конфигурации, реестр, правка диапазона), переустановка перезапишет состояние, с которым вам нужно было сравнивать. В любом случае — сначала прочитайте дерево, потом пересобирайте. Читают его две команды: npm ls и npm explain.

npm ls: где находится пакет и какой он версии?

npm ls <package> фильтрует установленное дерево, оставляя пути, которые заканчиваются на указанном пакете, и печатает каждое местоположение в виде name@version с родителями с отступом выше. Можно фильтровать и по диапазону версий, например npm ls semver@^6, если вас интересуют только копии в определённой мажорной версии.

# Every copy of semver, with the path down to each
npm ls semver

# The complete tree, not just direct dependencies
npm ls --all

# Cap the walk at two levels
npm ls --all --depth=2

# Only what ships to production
npm ls --all --omit=dev

Настройка depth по умолчанию равна 0, если не передан --all, — в этом случае она становится Infinity. Это значение по умолчанию управляет поведением «голой» команды npm ls без аргумента-пакета. Как только вы указываете имя пакета, npm проходит путь до каждой его копии независимо от глубины — именно поэтому в самой документации пример npm ls promzard показывает вложенное совпадение без --all; если нужно ограничить обход, передавайте --depth=<n> явно.

То, что печатает npm, — это карта зависимостей одного пакета от другого, поэтому она не будет совпадать с фактическим расположением папок на диске: дедуплицированный пакет отображается под каждым родителем, которому он нужен, а не только в том единственном месте, где реально лежат его файлы. Вывод также помечает пакеты как лишние (extraneous — установлены, но не объявлены), отсутствующие или имеющие версию, не удовлетворяющую объявленному диапазону; отсутствующие пакеты выводятся с меткой UNMET DEPENDENCY. Добавьте --package-lock-only, и npm покажет дерево, которое породил бы lock-файл, игнорируя текущее содержимое node_modules.

Два замечания по написанию флагов. Актуальные фильтры — --omit=dev и --include=dev; --production — устаревший псевдоним для --omit=dev, а --dev — устаревший псевдоним для --include=dev, при этом --development вообще не является документированной опцией. Кроме того, npm ls завершается с ненулевым кодом, когда пакет отсутствует или имеет некорректную версию, а также когда указанный пакет ничему не соответствует, — это делает команду пригодной для проверки в CI; сами по себе лишние пакеты её не «роняют».

npm explain: кто запросил пакет?

npm explain <package> печатает для каждой установленной копии цепочку объявлений зависимостей, из-за которых она там оказалась, поднимаясь вверх до корневого проекта. Там, где npm ls отвечает на вопрос «где», npm explain отвечает «кто».

npm explain semver
npm why semver              # identical
npm explain semver --json   # for jq

Каждый блок вывода начинается с разрешённого name@version и его пути в node_modules, затем с отступом идёт по одной строке на каждый переход: диапазон, объявленный родителем, собственная версия родителя и путь к нему, — и завершается строкой с именем корневого проекта. Читайте снизу вверх, чтобы проследить путь от вашего package.json до копии, которую вы не ожидали увидеть. Для дублирующихся пакетов выводится по блоку на каждую копию, поэтому конфликтующие диапазоны видны рядом. Можно также передать папку, например npm explain node_modules/foo/node_modules/semver, чтобы объяснить ровно одну вложенную копию.

В сводке по npm explain why указан как псевдоним, и остальные крупные пакетные менеджеры используют тот же глагол.

Пакетный менеджерКомандаФормат вывода
npmnpm explain <pkg> или npm why <pkg>По блоку на каждую установленную копию, цепочка до корня
pnpmpnpm why <pkg>Перевёрнутое дерево, с интересующим пакетом наверху
Yarnyarn why <pkg>Причины по каждому workspace, принимает pkg@range

Обновить родителя или добавить override?

Как только npm explain назвал родителя, запросившего неподходящий диапазон, первое решение — перевести этого родителя на релиз, который запрашивает диапазон получше. Выполните npm outdated <parent>, чтобы проверить, существует ли более новая версия, или прочитайте package.json родителя из реестра командой npm view <parent>@latest dependencies. Если более новый родитель объявляет приемлемый диапазон, обновите его и позвольте npm заново разрешить дочернюю зависимость.

И только если ни один релиз родителя не исправляет диапазон, стоит браться за overrides:

{
  "overrides": {
    "semver": "^7.5.4"
  }
}

Override заменяет версию вложенной зависимости независимо от диапазона, объявленного родителем, так что родитель теперь может работать с версией, с которой его никогда не тестировали. Это и есть компромисс, и именно поэтому overrides — второй шаг, а не первый. Несколько правил из документации: overrides учитываются только в корневом package.json; пакет, от которого вы зависите напрямую, можно переопределить только спецификацией, идентичной его собственной, иначе npm выбросит EOVERRIDE — для этого случая существует форма ссылки $name; значениями могут быть точная версия, диапазон, dist-tag либо спецификатор npm:, file: или Git. Помещайте override под именем родителя, если хотите применить его к одной ветке дерева, а не ко всему сразу.

npm config list: настройки, о которых вы забыли

npm config list печатает настройки, заданные вами, вашим окружением или файлом .npmrc; npm config list -l дополнительно печатает значения по умолчанию, а --json возвращает те же данные в формате JSON. Когда дерево разрешается так, что один только package.json этого не объясняет, причина часто кроется в конфигурационном значении, которое никто не помнит, чтобы записывал.

npm config list
npm config list -l

Вывод сгруппирован по источникам (командная строка, окружение, проектный .npmrc, пользовательский .npmrc, глобальный), что подсказывает, какой файл править. Два ключа заслуживают внимания в первую очередь. Нестандартный registry означает, что версии разрешались по зеркалу или приватному реестру, содержимое которого может отставать от публичного. Сохранённая настройка legacy-peer-deps заставляет npm строить дерево вообще без учёта peerDependencies — так, как он вёл себя вплоть до шестой версии, — из-за чего можно получить сочетания, которые текущий резолвер отверг бы. Есть и побочный эффект: как только lock-файл собран с этим флагом, каждый последующий npm ci тоже требует его, иначе установка ломается. Одна забытая строчка в проектном .npmrc может объяснить и странное локальное дерево, и красный прогон CI.

npm ci против npm install: что происходит, когда lock-файл расходится?

Когда lock-файл удовлетворяет package.json, npm install использует точные версии из lock-файла; когда нет — npm install заново разрешает зависимости и обновляет package-lock.json. npm ci вместо этого выдаёт ошибку.

Поведениеnpm installnpm ci
Требует package-lock.jsonНетДа
Lock-файл и package.json расходятсяРазрешает заново, перезаписывает lock-файлЗавершается с ошибкой
Существующий node_modulesПереиспользуетсяСначала удаляется
Пишет в package.json или lock-файлДаНикогда
Добавление одного пакетаДаНет

Документация npm install прямо описывает иерархию приоритетов: источником истины являются диапазоны в package.json, а lock-файл сохраняет свои зафиксированные версии лишь до тех пор, пока они вписываются в эти диапазоны. Это ровно то поведение, которое вам не нужно в CI, где незаметно перезаписанный lock-файл скрывает то самое расхождение, которое вы пытаетесь отловить. npm ci отказывается согласовывать два файла и падает громко — поэтому используйте его в пайплайнах, а npm install оставьте для машины, где вы намеренно меняете зависимости.

Заключение

Неожиданный пакет в дереве — это решение резолвера со своим бумажным следом, и npm ls вместе с npm explain читают этот след, не нарушая его. Проследите цепочку до родителя, объявившего диапазон, обновите родителя, если существует релиз получше, применяйте override только когда его нет, а затем проверьте npm config list на предмет настроек, которые изначально исказили разрешение зависимостей. Запускайте npm ci в CI, чтобы следующее расхождение ломало сборку, а не тихо переписывало lock-файл.

Часто задаваемые вопросы

Что означает пометка «deduped» рядом с пакетом в выводе npm ls?

Метка «deduped» означает, что npm ls показывает пакет в этой точке логического графа зависимостей, но отдельной копии там нет: единственная установленная копия выше по node_modules удовлетворяет диапазон этого родителя. Это не ошибка. Поскольку npm ls печатает логическое дерево, один и тот же пакет появляется под каждым родителем, которому он нужен, и только строка без метки соответствует физической папке.

Как удалить пакеты, которые npm ls помечает как extraneous?

Выполните npm prune. Команда удаляет из node_modules всё, от чего ничто больше не зависит; укажите один или несколько пакетов, чтобы ограничиться только ими. Добавьте --omit=dev или установите NODE_ENV в production — и ваши devDependencies тоже будут удалены. Используйте --dry-run, чтобы сначала увидеть план, и --json, чтобы получить изменения в формате JSON. Установки и сами по себе вычищают лишние пакеты, поэтому эта команда нужна в основном после сбоя или незавершённой установки.

Исправляет ли npm dedupe дубликаты версий, которые показывает npm ls, или нужны overrides?

npm dedupe объединяет только те копии, которые уже допускаются объявленными диапазонами. Команда обходит дерево и поднимает каждую зависимость настолько высоко, насколько может, так что родители с пересекающимися диапазонами начинают использовать одну общую копию; при этом ничего нового из реестра не скачивается. Если два родителя запрашивают диапазоны без общей версии, обе копии остаются, и решением будет обновление родителя или добавление записи в overrides. npm find-dupes выполняет тот же проход в режиме dry run, так что результат можно увидеть заранее.

Как посмотреть список глобально установленных npm-пакетов?

Выполните npm ls -g. Флаг --global направляет npm ls на глобальный префикс, перечисляя установленные там пакеты вместо пакетов текущего проекта. Действуют те же правила глубины: без --all выводятся только глобальные пакеты верхнего уровня, а npm ls -g --all разворачивает каждый из них в полное дерево зависимостей. Добавьте явное значение --depth, чтобы ограничить обход, или --json для машиночитаемого вывода.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.