
Самое главное в двух словах
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 <dep> --verbose --dry-run # имитирует процесс установки пакета с выводом подробных сведений об этом процессе
Итоги
Многое из того, о чём мы тут говорили, полагается на особенности выполнения различных операций средствами npm. Речь идёт об упаковке, публикации, установке пакетов, о работе с зависимостями. А если учесть то, что npm постоянно развивается, можно сказать, что всё это в будущем может измениться. Кроме того, возможность практического применения приведённых здесь рекомендаций зависит от того, как разработчик пакета воспринимает проблему использования различных версий зависимостей в разных средах.
Надеемся, этот материал помог вам лучше разобраться в том, как устроена экосистема работы с зависимостями в npm. Если вы хотите ещё сильнее углубиться в этот вопрос — здесь можно почитать о различиях команд
npm ci
и npm install
. Тут можно узнать о том, что именно попадает в файлы package-lock.json
и npm-shrinkwrap.json
. Вот — станица документации npm, на которой можно узнать о том, какие файлы проектов включаются и не включаются в пакеты.Уважаемые читатели! Пользуетесь ли вы файлом npm-shrinkwrap.json в своих проектах?

Комментарии (14)
index0h
03.06.2019 14:12npm с 5-ой версии при обновлении затрагивает не только ваши зависимости, а и зависимости зависимостей (ЗЗ). В случае появления ошибки в ЗЗ+ исправить ее можно будет только вручную пропатчив код конкретной зависимости, зафиксировать ее в своем проекте не выйдет.
Это значит, что при сборке и заливке на production приложения вы не знаете, какие зависимости подтянутся в процессе.
Пример: https://github.com/raml-org/raml-js-parser-2/issues/775, так получилось, что этот пакет был зависимостью 3-го уровня, фактически мы получили не работающее приложение, без возможности зафиксировать рабочую версию ЗЗЗ.
К чему я это все? Локфайлы необходимы. Мне не ясны причины такого наплевательского отношения команды npm к этой проблеме. Так же не ясна причина появления shrinkwrap, если уже есть package-lock.
На данный момент рабочий вариант работы с зависимостями:
yarn install --ignore-optional --frozen-lockfile --non-interactive
serf
03.06.2019 14:35Глупости написаны, лок файл необходим. Конечно в публикацию он не попадает и это потому что консьюмер модуля:
— Должен получить зависимости от зависимости согласно semver версиям указанным в засисимости. Если бы лок файл зависимости учитывался, то использовались бы зависимости на момент публикации что было бы плохой идее в большинстве случаев.
— Сам ответственен за работоспособность своего модуля. То есть проверяется работоспособность всей системы/модуля и лочатся все зависимости включая транзитивные при любом обновлении модулей. Это отвественность потребителя/консьюмера модуля.
slonopotamus
03.06.2019 19:04Я правильно понимаю, что лок-файл — это способ узнать "с какими версиями зависимостей разработчик библиотеки её проверял" и эту информацию предлагается от пользователей библиотеки умышленно скрыть? Весело у вас в мире джаваскрипта.
ReklatsMasters
03.06.2019 20:12+2Нет, локфайлы тут это конкретные версии библиотек, с которыми работает приложение. А распространяется же приложение с package.json файлом, в котором указаны минимальные версии библиотек, с которыми приложение работает. Все версии подчиняются semver, это значит, как минимум на уровне minor версии библиотеки будут обратно совместимы. Разработчик самой либы это гарантирует. И это позволит подхватывать все совместимые обновления.
В других языках тоже не стоит таскать локфайлы вместе с либами, у раста как минимум видел объяснение. Так что это не чисто js практика.
slonopotamus
03.06.2019 20:18-2локфайлы тут это конкретные версии библиотек, с которыми работает приложение
Я не понимаю чем это отличается от того что я сказал.
justboris
03.06.2019 22:34+1Если разработчик библиотеки захочет привязаться к конкретным версиям своих зависимостей, он может легко это сделать, указав их в
package.json
, то есть написать"lodash": "4.17.11"
, вместо"lodash": "4.x.x"
. Но этого не делают по двум причинам:
- Фиксированные версии ухудшают дедупликацию зависимостей. Если одна библиотека запросит lodash строго 4.17.11, а другая строго 4.17.9, то они получат ровно эти версии, хотя они фактически ничем не отличаются, по правилам semver все 4.x релизы совместимы. В самом запущенном случае у вас может оказаться 43 копии lodash – вся линейка их релизов 4й версии.
- Если зафисировать версии своих зависимостей, пользователи не получат баг-фиксы в них. В результате пользователи завалят вас запросами на обновление зависимостей. Примеры: раз, два, три. Если у вас много зависимостей, то придется каждую неделю выпускать такой псевдо-релиз.
А нынешняя ситуация всех устраивает. Авторы пакетов указывают допустимый диапазон версий и не отвлекаются на рутинные обновления. Пользователи записывают конкретные версии в package-lock и получают воспроизводимую сборку.
slonopotamus
03.06.2019 22:50Моя библиотека совместима с lodash-4.x.x, но тестировал я её с 4.17.9 и поскольку 100% обратной совместимости не бывает, имейте это в виду.
«Совместима с» — это package.json, а «тестировал с» — это лок-файл. Никто вам как пользователю библиотеки не запрещает положить болт как на одно, так и на другое, и насильно запустить мою библиотеку с другой версией lodash. Но зачем вы хотите убрать информацию о том с какой версией проверялась работоспособность?justboris
03.06.2019 22:58+1А как эту информацию можно использовать? Допустим, эта информация есть, вы запускаете npm install, и дальше что происходит?
slonopotamus
03.06.2019 23:10Очевидно как: если у вас что-то не работает в районе моей библиотеки, то проверить не чинится ли всё магическим образом при использовании именно той версии зависимостей с которой она проверялась. Проблеме «что делать если по транзитивным зависимостям приехало две версии одного и того же» примерно столько же лет, сколько и в принципе пакетным менеджерам, и способы их разрешения давно придуманы:
1. Молча взять максимальную из требуемых
2. Потребовать пользователя выбрать
3. Поюзать _обе_ версии. Не во всех языках и окружениях такое возможно
4. Прочие безумия. Например, Maven молча выбирает ту версию, которая ближе по графу зависимостей. Здесь же вариант npm «а давайте забьём на информацию о желаемой библиотекой версии зависимостей». Нет информации — нет проблемы с выбором.staticlab
04.06.2019 01:35В JS как раз используется третий вариант, но он плох тем, что некоторые пакеты, которые спроектированы как синглтоны, станут в нескольких экземплярах, отчего приложение начнёт вести себя некорректно. Конечно, таких пакетов не очень много, но ситуация реальная.
Другой момент — множественные версии пакетов увеличивают объём приложения. Это критично для фронтенда. Соответственно разработчикам конечного приложения лучше самостоятельно выбрать подходящую версию пакета.
L2jLiga
04.06.2019 13:05А что мешает указать, версию
^4.17.9
в списке зависимостей? В таком случае вы явно указываете, что надо брать lodash >= 4.17.9, но < 5. Это отлично вписывается в semver и если авторы lodash не будут нарушать правила семантического версионирования, то ваша библиотека не сломается.
aol-nnov
признаюсь, читал по диагонали, но…
выглядит так, будто вы собираетесь их поименно перечислить, проекты эти…
Сравним с оригиналом: «When Not to Use Lock Files with Node.js» — тоже не фонтан, конечно (не понятно, о каких локфайлах автор), но это «Когда не стоит использовать локфайлы в node.js»
ru_vds Автор
Название, которое было в оригинале, «Когда не стоит использовать локфайлы в node.js» нам показалось слишком общим. В статье речь идёт о том, что эти файлы лучше не использовать при разработке того, что будет публиковаться в npm, то есть о Node.js-проектах, в которых лучше не использовать такие файлы. В результате и название — «Node.js-проекты, в которых лучше не использовать lock-файлы»