×Закрыть

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

В итоге

Достоинства способа:

  1. Мы можем полностью уйти от глобальных пакетов.
  2. Мы всегда пользуемся последней версией пакета.
  3. У всей команды и у всех рабочих окружений версия пакета будет одна и та же. (Это утверждение спорное, но пускай останется для беседы с прочитавшими его).
  4. Для создания алиасов можно написать простой скрипт и запускать его на разных окружениях в качестве install-скрипта.

Недостатки способа:

  1. Тратится время на загрузку пакета (хотя, если пакет уже прогрузился, npx кеширует его).
  2. Не работает под Windows. Самый главный минус этого способа, однако если использовать разработку в контейнере, то он вместе с проблемами Windows отходит на второй план.

В качестве дополнения хотелось бы сказать, что автор не топит за какой-то отдельный фреймворк. Angular использовался только как пример. С этим способом работают любые тестовые фреймворки, бойлерплейты, такие как React, и любые CLI.

Полезные ссылки, о которых шла речь в статье:

  1. Resolving EACCES permissions errors when installing packages globally.
  2. Install npm packages globally without sudo on macOS and Linux.
  3. Представляем npx: утилиту для запуска npm-пакетов.
LinkedIn

9 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.
для людей, пользующихся ОС Windows, такой трюк не сработает

Не такой уж это и трюк, чтоб его нельзя было повторить на винде.

Использую npx под виндой — все отлично работает

Я думаю автор имел ввиду, что нельзя сделать алиас ток командой, что он описал, а сам npx работает нормально.

@Nick, ну зачем этот велосипед? Закинули пакет в dependencies/devDependenies, зафиксировали версию в package-lock.json и все работает.

Когда дело касается глобальных пакетов, все говорят, что это зло

Не глобальные пакеты зло, а неявные зависимости.

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

Версия пакета будет зависит от времени запуска npx и содержимого его кэша. То есть этот метод убивает repetability.

npx создан, чтобы запускать одноразовых скриптов, без их глобальной установки. Пример, npx sort-package-json

Я бы не назвал это велосипедом — скорее лайфхак. NPX это не только запуск одноразовых скриптов — вот тут прекрасно все описано medium.com/...​ckage-runner-55f7d4bd282b

Чего только не придумают, чтоб Docker не юзать

кое-то когда-то писал ./node_modules/jest/bin/jest.js —init :(((

ну не — уж писать так ./node_modules/.bin/jest -init :))

Подписаться на комментарии