Внести вклад
Инструменты разработчика
Angular CLI

Builders Angular CLI

Ряд команд Angular CLI запускает сложный процесс над вашим кодом, например сборку, тестирование или раздачу приложения. Команды используют внутренний инструмент Architect для запуска CLI builders, которые вызывают другой инструмент (bundler, test runner, сервер) для выполнения желаемой задачи. Пользовательские builders могут выполнять совершенно новую задачу или менять, какой сторонний инструмент используется существующей командой.

Этот документ объясняет, как CLI builders интегрируются с файлом конфигурации workspace, и показывает, как создать собственный builder.

ПОЛЕЗНО: Код из используемых здесь примеров можно найти в этом репозитории GitHub.

CLI builders

Внутренний инструмент Architect делегирует работу функциям-обработчикам, называемым builders. Функция-обработчик builder получает два аргумента:

Аргумент Тип
options JSONObject
context BuilderContext

Разделение ответственности здесь такое же, как у schematics, которые используются для других команд CLI, затрагивающих ваш код (например ng generate).

  • Объект options предоставляется опциями и конфигурацией пользователя CLI, а объект context предоставляется CLI Builder API автоматически.
  • Помимо контекстной информации, объект context также предоставляет доступ к методу планирования context.scheduleTarget(). Планировщик выполняет функцию-обработчик builder с данной конфигурацией цели.

Функция-обработчик builder может быть синхронной (возвращать значение), асинхронной (возвращать Promise) или отслеживать и возвращать несколько значений (возвращать Observable). Возвращаемые значения всегда должны быть типа BuilderOutput. Этот объект содержит Boolean-поле success и необязательное поле error, которое может содержать сообщение об ошибке.

Angular предоставляет некоторые builders, используемые CLI для команд вроде ng build и ng test. Конфигурации целей по умолчанию для этих и других встроенных CLI builders можно найти и настроить в секции «architect» файла конфигурации workspace angular.json. Также можно расширить и настроить Angular, создавая собственные builders, которые можно запускать напрямую с помощью команды CLI ng run.

Структура проекта builder

Builder находится в папке «project», похожей по структуре на Angular workspace, с глобальными файлами конфигурации на верхнем уровне и более специфичной конфигурацией в исходной папке с файлами кода, определяющими поведение. Например, папка myBuilder может содержать следующие файлы.

Файлы Назначение
src/my-builder.ts Основной исходный файл определения builder.
src/my-builder.spec.ts Исходный файл для тестов.
src/schema.json Определение входных опций builder.
builders.json Определение builders.
package.json Зависимости. См. https://docs.npmjs.com/files/package.json.
tsconfig.json Конфигурация TypeScript.

Builders можно публиковать в npm, см. Публикация библиотеки.

Создание builder

В качестве примера создайте builder, который копирует файл в новое расположение. Чтобы создать builder, используйте функцию CLI Builder createBuilder() и верните объект Promise<BuilderOutput>.

src/my-builder.ts (builder skeleton)

import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';

interface Options extends JsonObject {
  source: string;
  destination: string;
}

export default createBuilder(copyFileBuilder);

async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
}

Теперь добавим в него логику. Следующий код получает пути исходного и целевого файлов из опций пользователя и копирует файл из источника в назначение (используя Promise-версию встроенной функции Node.js copyFile()). Если операция копирования не удалась, он возвращает ошибку с сообщением о лежащей в основе проблеме.

src/my-builder.ts (builder)

import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import {promises as fs} from 'fs';

interface Options extends JsonObject {
  source: string;
  destination: string;
}

export default createBuilder(copyFileBuilder);

async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
  try {
    await fs.copyFile(options.source, options.destination);
  } catch (err) {
    return {
      success: false,
      error: (err as Error).message,
    };
  }
  return {success: true};
}

Обработка вывода

По умолчанию copyFile() ничего не печатает в стандартный вывод или ошибку процесса. Если возникает ошибка, может быть трудно понять, что именно пытался сделать builder, когда произошла проблема. Добавьте дополнительный контекст, логируя дополнительную информацию с помощью API Logger. Это также позволяет выполнять сам builder в отдельном процессе, даже если стандартный вывод и ошибка деактивированы.

Экземпляр Logger можно получить из контекста.

src/my-builder.ts (handling output)

try {
    await fs.copyFile(options.source, options.destination);
  } catch (err) {
    context.logger.error('Failed to copy file.');
    return {
      success: false,
      error: (err as Error).message,
    };
  }

Отчёт о прогрессе и статусе

CLI Builder API включает инструменты отчёта о прогрессе и статусе, которые могут давать подсказки для определённых функций и интерфейсов.

Чтобы сообщать о прогрессе, используйте метод context.reportProgress(), который принимает текущее значение, необязательный total и строку статуса как аргументы. Total может быть любым числом. Например, если известно, сколько файлов нужно обработать, total может быть числом файлов, а current — числом уже обработанных. Строка статуса не изменяется, пока вы не передадите новое строковое значение.

В нашем примере операция копирования либо завершается, либо всё ещё выполняется, поэтому отчёт о прогрессе не нужен, но можно сообщать статус, чтобы родительский builder, вызвавший наш builder, знал, что происходит. Используйте метод context.reportStatus() для генерации строки статуса любой длины.

ПОЛЕЗНО: Нет гарантии, что длинная строка будет показана полностью; она может быть обрезана, чтобы уместиться в UI, который её отображает.

Передайте пустую строку, чтобы удалить статус.

src/my-builder.ts (progress reporting)

context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
  try {
    await fs.copyFile(options.source, options.destination);
  } catch (err) {
    context.logger.error('Failed to copy file.');
    return {
      success: false,
      error: (err as Error).message,
    };
  }

  context.reportStatus('Done.');
  return {success: true};

Ввод builder

Builder можно вызвать косвенно через команду CLI вроде ng build или напрямую командой Angular CLI ng run. В любом случае необходимо предоставить обязательные входы, но другие входы могут использовать значения по умолчанию, предварительно настроенные для конкретной цели, указанные конфигурацией или заданные в командной строке.

Валидация ввода

Входы builder определяются в JSON-схеме, связанной с этим builder. Подобно schematics, инструмент Architect собирает разрешённые входные значения в объект options и проверяет их типы по схеме перед передачей в функцию builder.

Для нашего примера builder options должен быть JsonObject с двумя ключами: source и destination, каждый из которых — строка.

Можно предоставить следующую схему для валидации типов этих значений.

schema.json

{
  "$schema": "http://json-schema.org/schema",
  "type": "object",
  "properties": {
    "source": {
      "type": "string"
    },
    "destination": {
      "type": "string"
    }
  }
}

ПОЛЕЗНО: Это минимальный пример, но использование схемы для валидации может быть очень мощным. Дополнительную информацию см. на сайте JSON schemas.

Чтобы связать реализацию builder с его схемой и именем, нужно создать файл определения builder, на который можно указать в package.json.

Создайте файл с именем builders.json, который выглядит так:

builders.json

{
  "builders": {
    "copy": {
      "implementation": "./dist/my-builder.js",
      "schema": "./src/schema.json",
      "description": "Copies a file."
    }
  }
}

В файле package.json добавьте ключ builders, который сообщает инструменту Architect, где найти файл определения builder.

package.json

{
  "name": "@example/copy-file",
  "version": "1.0.0",
  "description": "Builder for copying files",
  "builders": "builders.json",
  "dependencies": {
    "@angular/build": "^21.2.0"
  }
}

Официальное имя нашего builder теперь @example/copy-file:copy. Первая часть — имя пакета, вторая — имя builder, как указано в файле builders.json.

Эти значения доступны в options.source и options.destination.

src/my-builder.ts (report status)

await fs.copyFile(options.source, options.destination);

Конфигурация цели

У builder должна быть определённая цель, связывающая его с конкретной входной конфигурацией и проектом.

Цели определяются в файле конфигурации CLI angular.json. Цель указывает используемый builder, его конфигурацию опций по умолчанию и именованные альтернативные конфигурации. Architect в Angular CLI использует определение цели для разрешения входных опций для данного запуска.

Файл angular.json имеет секцию для каждого проекта, а секция «architect» каждого проекта настраивает цели для builders, используемых командами CLI вроде 'build', 'test' и 'serve'. По умолчанию, например, команда ng build запускает builder @angular/build:application для выполнения задачи сборки и передаёт значения опций по умолчанию, указанные для цели build в angular.json.

angular.json

{
  "myApp": {
    "...": "...",
    "architect": {
      "build": {
        "builder": "@angular/build:application",
        "options": {
          "outputPath": "dist/myApp",
          "index": "src/index.html",
          "...": "..."
        },
        "configurations": {
          "production": {
            "fileReplacements": [
              {
                "replace": "src/environments/environment.ts",
                "with": "src/environments/environment.prod.ts"
              }
            ],
            "optimization": true,
            "outputHashing": "all",
            "...": "..."
          }
        }
      },
      "...": "..."
    }
  }
}

Команда передаёт builder набор опций по умолчанию, указанных в секции «options». Если передать флаг --configuration=production, используются значения переопределения, указанные в конфигурации production. Дальнейшие переопределения опций указываются индивидуально в командной строке.

Строки целей

Общая команда CLI ng run принимает в качестве первого аргумента строку цели следующей формы.

project:target[:configuration]
Подробности
project Имя проекта Angular CLI, с которым связана цель.
target Именованная конфигурация builder из секции architect файла angular.json.
configuration (необязательно) Имя конкретного переопределения конфигурации для данной цели, как определено в файле angular.json.

Если ваш builder вызывает другой builder, ему может потребоваться прочитать переданную строку цели. Разберите эту строку в объект с помощью утилитарной функции targetFromTargetString() из @angular-devkit/architect.

Планирование и запуск

Architect запускает builders асинхронно. Чтобы вызвать builder, вы планируете задачу, которая будет выполнена, когда всё разрешение конфигурации завершено.

Функция builder не выполняется, пока планировщик не вернёт управляющий объект BuilderRun. CLI обычно планирует задачи, вызывая функцию context.scheduleTarget(), а затем разрешает входные опции, используя определение цели в файле angular.json.

Architect разрешает входные опции для данной цели, беря объект опций по умолчанию, затем перезаписывая значения из конфигурации, затем дополнительно перезаписывая значения из объекта overrides, переданного в context.scheduleTarget(). Для Angular CLI объект overrides строится из аргументов командной строки.

Architect проверяет результирующие значения опций по схеме builder. Если входы корректны, Architect создаёт контекст и выполняет builder.

Дополнительную информацию см. в Конфигурация workspace.

ПОЛЕЗНО: Builder также можно вызвать напрямую из другого builder или теста, вызвав context.scheduleBuilder(). Вы передаёте объект options напрямую в метод, и эти значения опций проверяются по схеме builder без дальнейшей корректировки.

Только метод context.scheduleTarget() разрешает конфигурацию и переопределения через файл angular.json.

Конфигурация architect по умолчанию

Создадим простой файл angular.json, который помещает конфигурации целей в контекст.

Можно опубликовать builder в npm (см. Публикация библиотеки) и установить его следующей командой:

npm install @example/copy-file

Если создать новый проект с ng new builder-test, сгенерированный файл angular.json выглядит примерно так, только с конфигурациями builders по умолчанию.

angular.json

{
  "projects": {
    "builder-test": {
      "architect": {
        "build": {
          "builder": "@angular/build:application",
          "options": {
            "outputPath": "dist/builder-test",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json"
          },
          "configurations": {
            "production": {
              "optimization": true,
              "aot": true
            }
          }
        }
      }
    }
  }
}

Добавление цели

Добавьте новую цель, которая запустит наш builder для копирования файла. Эта цель сообщает builder скопировать файл package.json.

  • Мы добавим новую секцию цели в объект architect для нашего проекта
  • Цель с именем copy-package использует наш builder, который вы опубликовали в @example/copy-file.
  • Объект options предоставляет значения по умолчанию для двух определённых вами входов.
    • source — существующий файл, который вы копируете.
    • destination — путь, куда вы хотите скопировать.

angular.json

{
  "projects": {
    "builder-test": {
      "architect": {
        "copy-package": {
          "builder": "@example/copy-file:copy",
          "options": {
            "source": "package.json",
            "destination": "package-copy.json"
          }
        }
        // Existing targets...
      }
    }
  }
}

Запуск builder

Чтобы запустить наш builder с конфигурацией новой цели по умолчанию, используйте следующую команду CLI.

ng run builder-test:copy-package

Это копирует файл package.json в package-copy.json.

Используйте аргументы командной строки для переопределения настроенных значений по умолчанию. Например, чтобы запустить с другим значением destination, используйте следующую команду CLI.

ng run builder-test:copy-package --destination=package-other.json

Это копирует файл в package-other.json вместо package-copy.json. Поскольку вы не переопределили опцию source, копирование по-прежнему будет из файла package.json по умолчанию.

Тестирование builder

Используйте интеграционное тестирование для builder, чтобы можно было использовать планировщик Architect для создания контекста, как в этом примере. В каталоге исходников builder создайте новый тестовый файл my-builder.spec.ts. Тест создаёт новые экземпляры JsonSchemaRegistry (для валидации схемы), TestingArchitectHost (in-memory реализация ArchitectHost) и Architect.

Вот пример теста, который запускает builder копирования файла. Тест использует builder для копирования файла package.json и проверяет, что содержимое скопированного файла совпадает с источником.

src/my-builder.spec.ts

import {Architect} from '@angular-devkit/architect';
import {TestingArchitectHost} from '@angular-devkit/architect/testing';
import {schema} from '@angular-devkit/core';
import {promises as fs} from 'fs';
import {join} from 'path';

describe('Copy File Builder', () => {
  let architect: Architect;
  let architectHost: TestingArchitectHost;

  beforeEach(async () => {
    const registry = new schema.CoreSchemaRegistry();
    registry.addPostTransform(schema.transforms.addUndefinedDefaults);

    // TestingArchitectHost() takes workspace and current directories.
    // Since we don't use those, both are the same in this case.
    architectHost = new TestingArchitectHost(__dirname, __dirname);
    architect = new Architect(architectHost, registry);

    // This will either take a Node package name, or a path to the directory
    // for the package.json file.
    await architectHost.addBuilderFromPackage(join(__dirname, '..'));
  });

  it('can copy files', async () => {
    // A "run" can have multiple outputs, and contains progress information.
    const run = await architect.scheduleBuilder('@example/copy-file:copy', {
      source: 'package.json',
      destination: 'package-copy.json',
    });

    // The "result" member (of type BuilderOutput) is the next output.
    const output = await run.result;

    // Stop the builder from running. This stops Architect from keeping
    // the builder-associated states in memory, since builders keep waiting
    // to be scheduled.
    await run.stop();

    // Expect that the copied file is the same as its source.
    const sourceContent = await fs.readFile('package.json', 'utf8');
    const destinationContent = await fs.readFile('package-copy.json', 'utf8');
    expect(destinationContent).toBe(sourceContent);
  });
});

ПОЛЕЗНО: При запуске этого теста в вашем репозитории нужен пакет ts-node. Этого можно избежать, переименовав my-builder.spec.ts в my-builder.spec.js.

Режим watch

Большинство builders запускаются один раз и возвращают результат. Однако это поведение не полностью совместимо с builder, который отслеживает изменения (например, devserver). Architect может поддерживать режим watch, но есть несколько моментов, на которые стоит обратить внимание.

  • Для использования с режимом watch функция-обработчик builder должна возвращать Observable. Architect подписывается на Observable, пока он не завершится, и может переиспользовать его, если builder снова запланирован с теми же аргументами.

  • Builder всегда должен эмитить объект BuilderOutput после каждого выполнения. После выполнения он может войти в режим watch, запускаемый внешним событием. Если событие заставляет его перезапуститься, builder должен выполнить функцию context.reportRunning(), чтобы сообщить Architect, что он снова выполняется. Это предотвращает остановку builder Architect'ом, если запланирован другой запуск.

Когда ваш builder вызывает BuilderRun.stop() для выхода из режима watch, Architect отписывается от Observable builder и вызывает логику teardown builder для очистки. Это поведение также позволяет останавливать и очищать долго выполняющиеся сборки.

В общем, если ваш builder отслеживает внешнее событие, следует разделить запуск на три фазы.

Фазы Подробности
Running Выполняемая задача, например вызов компилятора. Заканчивается, когда компилятор завершается и ваш builder эмитит объект BuilderOutput.
Watching Между двумя запусками отслеживается внешний поток событий. Например, отслеживание файловой системы на любые изменения. Заканчивается, когда компилятор перезапускается и вызывается context.reportRunning().
Completion Либо задача полностью завершена, например компилятор, которому нужно запуститься несколько раз, либо запуск builder был остановлен (с помощью BuilderRun.stop()). Architect выполняет логику teardown и отписывается от Observable вашего builder.

Итог

CLI Builder API предоставляет средство изменения поведения Angular CLI с помощью builders для выполнения пользовательской логики.

  • Builders могут быть синхронными или асинхронными, выполняться один раз или отслеживать внешние события, а также планировать другие builders или цели.
  • У builders есть значения опций по умолчанию, указанные в файле конфигурации angular.json, которые могут быть перезаписаны альтернативной конфигурацией для цели и дополнительно перезаписаны флагами командной строки
  • Команда Angular рекомендует использовать интеграционные тесты для тестирования Architect builders. Используйте модульные тесты для валидации логики, которую выполняет builder.
  • Если ваш builder возвращает Observable, он должен очищать builder в логике teardown этого Observable.