[Перевод] Node.js-проекты, в которых лучше не использовать lock-файлы
Автор материала, перевод которого мы сегодня публикуем, говорит, что одна из проблем, с которыми приходится сталкиваться программистам, заключается в том, что у них их код работает, а у кого-то другого выдаёт ошибки. Эта проблема, возможно, одна из самых распространённых, возникает из-за того, что в системах создателя и пользователя программы установлены разные зависимости, которые использует программа. Для борьбы с этим явлением в менеджерах пакетов yarn и npm существуют так называемые lock-файлы. Они содержат сведения о точных версиях зависимостей. Механизм это полезный, но если некто занимается разработкой пакета, который планируется опубликовать в npm, lock-файлы ему лучше не использовать. Этот материал посвящён рассказу о том, почему это так.
Самое главное в двух словах
Lock-файлы крайне полезны при разработке Node.js-приложений вроде веб-серверов. Однако если речь идёт о создании библиотеки или инструмента командной строки с прицелом на публикацию в npm, то нужно знать о том, что lock-файлы в npm не публикуются. Это означает, что если при разработке применяются эти файлы, то у создателя npm-пакета, и у тех, кто этот пакет использует, будут задействованы разные версии зависимостей.
Что такое lock-файл?
Lock-файл описывает полное дерево зависимостей в том виде, который оно приобрело в ходе работы над проектом. В это описание входят и вложенные зависимости. В файле содержатся сведения о конкретных версиях используемых пакетов. В менеджере пакетов npm такие файлы называются package-lock.json
, в yarn — yarn.lock
. И в том и в другом менеджерах эти файлы находятся в той же папке, что и package.json
.
Вот как может выглядеть файл package-lock.json
.
{
"name": "lockfile-demo",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
}
}
}
Вот пример файла yarn.lock
. Он оформлен не так, как package-lock.json
, но содержит похожие данные.
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
dependencies:
color-convert "^1.9.0"
chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
Оба эти файла содержат некоторые крайне важные сведения о зависимостях:
- Точную версию каждой установленной зависимости.
- Сведения о зависимостях каждой зависимости.
- Данные о загруженном пакете, включая контрольную сумму, используемую для проверки целостности пакета.
Если все зависимости перечислены в lock-файле, зачем тогда сведения о них вносят ещё и в package.json
? Почему нужны два файла?
Сравнение package.json и lock-файлов
Цель поля dependencies
файла package.json
заключается в том, чтобы показать зависимости проекта, которые должны быть установлены для его правильной работы. Но сюда не входят сведения о зависимостях этих зависимостей. В сведения о зависимостях могут входить точные версии пакетов или некий диапазон версий, указанный в соответствии с правилами семантического версионирования. При использовании диапазона npm или yarn выбирают наиболее подходящую версию пакета.
Предположим, для установки зависимостей некоего проекта была выполнена команда npm install
. В процессе установки npm подобрал подходящие пакеты. Если выполнить эту команду ещё раз, через некоторое время, и если за это время вышли новые версии зависимостей, вполне может случиться так, что во второй раз будут загружены другие версии использованных в проекте пакетов. Например, если устанавливается зависимость, наподобие twilio
, с использованием команды npm install twilio
, то в разделе dependencies
файла package.json
может появиться такая запись:
{
"dependencies": {
"twilio": "^3.30.3"
}
}
Если посмотреть документацию npm по семантическому версионированию, то можно узнать, что значок ^
указывает на то, что подходящей является любая версия пакета, номер которой больше или равен 3.30.3 и меньше 4.0.0. В результате, если в проекте нет lock-файла и выйдет новая версия пакета, то команда npm install
или yarn install
автоматически установит эту новую версию пакета. Сведения в package.json
при этом обновляться не будут. При использовании lock-файлов всё выглядит иначе.
Если npm или yarn находят соответствующий lock-файл, они будут устанавливать пакеты, опираясь именно на этот файл, а не на package.json
. Это особенно полезно, например, при использовании систем непрерывной интеграции (Continuous Integration, CI) на платформах, на которых нужно обеспечить единообразную работу кода и тестов в окружении, характеристики которого известны заранее. В подобных случаях можно использовать особые команды или флаги при вызове соответствующих менеджеров пакетов:
npm ci # установит именно то, что перечислено в package-lock.json
yarn install --frozen-lock-file # установит то, что перечислено в yarn.lock, не обновляя этот файл
Это крайне полезно в том случае, если вы занимаетесь разработкой проекта наподобие веб-приложения или сервера, так как в CI-окружении нужно сымитировать поведение пользователя. В результате, если мы будем включать lock-файл в репозиторий проекта (например, созданный средствами git), мы можем быть уверенными в том, что каждый разработчик, каждый сервер, каждая система сборки кода и каждое CI-окружение используют одни и те же версии зависимостей.
Почему бы не сделать то же самое при публикации библиотек или других программных инструментов в реестре npm? Нам, прежде чем ответить на этот вопрос, нужно поговорить о том, как устроен процесс публикации пакетов.
Процесс публикации пакетов
Некоторые разработчики полагают, что то, что публикуется в npm, является в точности тем, что хранится в git-репозитории, или тем, во что превращается проект после завершения работы над ним. На самом деле это не так. В процессе публикации пакета npm выясняет то, какие файлы нужно опубликовать, обращаясь к ключу files
в файле package.json
и к файлу .npmignore
. Если же ничего из этого обнаружить не удаётся — используется файл .gitignore
. Кроме того, некоторые файлы публикуются всегда, а некоторые никогда не публикуются. Узнать о том, что это за файлы, можно здесь. Например, npm всегда игнорирует папку .git
.
После этого npm берёт все подходящие файлы и упаковывает их в файл tarball
, используя команду npm pack
. Если вам хочется взглянуть на то, что именно упаковывается в такой файл, можете выполнить в папке проекта команду npm pack --dry-run
и посмотреть на список материалов в консоли.
Результаты выполнения команды npm pack --dry-run
Затем полученный файл tarball
загружается в реестр npm. При запуске команды npm pack --dry-run
можно обратить внимание на то, что если в проекте есть файл package-lock.json
, он в tarball-файл не включается. Происходит это из-за того, что этот файл, в соответствии с правилами npm, всегда игнорируется.
В результате получается, что если кто-нибудь устанавливает чей-нибудь пакет, файл package-lock.json
в этом участвовать не будет. То, что имеется в этом файле, который есть у разработчика пакета, не будет учитываться при установке пакета кем-то другим.
Это может, по несчастливой случайности, привести к проблеме, о которой мы говорили в самом начале. В системе разработчика код работает нормально, а в других системах выдаёт ошибки. А дело тут в том, что разработчик проекта и те, кто проектом пользуются, применяют разные версии пакетов. Как это исправить?
Отказ от lock-файлов и использование технологии shrinkwrap
Для начала нужно предотвратить включение lock-файлов в репозиторий проекта. При использовании git для этого нужно включить в файл проекта .gitignore
следующее:
yarn.lock
package-lock.json
В документации к yarn говорится, что yarn.lock
нужно добавлять в репозиторий даже если речь идёт о разработке библиотеки, которую планируется публиковать. Но если вы хотите, чтобы вы и пользователи вашей библиотеки работали бы с одним и тем же кодом, я порекомендовал бы включить yarn.lock
в файл .gitignore
.
Отключить автоматическое создание файла package-lock.json
можно, добавив в папку проекта файл .npmrc
со следующим содержимым:
package-lock=false
При работе с yarn можно воспользоваться командой yarn install --no-lockfile
, которая позволяет отключить чтение файла yarn.lock
.
Однако то, что мы избавились от файла package-lock.json
, ещё не означает, что мы не можем зафиксировать сведения о зависимостях и о вложенных зависимостях. Есть ещё один файл, который называется npm-shrinkwrap.json
.
В целом, это такой же файл, как и package-lock.json
, он создаётся командой npm shrinkwrap
. Этот файл попадает в реестр npm при публикации пакета.
Для того чтобы автоматизировать эту операцию, команду npm shrinkwrap
можно добавить в раздел описания скриптов файла package.json
в виде prepack-скрипта. Того же эффекта можно добиться, используя хук git commit. В результате вы сможете быть уверены в том, что в вашем окружении разработки, в вашей CI-системе, и у пользователей вашего проекта используются одни и те же зависимости.
Стоит отметить, что этой методикой рекомендуется пользоваться ответственно. Создавая shrinkwrap-файлы, вы фиксируете конкретные версии зависимостей. С одной стороны это полезно для обеспечения стабильной работы проекта, с другой — это может помешать пользователям в установке критически важных патчей, что, в противном случае, делалось бы автоматически. На самом деле, npm настоятельно рекомендует не использовать shrinkwrap-файлы при разработке библиотек, ограничив их применение чем-то вроде CI-систем.
Выяснение сведений о пакетах и зависимостях
К сожалению, при всём богатстве сведений об управлении зависимостями в документации npm, в этих сведениях иногда сложно бывает сориентироваться. Если вы хотите узнать о том, что именно устанавливается при установке зависимостей или упаковывается перед отправкой пакета в npm, вы можете, с разными командами, воспользоваться флагом --dry-run
. Применение этого флага приводит к тому, что команда не оказывает воздействия на систему. Например, команда npm install --dry-run
не выполняет реальную установку зависимостей, а команда npm publish --dry-run
не запускает процесс публикации пакета.
Вот несколько подобных команд:
npm ci --dry-run # имитирует установку, основываясь на package-lock.json или на npm-shrinkwrap.json
npm pack --dry-run # выводит сведения обо всех файлах, которые были бы включены в пакет
npm install --verbose --dry-run # имитирует процесс установки пакета с выводом подробных сведений об этом процессе
Итоги
Многое из того, о чём мы тут говорили, полагается на особенности выполнения различных операций средствами npm. Речь идёт об упаковке, публикации, установке пакетов, о работе с зависимостями. А если учесть то, что npm постоянно развивается, можно сказать, что всё это в будущем может измениться. Кроме того, возможность практического применения приведённых здесь рекомендаций зависит от того, как разработчик пакета воспринимает проблему использования различных версий зависимостей в разных средах.
Надеемся, этот материал помог вам лучше разобраться в том, как устроена экосистема работы с зависимостями в npm. Если вы хотите ещё сильнее углубиться в этот вопрос — здесь можно почитать о различиях команд npm ci
и npm install
. Тут можно узнать о том, что именно попадает в файлы package-lock.json
и npm-shrinkwrap.json
. Вот — станица документации npm, на которой можно узнать о том, какие файлы проектов включаются и не включаются в пакеты.
Уважаемые читатели! Пользуетесь ли вы файлом npm-shrinkwrap.json в своих проектах?