Поговорим об обновлении зависимостей. Для обновления всех зависимостей нужно выполнить команду npm update. Чтобы выполнить обновление конкретной зависимости — npm update name, где name — имя библиотеки. А вот то, как будет происходить обновление, зависит от того, что написано в package.json.

Рассмотрим все доступные варианты:

dependencies {
  'package1': "*",
  'package2': "1.3.5",
  'package3': "~2.3.4",
  'package4': "^2.3.4",
}

* означает, что можно ставить любую версию библиотеки. После выполнения команды обновления в папке node_modules окажется последняя доступная версия package1.

1.3.5 - конкретный номер. Если версия библиотеки жестко зафиксирована, никакая команда не сможет обновить ее.

Самый интересный сценарий происходит в случае использования тильды (~). Напомню, что в семантическом версионировании считается, что patch (последняя цифра в версии) изменяется только в случае исправления ошибок, а значит обратная совместимость не должна теряться. На практике это не всегда так, код может работать с учетом ошибок в зависимостях. Как правило, в проектах десятки, а то и сотни зависимостей, причем обновляются они очень часто. С одной стороны, можно всегда писать *, но тогда мажорные обновления библиотек могут сломать систему. С другой стороны, можно зафиксировать все версии, но тогда обновлять все придется вручную, а значит, по закону Мерфи, никто не будет этого делать. Поэтому появился третий вариант. Добавление тильды приводит к тому, что в автоматическом режиме обновляются только патчи. Предположим, что после добавления зависимости в проект, версия была установлена в ~2.10.3. Если после нее в npm репозитории появилась 2.10.5, то она будет установлена командой обновления. То же самое произойдет, если потом будет выпущена версия 2.10.15. Но если создатель библиотеки опубликует изменения в мажорной или минорной версии, например 2.11.5 или 3.0.0, то npm их проигнорирует.

Примерно то же самое происходит и при использовании ^ ("крышки"), только в отличие от тильды, она фиксирует мажорную версию, а минорная обновляется наравне с патчем.

Lock

На предыдущем шаге каждая новая установка зависимостей сначала создавала, а потом обновляла файл package-lock.json.

Попытаемся разобраться, зачем он нужен. Как мы помним, в package.json указываются зависимости, и мы научились устанавливать и обновлять их. У каждой зависимости могут быть свои собственные зависимости, которые тоже обновляются — и так до бесконечности. Зависимости зависимостей называются транзитивными и с ними не все так просто. Настолько не просто, что существует понятие "dependency hell" (ад зависимостей).

Transitive
dependencies

Проблема заключается в том, что мы никак не фиксируем версии транзитивных зависимостей. Предположим, что в нашем пакете есть зависимость A с зафиксированной версией 1.3.2, у которой в зависимостях стоит пакет B с версией *. В такой ситуации в отсутствие лок файла npm install установит указанную версию зависимости A и последнюю доступную версию пакета B. Такое поведение не детерминировано. Если создатель пакета B обновит его так, что нарушится обратная совместимость, наш проект просто сломается, так как перестанет работать A. Если мы полгода не заходим в проект, а затем зайдем и поставим зависимости заново, удалив папку node_modules или выполнив новое клонирование, то почти наверняка ничего не заработает. Пакеты обновляются часто, и какой-нибудь из них обязательно изменит мажорную версию за столь длинный срок.

Очевидный, но не рабочий выход из данной ситуации — вручную отслеживать зависимости всех зависимостей и явно прописывать их версии в package.json. Такой способ сработает, но даже в проекте на js, который содержит всего 5 зависимостей, транзитивных зависимостей будут сотни! Вдумайтесь в эту цифру. Я уже не говорю про то, что пакеты обновляются и меняются. Такую ситуацию невозможно контролировать, и зависимости просто перестанут обновляться.

Другой выход — требовать, чтобы создатели всех библиотек всегда жестко указывали версии. Из-за человеческого фактора этот вариант обречен на провал. Автоматизация такого процесса привела бы к полному параличу системы пакетов и библиотек, а значит и разработки программ.

И тут на сцену выходит lock-файл. Он представляет собой автоматизированное решение первого способа. Его содержимое выглядит примерно так:

{
  "name": "hexlet-co",
  "version": "0.1.4",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "JSONStream": {
      "version": "1.3.1",
      "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
      "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
      "requires": {
        "jsonparse": "1.3.1",
        "through": "2.3.8"
      }
    },
    "abab": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
      "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=",
      "dev": true
    },
    "acorn": {
      "version": "4.0.13",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
      "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c="
    },
    ...
  }
}

Первый запуск установки зависимостей формирует этот файл. Туда записываются все установленные зависимости, в том числе транзитивные, и их версии. При дальнейших запусках npm install всегда ставится то, что указано в lock файле, даже если стереть папку node_modules, а в npm-хранилище добавятся новые версии пакетов. Повторный запуск через любой промежуток времени приведет к тому же результату. Теперь всегда можно быть уверенным, что если заработало сейчас, то заработает потом и не только у нас.

Наличие lock файла никак не влияет на поведение команды update для прямых зависимостей. Если пакет, указанный в package.json, обновился и может быть обновлен до указанной версии, то загрузится новая версия, а файл lock обновится автоматически. После этого нужно не забыть залить его в git-репозиторий.

На самом деле, lock-файл ведет себя сложнее, но для понимания схемы его работы достаточно описанного выше. Если хотите разобраться в теме от и до — изучите официальную документацию.

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

Схема работы

Фиксированная версия

npm fixed
version

Последняя версия

npm caret
version

Самостоятельная работа

  • Клонируйте репозиторий nodejs-package, а затем выполните внутри него команду npm update. Изучите вывод команды git diff.
  • Попробуйте изменить версию любого пакета в package.json, указав точную версию без использования ^, и выполните npm install. Доступные версии пакета можно посмотреть командой: npm view <packagename> versions, например: npm view eslint versions.

Дополнительные материалы

  1. Описание лока пакетов
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →