Разбираемся с lock-файлами в NPM 5

Перевод статьи Jiří Pospíšil: Understanding lock files in NPM 5. Опубликовано с разрешения автора.

Следующая мажорная версия NPM приносит ряд улучшений по сравнению с предыдущими версиями с точки зрения скорости, безопасности и множества других отличных вещей. Однако самым необычным с точки зрения пользователя является новый lock-файл. Фактически, теперь у нас несколько lock-файлов: «старый» npm-shrinkwrap.json и новый package-lock.json. Подробнее об этом чуть ниже, а пока небольшая вводная для непосвящённых. Файл package.json описывает зависимости верхнего уровня от других пакетов с помощью semver. Каждый пакет может, в свою очередь, зависеть от других пакетов и так далее, тому подобное. Lock-файл — это моментальный снимок всего дерева зависимостей, включающий все пакеты и их установленные версии.

В отличие от предыдущей версии, lock-файл теперь включает в себя поле целостности (integrity), использующее Subresource Integrity (SRI) для проверки того, что установливаемый пакет не был подменён или иным образом недействителен. В настоящее время он поддерживает SHA-1 для пакетов, опубликованных с помощью NPM предыдущей версии, и SHA-512 для текущей.

В файле больше нет поля from, которое вместе с иногда неконсистентной version, как известно, было источником боли при просмотре изменений файлов во время процесса код-ревью. Теперь изменения в пулл-реквестах должны быть намного чище.

Lock-файл теперь также содержит версию формата, указанную в поле lockfileVersion, установленную в значение 1. Это значит, что при будущих обновлениях формата не придётся угадывать, какую конкретную версию использует lock-файл. Предыдущий формат по-прежнему поддерживается и распознается как версия 0.

{
"name": "package-name",
"version": "1.0.0",
"lockfileVersion": 1,
"dependencies": {
"cacache": {
"version": "9.2.6",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-9.2.6.tgz",
"integrity": "sha512-YK0Z5Np5t755edPL6gfdCeGxtU0rcW/DBhYhYVDckT+7AFkCCtedf2zru5NRbBLFk6e7Agi/RaqTOAfiaipUfg=="
},
"duplexify": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz",
"integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=",
"dependencies": {
"end-of-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz",
"integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4="
},

Возможно, вы заметили, что поле resolved все еще присутствует в файле и указывает на определенный URI. Обратите внимание, однако, что NPM теперь может определить (на основе параметров в .npmrc), что система настроена на использование другого реестра, и если это так, он будет прозрачно использовать его вместо указанного. Это хорошо работает с полем целостности, потому что теперь, пока пакет соответствует сигнатуре, не имеет значения, откуда он пришел.

Еще одна вещь, о которой стоит упомянуть: lock-файл точно описывает физическое дерево каталогов в директории node_modules. Преимущество этого заключается в том, что даже если разные разработчики используют разные версии NPM, они все равно должны иметь не только одни и те же версии зависимостей, но и то же самое дерево каталогов. Этим NPM 5 отличается от других пакетных менеджеров, таких как Yarn. Yarn описывает только зависимости между отдельными пакетами в плоском формате и опирается на свою текущую реализацию для создания структуры каталогов. Это означает, что при изменении внутреннего алгоритма структура также изменится. Если вы хотите узнать больше о различиях между Yarn и NPM 5, когда дело доходит до lock-файла, почитайте про детерминизм Yarn.

Два lock-файла

Я уже упоминал, что сейчас на самом деле имеется больше одного lock-файла. Теперь NPM автоматически генерирует lock-файл с именем package-lock.json всякий раз, когда устанавливается новая зависимость или файл еще не существует. Как уже упоминалось в начале, lock-файл представляет собой моментальный слепок текущего дерева зависимостей и позволяет воспроизводить сборки между машинами разработчиков. Поэтому рекомендуется добавить его в свою систему контроля версий.

Возможно, вы думаете, что то же самое уже может быть достигнуто с помощью npm shrinkwrap и её npm-shrinkwrap.json. И вы правы. Причиной создания нового файла является попытка лучшего донесения мысли о том, что NPM действительно поддерживает блокировку зависимостей, что, видимо, было проблемой в прошлом.

Однако есть несколько отличий. Во-первых, NPM гарантирует, что package-lock.json никогда не будет опубликован. Даже если вы явно добавите его в свойство файлов пакета, оно не будет частью опубликованного пакета. Это не относится к файлу npm-shrinkwrap.json, который может быть частью опубликованного пакета, и NPM будет использовать его даже для вложенных зависимостей. Просто попробуйте сами, запустив npm pack и посмотрев, что находится внутри созданного архива.

Также вам может быть интересно узнать, что происходит, когда вы запускаете npm shrinkwrap в каталоге, который уже содержит package-lock.json. Ответ довольно прост: NPM просто переименует package-lock.json в npm-shrinkwrap.json. Это возможно, потому что формат файлов совпадает.

Самое любопытные также спросят, что происходит, когда присутствуют оба файла. В этом случае NPM полностью игнорирует package-lock.json и просто использует npm-shrinkwrap.json. Однако такая ситуация не должна возникать при манипулировании файлами средствами NPM.

Обобщая:

  • NPM автоматически создаст package-lock.json при установке пакетов. Если уже присутствует npm-shrinkwrap.json, то будет использован он (и при необходимости обновлён).
  • Новый package-lock.json никогда не публикуется и должен быть добавлен в вашу систему контроля версий.
  • Запуск npm shrinkwrap в случае наличия package-lock.json просто переименует его в npm-shrinkwrap.json.
  • Когда оба файла присутствуют по какой-либо причине, package-lock.json будет проигнорирован.

Это здорово, но как выбрать: использовать новый lock-файл вместо старого доброго shrinkwrap или наоборот? Обычно это зависит от типа пакета, над которым вы работаете.

При разработке библиотеки

Если вы работаете с библиотекой (то есть пакетом, от которого будут зависеть другие пакеты), вы должны использовать новый lock-файл. Альтернативой является использование shrinkwrap, но убедитесь, что он никогда не будет опубликован вместе с пакетом (новый lock-файл защищён от публикации автоматически). Почему бы не опубликовать shrinkwrap? Это связано с тем, что NPM обрабатывает shrinkwrap-файлы, которые он находит в пакетах, и поскольку shrinkwrap всегда указывает на конкретные версию отдельных пакетов, вы не сможете воспользоваться тем фактом, что NPM может использовать один и тот же пакет для разрешения зависимостей нескольких пакетов, если диапазон semver позволяет это. Другими словами, не заставляя NPM устанавливать определенные версии, вы позволяете NPM эффективнее повторно использовать пакеты, что в результате приводит к более компактному дереву зависимостей и упрощает сборку.

Однако есть одно предостережение. Когда вы работаете в своей библиотеке, вы получаете одинаковые зависимости каждый раз, потому что в репозитории присутствует либо package-lock.json, либо npm-shrinkwrap.json. То же самое относится к вашему серверу непрерывной интеграции, на котором вы проверяете ваш код. Теперь представьте, что в вашем package.json указана зависимость от некоторого пакета как ^1.0.0. Каждый раз устанавливается версия, указанная в lock-файле и всё прекрасно работает. А что происходит, если публикуется новая версия зависимости, случайно нарушившая semver, и ваш пакет ломается из-за этого?

К сожалению, вы не сможете заметить этого до тех пор, пока не появится отчет об ошибке. Без каких-либо lock-файлов в репозитории ваша сборка завершится неудачно, по крайней мере, в CI, поскольку она всегда будет устанавливать последние версии зависимостей и, таким образом, запускать тесты с новой сломанной версией (при условии, что сборка выполняется периодически, а не только для PR). Однако при наличии lock-файла CI-сервер всегда будет устанавливать рабочую заблокированную версию.

Есть несколько вариантов решения этой проблемы. Во-первых, вы можете пожертвовать точной воспроизводимостью и не добавлять lock-файл в свою систему контроля версий. Во-вторых, вы можете создать отдельную конфигурацию сборки, которая будет запускать npm update перед запуском тестов. В-третьих, вы просто удаляете lock-файл перед запуском тестов. Как на самом деле поступать с сломанной зависимостью, когда она была обнаружена, это отдельная тема, главным образом потому, что semver, реализованный NPM, не имеет концепции указания разрешения широкого диапазона и также не имеет черного списка конкретных версий.

Это, конечно, ставит вопрос о том, стоит ли добавлять lock-файл в систему управления версиями при работе с библиотеками. Однако следует иметь в виду, что lock-файл содержит не только runtime-зависимости, но и зависимости для разработки (dev dependencies). В этом смысле работа над библиотекой аналогична работе над приложением (смотрим следующий раздел).

При разработке приложения

Хорошо, что насчёт пакетов, используемых конечными пользователями для запуска в консоли или в комплекте исполняемых файлов? В этом случае пакет — это конечный результат, приложение, и вы хотите, чтобы конечные пользователи всегда получали точные зависимости, которые вы имели при публикации. Здесь вы захотите использовать shrinkwrap и не забудьте также опубликовать его с пакетом, чтобы NPM использовал его во время установки. Помните, что вы всегда можете проверить, как выглядит пакет, если он будет опубликован с использованием npm pack.

Обратите внимание, что указание на определенные версии зависимостей в package.json - не лучшая идея, потому что вы хотите, чтобы конечные пользователи получили то же самое дерево зависимостей, включая все подзависимости. Конкретная версия в package.json гарантирует версию только на верхнем уровне.


Что насчет других типов приложений, например, проектов, которые вы запускаете из своего репозитория? В этом случае это не имеет большого значения. Имеют значение только установленные правильные зависимости, и оба lock-файла могут это обеспечить. Выбор за вами.


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на GitHub

Show your support

Clapping shows how much you appreciated Andrey Melikhov’s story.