Python conf in Kharkiv, Nov 16 with Intel, Elastic engineering leaders. Prices go up 21.10

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 :))

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