NPX, или Прощайте, глобальные зависимости
Гасконцам от программирования посвящается.
Когда дело касается глобальных пакетов, все говорят, что это зло. Однако через некоторое время в файле README.md странным образом обнаруживается инструкция типа:
npm install -g typescript
Самые яростные кричат: «Тысяча чертей, я же сто раз говорил не делать этого!» На что слышат невнятный ответ: «Так наш пакет не собирался локально, дебажить мы не могли». Что тут скажешь? Давайте же разберемся, являются ли глобальные NPM-пакеты вселенским злом.
Для чего вообще нужны глобальные зависимости
Первое и самое главное: глобальные пакеты вносят раздрай в стройные ряды команды. И сколько бы вы ни требовали: «Каждый день обновляйте свои глобальные пакеты на рабочих машинах перед началом работы, да и вообще перед каждым коммитом», — это только воздух сотрясать. В один прекрасный день придет QA и скажет: «Написанное вами у меня не билдится!»
Если вы решите настроить CI/CD-систему, то все восклицания: «У нас прекрасно собирается локально!» — пойдут Бобику под хвост, потому что версии у вас и на билд-машине будут совершенно разные.
Теперь немного о безопасности. Знаете ли вы, что все глобальные NPM-пакеты ставятся под root в Linux и macOS? Не знаете? Так вот, они ставятся под root. (Кстати, чтобы избежать этого, предлагаю прочесть материал по ссылкам 1 и 2 в конце статьи.)
И еще раз о «Месье, у меня работает локально». Метод require в ноде резолвит имена зависимостей следующим образом:
- Вначале проверяет собственные
node_modules
проекта. - Потом require смотрит в глобальные модули, которые для Unix-систем и для macOS лежат по адресу
/usr/local/lib/node_modules
(вы можете узнать этот адрес, набрав командуnpm root -g
). - А далее, спускаясь от директории вашего юзера к вашей рабочей директории, он будет искать зависимости во всех встреченных им node_modules-папках.
А теперь рассмотрим следующий широко распространенный случай. У меня есть кусок кода, ссылающийся на модуль, который я перетер в своих локальных зависимостях (на самом деле я беру самый простой вариант — на практике они еще более изощренные). Локально все работает на ура, потому что в глобальных модулях сидит предустановленная мной когда-то старая зависимость. Но потом я буду долго мучиться и анализировать логи в поисках причины.
Казалось бы, что сразу приходит на ум после таких доводов? Так как глобальные зависимости являются злом, нужно убрать их в локальные. Этот путь имеет смысл — ваш покорный слуга даже сейчас делает так для некоторых проектов. Глобальные зависимости убираются в дев-депенденси, где тихо доживают свой век. Скрипты npm будут с радостью подхватывать их не хуже глобальных. Казалось бы, и волки сыты, и овцы целы. Однако такие действия угрожают большой перегрузкой дев-секции и, как следствие, способствуют долгому выполнению команды npm install, а также ведут к большому потреблению дискового пространства на девелоперской машине (пять проектов — пять карм, три еслинта, четыре тайпскрипта). То есть мы приходим к тому, зачем писались глобальные депенденси, — и это повторное потребление места различными пакетами для одних и тех же зависимостей.
NPX как способ уйти от глобальных пакетов
Но теперь рассмотрим новый путь. Новым путем будет использование на проекте NPX. Этот инструмент предназначен для облегчения запуска скриптов — и снижения зависимости разработчика и от глобальных, и от локальных привязок в целом. Как это работает?
NPX — это инструмент, который нужен для упрощения использования утилит и исполняемых файлов. Также он помогает использовать утилиты без run-скрипта и запускать команды с различными версиями ноды. Для тех, кто хочет больше узнать именно об NPX, я рекомендую материал по ссылке 3 в конце статьи, где в легкой форме изложены базовые принципы и возможности использования этой утилиты.
Допустим, я разработчик на Angular 8, но упорно не хочу ставить Angular CLI себе в глобальные сборки: он устареет, и мне надо будет каждый раз переставлять его. Есть выход — делать все руками. Но я ленивый программист, обладающий знаниями shell. И что я делаю в таком случае? Те программы, которые вызываются крайне редко, я буду вызывать так, как есть — через NPX. И для создания нового проекта на Angular мне нужно будет набрать следующую команду:
$npx -p @angular/cli ng new my-new-project
Определившись с тем, чего я хочу, получаю следующий проект с секцией скриптов в project.json.
"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }
Сейчас вы скажете: «Ага, ничего не получилось». Действительно, запустить я ничего не смогу, потому что у меня нет Angular CLI на локальном устройстве. А теперь давайте задумаемся, что такое ng? А ng — это всего лишь алиас. И потому мы пишем лаконичную строку в shell (для людей, пользующихся ОС Windows, такой трюк не сработает):
$alias ng='npx -p @angular/cli ng'
И теперь мы можем создавать и запускать новые проекты, компоненты, тесты и т. д. Следующей командой проверяем, не появилось ли новых глобальных зависимостей:
npm list -g --depth=0
Кроме уже существующих зависимостей, не должно ничего появиться.
И как только захотим убрать этот алиас, мы делаем команду:
unalias ng
В итоге
Достоинства способа:
- Мы можем полностью уйти от глобальных пакетов.
- Мы всегда пользуемся последней версией пакета.
- У всей команды и у всех рабочих окружений версия пакета будет одна и та же. (Это утверждение спорное, но пускай останется для беседы с прочитавшими его).
- Для создания алиасов можно написать простой скрипт и запускать его на разных окружениях в качестве install-скрипта.
Недостатки способа:
- Тратится время на загрузку пакета (хотя, если пакет уже прогрузился, npx кеширует его).
- Не работает под Windows. Самый главный минус этого способа, однако если использовать разработку в контейнере, то он вместе с проблемами Windows отходит на второй план.
В качестве дополнения хотелось бы сказать, что автор не топит за какой-то отдельный фреймворк. Angular использовался только как пример. С этим способом работают любые тестовые фреймворки, бойлерплейты, такие как React, и любые CLI.
Полезные ссылки, о которых шла речь в статье:
- Resolving EACCES permissions errors when installing packages globally.
- Install npm packages globally without sudo on macOS and Linux.
- Представляем npx: утилиту для запуска npm-пакетов.