После того, как произошла ситуация с удалением пакетов из NPM, которая затронула огромное количество пользователей пакетов babel, jscs и многих других (об этом можно почитать здесь: «A discussion about the breaking of the Internet»), многие разработчики Open Source начали рассуждать о будущем NPM и экосистемы JS в целом.
Мнения разделились:
- Одни ушли в критику слишком простых пакетов: «NPM и left-pad: мы разучились программировать?».
- Другие задумались о монополии NPM: «One programmer almost broke the internet by deleting 11 lines of code».
- Третьи заговорили о том, насколько хрупка экосистема JS: «Is left-pad Indicative of a Fragile JavaScript Ecosystem?».
Ну а команды крупных 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)
└── 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 (и будут ли реагировать). Посмотрим, приживется ли данный подход или найдутся иные альтернативы.
Автор: mdevils