Зависимости наших зависимостей
Эта история началась 30 ноября, утром. Когда вполне обычный билд на Test environment внезапно упал. Наверное, какой-то линтер отвалился, не проснувшись подумал я и был не прав.
Кому интересно чем закончилась эта история и на какие мысли навела – прошу под кат.
Открыв билд-лог, я увидел, что упал npm install. Странно подумал, я. Еще вчера вечером все работало отлично. После некоторого изучения логов, была найдена подозрительная строка:
node --eval 'if (require("./package.json").name === "coffee-script") { var red, yellow, cyan, reset; red = yellow = cyan = reset = ""; if (!process.env.NODE_DISABLE_COLORS) { red = "\x1b[31m"; yellow = "\x1b[33m"; cyan = "\x1b[36m"; reset = "\x1b[0m"; } console.warn(red + "CoffeeScript has moved!" + reset + " Please update references to " + yellow + "\"coffee-script\"" + reset + " to use " + yellow + "\"coffeescript\"" + reset + " (no hyphen) instead."); console.warn("Also, a new major version has been released under the " + yellow + "coffeescript" + reset + " name on NPM. This new release targets modern JavaScript, with minimal breaking changes. Learn more at " + cyan + "http://coffeescript.org" + reset + "."); console.warn(""); }
Здесь я опять удивился. Опять же, еще вчера мы coffee-script на проекте не использовали и за ночь вряд ли что-то сильно изменилось. Быстрый просмотр package.json подтвердил, что никакой супостат ничего нового туда не добавлял. Значит, наверное, у нас обновилась какая-то зависимость, которая использует coffee-script. Но против этой идеи говорило то, что уже довольно давно я выставил строгие версии для всех зависимостей проекта и, как мне казалось, такого случиться не могло. Бесплодно поискав похожую проблему в интернете, я опять вернулся к мысли об обновившейся зависимости. Поэтому на коленке был набросан скрипт который обошел все package.json-ы в node_modules в поисках coffee-script. Таких зависимостей оказалось около 5-6 штук. Это еще больше укрепило мои подозрения, и не сильно долго размышляя я снес весь node_modules, а заодно и все dependencies кроме одной в локальном репозитории и запустил npm install снова. Процесс прошел успешно. Дальше, шаг за шагом была найдена та зависимость, которая и валила install.
Это оказалась karma-typescript у которой в транзитивной зависимости оказался "pad", который в свою очередь зависел от coffee-script. И тут я снова приуныл. Вариантов было немного. Или временно отключать тесты, или ждать фикса, или делать форк и чинить самому (причем не очень понятно, что же конкретно нужно чинить). Без особой надежды я отправился на Github создавать issue. Каково же было мое удивление, когда мне ответили буквально через 20 минут. Оказалось, что некоторый товарищ, решил обновить coffee-script пакет в npm и вместо того, чтобы объявить старый пакет устаревшим он просто сделал ему unpublish.
![](https://habrastorage.org/webt/ze/c4/g1/zec4g1ovo_nqruhnh_fns5yahxe.png)
На мое счастье, пользователь @ondrejbase уже предложил костыль временное решение которое мне помогло. И вся эта история закончилась относительно дешево. Но могло быть совсем не так.
Это была присказка. А теперь я предлагаю поговорить о зависимостях наших проектов и проблемах, которые они нам приносят.
Давайте начнём с простого.
Глобальные зависимости
Недавно я в очередной раз наткнулся на статью, для новичков которая начиналась со строки
npm install -g typescript
И не выдержал. По моему мнению это один из самых плохих советов который вы можете дать начинающему разработчику. Я серьезно. Вот проблемы, к которым приводит этот совет:
- Конфликт локальных репозиториев. Собирая весь код с помощью глобальных зависимостей (tsc, например) или валидируя его глобальным линтерами мы уже не можем так легко обновлять эти зависимости. Ведь обновив что-то глобальное, мы ставим под угрозу все репозитории в которых это используется. В итоге, придется или тратить время на то что бы обновить эти зависимости во всех репозиториях, или не обновлять их совсем.
- Конфликт с членами команды. Использование глобальных зависимостей в командной работе приводит к их рассинхронизации. И, как следствие, к тому, что мой код не соберется или не пройдет проверку у моего коллеги.
- Конфликт с билд-машиной. То, что соберется у меня, не гарантировано соберется на билд машине. Или, что гораздо хуже и коварнее — соберется не так, как у меня. А это гарантированный ад дебага на ближайший час или больше.
И все это происходит потому, большинство пособий начинается с npm install abc -g
. Хотя можно поставить все локально и подключить в package.json как ./node_modules/.bin/tsc
Если вы соберетесь писать свой гайд, пожалуйста, вспомните эту статью и спасите мои нервы, а также нервы всех тех, кто будет ее использовать на практике.
N.B. Глобально устанавливать генераторы кода (create-react-app, create-angular-app) это нормально. Они один раз отработают и все. Кроме того, вам не понадобится ставить их заново, когда вы решите создать следующий репозиторий.
Нестрогие зависимости
Давайте идти дальше. Установим create-react-app, и создадим базовое приложение. Заходим в package.json и что мы там видим?
"react": "^16.2.0"
Все (или почти все) знают, что значит символ ^. Он значит, что npm может установить любую версию старше или равной указанной, в пределах мажорного релиза. И что в этом плохого? Не хочется быть категоричным, но, как по мне этот подход тоже "не очень", и вот почему:
- Потеря контроля. Как только вы оставили ^ или ~ в своем package.json вы потеряли контроль над своим кодом. Конечно я утрирую, но подумайте. Вы, грузите в проект версию сторонней библиотеки о которой не знаете даже ее версии. Только то, что она лежит в репозитории от какого-то издателя. И вдруг что случится, остается только надеяться, что у этого пакета достаточно организованное сообщество что бы это заметить.
- Изменения ломающие обратную совместимость. Конечно, публикуя мажорную версию, разработчики обещают, что в ее рамках breaking changes не будет. Но, господа, давайте быть реалистами. Это интернет, и это open source: вам никто ничего не должен. То, что работало час назад, может отвалиться с комментарием типа: “This new release targets modern JavaScript, with minimal breaking changes”.
- Неочевидность обновлений. Допустим есть у меня библиотека вида abc: ^2.1.1. И, получается, я не знаю какая на самом деле версия библиотеки у меня стоит. Может быть 2.1.1, а может быть 2.9.9. И вроде бы это не проблема, наоборот. Мне не нужно искать документацию для определенной версии, достаточно всегда смотреть самую свежую. И вот я ее смотрю, и вижу новую фишку, и она не работает. А не работает она потому что я просто забыл обновить библиотеку. Я ее обновляю, комичу код, и через 30 минут ко мне прибегает мой коллега потому что у него приложение не работает! А за ним приходят еще трое и делают мне грустно. Кому это нужно?
- И наконец: зависимости зависимостей. Это вишенка на торте и это то, о чем я бы хотел, чтобы вы задумались.
Зависимости наших зависимостей или ЗНЗ
Давайте начнем с того, почему у нас сломался билд. Все зависимости были заданы жестко и тем не менее мы свалились. Несмотря на то, что версии наших основных зависимостей оставались прежними (напомню, никаких ^, ~ в package.json) их зависимости были не такими строгими. Мы не контролировали зависимости наших зависимостей, хотя и пытались. И, что самое неприятное такое поведение поощряется по умолчанию. Я не знаю кто и зачем это сделал, но он подложил большую свинью всем нам, а особенно тем, кто практикует continuous-integration.
Конечно, конкретно эту проблему исправить легко. Достаточно создать lock-file (например, с помощью команды npm shrinkwrap) или использовать yarn — пакетный менеджер который по умолчанию фиксирует все зависимости вашего проекта.
Однако это решает только часть проблемы. Остается еще одна, гораздо более опасная ее часть и имя ей unpublish. Если вы не сталкивались с этой проблемой до этого, то вот тут прекрасная статья которая показывает всю уязвимость современной веб разработки. В любой момент, умышленно или по неосторожности, ваш проект может перестать собираться только потому, что кто-то удалил свой пакет из npm. И сделать это отнюдь не сложно. Достаточно просто ввести команду unpublish. С этой бедой можно бороться. Но давайте будем честны перед собой? У кого из нас стоит свой локальный npm? А кто хотя бы задумывался об этом? Боюсь, что не так много. И я лишь надеюсь, что теперь вы предупреждены.
Кстати, если вы зайдете в мой репозиторий (не делайте этого, прошу), то увидите, что почти все мои проекты нарушают все, что было сказано выше. И это еще одно доказательство того, насколько проблема распространена.
Выводы
- Используйте флаг глобальной установки с умом. Не стоит использовать его для тех зависимостей от которых будет постоянно зависеть ваш проект.
- Если вы работаете не один или на нескольких физических машинах, подумайте о том, чтобы задать зависимости строго. Это позволит вам иметь одинаковые основные зависимости проекта во всех репозиториях и у коллег. Кроме того, вы увеличите контроль над собственным кодом и процессом его обновления.
- Используйте lock-файл. Особенно если вы собираетесь использовать continues-integration.
- Задумайтесь о частном регистре пакетов. Это не только убережет вас от внезапного удаления пакетов, но и ускорит установку зависимостей на билд машине. А это сэкономит ваше время и количество кофе, которое выпивается пока проходит билд во время пулл реквеста.
На этом у меня все, надеюсь было интересно и полезно. По странной традиции здесь должны быть какая-то реклама, но ее у меня нет, поэтому я просто передам спасибо коллегам, которые вместе со мной тушили этот пожарчик.
И приношу свои извинения за английские слова, но в русском эквиваленте они сильно режут глаз.
Комментарии (34)
megaboich
14.12.2017 12:52Странно мне казалось что после скандала с left-pad такое не должно больше происходить. Да и в документации указано что:
With the default registry (registry.npmjs.org), unpublish is only allowed with versions published in the last 24 hours. If you are trying to unpublish a version published longer ago than that, contact support@npmjs.com.
Drag13 Автор
14.12.2017 13:26Кейс свежий, от 30.11.2017 года. Так что все не так просто как оказалось.
devlev
14.12.2017 13:50Первое правило package.json: чем меньше зависимостей, тем лучше.
Второе правило package.json: смотри первое правило.serf
14.12.2017 14:27То есть предлагаете писать свои велосипеды для всех нужд вместо использования модулей? А ведь их нужно будет тестировать самому, поддерживать и тд.
acmnu
15.12.2017 12:56Их и так приходится тестировать, поскольку никакой гарантии, что это кто-то сделал в апстриме.
serf
15.12.2017 19:04Тестировать реализованный функицонал в своем приложении, и тестировать дополнительно функционал зависимостей это очень разные вещи. Если существует достаточно прилично сделанный модуль, почему не использовать его? Приличность модуля определить не сложно просмотрев код, тесты и issues, это делается один раз и занимает ну пол часа времени.
serf
14.12.2017 14:30Строгое указания без ^, ~ особо не поможет, ведь нет гарантии что у зависимостей будет также, поэтому лок файлы. Указывать ^, ~ для библиотек не такая и плохая идея, а для проектов предпочитаю указывать точные версии и допустим раз в неделю или раз в месяц обновлять используя npm-check-updates.
Loki3000
14.12.2017 14:30Если все зависимости намертво прибиты гвоздями, то зачем вообще нужен пакетный менеджер? Какую роль в этом случае он выполняет в проекте?
serf
14.12.2017 14:32А какую роль решают пакетные менеджеры допустим в линуксах (pacman например)?
Loki3000
14.12.2017 14:42Собирают зависимости на клиенте, чтобы ему не таскать лишнего. В случае веб приложения смысла собирать зависимости на рабочем сервере я не вижу — ведь можно просто выложить рабочий и протестированный комплект кода вместе с зависимостями. Тем более что зависимости все равно предлагается обновлять вручную.
serf
14.12.2017 14:51Существуют разные типы софта, например библиотеки и приложения (проекты которые куда-либо деплояться). Если ты релизишь веб библиотеку, то релизить ее с намертво прибитыми гвоздями зависимостями идея так себе, тк при включении этой бибиотеки в проект ее зависимость может например конфликтовать с такой же зависимостью но другой версии которая уже использовалась в проекте для других целей, уже не пишу об обновлениях безопасности. В вебе в принципе можно изолировать зависимости библиотеки и предотвратить тем самым конфликты, но это тоже плохая идея, так отдавать в браузере разные версии одной и тойже библиотеки идея так себе (трафик).
pletinsky
14.12.2017 17:40Даже зафиксированную зависимость можно обновить, перейти на более новую версию, используя тот же менеджер пакетов, это не совсем вручную.
Плюс менеджер пакетов предоставляет возможность видеть списком зависимости и их верссии и поддерживать этот список в системе контроля версий, что само по себе уже немало.
Если же вы положите зависимости к себе явно, то вы будете лишены этих возможностей. Да еще потеряете уверенность, что никто не вносит туда изменения, что лишит вас возможности переходить на новые версии. Класть зависимости в свой код — очень плохая практика. Или вы поддерживаете код или нет.
Альтернативой является например зависимые гит репозитории. Где можно и обновляться и при этом отслеживать локальные изменения. Но это тоже по сути разновидность пакетного менеджера.vintage
15.12.2017 09:58Гит субмодули — это такие лок файлы с криптографической гарантией целостности :-)
VolCh
14.12.2017 15:17+1- не хранить их в репе
- легко проверить доступность обновлений
- легко обновить
- унифицированный способ управления зависимостями
wdforge
15.12.2017 09:38Можно сделать форки на все зависимости и не переживать, что кто-то что-то поменяет.
slonopotamus
15.12.2017 08:49+1Никогда такого не было, и вот опять.
- У вас нарушена воспроизводимость билдов, один и тот же билд одного и того же коммита произведет разное в зависимости от неконтролируемого вами окружения.
- Я не знаю как принято у js-хипстеров, в мире java всегда было хорошим тоном поднять внутри компании локальный кэширующий maven-proxy. Чтобы не попадать в ситуацию "все пропало" при отсутствии интернета, да и просто чтобы все было быстрее
- В npm что, нельзя взять и распечатать дерево зависимостей?
mayorovp
15.12.2017 09:55В npm что, нельзя взять и распечатать дерево зависимостей?
Можно. Но простейшая комбинация babel-cli + babel-preset-latest + webpack уже дает 636 узлов в дереве и 21 килобайт текстового представления. :-)
slonopotamus
15.12.2017 11:19Если можно, зачем тогда вот это вот всё?
Поэтому на коленке был набросан скрипт который обошел все package.json-ы в node_modules в поисках coffee-script. Таких зависимостей оказалось около 5-6 штук. Это еще больше укрепило мои подозрения, и не сильно долго размышляя я снес весь node_modules, а заодно и все dependencies кроме одной в локальном репозитории и запустил npm install снова. Процесс прошел успешно. Дальше, шаг за шагом была найдена та зависимость, которая и валила install.
Drag13 Автор
15.12.2017 10:081. Об этом и речь. И это не только у меня, это подход по-умолчанию.
2. У кого-то принято у кого-то нет.
3. Можно см. ниже.
И давайте без странных подначек про хипстеров, это не к месту.slonopotamus
15.12.2017 11:28И давайте без странных подначек
А вы не провоцируйте. Второй раз наступание на те же грабли что были с left-pad. Потом будет что-нибудь типа «у хостинга npm-репозитория отгнило электричество, поэтому репозиторий был несколько часов недоступен и половина интернета сломалась».Drag13 Автор
15.12.2017 12:01А в мире Java все наступают на грабли только один раз? Или в мире .Net? Не надо применять стереотипы, если не хотите что бы их применяли к вам.
VolCh
15.12.2017 11:21- Это дополнительные сложности. В крупной компании или просто при наличии квалифицированных админов/девопсов (причём особо не нагруженными задачами к разработке мало относящихся) может разработка поставить задачу под поднятию, настройке и поддержке такого прокси. Если же это условие не выполняется, грубо, вы единственный в компании кто вообще понимает о чём речь, то ресурсы вам на это могут и не выделить.
slonopotamus
15.12.2017 11:36Если вы не можете выполнить инструкцию на www.npmjs.com/package/npm-proxy-cache то так и скажите, зачем приплетать какие-то дополнительные сложности с «особо не нагруженными админами/девопсами, ещё и не входящими в команду разработки».
acmnu
15.12.2017 12:58У вас нарушена воспроизводимость билдов, один и тот же билд одного и того же коммита произведет разное в зависимости от неконтролируемого вами окружения.
Интересно как вы гарантируете воспрозводимость, работая с Java. Можно подробнее?
kuftachev
16.12.2017 04:09Если речь идёт о фронт-энд, то там особых последствий не будет. Ну придется потратить времени и немного покопаться. Критичного ничего нет.
А вот когда мы уже говорим о бек-энд. На сколько люди реально проверяют библиотеки, которые они используют?
В принципе, в большинстве фреймворков никто не мешает получить объект запроса, проверить ip, и если он нужный отправить в ответ всю базу данных. И это можно сделать не сразу, а с каким-то обновлением. Все ли отслеживают каждую строчку? Конечно за основными библиотеками следят много людей и большая вероятность, что что-то найдут, но если речь идёт о какой-то второстепенной вещи без большого сообщества.
Я понимаю, что пример про базу данных сильно натянутый, но сама идея, что код может вообще все делать на сервере.
А если совместить с темой поднятой автором во второй части статьи, что и у зависимостей есть свои зависимости...
Жизнь — боль!
VolCh
При реализации третьего правила, второе особо не нужно.
Drag13 Автор
Вообще не нужно, но это решение немного проще.
serf
Указание основных зависимостей строго это ведь полумера, лок файл решает задачу целиком.
index0h
lock файл в npm — это чисто информационная штука. Он не гарантирует то, что будут установлены именно те зависимости, что в нем указаны.
Так что проблему зависимостей зависимостей он ни капли не решает. Единственное на что можно надеяться — что при полной фиксации ваших зависимостей, их зависимости не сломают вам ничего.
Хотите рабочий lock файл — используйте yarn
serf
А об npm выше никто и не писал.