Русский
Русский
English
Статистика
Реклама

Dependency management

Установка и обновление зависимостей в JavaScript

14.07.2020 12:05:49 | Автор: admin

Установка и обновление зависимостей JavaScript


И снова привет! В прошлом посте мы начали рассматривать процесс управления зависимостями в JavaScript, разобрали основы: что такое npm-пакет; как выглядит манифест пакета; в каких полях прописываются зависимости; что такое дерево зависимостей; а также основы семантического версионирования (semver). Если вы пропустили предыдущий пост, то рекомендую начать с него.


Сегодня мы пойдем немного дальше и подробнее рассмотрим, как работает semver, как правильно прописывать диапазоны зависимостей, а также устанавливать и обновлять их.


npm shell autocomplete


В моих постах я буду часто упоминать npm и различные команды с его использованием. Чтобы сделать набор команд в терминале чуточку удобнее, предлагаю установить автодополнения в ваш shell.


Сделать это достаточно легко, достаточно выполнить следующие команды:


  • Для Bash:


    npm completion >> ~/.bashrcsource ~/.bashrc
    

  • Для Z shell:


    npm completion >> ~/.zshrcsource ~/.zshrc
    

    Это добавит в конфигурационный файл shell-а необходимый скрипт. После этого, если вы напишите npm smth, а затем нажмете [TAB], shell автоматически дополнит вашу команду или предложит варианты дополнения.



Инициализация проекта


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


Однако я предпочитаю вызывать команду следующим образом: npm init -y, а затем править манифест в редакторе. При таком вызове npm не будет задавать вопросов, а просто сгенерирует минимальный манифест со значениями по умолчанию.


Использование инициализаторов


Говоря про npm init нельзя не упомянуть про возможность использования специальных пакетов инициализаторов (npm initializers). Они облегчают создание новых проектов, генерируя необходимый boilerplate-код.


Используются пакеты так: npm init <initializer>, где <initializer> это название инициализатора (например, esm или react-app).


Инициализатор, по сути, это специальный npm-пакет с префиксом create-, который загружается из npm registry в момент вызова команды и выполняется. У каждого инициализатора могут быть свои аргументы, настройки и поведение.


Например, можно создать React-приложение с помощью инициализатора `create-react-app: npm init react-app -- my-react-app. Два минуса позволяют разделить аргументы CLI, которые передаются в команду npm init от тех, что передаются в сам инициализатор. Особенность инициализатора React, например, в том, что проект будет создан не в текущей директории, а в поддиректории с названием, которое вы укажете при вызове (в примере: my-react-app).


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


Добавление зависимостей в проект


Как мы выяснили ранее, зависимости прописываются в манифесте проекта в полях dependencies, devDependencies, peerDependencies или optionalDependencies. Чтобы добавить новую зависимость в проект, необходимо использовать команду: npm install <package-name>, или сокращенно: npm i <package-name>.


Пример: npm i lodash.


Эта команда установит lodash самой свежей стабильной версии и добавит эту зависимость в поле dependencies манифеста проекта.


Аналогично можно устанавливать сразу несколько зависимостей одновременно: npm i lodash express passport.


Команда install с флагами также позволяет выбрать, в какое поле будет добавлена зависимость:


  • -P, --save-prod
    установка в dependencies (работает по умолчанию)
  • -D, --save-dev
    установка в devDependencies
  • -O, --save-optional
    установка в optionalDependencies

Если вам нужно установить зависимость в peerDependencies, то придётся сделать это вручную т. к. npm не предусматривает для этого специальной команды. Как вариант, можно сначала установить зависимость в dependencies при помощи команды npm install, а потом перенести ее вручную в peerDependencies. В этом случае вам не придется угадывать свежую версию пакета (если вдруг ваш IDE не поддерживает автоматическую интеграцию с npm).


Конечно, зависимость можно добавить в список и самостоятельно в редакторе кода, но я всегда рекомендую использовать команды npm для добавления зависимостей, т. к. это дает более надежный результат: вы гарантированно получаете последнюю версию нужного пакета, а список зависимостей будет корректно отсортирован в лексикографическом порядке (что позволит избежать конфликтов при мерже в Git).


Команда npm install является многоцелевой командой, т. к. одновременно решает сразу несколько задач: установку уже прописанных зависимостей в проект и добавление новых зависимостей с их прописыванием в манифесте.

Добавление зависимости старой версии


Если по какой-то причине вы хотите добавить зависимость не самой свежей версии, то вы можете указать нужную версию через символ @:


  • npm i lodash@3.9.2
  • npm i lodash@3

Однако делать это рекомендуется только в самом крайнем случае. Об этом я расскажу подробнее чуть позже.


Установка зависимостей


Выше мы рассмотрели варианты добавления зависимостей в проект. Но как установить зависимости, которые уже прописаны в манифесте, если вы, к примеру, только сделали git clone?


Для этого достаточно просто выполнить команду npm install или npm i без аргументов. Npm прочитает содержимое манифеста, найдет указанные зависимости и установит их в проект.


Существует возможность установить только одну категорию зависимостей:


  • --only=prod[uction]
  • --only=dev[elopment]

Это может быть полезно, если вы хотите, к примеру, только запустить программу на Node.js, но не работать над ней.


Просмотр установленных зависимостей


Прежде чем мы поговорим про обновление зависимостей, было бы полезно научиться просматривать их. Для этой цели в npm также предусмотрена специальная команда: npm ls.


Синтаксис команды выглядит следующим образом: npm ls [<package-name>], где <package-name> опциональное название пакета, который вы хотите найти в дереве зависимостей.


Если вызвать команду без аргументов, то она выведет полное дерево зависимостей (не ослепните, дерево может быть огромным):



Крошечная порция результата выдачи команды npm ls в большом проекте.


Иногда бывает необходимо найти ту или иную транзитивную зависимость в дереве проекта, чтобы понять, какие пакеты тянут ее, для этого можно передать название пакета в команду: npm ls lodash.



Результат поиска пакета lodash в дереве зависимостей крупного проекта при помощи команды npm ls lodash.


Нужно заметить, что команда npm ls позволяет ограничивать глубину поиска при помощи опции depth. Например, следующая команда выведет только список прямых зависимостей проекта:


npm ls --depth=0

Как вы уже догадались, по умолчанию команда работает со всем деревом целиком.


Также вы можете использовать опции dev или prod для того, чтобы вывести только зависимости из полей dependencies или devDependencies:


  • npm ls --dev[elopment]
  • npm ls --prod[uction]

Использование же опции json позволяет получить дерево зависимостей в формате. пригодном для машинной обработки. В этом режиме npm выводит более подробную информацию о пакетах, а не только название и версию. Режим может быть также полезен для отладки и сравнения двух деревьев (например, в разных ветках проекта):


npm ls --json

Обновление зависимостей


Как мы уже рассмотрели в предыдущем посте, в npm для управления зависимостями используется система семантического версионирования (semver). Благодаря ей вы можете обновлять зависимости в своем проекте с предсказуемыми результатами: к примеру, если зависимость обновилась с версии 1.2.3 до версии 1.2.4 (patch update) или 1.3.0 (minor update), то это не сломает ваш проект, т. к. по правилам semver такие обновления не должны нарушать обратной совместимости. А если обновление производится с версии 1.2.3 до версии 2.0.0 или выше, то здесь вам следует обязательно заглянуть в журнал изменений (changelog) данного пакета, чтобы убедиться, что обновление ничего не сломает. Возможно, вам придется внести изменения в свой код, чтобы восстановить совместимость.


Несмотря на то, что semver гарантирует достаточно высокий уровень безопасности при обновлении, бывают случаи, когда разработчики какого-то пакета могут нарушать правила и вносить критические изменения в patch- или minor-обновлениях (в первую очередь это касается непопулярных пакетов, которыми управляют менее опытные разработчики).

В моей практике такое случается очень редко, но никто не застрахован от ошибок. Поэтому после любого обновления зависимостей в проекте необходимо обязательно проводить полный цикл сборочных и тестовых мероприятий, чтобы убедиться, что всё работает как нужно, особенно перед выкаткой в эксплуатацию (надеюсь, это происходит у вас автоматически).

Версии зависимостей


Давайте теперь рассмотрим, как именно прописываются версии зависимостей в манифесте проекта и какие механизмы дает нам semver для управления процессом обновления. Как я уже упомянул выше, при установке зависимости npm автоматически устанавливает самую свежую версию и включает наиболее свободный режим обновления для этой зависимости: разрешает как patch-, так и minor-обновления.


В package.json это выглядит следующим образом:


{  "dependencies": {    "lodash": "^4.17.17"  }}

Символ ^ (caret, hat или крышечка) указывается перед номером версии и имеет особое значение в semver. В данном случае это означает, что версия зависимости lodash должна обновляться до максимально доступной, но не выше 5.0.0, т. е. разрешает patch- и minor-обновления, но запрещает обновления, нарушающие обратную совместимость.


Помимо крышечки существует еще довольно много способов задать диапазон версии, все их можно посмотреть в описании пакета semver, который и реализует эту функциональность в npm. Однако в 99 % случаев зависимости стоит указывать именно через крышечку (как делает npm по умолчанию), т. к. это дает наибольшую гибкость при обновлении зависимостей, и в то же время защищает вас от критических изменений.


Фиксация версий зависимостей


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


{  "dependencies": {    "lodash": "4.17.17"  }}

Это гарантирует, что lodash будет установлен именно версии 4.17.17, ни больше, ни меньше.


Однако фиксация версий зависимостей имеет ряд существенных недостатков:


  • такие зависимости перестают обновляться, вы перестанете автоматически получать исправления багов, а самое главное улучшения безопасности,
  • если в дереве зависимостей вашего проекта этот пакет встречается более одного раза, то это приведет к тому, что он будет установлен несколько раз (несколько разных версий). Про дублирование зависимостей мы подробнее поговорим в будущем, но пока поверьте мне на слово, это может приводить к существенным сложностям.

По этим причинам использовать фиксацию зависимостей нужно в очень редких исключительных случаях и только в качестве временной меры.


Просмотр устаревших зависимостей


Новые версии пакетов выходят регулярно. Поэтому пакеты, установленные в вашем проекте, могут устаревать и их необходимо регулярно обновлять. Команда npm outdated позволяет вывести список устаревших пакетов.



Результат команды npm outdated в проекте, где установлены две устаревшие зависимости.


Эта команды выводит таблицу со следующими колонками:


Колонка Описание
Package Название пакета
Current Текущая установленная версия
Wanted Максимальная версия, которая удовлетворяет диапазону semver, прописанному в манифесте проекта
Latest Версия пакета, которую автор указал в качестве самой свежей (как правило, максимально доступная версия пакета)
Location Место расположения зависимости в дереве

По умолчанию команда npm outdated выводит список прямых зависимостей вашего пакета. Однако, если использовать аргумент depth с указанием глубины просмотра, то npm покажет устаревшие зависимости, в том числе и на заданной глубине дерева зависимости:


npm outdated --depth=10

Чтобы просмотреть полный список всех устаревших зависимостей, можно использовать следующую команду:


npm outdated --depth=9999

Обновление устаревших зависимостей


Обновить устаревшие зависимости в npm можно при помощи команды npm update.


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


Приведем пример: допустим, в вашем проекте указан диапазон версий пакета lodash: ^4.16.4. Вызов npm update приведет к тому, что пакет будет обновлен до версии 4.17.19, а манифест будет автоматически изменен, чтобы содержать диапазон ^4.17.19.


По аналогии с командой npm outdated, команда npm update также поддерживает аргумент depth и по умолчанию обновляет только прямые зависимости проекта, не трогая зависимости в глубине дерева. Поэтому, чтобы обновить все зависимости в проекте необходимо вызывать команду следующим образом:


npm update --depth=9999

Это гарантирует. что абсолютно все зависимости в дереве (включая транзитивные) будут обновлены до самых последних версий. Такую процедуру рекомендуется делать регулярно, чтобы в вашем проекте всегда был самый свежий код с последними исправлениями багов и проблем безопасности.


npm-check


В качестве альтернативы командам npm outdated и npm update хочу предложить интересный инструмент под названием npm-check.


Вы можете установить его при помощи следующей команды:


npm i -g npm-check

Инструмент поддерживает интерактивный режим, позволяя вам выбрать галочками те пакеты, которые вы хотите обновить. Кроме того, он позволяет обновлять пакеты не только в рамках ограничений semver, но и игнорируя их, перепрыгивая с одной мажорной версии зависимости на другую. Разумеется, делать это нужно осмысленно: тщательно изучая журнал изменений каждого мажорно-обновляемого пакета на наличие нарушений обратной совместимости. И никогда не забывайте тестировать ваш код после любых обновлений!



Результат вызова npm-check в проекте: доступно два обновления, одно мажорное и одно минорное.



Также результат вызова npm-check, но уже в интерактивном режиме: галочками можно выбрать зависимости, которые вы хотите обновить.


В качестве очень полезного бонуса: npm-check позволяет обнаружить, если какая-то из зависимостей не используется в проекте:



npm-check сообщает о том, что пакет lodash, возможно, не используется в проекте.


Рекомендую всегда держать этот незаменимый инструмент (или аналогичный) в своем арсенале.


Удаление зависимостей


Ну и наконец давайте рассмотрим, как мы можем удалять ранее добавленные зависимости в проект. Для этого существует команда npm uninstall, или сокращенно rm, r или un. Чтобы удалить один или несколько проектов, мы можем вызвать команду следующим образом:


npm rm lodash express

Будут удалены указанные пакеты как из файловой системы проекта, так и из его манифеста.


Workflow работы с npm-проектом


Мы уже рассмотрели несколько полезных команд и инструментов, давайте попробуем обобщить наши знания для того, чтобы выработать некий workflow для работы над JavaScript-проектами. Не переживайте, потом мы его доработаем, когда углубимся в другие важные вопросы.


  • Инициализация проекта:
    • npm init
      (интерактивно)
    • npm init -y
      (с последующим редактированием в IDE)
  • Добавление зависимостей:
    • npm install <dependency>
    • npm install <dependency-1> <dependency-2>
    • npm install -D <dev-dependency>
  • Проверка и обновление зависимостей:
    • npm outdated
      (просмотр прямых устаревших зависимостей)
    • npm outdated --depth=9999
      (просмотр всех устаревших зависимостей)
    • npm update
      (обновление прямых устаревших зависимостей с учетом semver)
    • npm update --depth=9999
      (обновление всех устаревших зависимостей с учетом semver)
    • npm-check
      (просмотр прямых устаревших зависимостей)
    • npm-check -u
      (интерактивное обновление прямых устаревших зависимостей)
  • Удаление зависимостей:
    • npm rm <dependency>

Продолжение следует


Мы подробнее рассмотрели процесс инициализации проекта, добавления, установки и обновления зависимостей. Рассмотрели, как semver работает на практике при обновлении зависимостей.


В следующей статье мы детально поговорим про такую важную концепцию, как lock-файлы npm (shrinkwrap), и научимся пользоваться этими механизмами для повышения стабильности при работе с зависимостями. Увидимся через неделю, а пока подписывайтесь и следите за обновлениями!

Подробнее..

Lock-файлы npm

30.07.2020 14:08:36 | Автор: admin

Lock-файлы npm


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


Когда манифеста мало


Как мы уже определили, npm берёт на входе манифест проекта (файл package.json) и описанные в нем зависимости, а на выходе мы получаем локально сгенерированную директорию node_modules, в которой содержатся все установленные зависимости.


Не будем сейчас касаться деталей структуры node_modules, однако замечу, что установленные пакеты не всегда имеют плоскую структуру, а могут в отдельных случаях формировать некую иерархию, когда один или несколько пакетов вложены в другой пакет. Как правило, это происходит, когда в дереве зависимостей присутствует один и тот же пакет, но разных версий. При этом сама структура в файловой системе не детерминирована на 100 %, и может зависеть, в том числе, от порядка установки зависимостей.


Также разные версии npm могут иметь различные алгоритмы установки зависимостей, и структура файлов будет отличаться.


Вы помните, что список зависимостей в манифесте проекта содержит диапазон версий semver, что позволяет обновлять зависимости? Получается, что установка зависимостей в разное время будет приводить к разным результатам, потому что содержимое npm registry постоянно меняется, регулярно выходят новые пакеты. Кроме того, поскольку мы имеем дело с деревом зависимостей, то транзитивные зависимости (зависимости ваших зависимостей) тоже будут меняться во времени.


А еще может возникнуть ситуация, когда содержимое пакета, лежащего в npm registry, было заменено без обновления версии. Это называется мутацией пакетов и запрещено в официальном npm registry, однако в частных реестрах вполне возможно. А что, если содержимое пакета было заменено злоумышленником, чтобы внести в код какие-то закладки?


Учитывая все эти факторы, становится очевидно, что структура данных в директории node_modules очень нестабильна и может меняться во времени, даже если ваш манифест при этом остается нетронутым.


Наивно было бы попытаться зафиксировать зависимости, прописывая строгие версии в манифесте проекта (вместо диапазонов semver): как мы рассмотрели выше, это не даст существенных результатов, потому что транзитивные зависимости всё равно будут обновляться. Да и другие факторы не перестанут влиять на установку зависимостей. Кроме того, если вы заморозите ваши прямые зависимости, то получится ситуация, когда более старые версии будут работать с более свежими версиями транзитивных зависимостей, и потенциально это повышает вероятность проблем с интеграцией.


А теперь представьте, что у нас в проекте есть конвейер CI/CD и специальный сервер, который собирает, тестирует и выкатывает приложения в разные среды выполнения. Как правило, такие решения привязываются к ID коммита в Git (или к Git-тегам), и на каждый коммит система генерирует готовый к выкатке артефакт (архив с готовыми для выполнения файлами). Таким образом, на вход конвейера поступает код из Git-репозитория, версионированный через ID коммита, а на выходе вы получаете протестированный и готовый к выкатке артефакт. В идеале, это должно работать как чистая функция (pure function): если вы пересоберёте коммит, созданный несколько месяцев назад, то должны получить на выходе тот же самый артефакт. Однако мы не можем хранить содержимое node_modules в Git, и получается, что после клонирования репозитория нам необходимо вызывать установку зависимостей из реестра npm. А, как мы уже выяснили, этот процесс довольно нестабилен и привязан к глобальному состоянию экосистемы (содержимому npm registry, версиям npm и т. д.). Получается, что npm вносит хаос в наш конвейер CI/CD и мы уже не можем получить одинаковую сборку по ID коммита.


Lock-файлы приходят на помощь


Чтобы предотвратить все описанные выше проблемы и сделать использование зависимостей гораздо более стабильным, npm (как и любой другой современный менеджер) предлагает специальный механизм заморозки зависимостей. Работает это автоматически и прямо из коробки: впервые вызывая команду npm install, npm не только устанавливает все зависимости и создаёт директорию node_modules, он также создает специальный файл package-lock.json. Этот файл называется lock-файлом и содержит в себе полную информацию обо всех установленных зависимостях, включая их точные версии, URL npm registry, из которого был скачан пакет, а также SHA-хэш самого архива с пакетом. Помимо прочего, lock-файл npm описывает еще и порядок установки зависимостей, и их вложенность в файловой системе.


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


Чтобы использовать преимущества lock-файла, его необходимо добавить в систему контроля версий. Таким образом, вы строго привяжете полное дерево зависимостей к коммитам в Git. Это будет гарантировать стабильное воспроизводство сборок в вашей системе CI/CD и позволит надежно путешествовать во времени.


Кроме того, каждый разработчик, который склонирует Git-репозиторий к себе на машину, получит точно такое же дерево зависимостей, как и у вас. Это устранит известную проблему из разряда странно, а у меня всё работает (it works on my machine).


it worked on my machine


Структура package-lock.json


Npm генерирует lock-файл полностью автоматически на основе данных из манифеста проекта, глобального состояния npm registry и алгоритма установки зависимостей npm. Однако содержимое файла вполне читаемо человеком и может быть использовано даже на этапе code review. Diff lock-файла покажет, какие зависимости в дереве были обновлены, какие были удалены, а какие добавлены. Наверное, нет смысла изучать изменения этого файла при каждом обновлении, но при обнаружении каких-то деградаций это может сильно помочь в поиске виновного пакета и сэкономить вам кучу времени. Но чтобы это работал эффективнее и размер изменений был минимальным, я рекомендую обновлять зависимости как можно чаще (гораздо проще выявить проблему, если у вас обновилось три пакета в дереве зависимостей, а не сотня).


Давайте теперь рассмотрим содержимое файла package-lock.json в тестовом проекте, где установлена только одна зависимость express.


Оригинальный файл содержит почти 400 строк, но чтобы пример получился достаточно читабельным и наглядным, я показал только часть дерева зависимостей.

package-lock.json


{  "name": "test",  "version": "1.0.0",  "lockfileVersion": 1,  "requires": true,  "dependencies": {    "express": {      "version": "4.17.1",      "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",      "integrity": "sha512-mHJ9O79RqluphRr7xlEMXTnYt4g==",      "requires": {        "debug": "2.6.9",        "send": "0.17.1"      }    },    "debug": {      "version": "2.6.9",      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",      "integrity": "sha512-bC7ElrdJaJnPbAPeAPVMNcKGsHMA==",      "requires": {        "ms": "2.0.0"      }    },    "ms": {      "version": "2.0.0",      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="    },    "send": {      "version": "0.17.1",      "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",      "integrity": "sha512-BsVKsiGcQMFwT8UcNuE3V4fT9sAg==",      "requires": {        "debug": "2.6.9",        "depd": "~1.1.2",        "destroy": "~1.0.4",        "encodeurl": "~1.0.2",        "escape-html": "~1.0.3",        "etag": "~1.8.1",        "fresh": "0.5.2",        "http-errors": "~1.7.2",        "mime": "1.6.0",        "ms": "2.1.1",        "on-finished": "~2.3.0",        "range-parser": "~1.2.1",        "statuses": "~1.5.0"      },      "dependencies": {        "ms": {          "version": "2.1.1",          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",          "integrity": "sha512-tgp+dl5cGk28utYYaD/kOWhYQvyg=="        }      }    }  }}

Итак, давайте разбираться. Начнем с основных корневых свойств:


  • name и version тут всё просто, это название и версия проекта из его манифеста на момент создания lock-файла.
  • lockfileVersion это версия формата, в котором представлен lock-файл. Она нужна для расширяемости, если разработчики npm в будущем придумают какой-то новый формат хранения.
  • dependencies полное плоское дерево зависимостей вашего проекта; объект, в котором ключ это название пакета, а значение дескриптор.

Дескриптор каждой отдельно взятой зависимости выглядит следующим образом:


  • version точная версия пакета на момент установки.
  • resolved URL пакета в реестре npm, откуда он был скачан.
  • integrity SHA-хэш пакета; проверочная сумма, которая позволяет убедиться, что в пакет не было внесено изменений как в процессе скачивания, так и на стороне хранилища (защита от мутации). Это очень важный элемент безопасности при работе с npm, который гарантирует, что злоумышленник не сможет как-то вмешаться в код пакета. При обнаружении несоответствия вызов npm install будет прерван с ошибкой.
  • requires объект, описывающий транзитивные зависимости (копируется из поля dependencies манифеста стороннего пакета). Ключ является названием пакета, а значение диапазоном версий semver.
  • dependencies аналогично полю dependencies, описанному выше. Позволяет рекурсивно описывать структуру вложенных пакетов, когда в дереве зависимостей содержится один и тот же пакет, но разных версий.
  • dev если true, то ээта зависимость является только зависимостью для разработки (необходимо для раздельной установки зависимостей).

Дублирующиеся зависимости


Обратите внимание, что в примере выше пакет express (наша прямая зависимость) зависит от пакета debug, а тот, в свою очередь, от ms@2.0.0. В то же время, пакет send также зависит от ms, но уже версии 2.1.1. Получается, что в директории node_modules пакет ms должен быть установлен два раза (разных версий), но, в силу сложившихся правил в Node.js, два пакета разных версий не могут быть установлены в корне. По этой причине одна версия устанавливается в корень (ms@2.0.0), а вторая в поддиректорию пакета send (ms@2.1.1). Это решение как раз и отражено в lock-файле. В том числе благодаря этому достигается стабильность директории node_modules.


Подробнее про дублирование зависимостей мы поговорим в будущих статьях.


Ручные правки


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


Конфликт в lock-файлах npm


Если несколько разработчиков трудятся в одной ветке и используют lock-файлы, то в какой-то момент может возникнуть merge-конфликт в Git. В этом случае достаточно просто устранить конфликты в файле манифеста (если они есть), а потом выполнить npm install: менеджер пакетов автоматически исправит конфликты в lock-файле.


Если вам не хочется править конфликты в lock-файлах вручную, то можете установить специальный merge-драйвер для Git, который умеет работать с npm. Это позволит исправлять конфликты в файле package-lock.json автоматически. Однако, если конфликт возникнет в манифесте, то вам всё равно будет необходимо исправить конфликт вручную, а потом вызвать npm install.


Установить merge-драйвер для npm можно следующим образом:


npx npm-merge-driver install -g

При возникновении конфликта при вызове команд Git в консоли будет выведено:


npm WARN conflict A git conflict was detected in package-lock.json. Attempting to auto-resolve.Auto-merging package-lock.json

Обновление lock-файла


Для работы с lock-файлами не требуется каких-то особых действий со стороны разработчика, npm автоматически обновляет lock-файл, когда в этом есть необходимость. Например, если вы вызываете команду npm install lodash, то помимо того, что npm добавит новую зависимость в манифест и установит её, он автоматически обновит lock-файл. Таким образом, npm всегда следит, чтобы lock-файл был в актуальном состоянии.


Однако если вы вносите изменения в манифест проекта вручную, например, добавляя новую зависимость, то возникает дрифт (рассинхронизация) между манифестом и lock-файлом. Менеджер пакетов достаточно умён, чтобы обнаружить этот дрифт: когда вы вызовете команду npm install, npm проанализирует актуальность lock-файла, и если он устарел, то менеджер пакетов автоматически обновит lock-файл, используя данные из манифеста.


Установка зависимостей в CI/CD


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


Чтобы этого не происходило, разработчики npm добавили специальную команду npm ci. В отличие от npm install, эта команда никогда не обновляет lock-файл. Более того, если в проекте отсутствует или устарел lock-файл, то npm ci вернет код ошибки и прервет выполнение конвейера, гарантируя, что ничего плохого не случится (принцип Fail-fast). Кроме того, npm ci полностью удаляет директорию node_modules перед установкой зависимостей, что гарантирует установку на чистый лист.


По этой причине никогда не следует использовать команду npm install в рамках конвейера CI/CD, обязательно используйте npm ci вместо нее. Идите и проверьте это прямо сейчас! (я подожду).


Разные типы проектов


Давайте теперь поговорим про особенности использования lock-файлов в проектах разных типов. Первое, о чём стоит сказать: файл package-lock.json не публикуется в npm registry. Это означает, что если вы публикуете свой пакет в реестр npm (библиотека), то содержимое вашего lock-файла не будет оказывать влияния на дерево зависимостей при установке вашего пакета в чей-то проект. В этом случае играет роль только содержимое вашего манифеста. Это и хорошо: если бы каждая библиотека замораживала свои зависимости, то дерево зависимостей в конечном проекте было бы ещё больше (куда уж больше?) и содержало огромное количество дублей. Адекватно управлять зависимостями стало бы невозможно.


Shrinkwrap


Однако в npm есть специальная команда [npm shrinkwrap](http://personeltest.ru/aways/docs.npmjs.com/cli/shrinkwrap). Она создает файл npm-shrinkwrap.json в корне проекта, который является тем же lock-файлом, только с другим именем и семантикой. Особенность его в том, что, в отличие от package-lock.json, он таки публикуется в реестр npm и оказывает непосредственное влияние на дерево зависимостей при установке вашего пакета. Фактически, он замораживает дерево зависимостей вашего пакета, даже если тот устанавливается в другой проект.


Как я сказал выше, использовать это решение для библиотек очень вредно, поэтому не стоит этого делать. Однако, оно может быть полезно, если вы разрабатываете программу на Node.js, которая должна выполняться на компьютере пользователя (например, аналог webpack, gulp, create-react-app и т. д.). Если программа устанавливается преимущественно глобально на компьютере пользователя (npm i -g), то использование shrinkwrap-файла гарантирует, что на машине пользователя программа будет иметь те же зависимости, что и на вашей машине. Так что, если у вас есть явные причины опасаться дрифта зависимостей в вашей программе, то вы можете воспользоваться npm shrinkwrap. В остальных случаях я не рекомендовал бы использовать эту команду.


Кстати, файл npm-shrinkwrap.json имеет приоритет над файлом package-lock.json. В проекте достаточно только одного файла.


Тестирование пакета-библиотеки


Важным этапом в работе над проектом библиотеки является его тестирование. Дело в том, что вы полностью не контролируете внешние условия, в которых будет вызываться ваша библиотека. Даже если вы зафиксируете прямые зависимости, вы не сможете зафиксировать транзитивные (разве что будете использовать shrinkwrap, но мы уже выяснили, что это плохая практика).


По этой причине вы не можете гарантировать среду, однако можете протестировать работу библиотеки со свежими версиями всех зависимостей, чтобы убедиться, что в новых и свежих проектах ничего не сломается. Однако, если вы будете использовать lock-файл в вашем проекте, то он будет постоянно оттягивать версии ваших зависимостей назад, не давая вам полноценно тестировать совместимость с самыми свежими версиями всех зависимостей (которые наверняка будут у ваших пользователей). Либо вам придется постоянно вызывать глубокий npm update перед каждым тестом.


Руководствуясь этим, некоторые разработчики советует вообще не использовать lock-файлы для проектов библиотек. Однако, я считаю это слишком радикальным советом. Дело в том, что помимо продуктовых runtime-зависимостей вашего пакета существуют еще и dev-зависимости. Если вы откажетесь от lock-файла, то ваши dev-зависимости выйдут из-под контроля, то есть вы потеряете некий островок стабильности.


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


С глаз долой


У многих разработчиков lock-файлы вызывают чувство раздражения, видимо, из-за непонимания их предназначения или особенностей их работы. Такие разработчики норовят добавить package-lock.json в .gitignore или вообще настроить npm, чтобы запретить генерирование lock-файлов. При этом они (часто сами того не понимая) жертвуют стабильностью и безопасностью своего приложения, а также теряют достаточно мощный инструмент отслеживания изменений зависимостей в проекте. Часто эти же люди начинают строго прописывать версии зависимостей в основном манифесте, чтобы как-то компенсировать эту нестабильность, не отдавая себе отчета в том, что это не решает проблему, а только создает иллюзию её решения. Я уже не говорю о том, что они используют инструмент не по назначению, теряя гибкость разработки, которая обеспечивается, в том числе, и механизмами семантического версионирования.


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


Обновляйтесь чаще!


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


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


С другой стороны, если вы не будете полгода обновлять зависимости, то потом сделать это будет очень сложно, потому что крупное обновление очень сложно разделить на маленькие части. Вы получите огромный ворох изменений разной степени тяжести, а в случае возникновения проблемы (а она, скорее всего, придет не одна) выявить точное место будет гораздо сложнее (diff здесь уже будет бесполезен в силу его гигантского размера).


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


Продолжение следует


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


Также мы немного коснулись структуры директории node_modules и явления дублирования пакетов. В следующей статье я хочу поговорить об этом подробнее. Следите за обновлениями!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru