После того, как произошла ситуация с удалением пакетов из NPM, которая затронула огромное количество пользователей пакетов babel, jscs и многих других (об этом можно почитать здесь: «A discussion about the breaking of the Internet»), многие разработчики Open Source начали рассуждать о будущем NPM и экосистемы JS в целом.

Мнения разделились:


Ну а команды крупных Open Source пакетов, на которые свалилось огромное количество тикетов в github, всерьез задумались о том, как не допустить такой ситуации в будущем. Особенно пострадали те проекты, которые часто используются в CI, так как именно пользователи CI в первую очередь заметили проблемы с отсутствующими зависимостями. Одним из таких проектов является ESLint, имеющий порядка 70 тысяч установок в день, большинство из которых приходится на CI-сборки.

Решение команды разработчиков ESLint было довольно радикальным, но вполне рабочим. Они решили опубликовать пакет ESLint со всеми зависимостями. То есть скачивая архив ESLint, утилита npm скачает также и весь рабочий node_modules для необходимой версии ESLint.

Подробнее об этом изменении: «ESLint v2.5.0 released».

Первая же мысль, которая пришла мне в голову после того, как я узнал эту новость, была о производительности. Что станет с установкой ESLint? Станет ли она быстрее или медленнее. Небольшой эксперимент показал, что установка пакета со встроенными зависимостями занимает больше времени. В случае ESLint — на 2 секунды (~25%):

npm install eslint@2.4.0  6.22s user 2.68s system 108% cpu 8.207 total
npm install eslint@2.5.0  7.70s user 4.16s system 109% cpu 10.864 total

Полный вывод
$ rm -Rf node_modules && time npm install eslint@2.4.0 && rm -Rf node_modules && time npm install eslint@2.5.0

eslint@2.4.0 node_modules/eslint
+-- path-is-absolute@1.0.0
+-- ignore@2.2.19
+-- pluralize@1.2.1
+-- path-is-inside@1.0.1
+-- globals@8.18.0
+-- estraverse@4.2.0
+-- strip-json-comments@1.0.4
+-- esutils@2.0.2
+-- progress@1.1.8
+-- text-table@0.2.0
+-- user-home@2.0.0 (os-homedir@1.0.1)
+-- is-resolvable@1.0.0 (tryit@1.0.2)
+-- shelljs@0.5.3
+-- json-stable-stringify@1.0.1 (jsonify@0.0.0)
+-- resolve@1.1.7
+-- debug@2.2.0 (ms@0.7.1)
+-- doctrine@1.2.0 (esutils@1.1.6, isarray@1.0.0)
+-- optionator@0.8.1 (fast-levenshtein@1.1.3, type-check@0.3.2, levn@0.3.0, wordwrap@1.0.0, deep-is@0.1.3, prelude-ls@1.1.2)
+-- mkdirp@0.5.1 (minimist@0.0.8)
+-- require-uncached@1.0.2 (resolve-from@1.0.1, caller-path@0.1.0)
+-- chalk@1.1.1 (escape-string-regexp@1.0.5, supports-color@2.0.0, has-ansi@2.0.0, strip-ansi@3.0.1, ansi-styles@2.2.0)
+-- concat-stream@1.5.1 (inherits@2.0.1, typedarray@0.0.6, readable-stream@2.0.6)
+-- espree@3.1.3 (acorn@3.0.4, acorn-jsx@2.0.1)
+-- is-my-json-valid@2.13.1 (jsonpointer@2.0.0, generate-function@2.0.0, xtend@4.0.1, generate-object-property@1.2.0)
+-- inquirer@0.12.0 (ansi-regex@2.0.0, strip-ansi@3.0.1, ansi-escapes@1.3.0, figures@1.5.0, rx-lite@3.1.2, through@2.3.8, cli-width@2.1.0, run-async@0.1.0, cli-cursor@1.0.2, string-width@1.0.1, readline2@1.0.1)
+-- table@3.7.8 (slice-ansi@0.0.4, tv4@1.2.7, xregexp@3.1.0, strip-ansi@3.0.1, string-width@1.0.1, bluebird@3.3.4)
+-- js-yaml@3.5.5 (esprima@2.7.2, argparse@1.0.7)
+-- glob@6.0.4 (inherits@2.0.1, inflight@1.0.4, once@1.3.3, minimatch@3.0.0)
+-- file-entry-cache@1.2.4 (object-assign@4.0.1, flat-cache@1.0.10)
+-- es6-map@0.1.3 (d@0.1.1, es6-symbol@3.0.2, event-emitter@0.3.4, es6-iterator@2.0.0, es6-set@0.1.4, es5-ext@0.10.11)
+-- escope@3.6.0 (esrecurse@4.1.0, es6-weak-map@2.0.1)
L-- lodash@4.6.1
npm install eslint@2.4.0  6.22s user 2.68s system 108% cpu 8.207 total

eslint@2.5.0 node_modules/eslint
npm install eslint@2.5.0  7.70s user 4.16s system 109% cpu 10.864 total

Для включения зависимостей в архив используется настройка bundledDependencies для package.json (подробнее: «package.json bundledDependencies»). Настройка эта нехитрая: принимает массив имен пакетов, которые будут включены в публикуемый архив.

Для автоматизации процесса публикации зависимостей, в ESLint используют небольшой пакет bundle-dependencies, который попросту формирует bundledDependencies из dependencies и записывает в package.json.

По сути данный подход несколько противоречит идеям NPM, ведь опция bundledDependencies была придумана для того, чтобы включать в архив те зависимости, которые не опубликованы в реестре NPM.

Мы в JSCS еще не решили, что мы будем делать. Будет интересно наблюдать за тем, как другие команды разработчиков Open Source проектов отреагируют на ситуацию с NPM (и будут ли реагировать). Посмотрим, приживется ли данный подход или найдутся иные альтернативы.

UPD: Из-за появившихся проблем, в ESLint откатили изменения, связанные с bundledDependencies. Подробнее: «npm shrinkwrap fails >= 2.5.0».

Комментарии (21)


  1. DenQ
    28.03.2016 13:03
    +10

    Что-то мне подсказывает, что скоро появится новый npm.


    1. SerafimArts
      28.03.2016 13:28
      +1

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

      Если по-хорошему — имена пакетов стоит переделать на вид "юзер/название" — это показало себя с хорошей стороны в composer (php), что позволит исключить всем известную проблему с kik. Зависимости стоит ставить в одну единственную папку с путями вида: "node_modules/username/package/{version}/..." — подобный способ показал себя с хорошей стороны в бандлере (ruby) и исключает рекурсию в зависимостях.


      1. shoomyst
        28.03.2016 13:46

        Как такой пакет с версией подключается в коде?


        1. SerafimArts
          28.03.2016 14:59

          В случае бандлера — есть некоторый конфиг, генерируемый после установки, который знает о версии, которая требуется для нужного пакета и обычный require('some') должен подключать именно её, а для другой зависимости свою версию. В случае языков с наличием модулей и манки-патчингом такие операции не представляют сложностей.


      1. vintage
        28.03.2016 13:48
        +7

        1. SerafimArts
          28.03.2016 14:52
          +2

          Да, npm -v => 2.11.3, что-то я отстал от жизни. Спасибо большое, претензии снимаю.


          1. heilage
            29.03.2016 06:09

            Зря снимаете :) Случаев, когда версия модуля-зависимости не совпадает и потому ставится не в корень, а в node_modules модуля, объявившего зависимость — может оказаться субъективно больше (на моем проекте это так, например).
            Кроме того, я в упор не понимаю, как оно решает, какую версию поставить в корень, похоже оно туда тащит первую попавшуюся в списке зависимостей.


            1. SerafimArts
              29.03.2016 14:11

              Мдэ. Я ещё не успел ничего проверить и порадоваться как всё стало замечательно, а тут уже такой облом :)


      1. DenQ
        28.03.2016 19:29

        Меня лично npm как продукт устраивает. Нот политика администрации — не очень(в связи с последними событиями).


    1. MarcusAurelius
      29.03.2016 01:57

      Мои студенты не дадут соврать, я 3 недели назад на лекции в КПИ рассказывал, что NPM имеет много проблем, а 99% модулей — полоный треш, ничего не тестируется на совместимость, и что нам нужен новый менеджер пакетов в ноде с новой стратегией, ибо так не может быть дальше.


      1. vintage
        29.03.2016 02:01

        Как минимум стоит автоматически прогонять все публикуемые модули через модульные тесты и требовать 90% покрытия.


        1. MarcusAurelius
          29.03.2016 03:19

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


    1. Asgator
      29.03.2016 10:29
      +1

      Что-то подобное уже есть.
      github.com/rstacruz/pnpm


  1. jbaruch
    28.03.2016 17:33
    +1

    А почему бы им (ну, и вам) не взять, repository manager? Artifactory какой-нибудь? Это решает проблему и не требует пере-изобретать велосипед. Потому что "скачайте наш uber-jar" мы проходили 12 лет назад и благополучно забыли как страшный сон.


  1. fog
    28.03.2016 22:10
    +1

    Ха, а когда я оправдывал такой подход (предполагая такую ситуацию) меня заминусовали ;)
    https://habrahabr.ru/post/268871/


    1. jbaruch
      28.03.2016 23:04

      Потому что "скачайте наш uber-jar" мы проходили 12 лет назад и благополучно забыли как страшный сон.


  1. apla
    28.03.2016 23:43

    А есть ли ссылка? А то утверждение выглядит несколько голословным:

    По сути данный подход несколько противоречит идеям NPM, ведь опция bundledDependencies была придумана для того, чтобы включать в архив те зависимости, которые не опубликованы в реестре NPM


  1. egoroof
    29.03.2016 10:29

    ESLint выпустили патч v2.5.3 в котором отказались от bundledDependencies из-за появившихся проблем github.com/eslint/eslint/issues/5687.


    1. mdevils
      29.03.2016 10:31

      Ого, интересный поворот :)


  1. ckr
    29.03.2016 21:17
    -1

    Я всегда утверждал, что нужно бороться с количеством зависимостей. Ни в коем случае нельзя допускать "двух-килобайтные" зависимости. Да и, если используешь не всю мощь пакета, со временем надо щипать необходимый код зависимости и вставлять в свой, со своими тестами.
    Ни раз меня ругали за это. А потом удивляются, как так получается, что сайт работает на expressjs, а в папке node_modules поиском можно найти три разные версии connect.
    На сегодняшний момент ~10% кода вручную копирую из старых проектов в свежесозданные. По крайней мере, за пару лет, я переписал все middleware (и еще некоторые модули), которые использую в разработке и продакшене. Что я там только не находил, left-pad — это еще цветочки! В каком-то из пакетов видел реализацию урезанной версии util.inspect().
    По началу было тяжело, приходилось самому следить за обновлениями зависимостей и внедрять их вручную. Но, чем дольше живет npm-пакет, тем реже в него вносятся обновления (да и сами обновления все меньше и меньше). Последние пол года уже за ними не слежу. Все костыли, которые вносят в старые пакеты, к моему коду уже почти отношения не имеют.
    Единственное, в одном я упростил себе задачу — не заморачиваюсь с кроссплатформенностью. Так уж получилось, что работаю и развертываю код на Linux, мне не важно как код работает в Windows или в браузере.


  1. zorro1211
    03.04.2016 17:07

    Вот достаточно интересное решение (https://github.com/JamieMason/shrinkpack), которое решает группу проблем:
    1) не нужно постоянно скачивать с registry.npmjs.org
    2) Каждый пара пакет/версия может быть добавлена в репозиторий как единый тар-архив, что помогает избегать проблем с сотнями файлов во время слияния веток.
    3) Помогает избегать проблемы кроссплатформенных зависимостей в репозитории
    4) Расширяет возможности npm shrinkwrap