[Перевод] Node.js-проекты, в которых лучше не использовать lock-файлы

Автор материала, перевод которого мы сегодня публикуем, говорит, что одна из проблем, с которыми приходится сталкиваться программистам, заключается в том, что у них их код работает, а у кого-то другого выдаёт ошибки. Эта проблема, возможно, одна из самых распространённых, возникает из-за того, что в системах создателя и пользователя программы установлены разные зависимости, которые использует программа. Для борьбы с этим явлением в менеджерах пакетов yarn и npm существуют так называемые lock-файлы. Они содержат сведения о точных версиях зависимостей. Механизм это полезный, но если некто занимается разработкой пакета, который планируется опубликовать в npm, lock-файлы ему лучше не использовать. Этот материал посвящён рассказу о том, почему это так.

_rwghp7lka8tkw1fdaqwyhfmqfg.png

Самое главное в двух словах


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 и посмотреть на список материалов в консоли.

83e1a1edc6fc37c9573fd816103f5f3e.png


Результаты выполнения команды 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 в своих проектах?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru