Зависимости наших зависимостей



Эта история началась 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.


один из комментариев от сообщества:

На мое счастье, пользователь @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)


  1. VolCh
    14.12.2017 12:52

    При реализации третьего правила, второе особо не нужно.


    1. Drag13 Автор
      14.12.2017 13:26

      Вообще не нужно, но это решение немного проще.


      1. serf
        14.12.2017 14:31
        +1

        Указание основных зависимостей строго это ведь полумера, лок файл решает задачу целиком.


        1. index0h
          14.12.2017 16:31

          lock файл в npm — это чисто информационная штука. Он не гарантирует то, что будут установлены именно те зависимости, что в нем указаны.


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


          Хотите рабочий lock файл — используйте yarn


          1. serf
            14.12.2017 16:36

            А об npm выше никто и не писал.


  1. 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.


    1. Drag13 Автор
      14.12.2017 13:26

      Кейс свежий, от 30.11.2017 года. Так что все не так просто как оказалось.


  1. devlev
    14.12.2017 13:50

    Первое правило package.json: чем меньше зависимостей, тем лучше.
    Второе правило package.json: смотри первое правило.


    1. serf
      14.12.2017 14:27

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


      1. acmnu
        15.12.2017 12:56

        Их и так приходится тестировать, поскольку никакой гарантии, что это кто-то сделал в апстриме.


        1. serf
          15.12.2017 19:04

          Тестировать реализованный функицонал в своем приложении, и тестировать дополнительно функционал зависимостей это очень разные вещи. Если существует достаточно прилично сделанный модуль, почему не использовать его? Приличность модуля определить не сложно просмотрев код, тесты и issues, это делается один раз и занимает ну пол часа времени.


  1. serf
    14.12.2017 14:30

    Строгое указания без ^, ~ особо не поможет, ведь нет гарантии что у зависимостей будет также, поэтому лок файлы. Указывать ^, ~ для библиотек не такая и плохая идея, а для проектов предпочитаю указывать точные версии и допустим раз в неделю или раз в месяц обновлять используя npm-check-updates.


  1. Loki3000
    14.12.2017 14:30

    Если все зависимости намертво прибиты гвоздями, то зачем вообще нужен пакетный менеджер? Какую роль в этом случае он выполняет в проекте?


    1. serf
      14.12.2017 14:32

      А какую роль решают пакетные менеджеры допустим в линуксах (pacman например)?


      1. Loki3000
        14.12.2017 14:42

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


        1. serf
          14.12.2017 14:51

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


        1. pletinsky
          14.12.2017 17:40

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

          Если же вы положите зависимости к себе явно, то вы будете лишены этих возможностей. Да еще потеряете уверенность, что никто не вносит туда изменения, что лишит вас возможности переходить на новые версии. Класть зависимости в свой код — очень плохая практика. Или вы поддерживаете код или нет.

          Альтернативой является например зависимые гит репозитории. Где можно и обновляться и при этом отслеживать локальные изменения. Но это тоже по сути разновидность пакетного менеджера.


          1. vintage
            15.12.2017 09:58

            Гит субмодули — это такие лок файлы с криптографической гарантией целостности :-)


    1. Dreyk
      14.12.2017 14:36

      организационную. ну и удобство при обновлениях


    1. VolCh
      14.12.2017 15:17
      +1

      • не хранить их в репе
      • легко проверить доступность обновлений
      • легко обновить
      • унифицированный способ управления зависимостями


    1. wdforge
      15.12.2017 09:38

      Можно сделать форки на все зависимости и не переживать, что кто-то что-то поменяет.


  1. slonopotamus
    15.12.2017 08:49
    +1

    Никогда такого не было, и вот опять.


    1. У вас нарушена воспроизводимость билдов, один и тот же билд одного и того же коммита произведет разное в зависимости от неконтролируемого вами окружения.
    2. Я не знаю как принято у js-хипстеров, в мире java всегда было хорошим тоном поднять внутри компании локальный кэширующий maven-proxy. Чтобы не попадать в ситуацию "все пропало" при отсутствии интернета, да и просто чтобы все было быстрее
    3. В npm что, нельзя взять и распечатать дерево зависимостей?


    1. mayorovp
      15.12.2017 09:55

      В npm что, нельзя взять и распечатать дерево зависимостей?

      Можно. Но простейшая комбинация babel-cli + babel-preset-latest + webpack уже дает 636 узлов в дереве и 21 килобайт текстового представления. :-)


      1. slonopotamus
        15.12.2017 11:19

        Если можно, зачем тогда вот это вот всё?

        Поэтому на коленке был набросан скрипт который обошел все package.json-ы в node_modules в поисках coffee-script. Таких зависимостей оказалось около 5-6 штук. Это еще больше укрепило мои подозрения, и не сильно долго размышляя я снес весь node_modules, а заодно и все dependencies кроме одной в локальном репозитории и запустил npm install снова. Процесс прошел успешно. Дальше, шаг за шагом была найдена та зависимость, которая и валила install.


        1. Drag13 Автор
          15.12.2017 11:27

          Здесь все просто, по запарке просто не пришло в голову :).


    1. Drag13 Автор
      15.12.2017 10:08

      1. Об этом и речь. И это не только у меня, это подход по-умолчанию.
      2. У кого-то принято у кого-то нет.
      3. Можно см. ниже.

      И давайте без странных подначек про хипстеров, это не к месту.


      1. slonopotamus
        15.12.2017 11:28

        И давайте без странных подначек


        А вы не провоцируйте. Второй раз наступание на те же грабли что были с left-pad. Потом будет что-нибудь типа «у хостинга npm-репозитория отгнило электричество, поэтому репозиторий был несколько часов недоступен и половина интернета сломалась».


        1. Drag13 Автор
          15.12.2017 12:01

          А в мире Java все наступают на грабли только один раз? Или в мире .Net? Не надо применять стереотипы, если не хотите что бы их применяли к вам.


    1. VolCh
      15.12.2017 11:21

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


      1. slonopotamus
        15.12.2017 11:36

        Если вы не можете выполнить инструкцию на www.npmjs.com/package/npm-proxy-cache то так и скажите, зачем приплетать какие-то дополнительные сложности с «особо не нагруженными админами/девопсами, ещё и не входящими в команду разработки».


    1. acmnu
      15.12.2017 12:58

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

      Интересно как вы гарантируете воспрозводимость, работая с Java. Можно подробнее?


      1. serf
        15.12.2017 19:07

        В java тоже бывает library hell, иногда он проявляется только в рантайме, там еще и класслоадеры есть разные, не стоит как-то возвышать эти жавы.


        1. acmnu
          18.12.2017 12:15

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


  1. kuftachev
    16.12.2017 04:09

    Если речь идёт о фронт-энд, то там особых последствий не будет. Ну придется потратить времени и немного покопаться. Критичного ничего нет.


    А вот когда мы уже говорим о бек-энд. На сколько люди реально проверяют библиотеки, которые они используют?
    В принципе, в большинстве фреймворков никто не мешает получить объект запроса, проверить ip, и если он нужный отправить в ответ всю базу данных. И это можно сделать не сразу, а с каким-то обновлением. Все ли отслеживают каждую строчку? Конечно за основными библиотеками следят много людей и большая вероятность, что что-то найдут, но если речь идёт о какой-то второстепенной вещи без большого сообщества.
    Я понимаю, что пример про базу данных сильно натянутый, но сама идея, что код может вообще все делать на сервере.


    А если совместить с темой поднятой автором во второй части статьи, что и у зависимостей есть свои зависимости...


    Жизнь — боль!