Начало любого JavaScript проекта сопровождается амбициозным намерением - использовать как можно меньше npm пакетов в процессе разработки. Но сколько бы усилий мы не предпринимали, рано или поздно пакеты начинают накапливаться. Со временем строк в package.json становится всё больше, а благодаря package-lock.json пул реквесты приобретают все более устрашающий вид со всеми своими дополнениями или удалениями в процессе добавления зависимостей.

"И нормально", - скажет лидер команды разработчиков, а все остальные только кивнут в ответ. А что еще остается делать в такой ситуации? Ведь у нас есть счастливая возможность наблюдать, как оживает и процветает экосистема JavaScript. Нам не нужно каждый раз изобретать колесо и ломать голову над вопросами, которые уже давно решены сообществом открытого ПО.

Предположим, вы захотели создать блог и выбрали для этого Gatsby.js. Попробуйте установить и сохранить его в число ваших зависимостей. Поздравляю! Вместе с этим фреймворком вы только что получили 19000 дополнительных зависимостей. Как вам такой подарок? До какой же степени может разрастаться дерево зависимостей JavaScript? Как же мы оказываемся в аду зависимостей? Давайте копнем поглубже и выясним.

Что же такое пакет JavaScript?

npm, менеджер пакетов, входящий в состав Node.js, содержит самый полный реестр пакетов JavaScript в мире! Он больше, чем RubyGems, PyPi и Maven вместе взятые! Данные приведены согласно исследованиям веб-сайта Module Counts, который отслеживает количество пакетов самых популярных реестров.

"Ничего себе сколько кода", - подумали вы. Так и есть. Чтобы фрагмент вашего кода стал npm пакетом, в проекте нужно использовать package.json. Именно так код становится пакетом, который вы можете отправить в npm реестр.

Что такое package.json?

Согласно определению package.json:

  • выводит список пакетов, от которых зависит ваш проект (перечисляет зависимости);
  • определяет версии пакета, которые могут использоваться в проекте, руководствуясь правилами семантического управления версиями;
  • обеспечивает воспроизводимость сборки, и таким образом облегчает ее совместное использование с другими разработчиками.

Для полной картины просто представьте README на стероидах. Вы можете определять зависимости пакета, писать сборку и тестировать скрипты, а также версионировать пакет по усмотрению и описывать его функционал. Для нас же наибольший интерес представляет возможность определять зависимости внутри package.json. 

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

Типы зависимостей в package.json

Чтобы прояснить вопрос накопления зависимостей с течением времени, рассмотрим разные типы зависимостей проекта. В пакете package.json встречаются несколько из них:

  • dependencies - основные зависимости, которые вы можете использовать и вызывать в коде проекта.
  • devDependencies - зависимости разработки, например библиотека Prettier для форматирования кода.
  • peerDependencies - равноправные зависимости, при включении которых в package.json, вы сообщаете человеку, устанавливающему ваш пакет, что ему нужна та же зависимость с указанной версией.
  • optionalDependencies - это необязательные зависимости. Если во время установки с ними возникнут какие-то проблемы, то это не повлияет на удачное завершение всего установочного процесса.
  • bundledDependencies - это массив пакетов, которые объединяются с вашим пакетом. Они пригодятся, если вы захотите использовать стороннюю библиотеку, не входящую в npm, или включить некоторые проекты в качестве модулей.

Назначение package-lock.json

Всем известен тот самый файл, который получает много дополнений и удалений в пул реквестах, и это принимается как должное. package-lock.json автоматически создается каждый раз при изменении файла package.json или директории node_modules. Он сохраняет в неизменном виде дерево зависимостей, созданное при установке, чтобы все последующие зависимости могли создавать идентичное дерево. Это решает проблему, при которой у меня одна зависимость, а у вас другая. 

Рассмотрим проект, имеющий среди своих зависимостей React. Если вы перейдете в package-lock.json, то увидите:

"react": {
    "version": "16.13.0",
    "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz",
    "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==",
    "requires": {
        "loose-envify": "^1.1.0",
        "object-assign": "^4.1.1",
        "prop-types": "^15.6.2"
    }
}

package-lock.json является длинным списком зависимостей в проекте. Он указывает их версию, положение модуля (URI), хэш, отображающий взаимодействие модулей и необходимых для них пакетов. Продолжив чтение списка, вы найдете каждую запись для каждого пакета, необходимого для React и т.д. Вот тут-то и находится настоящий ад зависимостей. Он определяет все, что нужно проекту.

Разбираемся с зависимостями Gatsby.js

Итак, как же нам выйти из ситуации, в которой при установке одной зависимости мы получили в нагрузку 19 000? Ответ -  зависимости зависимостей. Вот что происходит при установке Gatsby.js:

$ npm install --save gatsby
...
+ gatsby@2.19.28
added 1 package from 1 contributor, removed 9 packages, updated 10 packages and audited 19001 packages in 40.382s

В package.json можно увидеть только одну зависимость. Но присмотревшись к package-lock.json, нельзя не заметить новорожденного монстра, раскинувшего свои 14 тысяч строк. Более детальную информацию можно получить в package.json, расположенном в GitHub репозитории Gatbsy.js. По подсчетам npm число прямых зависимостей составляет 136. А теперь представьте, что каждая из этих зависимостей имеет еще одну зависимость, и в итоге вы получаете 272 зависимости. И это я еще преуменьшил! В действительности у каждой зависимости может быть больше одной зависимости, так что их список продолжит пополняться.

Например, посмотрим, сколько библиотек требует lodash.

$ npm ls lodash
example-js-package@1.0.0
└─┬ gatsby@2.19.28
  ├─┬ @babel/core@7.8.6
  │ ├─┬ @babel/generator@7.8.6
  │ │ └── lodash@4.17.15  deduped
  │ ├─┬ @babel/types@7.8.6
  │ │ └── lodash@4.17.15  deduped
  │ └── lodash@4.17.15  deduped
  ├─┬ @babel/traverse@7.8.6
  │ └── lodash@4.17.15  deduped
  ├─┬ @typescript-eslint/parser@2.22.0
  │ └─┬ @typescript-eslint/typescript-estree@2.22.0
  │   └── lodash@4.17.15  deduped
  ├─┬ babel-preset-gatsby@0.2.29
  │ └─┬ @babel/preset-env@7.8.6
  │   ├─┬ @babel/plugin-transform-block-scoping@7.8.3
  │   │ └── lodash@4.17.15  deduped
  │   ├─┬ @babel/plugin-transform-classes@7.8.6
  │   │ └─┬ @babel/helper-define-map@7.8.3
  │   │   └── lodash@4.17.15  deduped
  │   ├─┬ @babel/plugin-transform-modules-amd@7.8.3
  │   │ └─┬ @babel/helper-module-transforms@7.8.6
  │   │   └── lodash@4.17.15  deduped
  │   └─┬ @babel/plugin-transform-sticky-regex@7.8.3
  │     └─┬ @babel/helper-regex@7.8.3
  │       └── lodash@4.17.15  deduped
  ...

К счастью, большинство из них используют одну и ту же версию lodash, которая требует установки лишь одной библиотеки lodash в node_modules. Но в реальных проектах продакшена ситуация не такая оптимистичная: иногда разные пакеты требуют разных версий других пакетов. В связи с этим как только не шутили по поводу тяжелого веса директории node_modules! В нашем же случае не все так плохо:

$ du -sh node_modules
200M    node_modules

200 мегабайт, как я уже говорил, совсем неплохой результат. Бывали случаи, когда размер директории мог легко перевалить за 700 мегабайт. Если вам интересно, какие модули занимают большую часть памяти, можете выполнить следующую команду:

$ du -sh ./node_modules/* | sort -nr | grep '\dM.*'
 17M    ./node_modules/rxjs
8.4M    ./node_modules/@types
7.4M    ./node_modules/core-js
6.8M    ./node_modules/@babel
5.4M    ./node_modules/gatsby
5.2M    ./node_modules/eslint
4.8M    ./node_modules/lodash
3.6M    ./node_modules/graphql-compose
3.6M    ./node_modules/@typescript-eslint
3.5M    ./node_modules/webpack
3.4M    ./node_modules/moment
3.3M    ./node_modules/webpack-dev-server
3.2M    ./node_modules/caniuse-lite
3.1M    ./node_modules/graphql
...

Ага, rxjs, ну и хитрая же ты штучка. Есть одна простая команда, которая поможет вам с размером node_modules и уменьшением дублирования зависимостей - npm dedup:

$ npm dedup
moved 1 package and audited 18701 packages in 4.622s

51 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Действие дедупликации призвано упростить структуру дерева зависимостей путем поиска общих пакетов между ними и их перемещением для последующего переиспользования. Как раз то, что происходит в нашем примере с lodash. Большинство пакетов останавливаются на lodash@4.17.15, поэтому нет других версий lodash для установки. Мы добились этого результата в самом начале, так как только что установили наши зависимости, но если вы в течение какого-то времени добавляли зависимости в package.json, то лучше выполнить npm dedup.

Чем больше сила, тем больше ответственность

Подводя итоги, без преувеличения скажу, что JavaScript и npm просто супер, а возможность гибкого подхода при выборе из океана зависимостей - еще лучше. Сущий пустяк - выполнить npm install для сохранения пары строк кода, но иногда мы почему-то забываем, что скрывается за этим действием.