Есть несколько десятков взаимосвязанных пакетов в рабочем дереве (Yarn Workspaces).
Надо собирать несколько из них. Часто, быстро, и в правильном порядке.
Существующие инструменты либо собирают всё сразу и долго, либо собирают в произвольном порядке, что некорректно и не всегда возможно.
Установка
npm install run-z --save-dev # Используя NPMyarn add run-z --dev # Используя Yarn
Теперь в package.json
можно добавлять задачи
{ "scripts": { "all": "run-z build lint,test", "build": "run-z --then tsc -p .", "clean": "run-z --then shx rm -rf ./dist", "lint": "run-z --then eslint .", "test": "run-z --then jest", "z": "run-z" }}
И запускать их
npm run all # Запуск одной задачи, используя NPMyarn all # Запуск одной задачи, используя Yarnyarn clean build # Запуск нескольких задач, используя Yarnnpm run clean -- build # Запуск нескольких задач, используя NPMnpm run z -- clean build # Запуск через пустую задачу `z`
Рекомендую всегда добавлять пустую задачу, например
z
. Она позволит передать дополнительные параметры в
run-z
, а не в npm
или yarn
.
Например, вот так можно вызвать справку:
yarn z --helpnpm run z -- --help
Как видите, синтаксис вызова у Yarn проще, чем у NPM.
Ниже в тексте я буду использовать Yarn в примерах.
Задачи
Задачи записываются как обычные сценарии в разделе
scripts
файла package.json
. Если такой
сценарий запускает команду run-z
, то последняя
трактует все сценарии как свои задачи и может запустить
сразу несколько.
Если перечислить несколько задач в командной строке
run-z
, то выполнение каждой из них станет
предварительным условием для следующей:
run-z prerequisite1 prerequisite2 --then node ./my-script.js --arg
Такая задача запустит сначала prereqiusite1
, затем,
дождавшись её завершения prerequisite2
, и только по её
окончании запустит сценарий node ./my-script.js
--arg
.
Поддерживаются четыре типа задач:
- Команда содержит опцию
--then
. Всё, что следует за этой опцией это команда с аргументами, которая будет выполнена. - Сценарий NPM это любой сценарий в разделе
scripts
файлаpackage.json
, отличный отrun-z
.run-z
запускает такие сценарии черезnpm run
илиyarn run
. - Группа содержит список задач для запуска, но не содержит команды. Список задач может быть пустым.
- Неизвестная задача создаётся, если не
соответствует ни одному сценарию в
package.json
. При попытке её запуска возникнет ошибка. Но задачи можно пропускать, тогда никакой ошибки не будет.
Параметры выполнения задач
Можно передавать дополнительные параметры в вызываемые задачи. Для этого предназначен особый синтаксис:
run-z test/--ci/--runInBand # Передача `--ci` и `--runInBand` # в команду или сценарий NPM, # запущенный задачей `test`.run-z test //--ci --runInBand// # Несколько параметров сразу.
Отдельный синтаксис нужен, чтобы передавать параметры конкретной
задаче, а не команде run-z
. Впрочем, неопознанные
параметры будут переданы задаче и так, без знака
/
.
Одиночные параметры отделяются от задачи одиночным знаком
/
. Перед ним можно добавлять пробелы.
Много параметров можно заключать между ограничителями из
нескольких (двух или более) знаков /
.
Атрибуты задач
Атрибуты это пары ключ/значение, которые можно передавать задачам примерно так же, как и параметры:
run-z test/attribute=value # Атрибут `attribute` со значением `value` # для задачи `test`run-z build test attribute=value # Атрибут `attribute` со значением `value` # для самой задачи и всех предварительных задач.run-z test/=if-present # Сокращённо `if-present=on`.
Атрибуты пока не очень полезны, но уже могут быть использованы в некоторых случаях:
- Когда установлен атрибут
if-present
для задачи, отсутствующей вpackage.json
, то попытки выполнить такую задачу не будет предпринято и ошибка не возникнет.
Может быть полезно, когда задача выполняется в нескольких пакетах одновременно, но в некоторых пакетах такая задача не определена. - Когда для задачи установлен атрибут
skip
, то выполняться такая задача не будет.
Дополнения к задачам
run-z
запускает каждую задачу только раз, сколько
бы раз она ни была вызвана. Даже если одна задача требуется для
выполнения нескольких других. И каждый раз, как задача вызывается,
ей можно передавать параметры и атрибуты. Параметры таких вызовов
объединяются.
Также есть особый синтаксис вызова задачи, называемый
дополнением. Если перед именем задачи поставить знак
+
, то параметры в задачу будут
переданы, но вот выполнение задачи инициировано не будет.
Это можно использовать, например, для задания параметров задачи
по умолчанию. Так что с таким package.json
:
{ "scripts": { "test": "run-z +z jest", "z": "run-z +test/--runInBand" }}
при исполнении задачи test
, jest
всегда будет вызываться с опцией --runInBand
.
Параллельное и последовательное выполнение
Любые две задачи выполняются последовательно, если только им не разрешено выполняться параллельно.
Запятая между именами задач разрешает их параллельное выполнение.
Например, задача
run-z clean build lint,test
Выполнит lint
и test
параллельно, но
лишь когда build
завершится. А задача
build
начнёт выполняться, только когда завершится
clean
.
Также можно выполнять команды параллельно с другими задачами.
Для этого достаточно опцию --then
заменить на опцию
--and
:
run-z copy-assets --and tsc -p . # Копирует файлы и компилирует # TypeScript одновременно.
Число одновременно выполняемых задач ограничено. По умолчанию
числом процессоров. Но можно это исправить опцией
--max-jobs
(сокращённо -j
):
run-z build,lint,test -j2 # Только две задачи одновременно.run-z build,lint,test -max-jobs 1 # Отключает параллельное выполнение.run-z build,lint,test -j0 # Убирает ограничение.
Пакетное выполнение
Можно выполнить задачу в другом пакете:
run-z ../package1 build test . build test
Эта задача выполнит build
и test
в
пакете из директории ../package1
, а затем в
текущем.
Опции командной строки .
, ..
, а также
начинающиеся с ./
и ../
это URL
указывающие на директории с пакетами. Задачи, перечисленные после
них, будут найдены и выполнены в целевом пакете.
В общем случае такие опции селекторы могут выбрать сразу несколько пакетов. Тогда задача с одним и тем же именем будет выполнена во всех. Партией:
run-z ./packages// build # Выполнит `build` в каждом пакете # непосредственно внутри директории `./packages`.run-z ./packages/// build # Выполнит `build` в директории `./packages` # и в каждом пакете найденном как угодно глубже.
//
выбирает непосредственно вложенные директории.
///
выбирает директорию и её поддиректории на любую
глубину. Скрытые директории и директории без файла
package.json
игнорируются.
Можно указать сразу несколько селекторов. Результаты выборок будет объединены:
run-z ./3rd-party// ./packages// build # Выполнит `build` в каждом пакете # из директорий `./3rd-party` # и `./packages`.
Порядок выполнения задач из партии определяется зависимостями между пакетами. То есть сначала задача выполняется для зависимости, а затем для зависящего от него пакета. Задачи в независимых пакетах выполняются параллельно.
Можно разрешить всем задачам в партии выполняться параллельно
опцией --batch-parallel
, сокращённо
--bap
:
run-z --batch-parallel ./packages// lint # Выполнит `lint` в каждом пакете # внутри директории `./packages # параллельно.
Подзадачи
Задача типа "группа" не только выполняет перечисленные задачи. Она также может быть использована для запуска произвольных задач в выбранных пакетах.
Для этого группе можно передать параметры вызова. И первым параметром будет имя (под-)задачи, которую нужно выполнить. Остальные параметры будут переданы уже в эту подзадачу.
Так что с таким package.json
:
{ "scripts": { "each": "run-z ./3rd-party// ./packages//" }}
можно выполнить задачи партией внутри директорий
3rd-party/
и packages/
:
yarn each /build each /test # Выполнит `build`, а затем `test` во всех пакетах.
Именованные партии задач
Для удобства работы в рабочем дереве (например Yarn Workspaces) партии задач можно именовать.
Допустим, есть корневой пакет с таким
package.json
:
{ "scripts": { "all/*": "run-z ./packages//", "z": "run-z" }}
Здесь сценарий с именем "all/*" это описание именованной партии. С таким описанием становится возможным пакетное выполнение задач как находясь в корне, так и находясь во вложенных директориях:
yarn z build --all # Выполняет `build` во всех пакетах.
Когда указана опция --all
, run-z
ищет
самый верхний пакет, содержащий именованные партии задач, и
выполняет задачи в этих партиях.
Имя именованной партии может быть "имя_партии/имя_задачи". Такая именованная партия используется вместо "имя_партии/*", когда выполняется задача "имя_задачи". Это полезно, например, когда нужно передать дополнительные опции в конкретную задачу:
{ "scripts": { "all/*": "run-z ./packages//", "all/test": "run-z ./packages// +test/--runInBand", "z": "run-z" }}
yarn z build --all # Выполняет `build` партией.yarn z test --all # Выполняет `test` партией с опцией `--runInBand`.
Граф зависимостей
Именованные партии позволяют выполнять задачи в подмножестве графа зависимостей текущего пакета:
yarn build --with-deps # Выполняет `build` в зависимостях # и в самом пакете.yarn build --only-deps # Выполняет `build` только в зависимостях.yarn build --with-dependants # Выполняет `build` в пакете, # а затем во всех зависимых пакетах.yarn build --only-dependants # Выполняет `build` только в зависимых пакетах.
Сравнение с npm-run-all
npm-run-all это весьма популярный инструмент для сборки. Прежде я использовал именно его и могу сравнивать.
Вот так выглядели сценарии сборки типичного проекта, использующего TypeScript, Rollup, ESLint и Jest:
{ "scripts": { "all": "run-p --aggregate-output build:all \"test {@}\" --", "build": "rollup --config ./rollup.config.js", "build:all": "run-p --aggregate-output rebuild lint", "ci:all": "run-p --aggregate-output build:all ci:test", "ci:test": "jest --ci --runInBand", "clean": "shx rm -rf d.ts dist target", "lint": "eslint .", "rebuild": "run-s clean build", "test": "jest", }}
run-p
здесь выполняет перечисленные задачи
параллельно. run-s
последовательно.
И вот как это выглядит теперь:
{ "scripts": { "all": "run-z build,lint,test", "build": "run-z +z rollup --config ./rollup.config.js", "ci:all": "run-z all +test/--ci/--runInBand", "clean": "run-z +z --then shx rm -rf d.ts dist target", "lint": "run-z +z --then eslint .", "test": "run-z +z --then jest", "z": "run-z +build,+doc,+lint,+test" }}
- Вспомогательная задача "ci:test" для запуска тестов в окружении CI больше не нужна. Все необходимые параметры можно передать прямо в задачу "test".
- Вспомогательная задача "build:all" была нужна только чтобы выполнять задачи параллельно. Теперь их можно перечислить через запятую.
- Задача "rebuild" тоже стала не нужна, поскольку можно вызывать
несколько задач прямо из командной строки:
yarn clean build
. - Появилась задача "z". Она не просто для удобства, а ещё и
разрешает параллельное выполнение некоторых задач. Так что
yarn build lint test
выполнит эти задачи параллельно. Не нужно каждый раз вспоминать, где поставить запятую. - Все сценарии теперь начинаются с
run-z
. Это необязательно для простых сценариев, но даёт возможность применить настройки по умолчанию, а также запустить несколько задач сразу, напримерyarn clean build
. - Немаловажно также то, что
run-z
запускается единожды, сколько бы заданий не требовалось выполнить. Аrun-p
илиrun-s
запускаются каждым сценарием. Запуск V8 совсем не бесплатен.
Планы на будущее
В ближайших планах добавить поддержку расширений. Основной
замысел в том, чтобы уметь запускать не только внешние команды, но
и использовать thread_workers. Хотя бы вместо некоторых команд. Это
позволит как сэкономить ресурсы, так и существенно ускорить сборку.
Особенно для быстрых утилит типа shx rm
. Ведь
последняя тратит на порядки больше времени на свой запуск, чем,
собственно, на работу.