- Регистрация
- 21.07.20
- Сообщения
- 40.408
- Реакции
- 1
- Репутация
- 0
Есть несколько десятков взаимосвязанных пакетов в рабочем дереве (
Надо собирать несколько из них. Часто, быстро, и в правильном порядке.
Существующие инструменты либо собирают всё сразу и долго, либо собирают в произвольном порядке, что некорректно и не всегда возможно.
Решение — run-z
Так выглядит сборка
Установка
npm install run-z --save-dev # Используя NPM
yarn 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 # Запуск одной задачи, используя NPM
yarn all # Запуск одной задачи, используя Yarn
yarn clean build # Запуск нескольких задач, используя Yarn
npm run clean -- build # Запуск нескольких задач, используя NPM
npm run z -- clean build # Запуск через пустую задачу `z`
Рекомендую всегда добавлять пустую задачу, например z. Она позволит передать дополнительные параметры в run-z, а не в npm или yarn. Например, вот так можно вызвать справку:
yarn z --help
npm 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.
Поддерживаются четыре типа задач:
Параметры выполнения задач
Можно передавать дополнительные параметры в вызываемые задачи. Для этого предназначен особый синтаксис:
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`.
Атрибуты пока не очень полезны, но уже могут быть использованы в некоторых случаях:
Дополнения к задачам
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` во всех пакетах.
Именованные партии задач
Для удобства работы в рабочем дереве (например
Допустим, есть корневой пакет с таким 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
Вот так выглядели сценарии сборки типичного проекта, использующего 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"
}
}
Планы на будущее
В ближайших планах — добавить поддержку расширений. Основной замысел в том, чтобы уметь запускать не только внешние команды, но и использовать thread_workers. Хотя бы вместо некоторых команд. Это позволит как сэкономить ресурсы, так и существенно ускорить сборку. Особенно для быстрых утилит типа shx rm. Ведь последняя тратит на порядки больше времени на свой запуск, чем, собственно, на работу.
You must be registered for see links
).Надо собирать несколько из них. Часто, быстро, и в правильном порядке.
Существующие инструменты либо собирают всё сразу и долго, либо собирают в произвольном порядке, что некорректно и не всегда возможно.
Решение — run-z
Так выглядит сборка

Установка
npm install run-z --save-dev # Используя NPM
yarn 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 # Запуск одной задачи, используя NPM
yarn all # Запуск одной задачи, используя Yarn
yarn clean build # Запуск нескольких задач, используя Yarn
npm run clean -- build # Запуск нескольких задач, используя NPM
npm run z -- clean build # Запуск через пустую задачу `z`
Рекомендую всегда добавлять пустую задачу, например z. Она позволит передать дополнительные параметры в run-z, а не в npm или yarn. Например, вот так можно вызвать справку:
yarn z --help
npm 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` во всех пакетах.
Именованные партии задач
Для удобства работы в рабочем дереве (например
You must be registered for see links
) партии задач можно именовать.Допустим, есть корневой пакет с таким 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
You must be registered for see links
— это весьма популярный инструмент для сборки. Прежде я использовал именно его и могу сравнивать.Вот так выглядели сценарии сборки типичного проекта, использующего 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. Ведь последняя тратит на порядки больше времени на свой запуск, чем, собственно, на работу.