Не пропусти это при работе с Nx
Работая с монорепозиторием, ты наверняка слышал о наборе инструментов Nrwl Nx. Если вкратце, то Nx ускоряет и упрощает работу с монорепой, снабжает полезными утилитами. Держи документацию.
Часто работа с Nx заканчивается на имеющихся дефолтных настройках. Все остальное остается черным ящиком. Время сборки проектов начинает расти, а причины так и остаются неизвестными. Может, есть что-то, что способно улучшить работоспособность монорепы и хотя бы частично решить проблемы?
Предлагаю отправиться на изучение официальной документации и поискать лайфхаки вместе со мной. Ниже приведу список полезных инструментов и механизмов, вкратце расскажу о них и дам ссылки для детального изучения.
Итак, что же ты мог упустить при работе с Nx:
— nx run-many
— dependency graph + nx affected
— nx-enforce-module-boundaries es-linting
— computation cache
— nx cloud
— buildable libs
Начнем с примера
Есть монорепозиторий с:
приложениями: App1, App2, App3
библиотеками: Lib1, Lib2, Lib3.
Вот ссылка для освежения в памяти этих понятий.
На графе представлено их взаимодействие друг с другом.
Граф зависимостей в монорепозитории.
Затем мы вносим изменения в Lib3: пилим новую фичу (или докидываем еще больше багов в код, тут как повезет), и в конце всего этого хотим проверить, а не сломали ли мы чего своими правками, надо бы запустить тесты.
Так как сломаться могло что угодно, то лучше прогнать тесты по всей монорепе.
Начинаем выполнять:
nx test Lib3
nx test App1
nx test App2….
Стоп. Слишком долго, попробуем иначе.
Nx run-many
nx run-many
Вот мы и встретили первое упрощение нашей работы (штука популярная, но мало ли, кто не знал). Команда run-many выполняет задачу параллельно для нескольких проектов, можно указать как все (--all), так и какие-то конкретные (-p App1).
В дефолтном состоянии команда выполняет параллельно 3 задачи, это конфигурируется с помощью parallel опции (--parallel=6).
Выполним nx run-many --target=test --all для нашего случая.
Тесты будут запущены для всего: App1, App2, App3, Lib1, Lib2, Lib3. Работоспособно, но если изменения повлияли только на один проект? Не хочется лишний раз прогонять все тесты.
Affected
У Nx есть решение для этого: команда nx affected
Запуская nx affected --target=test, будут выполнены тесты только для Lib3 и App3. С Lib3 все понятно, мы туда внесли изменения. Но откуда взялось App3?
Все дело в dependency graph. По сути, граф в начале статьи и является таким графом зависимостей. Визуализировать у себя его можно с помощью команды nx graph.
При запуске nx affected Nx под капотом строит такой граф из имеющихся проектов, запускает анализ изменений в коде и определяет список затронутых изменениями либ/приложений.
Отдельной статьи заслуживает вопрос, а как именно Nx анализирует изменения? Вероятно, руки дойдут, и статья будет. Либо можно самостоятельно ознакомиться с source файлами.
Отлично, теперь прогон тестов занимает не так много времени. Но что может нам подпортить производительность?
Циклические зависимости
Котик запутался в твоих циклических зависимостях.
Нужно выручать.
Пока в монорепозитории царят циклические зависимости, пользы от nx affected становится с каждым разом все меньше. Настало время рефакторинга!
Чтобы было удобнее отслеживать циклические зависимости, используем следующее правило линтера, которое подсветит все проблемные места: @nrwl/nx/enforce-module-boundaries.
Внимательно изучи документацию, правило имеет разные способы конфигурации.
Супер. Теперь мы запускаем таски только на нужных проектах и не имеем циклических зависимостей. А есть какой-нибудь кэш?
Cache
Nx cache
Конечно! Nx computation cache.
Здесь все просто: nx перед запуском таски вычисляет хеш. Если такой уже существует в кэше, то повторно выполняться таска не будет, результат возьмем из кэша. Список кэшируемых тасок может настраиваться.
Что учитывается при построении хеша:
source файлы и зависимости
конфигурация
версии внешних зависимостей
флаги cli
версия ноды (и прочие райнтайм значения)
Можем ли мы как-то кастомизировать это? Да, без проблем! Хеш для линтера, например, может учитывать только исходный код и глобальный конфиг. Подробней здесь.
А где находится кэш? Его стандартное расположение — node_modules/.cache/nx. И даже это можно изменить.
Если в твоей команде нет жадных коллег, то можно с ними обмениваться кэшем.
Remote cache
Remote cache
Для этого нужно либо реализовать собственный кэш, либо воспользоваться готовым решением от Nx — Nx Cloud.
При выполнении таски сперва проверяется наличие хеша в локальном хранилище, а если использовать Nx cloud, то поиск выполняется и в удаленном кэше. То есть запускаешь тесты у себя, а твои коллеги могут воспользоваться этим результатом без затрат времени на повторное выполнение.
Перейдем к пункту со звездочкой?
Buildable libs
Как мы знаем, в качестве target для библиотеки может быть lint или test. Отдельно взять и сбилдить библиотеку просто так мы не можем, только с помощью билда приложения, которое ее использует.
Обычно это происходит так: при выполнении build/serve приложения, webpack отдельно билдит зависимости и кладет их к бандлу приложения.
Для небольших проектов это еще работает, но с увеличением кодовой базы продолжительность build/serve будет только увеличиваться, здесь нам помогут buildable libs.
С помощью опции buildable: nx g @nx/angular: lib Lib1 --buildable мы даем возможность Lib1 быть так сказать buildable и билдиться отдельно от приложения. Данная опция поддерживается не только для Angular, но и для React, NestJs, Node.
Теперь при повторном выполнении build/serve не придется повторно билдить Lib1, так как Lib1 прекомпилируется и в случае наличия кэша берется из него.
Заключение
Хочу поблагодарить тебя, читатель, что уделил время и дочитал статью до конца.
Мы рассмотрели некоторые возможности и механизмы Nx, которые могли быть пропущены при знакомстве с ним. Имея в своем арсенале хотя бы такое минимальное представление об этом, уже можно наметить пути ускорения работы с Nx у себя в монорепе.
Возможно, следующие статьи будут посвящены более детальному обзору, если будет потребность. А пока расскажите, удалось узнать что-то новое из статьи?