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

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


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


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


    npm shell autocomplete


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


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


    • Для Bash:


      npm completion >> ~/.bashrc
      source ~/.bashrc

    • Для Z shell:


      npm completion >> ~/.zshrc
      source ~/.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 работает на практике при обновлении зависимостей.


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


    Читать продолжение →

    ДомКлик
    Место силы

    Комментарии 9

      +2

      В контексте управления зависимостями полезно было бы ещё упомянуть про npm audit — команда сканирует зависимости и ищет уязвимости в используемых версиях библиотек.

        +1

        Как раз планирую написать про надежность и безопасность в следующем посте.

        –3

        я конечно так и не понял причем тут nodejs и js? укажите это тогда в статье

          +1

          Так как js сам по стандарту нечего не знает о файлах, сетях etc, используется nodejs имплементирующий commonJS. С его помощью работает большенство тулзов под js, упаковщиков и в целом весь инструментарий современного фронта, идет через npx, npm, yarn etc.
          Ну а js, потому, что речь про js ))

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

          Есть и другой взгляд на вещи. Когда вы выпускаете библиотеку/компонент в свет, именно вы ручаетесь за то, что она будет работать. А как можно ручаться за то, что находится вне вашего контроля? Вы даже тесты запустить не сможете, потому что не узнаете что какая то из зависимостей изменилась.


          Особенно актуально это становится, если вы используете зависимости версии 0.х.х, в которых по-умолчанию, апи не стабильно и может измениться в любой момент.


          Мы как-то участвовали в хакатоне и нам нужно было очень быстро стартовать react-app приложение с материал юаем, редаксом и чем-то еще. 3 из 4 найденых шаблонов мы запустить даже не смогли именно из-за "расхлябаных" зависимостей.


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


          И есть инструменты для автоматизации поддержки зависимостей, например greenkeeper.

            +1
            А как можно ручаться за то, что находится вне вашего контроля?

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


            Если же говорить про конечные продукты (веб-приложения или серверы), то снижение рисков как раз таки достигается автотестами и какие-либо деградации не должны выходить на уровень пользователя. Если же программа выполняется на Node.js и ставится глобально (npm i -g), то скорее всего это какой-то инструмент разработки, который не имеет столь жестких требований по надежности и даже, если что-то сломается, то это можно будет исправить без наступления критически последствий.


            Особенно актуально это становится, если вы используете зависимости версии 0.х.х, в которых по-умолчанию, апи не стабильно и может измениться в любой момент.

            По этой причине semver по особому относится к версиям начинающимся с нуля и сильно ограничивает диапазон обновлений (писал об этом в первом посте). Такие зависимости как раз имеет смысл фиксировать в манифесте.

              +1
              Если же говорить про конечные продукты ...

              Об этом речь не идет, т.к. там работают (должны) lock files и там этой проблемы нет.


              npm i -g

              Это добровольный выстрел в ногу, тут тоже проблем нет.


              Никто от вас этого и не может ожидать — правила экосистемы достаточно просты и известны всем участникам.

              Риски тоже известны почти всем участникам. Их желательно учитывать или, если игнорировать, то хотя бы сознательно, а не потому что так написано в интернете.


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


              Я утрирую но думаю вы меня поняли. Мне бы просто не хотелось что бы потом, мне доказывали ссылаясь на вашу статью, что плавающие зависимости это панацея.

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

              Спасибо, что собрали все это в 1 статью.
              Постоянно гуглил про эти вещи, тк еще не запомнил все. Много очень разрозненной информации.

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

              Самое читаемое