Файлы блокировки демистифицированы

Эта тема может показаться немного неактуальной для фанатов NTF/Web3/Metaverse, но я обнаружил, что иногда стоит вернуться к основам и объяснить то, что *все* понимают.

Это попытка объяснить, почему были изобретены файлы блокировки (например, yarn.lock, package-lock.json), каковы их ограничения и как ими следует манипулировать изо дня в день. Ожидается очень базовое знание менеджеров пакетов. По пути вы найдете несколько внешних ссылок, если хотите углубить свое понимание.

TL;DR

Файл блокировки — это файл, который отслеживает точные версии, к которым ваш менеджер пакетов разрешил ваши зависимости в данный момент времени. Он существует для того, чтобы две последующие команды install всегда создавали одно и то же дерево зависимостей, независимо от промежуточных обновлений зависимостей. Это необходимо, потому что файл package.json сам по себе не является достаточно явным для обеспечения такого детерминизма.

Содержание

Лексикон

  • Пакет: пакет исходного кода, обычно характеризующийся файлом package.json в корне. Пакет, предназначенный для использования в других пакетах, также может называться библиотекой.
  • Зависимость: пакет, от которого зависит ваш пакет. Вы наверняка добавили его с помощью команды yarn add или npm install. Он указан в вашем package.json файле под ключом dependencies, devDependencies или peerDependencies (разницу не знаете? прочитайте это).
  • Транзитивная зависимость: пакет, который попадает в ваше дерево зависимостей, потому что от него зависит одна из ваших зависимостей. Например, react зависит от loose-envify, поэтому, если вы установите react, loose-envify окажется в вашей папке node_modules вместе с ним.
  • Дерево зависимостей: логическое представление всех пакетов, от которых зависит ваш пакет (как обычных, так и транзитивных): корень состоит из пакетов, перечисленных в вашем файле package.json, и каждый последующий уровень представляет собой одну дополнительную степень транзитивности. .

Введение

Есть несколько способов описать зависимость в файле package.json, каждый из которых имеет свое значение:

  • ^17.0.0 означает « Дайте мне любую версию, совместимую с 17.0.0. ». Это диапазон по умолчанию и самый полезный.
  • >17.0.0 означает « Дайте мне любую версию выше 17.0.0, независимо от того, совместима она с 17.0.0 или нет. ». Это опасно, так как может привести к критическим изменениям.
  • 17.0.0 означает « Дайте мне ровно 17.0.0 ». Вы не должны использовать это часто и вместо этого быть максимально свободным, чтобы оптимизировать совместное использование зависимостей.
  • … и так далее (полный список).

Одной из основных функций менеджеров пакетов, таких как Yarn или npm, является разрешение ваших зависимостей, то есть преобразование каждого предоставленного вами дескриптора (или набора пакетов, например react@^17.0.0) в локатор (или уникальный пакет, например [email protected]). Каждый менеджер пакетов делает это по-своему, но все они ищут самую старшую версию, которая удовлетворяет вашим требованиям.

Проблема

Скажем на секунду, что мы живем в месте, где файлов блокировки не существует. Здесь файл package.json является единственным органом, от которого ваш менеджер пакетов получает свои указания. Звучит захватывающе? Ну, не должно, и вы поймете, почему.

В этом файле package.json вы указываете, что react@^17.0.0 является зависимостью. Когда вы запускаете команду install в первый раз, последней версией react будет, скажем, 17.0.2. Таким образом, менеджер пакетов преобразует react@^17.0.0 в [email protected], потому что, помните, он ищет самую старшую версию, которая удовлетворяет вашим требованиям.

Через несколько недель кто-то присоединяется к вашей команде. Он загружает исходный код на свою машину и запускает команду install. Но прошло время, и последняя версия react теперь 17.0.3. Это по-прежнему удовлетворяет диапазону ^17.0.0, поэтому менеджер пакетов на этот раз преобразует react@^17.0.0 в [email protected]. Видите проблему? Теперь у вас и вашего коллеги есть разные версии react в соответствующих папках node_modules. И однажды вы, вероятно, скажете что-то вроде:

«Брух, почему это работает на твоей машине, а не на моей?! Нет, нет, на этот раз это слишком, я бросил программировать. »

Поскольку вы указываете диапазоны, а не точные версии для своих зависимостей, ваш файл package.json на самом деле может быть разрешен различными способами, в зависимости от последних выпусков целевых библиотек. Это нехорошо, так как делает результаты команды install непредсказуемыми. Выполнение его дважды с одним и тем же файлом package.json может привести к созданию двух разных папок node_modules.

🤔 Подождите секунду

Я знаю, о чем вы думаете. Почему бы не указать только точные версии для ваших зависимостей (например, [email protected])? Таким образом, при двойном запуске команды install не будет ничего удивительного. Что ж, несмотря на заманчивость, это рассуждение ошибочно, потому что вы кое-что забываете: у каждой из ваших зависимостей есть собственный файл package.json, в котором она объявляет свои зависимости, за разрешение которых также отвечает ваш менеджер пакетов. Таким образом, даже если вы укажете только точные версиисвоихзависимостей, вы не сможете контролировать то, как ваши зависимости объявляютсвои, которые все еще могут находиться в диапазонах. Нет, нам нужно что-то еще.

Как работает файл блокировки?

Файл блокировки — это файл, который отслеживает точные версии, к которым ваш менеджер пакетов разрешил ваши зависимости. Иными словами, это моментальный снимок того, как ваш файл package.json был разрешен в данный момент времени.

Допустим, ваш файл package.json выглядит так:

После первого запуска yarn install (или npm install) вот что вы можете найти в сгенерированном файле yarn.lock (или package-lock.json):

Проверьте первую пару ключ/значение. В основном это означает, что зависимость react, указанная вами как диапазон в вашем файле package.json ("react": "^17.0.0"), была преобразована в 17.0.2 точную версию react.

Если вы снова запустите команду install, менеджер пакетов заметит, что там лежит файл блокировки, и пропустит шаг разрешения для уже разрешенных зависимостей. Предположим, что react выпускает версию 17.0.3 в середине ваших двух команд install, менеджер пакетов все равно разрешит ^17.0.0 в 17.0.2 благодаря файлу блокировки.

Почему это круто?

Теперь вы можете подумать:

« Итак, я навсегда застрял на версии 17.0.2. В чем смысл? Что, если бы я действительно хотел, чтобы react был обновлен до 17.0.3? »

Ну, это правда, что версия react здесь как бы заблокирована. Но это неправда, что вы застряли с этим. На самом деле, вы можете решить обновиться в любое время. И это ключевой вывод. Благодаря файлу блокировки менеджер пакетов может предоставить полную власть вам. Если вы ничего не делаете, ваша папка node_modules не должна изменяться. Другими словами, две последующие команды install всегда должны давать одинаковые результаты. Это называется детерминизмом: это когда в процессе не участвует случайность.

Так что решать, переходить на 17.0.3 или нет, решать вам. И вы выражаете это с помощью простой команды: upgrade.

Возвращаясь к нашему последнему примеру. Если вы запустите yarn upgrade react (или npm upgrade react) после выпуска версии 17.0.3 из react, ваш файл yarn.lock (или package-lock.json) будет обновлен следующим образом:

✨ Профессиональный совет

В Yarn есть очень удобная команда upgrade-interactive, которая отображает все устаревшие пакеты и позволяет вам выбрать, какой из них обновить. Проверьте пакет npm-check для эквивалента npm.

Но файлы блокировки не являются чистым солнечным светом

Почему? Потому что они могут вызвать дублирование.

Дубликаты определяются Yarn как « дескрипторы с перекрывающимися диапазонами, которые разрешаются и блокируются для разных локаторов »¹. Они являются естественным следствием детерминированных установок менеджеров пакетов, но иногда они могут накапливаться и излишне увеличивать размер вашего проекта.

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

Пример успеха. Если react@^17.0.0 (зависимость от зависимости) уже разрешено в [email protected], запуск yarn add react@* заставит Yarn повторно использовать [email protected], даже если последней версией react будет 17.0.3, что предотвратит ненужное дублирование.

Пример ошибки. Если react@^17.0.0 (зависимость от зависимости) уже разрешено в [email protected], запуск yarn add [email protected] заставит Yarn установить [email protected], потому что существующее разрешение не соответствует диапазону 17.0.2. Такое поведение может привести к нежелательному дублированию, поскольку теперь файл блокировки содержит два отдельных разрешения для двух дескрипторов react, хотя они и перекрываются.

Как действовать? На помощь приходит команда dedupe. Эту команду следует использовать с осторожностью, так как она модифицирует дерево зависимостей, что иногда может вызывать проблемы, когда пакеты строго не следуют рекомендациям semver. Из-за этого рекомендуется также просматривать изменения вручную.

✨ Профессиональный совет

Используйте параметр --check команды Yarn v2+ dedupe, чтобы проверить, содержит ли дерево зависимостей какие-либо дубликаты, но пока не применяя никаких изменений. Сделайте это частью рабочего процесса CI.

Распределенные библиотеки

Важно отметить, что менеджеры пакетов заботятся об одном единственном файле блокировки: том, который находится на верхнем уровне вашего проекта. Это означает, что если какая-то ваша зависимость поставляется с собственным файлом блокировки, этот файл будет полностью проигнорирован Yarn или npm².

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

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

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

  • Ограничьте количество пакетов, от которых зависит ваша библиотека. react, например, имеет только одну зависимость: loose-envify.
  • Выбирайте только хорошо сохранившиеся пакеты. Если у них есть надежный набор тестов, они с меньшей вероятностью допустят ошибку в выпуске.
  • (Не рекомендуется) Используйте более строгие диапазоны или точные версии для описания ваших зависимостей. Имейте в виду, что чем больше вы это делаете, тем меньше у менеджера пакетов возможностей для оптимизации дерева.

✨ Профессиональный совет

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

Файлы блокировки и Git

Должны ли файлы блокировки быть переданы в репозиторий? В Интернете можно найти множество разных ответов на этот вопрос, но только один из них правильный: да, всегда.

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

Заключение

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

[1]: https://yarnpkg.com/cli/dedupe

[2]: За исключением файла npm-shrinkwrap.json npm (также известного как публикуемый файл блокировки ). Но его использование не рекомендуется в большинстве ситуаций.