Особенности npm и хранение node_modules в git

Original author: Mikeal Rogers
  • Translation


Для управления зависимостями в проекте, node.js, как и многие другие платформы, предоставляет собственный пакетный менеджер — npm. И несмотря на то, что он внешне похож, например, на Ruby Gems, и вроде бы выполняет те же самые функции, npm обладает некоторыми особенностями, которые стоит учитывать при разработке приложений на node.js. Одна из таких особенностей — это способ хранения директории node_modules в проекте. Многие, по аналогии с другими системами, оставляют в проекте только package.json с зафиксированными версиями модулей, а node_modules добавляют в .gitignore. Такая стратегия не всегда верна, если мы обратимся в FAQ на npmjs.org, то увидим там следующее:
Q: Стоит ли хранить node_modules в git?
A: Mikeal Rogers очень хорошо ответил на этот вопрос:
http://www.mikealrogers.com/posts/nodemodules-in-git.html
tl;dr
  • Храните node_modules в git для проектов, которые требуется разворачивать, таких как вебсайты и приложения.
  • Добавляйте node_modules в .gitignore для библиотек и повторно используемых модулей.
  • Используйте npm для управления зависимостями в dev окружении, но не в скриптах используемых для деплоя.


Под катом перевод статьи Mikeal Rogers, в котором подробно описывается, с чем связан такой непривычный подход.

Одна из проблем, потребовавших переосмысления в мире node.js — это вопрос управления зависимостями приложения.

Одним из больших изменений, которые пришли вместе с node 0.4.0, стала поддержка node_modules. Это изменение имело серьезные последствия. Оно подняло приоритет локальных модулей в локальной директории над модулями установленными глобально. Вместе с тем, npm изменил установку по умолчанию с глобальной на локальную и мы увидели практически единогласный переход на локальные модули.

Переход с глобальных модулей на локальные — это то, что отличает node от платформ предыдущих поколений. Ruby и Python полностью провалились в этой области, и тот факт, что стандартной практикой стала разработка и деплой в изолированной песочнице для всей платформы (virtualenv, rvm) — это признание провала.

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

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

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

Но теперь это уже не Ruby или Python, это node.js, и мы сделали модули гораздо более лучшими. Если у вас есть приложение, которое вы разворачиваете, поместите все зависимости в директории node_modules в систему контроля версий. Если вы хотите использовать npm для деплоя, подключите эти модули только в bundleDependencies. Если у вас есть зависимости, требующие компиляции, все равно закоммитте их код и просто запустите npm rebuild при деплое.

Все, кому я это рассказывал, говорили мне, что я идиот, а затем, через пару недель, говорили, что я был прав, и что размещение node_modules в git было благословением и для деплоя и для разработки. Это объективно лучше, но вот несколько вопросов/претензий, которые возникают:

Почему бы мне просто не фиксировать версии, чтобы гарантировать, что все инсталляции получат одинаковые зависимости?

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

Почему бы нам не поощрять мэйнтейнеров библиотек тоже фиксировать версии своих зависимостей?

На сегодняшний день в реестре npm примерно 5,500 пакетов (Примечание переводчика: данные на декабрь 2011, сейчас их уже почти 34 тысячи.). За прошлый месяц прибавилось более 600 новых пакетов и более 5,000 было обновлено. Для некоторых пакетов было выпущено несколько обновлений в течение недели. Как сообщество, мы должны распределять между собой часть интеграционного тестирования. Это невозможно для большинства мэйнтейнеров — сидеть и тестировать свой пакет со всеми обновлениями, которые приходят для зависимостей пакета. Вот почему разработчики библиотек не должны фиксировать версии и не должны коммитить свои зависимости в репозиторий. Нам нужны новые люди для обновления зависимостей локально и отправки баг-репортов. Нам нужно поддерживать движение сообщества вперед и держаться на острие последних версий и новых пакетов.

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

Не создает ли хранение node_modules в репозитории слишком много, не имеющего отношения к моему приложению, мусора в дереве исходников?

Нет, вы ошибаетесь, этот код используется вашим приложением, он — часть вашего приложения, если будете делать вид, что это не так, это приведет к проблемам. Вы зависите от кода других людей, а они делают ошибки и плодят новые баги не реже вас, может быть даже чаще. Хранение всего кода в репозитории дает вам возможность следить за каждой строчкой, которая когда-либо менялась в вашем приложении. Это позволит вам использовать git bisect локально и быть уверенным, что результат будет аналогичным в production и что все машины в production — идентичны. Нет больше нужды следить за неведомыми изменениями в зависимостях, все изменения, в каждой строчке, доступны через систему контроля версий.

Ok, отлично, что же нам теперь делать?

Резюмируем.

  • Храните node_modules в репозитории для приложений, которые вы разворачиваете, но не для библиотек, которые вы поддерживаете.
  • Все компилируемые зависимости должны хранить в репозитории исходники, а не результат компиляции и должны собираться с помощью npm rebuild при деплое.

Вы все, кто добавили node_modules в ваш .gitignore, удаляйте этот отстой, сегодня. Это пережиток эры, которую мы слишком счастливы оставить позади. Эра глобальных модулей мертва.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 33

    +1
    Все это от того, что npm не создает lock-файл, как это делает bundler/carton/composer, файл который создавался бы после выполнения «nmp install» и фиксировал бы точные версии всех модулей, которые нужны(даже косвенно) для нашего приложения. Вот lock-файл и нужно добавлять в git.
      0
      Не верно, в package.json вполне можно фиксировать версии, и npm install --save так и делает, суть в другом, т.к. локальные модули имеют приоритет перед глобальными, наличие кода модулей в репе, гарантирует то, что они будут использоваться.
      А lock для _всех_ модулей создать в npm нельзя, так как они имеют иерархическую структуру, и каждая зависимость имеет свой package.json на содержимое которого мы влиять не можем.
      Почитайте внимательнее, все очень подробно расписано, в том числе и вопрос фиксации версий.
        0
        Нельзя только потому что это не предусмотрено. То есть приняли решение не фиксировать.
          0
          А как вы вообще представляете реализацию? Хранить версии для всего дерева? типа:

          {
            'brfs': '0.0.5',
            'brfs/through': '2.2.7',
            'browserify': '2.22.0',
            'browserify/through': '2.3.4',
            'foo/bar/buzz': '123'
          }
          


          Ну может быть это и реализуется технически, но зачем? Ведь можно просто добавить в реп node_modules и не иметь проблем.
          lock был актуален для плоской схемы, в которой нельзя было иметь 2 версии одной и той же библиотеки, а глобальные модули имели приоритет. С npm это проблемы просто нет.
            0
            Ну вот открыл тестовый проектик: в composer.json 1 пакет (phpunit/phpunit), в composr.lock — кроме него ещё 7-8 его зависимостей с зафиксированными версиями, при том что в composer.json версия phunit/phpunit указана достаточно свободно.
              0
              У композера не иерархическая структура модулей, в нем нельзя иметь две версии одной библиотеки в проекте, иерархическая система модулей в npm дает такую возможность.

              Модуль A может зависеть от модуля C v1.7, а модуль B от C v2.0.11 при этом оба модуля А и В бесконфликтно установятся в проект и подтянут свои зависимости локально.

              В моем комменте выше акцент был на том, что мы имеем 2 различные версии библиотеки through. Это кстати пример из реального проекта, с актуальными версиями.

              Но суть даже не в том, что мы можем лочить или нет версии зависимых библиотек, суть в том, что этого вообще не требуется, т.к. можно безбоязненно коммитить node_modules в реп и быть уверенным в том, что проект получит одни и те же библиотеки на всех инсталляциях.
                0
                Отчасти пакетные менеджеры для того и создавались, чтобы не таскать с проектом все его зависимости. Не то чтобы одно или другое однозначно плохо или однозначно хорошо, но лучше иметь альтернативу, чем не иметь. Не согласны?
                  0
                  Отчасти может быть и да, но, на мой взгляд, от очень маленькой части, и вообще, это спорное утверждение. Основная цель пакетного менеджера — поддерживать рабочее окружение проекта. И бандлер справляется с этим хуже чем npm. Отсюда костыли в виде rvm.
          0
          В том же Bundler, который и был предтечей всех современных систем контроля зависимостей, Gemfile.lock именно и предназначен для полной фиксации всех зависимостей приложений, на всю глубину дерева зависимостей.

          Типичный пример:

          Из
          source "https://rubygems.org"
          
          group :jekyll do
            gem "jekyll"
            gem "json"
            gem "nokogiri"
            gem "redcarpet"
            gem "textpow", git: 'https://github.com/regru/textpow.git'
            gem "ultraviolet", git: 'https://github.com/regru/ultraviolet.git'
          end
          


          Получаем
          ➜  bem-guide git:(master) cat Gemfile.lock
          GIT
            remote: https://github.com/regru/textpow.git
            revision: ff81fdebcc72baeed82d80dffd09a58d6aa9802f
            specs:
              textpow (1.3.1)
                plist (>= 3.0.1)
          
          GIT
            remote: https://github.com/regru/ultraviolet.git
            revision: a507f9e422b4b870d3437751b8d2f054165e97de
            specs:
              ultraviolet (1.0.1)
          
          GEM
            remote: https://rubygems.org/
            specs:
              classifier (1.3.3)
                fast-stemmer (>= 1.0.0)
              colorator (0.1)
              commander (4.1.3)
                highline (~> 1.6.11)
              directory_watcher (1.4.1)
              fast-stemmer (1.0.2)
              highline (1.6.19)
              jekyll (1.0.3)
                classifier (~> 1.3)
                colorator (~> 0.1)
                commander (~> 4.1.3)
                directory_watcher (~> 1.4.1)
                kramdown (~> 1.0.2)
                liquid (~> 2.3)
                maruku (~> 0.5)
                pygments.rb (~> 0.5.0)
                safe_yaml (~> 0.7.0)
              json (1.8.0)
              kramdown (1.0.2)
              liquid (2.5.0)
              maruku (0.6.1)
                syntax (>= 1.0.0)
              mini_portile (0.5.0)
              nokogiri (1.6.0)
                mini_portile (~> 0.5.0)
              plist (3.1.0)
              posix-spawn (0.3.6)
              pygments.rb (0.5.1)
                posix-spawn (~> 0.3.6)
                yajl-ruby (~> 1.1.0)
              redcarpet (2.3.0)
              safe_yaml (0.7.1)
              syntax (1.0.0)
              yajl-ruby (1.1.0)
          
          PLATFORMS
            ruby
          
          DEPENDENCIES
            jekyll
            json
            nokogiri
            redcarpet
            textpow!
            ultraviolet!
          
            0
            Bundler позволяет иметь в одном окружении 2 версии одной и той же библиотеки?
              0
              Нет.
                0
                То есть там фактически и нет дерева зависимостей. Список библиотек — плоский, по одной штуке каждой версии. А если требуется поставить 2 зависимости, которые в свою очередь зависят от одной библиотеки, но разных версий, получим конфликты. npm принципиально отличается в этом плане, проблема в том, что люди тащат свои старые привычки в новую среду, не понимая особенностей инструмента, с которым работают. lock был нужен, так как не было гарантии, что приложение будет использовать код из репозитория в новой среде, даже если мы закоммитим зависимости. Теперь он просто не нужен, хотя возможность его сделать есть, как указали в комментах npm shrinkwrap. Но в любом случае, необходимость отпала, нужно просто переступить через свои старые привычки :)
            0
            Почему в bundler/composer таких проблем нет, а в Node.js есть?
              +2
              Вот же… Вся статья как раз о том, что в npm нет проблем, которые есть в bundler/composer, таких как конфликты версий, например.

              Вы можете представить себе 2 больших сложных проекта на рельсах на одной машине и при этом не в сэндбоксе? Это же просто фантастика и сказки. А на ноде, раз плюнуть.
            +1
            Хм:

            $ npm shrinkwrap    
            npm WARN package.json dateformat@1.0.2-1.2.3 No repository field.
            npm WARN package.json vows@0.7.0 No repository field.
            npm WARN package.json growl@1.7.0 No repository field.
            npm WARN package.json ms@0.3.0 No repository field.
            npm WARN package.json eyes@0.1.8 No repository field.
            wrote npm-shrinkwrap.json
            
            $ cat npm-shrinkwrap.json 
            {
              "name": "kantaina",
              "version": "0.1.5",
              "dependencies": {
                "lodash": {
                  "version": "1.2.1",
                  "from": "lodash@~1.2"
                },
                "when": {
                  "version": "2.1.1",
                  "from": "when@~2.1"
                },
                "dep-graph": {
                  "version": "1.1.0",
                  "from": "dep-graph@~1.1",
                  "dependencies": {
                    "underscore": {
                      "version": "1.2.1",
                      "from": "underscore@1.2.1"
                    }
                  }
                },
                "chai": {
                  "version": "1.6.1",
                  "from": "chai@~1.6"
                },
                "chai-as-promised": {
                  "version": "3.3.1",
                  "from": "chai-as-promised@~3.3"
                },
                "sinon-chai": {
                  "version": "2.4.0",
                  "from": "sinon-chai@~2.4"
                },
                "sinon": {
                  "version": "1.7.3",
                  "from": "sinon@~1.7",
                  "dependencies": {
                    "buster-format": {
                      "version": "0.5.5",
                      "from": "buster-format@~0.5",
                      "dependencies": {
                        "buster-core": {
                          "version": "0.6.4",
                          "from": "buster-core@>=0.6.2"
                        }
                      }
                    }
                  }
                },
            ...
            
              0
              Ухты, не знал :)

              Но тем не менее, в описании shrinkwrap есть:

              If you wish to lock down the specific bytes included in a package, for example to have 100% confidence in being able to reproduce a deployment or build, then you ought to check your dependencies into source control, or pursue some other mechanism that can verify contents rather than versions.
            0
            npm shrinkwrap уже посоветовали, и это хорошо. Однако не защищает от ситуации, когда мейнтейнер случайно или по злому умыслу публикует новую версию под тем же номером. И у вас что-то ломается.

            Мы решили эту проблему так — вместе с проектом идёт небольшой кеширующий веб-сервер, который подменяет собой официальный NPM-репозитарий, и кэширует все версии всех пакетов, которые загружаются во время npm install. Кэш потом чекинится в репозиторий нашего проекта, и мы имеем железобетонную гарантию, что ни один байт зависимых пакетов не поменяется.

            Если хотим сделать апгрейд, частично или полностью сносим кэш и делаем npm install заново. Кэш наполняется актуальными версиями пакетов.
              0
              А в чем причина отказа от хранения node_modules в репе?
                0
                Как минимум, потому что там встречаются бинарные модули.
                  0
                  Мне кажется, это не проблема. npm rebuild при деплое их соберёт. Я понимаю, что процесс деплоя уже может быть давно отлажен, хорошо работает и нет смысла что-то менять. Просто ищу аргументы против node_modules в репе. Пока внятных не нашел :)
                    0
                    Теоретически, build-скрипт может что-то поменять в сорцах, и эти изменения будут попадать в репозиторий при последующих коммитах. Практически, может такого никогда и не встретится. Я не стал рисковать в этом плане.
                      0
                      Хм… Не сталкивался с такими build-скриптами, и имею привычку смотреть git status и git diff перед коммитом. Может быть какой-то смысл в этом есть, но тут уже ответственность полностью на разработчике, и всегда есть возможность откатиться. Ну а от всего не застрахуешься, по-пьяни можно и rm -rf /* на сервере набрать.
              • UFO just landed and posted this here
                  0
                  Насколько я могу судить, на сáмом-то деле всё именно так и устроено.

                  Автор пакета не может опубликовать новую версию под старым номером совершенно случайно, потому что никто не наберёт случайно «npm publish --force» вместо простого «npm publish». (Это была бы не такая случайность, когда подбрасываешь монету и выпадает решка — а такая случайность, когда подбрасываешь монетку, а выпадает полуторатонный вилочный штабелеукладчик.) Если автор пакета вдруг команду «npm publish --force» набрал и запустил, то можно быть совершенно уверенным в том, что именно это он и имел в виду, то есть руководствовался некоторыми соображениями — скорее всего, совершенно разумными.

                  Поэтому вызывает понятное удивление то обстоятельство, что Роджерс сперва пишет «Как сообщество, мы должны распределять между собой часть интеграционного тестирования», а затем теми же руками сомневается в том, что автор пакета знает, что он делает. И в том же aml сомневается.

                  Впрочем, надо признаться, что в известной мере я сужу здесь по себе. Последний раз я «npm publish --force» запускал буквально сегодня утром — и хорошо знаю о себе, что у меня была на то веская причина: я изменил текст README, а больше ничего. И так как кто угодно (и когда угодно) может зайти на Гитхаб и прочесть там свежайший текст README, то не великá беда, если у кого-то в пакете README останется старым из-за того, что «npm update имяМодуля» не сработает без роста номера версии. С другой стороны, новым потребителям пакета (которые только-только ещё «npm install имяМодуля» набирают) неплохо бы иметь новую версию README в нём. Так что в этаком случае уместно подменить последнюю версию пакета, а не выпускать новую версию, которая только реестр npm засоряет.

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

                  +1
                  Пакет A зависит от пакета B версии 1.
                  Пакет C тоже зависит от пакета B, но версии 2.
                    +1
                    Не проверял их на конфликтность но на моем текущем проекте есть по 3 версии commander, methods и mkdirp, и по 2 версии bson, cookie, esprima, formidable, lru-cache, mime, minimatch, mongodb, pause, pkginfo, sigmund, sliced, uglify-js, underscore. Такое происходит, когда разные библиотеки, которые ты зарекваирил тянут за собой в зависимостях разные версии одной и той же библиотеки. Ситуация вполне типичная для большого проекта.
                      0
                      Одна из рекомендаций для разработиков — как можно более явно указывать версию зависимости, несмотря на возможность указать любой диапазон или вообще "*" в значении «любая последняя версия». npm install --save тоже дописывает конкретную версию пакета. В результате, отслеживание и обновление зависимостей ложится на маинтейнеров пакетов. Если вы посмотрите, к примеру, дерево зависимостей cloud9 IDE — обязательно наткнетесь на несколько разных версий. Для отслеживания обновления зависимостей удобно пользоваться gemnasium.com — он присылает письмо, если какая-то из зависимостей обновлена.
                      0
                      Мне хочется подметить, и подмечу, что за полтора года, прошедших со времени публикации Роджерсом этой блогозаписи, я так и не мог совершенно понять, возобладала ли его точка зрения или не особенно возобладала.

                      А всё потому, что за это время я повидал много-много репозиториев с кодами модулей и библиотек для Node (которые массово «node_modules» добавляют у себя в .gitignore да в .npmignore, но которым это и Роджерс рекомендует). Зато повидал очень мало репозиториев с кодами приложений и сайтов. (Может быть, и ни одного не повидал такого.)

                      Это у совета Роджерса невеликá аудитория, или это у меня невелик кругозор?

                      Может ли кто-нибудь из тутошних читателей назвать с полдесятка (а не то и с десяток) наиболее популярных приложений и сайтов с открытым исходным кодом для Node — чтобы можно было посмотреть в их репозиториях списки игнорируемых файлов да сделать вывод о том, насколько Роджерс был услышан, насколько методика его пошла в массы?
                        +1
                        Я совсем никакой не разработчик, но мне кажется, это как-то неправильно, помещать в репозиторий с кодом проекта на может пару сотен, ну тысяч строк динамично обновляющуюся папку, которая может содержать десятки тысяч изменяющегося кода. С каждым коммитом помимо истории непосредственно изменений проекта будут кучи diff'ов файлов, до которых по сути нет никакого дела.

                        Если и хранить где-нибудь node_modules, то в отдельном репозитории, связанном субмодулем с основным. Но всё равно как-то не особо правильно. Было бы здорово, если бы был .lock файл, что выше говорили. Чтобы можно было написать npm lock, оно вернуло json с текущими зависимостями с конкретными версиями (которые сейчас локально), а потом или npm install менял своё поведение при наличии такого файла (при этом ругаясь, если он расходится с зависимостями в package.json), или отдельная команда, которая бы подтягивала конкретные зависимости. Хотя вариант с install лучше, мне кажется. Всякие там heroku бы правильно подтягивали бы зависимости, без каких-либо модификаций.
                          0
                          Папка node_modules не обновляется сама по себе, если по какой-то причине разработчик решил обновить версии модулей, это изменение конечно же пойдет отдельным коммитом с соответствующим commit message. Для аналога lock в npm, как уже указали, есть npm shrinkwrap.
                          0
                          Хранить всё в репозитории это конечно хорошо. Потому что деплоить мы будем ровно то, что тестировали. И при этом не зависим от всяких сторонних тормозных сервисов типа npm.org. Более того, по аналогии с heroku в репозиторий стоит заливать и образ системы на которой всё это будет крутиться. Ой, как-то дофига весить оно будет ;-)

                          На самом деле всё проще: фиксируем версии или нет — не важно. деплоим на тестовый сервер, прогоняем тесты, если всё хорошо — клонируем тестовый сервер на боевой. не надо на каждом продакшен сервере запускать по деплою с его выкачиванием исходников, выкачиванием модулей, сборкой скриптов/стилей/спрайтов, компиляцией в байт-код… недавно ж даже битторрент выпустил приблуду для п2п синхронизации директорий.

                          а вот насчёт «офигенной фичи» иметь несколько модулей разных версий — тут я бы поспорил. Что есть в новой версии какого-то модуля пофиксили дыру в безопасности? Или существенно увеличили скорость? Да и вообще, иметь несколько версий одного модуля — лишнее потребление памяти. особенно, если модуль держит внутри себя кэш. Нафиг такое счастье. Как разработчик приложения я хочу чтобы все библиотеки были последней версии. И было бы классно, чтобы разработчики библиотек тоже шли в ногу со временем. А если не поспевают — им помогут те, кому больше всех надо. А поощрять использование устаревших версий — это плохо. В конце концом, если вылезла какая-то совсем большая несовместимость, которую самому не поправить — ну что ж, не будем обновлять этот злополучный модуль (а остальные будем) или найдём ему замену.
                            +1
                            Security фиксы — это одно, а изменение api — другое. Библиотеки обычно привязываются к мажорной ветке зависимости, например mkdirp: '0.3.x'. В рассчете на то, что в 'x' как раз будут исправления безопасности, и в то же время можно рассчитывать на стабильный api.

                          Only users with full accounts can post comments. Log in, please.