Un certain nombre de commandes de l'Angular CLI exécutent un processus complexe sur votre code, comme compiler, tester ou servir votre application. Ces commandes utilisent un outil interne appelé Architect pour exécuter des builders de la CLI, qui invoquent à leur tour un autre outil (bundler, test runner, serveur) pour accomplir la tâche souhaitée. Des builders personnalisés peuvent réaliser une tâche entièrement nouvelle, ou changer l'outil tiers utilisé par une commande existante.
Ce document explique comment les builders de la CLI s'intègrent au fichier de configuration du workspace, et montre comment créer votre propre builder.
Utile : Retrouvez le code des exemples utilisés ici dans ce dépôt GitHub.
Les builders de la CLI
L'outil interne Architect délègue le travail à des fonctions de traitement appelées builders. La fonction de traitement d'un builder reçoit deux arguments :
| Argument | Type |
|---|---|
options |
JSONObject |
context |
BuilderContext |
La séparation des responsabilités est ici la même qu'avec les schematics, utilisés par d'autres commandes de la CLI qui modifient votre code (comme ng generate).
- L'objet
optionsest fourni à partir des options et de la configuration de l'utilisateur de la CLI, tandis que l'objetcontextest fourni automatiquement par l'API CLI Builder. - En plus des informations contextuelles, l'objet
contextdonne aussi accès à une méthode de planification,context.scheduleTarget(). Le planificateur exécute la fonction de traitement du builder avec une configuration de cible donnée.
La fonction de traitement du builder peut être synchrone (renvoyer une valeur), asynchrone (renvoyer une Promise), ou observer et renvoyer plusieurs valeurs (renvoyer un Observable).
Les valeurs renvoyées doivent toujours être de type BuilderOutput.
Cet objet contient un champ booléen success et un champ facultatif error qui peut contenir un message d'erreur.
Angular fournit des builders utilisés par la CLI pour des commandes comme ng build et ng test.
Les configurations de cibles par défaut de ces builders et des autres builders intégrés à la CLI se trouvent et se configurent dans la section « architect » du fichier de configuration du workspace, angular.json.
Vous pouvez aussi étendre et personnaliser Angular en créant vos propres builders, que vous pouvez exécuter directement avec la commande CLI ng run.
Structure d'un projet de builder
Un builder se trouve dans un dossier de « projet » dont la structure est semblable à celle d'un workspace Angular, avec les fichiers de configuration globaux au niveau supérieur, et une configuration plus spécifique dans un dossier source contenant les fichiers de code qui définissent le comportement.
Par exemple, votre dossier myBuilder pourrait contenir les fichiers suivants.
| Fichiers | Rôle |
|---|---|
src/my-builder.ts |
Fichier source principal de la définition du builder. |
src/my-builder.spec.ts |
Fichier source des tests. |
src/schema.json |
Définition des options d'entrée du builder. |
builders.json |
Définition des builders. |
package.json |
Dépendances. Consultez https://docs.npmjs.com/files/package.json. |
tsconfig.json |
Configuration TypeScript. |
Les builders peuvent être publiés sur npm : consultez Publier votre library.
Créer un builder
À titre d'exemple, créez un builder qui copie un fichier vers un nouvel emplacement.
Pour créer un builder, utilisez la fonction createBuilder() de CLI Builder et renvoyez un objet Promise<BuilderOutput>.
src/my-builder.ts (skeleton du builder)
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> {
}
Ajoutons-y maintenant un peu de logique.
Le code suivant récupère les chemins des fichiers source et de destination dans les options de l'utilisateur, puis copie le fichier de la source vers la destination (à l'aide de la version promise de la fonction Node.js intégrée copyFile()).
Si l'opération de copie échoue, il renvoie une erreur avec un message décrivant le problème sous-jacent.
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};
}
Gérer la sortie
Par défaut, copyFile() n'affiche rien sur la sortie standard ni sur la sortie d'erreur du processus.
Si une erreur survient, il peut être difficile de comprendre exactement ce que le builder tentait de faire au moment du problème.
Ajoutez du contexte en journalisant des informations supplémentaires à l'aide de l'API Logger.
Cela permet aussi d'exécuter le builder lui-même dans un processus séparé, même si la sortie standard et la sortie d'erreur sont désactivées.
Vous pouvez récupérer une instance de Logger depuis le contexte.
src/my-builder.ts (gestion de la sortie)
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,
};
}
Signaler la progression et le statut
L'API CLI Builder inclut des outils de signalement de la progression et du statut, qui peuvent fournir des indications pour certaines fonctions et interfaces.
Pour signaler la progression, utilisez la méthode context.reportProgress(), qui prend en arguments une valeur courante, un total facultatif et une chaîne de statut.
Le total peut être n'importe quel nombre. Par exemple, si vous savez combien de fichiers vous devez traiter, le total pourrait être le nombre de fichiers, et la valeur courante le nombre de fichiers traités jusqu'ici.
La chaîne de statut reste inchangée tant que vous ne passez pas une nouvelle valeur de chaîne.
Dans notre exemple, l'opération de copie est soit terminée, soit encore en cours : un rapport de progression est donc inutile, mais vous pouvez signaler le statut pour qu'un builder parent ayant appelé notre builder sache ce qui se passe.
Utilisez la méthode context.reportStatus() pour générer une chaîne de statut de n'importe quelle longueur.
Utile : Rien ne garantit qu'une longue chaîne sera affichée en entier ; elle peut être tronquée pour s'adapter à l'interface qui l'affiche.
Passez une chaîne vide pour supprimer le statut.
src/my-builder.ts (signalement de la progression)
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};
Entrées d'un builder
Vous pouvez invoquer un builder indirectement via une commande de la CLI comme ng build, ou directement avec la commande ng run de l'Angular CLI.
Dans les deux cas, vous devez fournir les entrées obligatoires, mais vous pouvez laisser les autres entrées prendre des valeurs par défaut préconfigurées pour une cible spécifique, définies par une configuration, ou indiquées sur la ligne de commande.
Validation des entrées
Vous définissez les entrées d'un builder dans un schéma JSON associé à ce builder.
Comme pour les schematics, l'outil Architect regroupe les valeurs d'entrée résolues dans un objet options, et valide leurs types par rapport au schéma avant de les passer à la fonction du builder.
Pour notre builder d'exemple, options doit être un JsonObject avec deux clés :
source et destination, chacune étant une chaîne.
Vous pouvez fournir le schéma suivant pour valider le type de ces valeurs.
schema.json
{
"$schema": "https://json-schema.org/schema",
"type": "object",
"properties": {
"source": {
"type": "string"
},
"destination": {
"type": "string"
}
}
}
Utile : Il s'agit d'un exemple minimal, mais l'utilisation d'un schéma pour la validation peut s'avérer très puissante. Pour plus d'informations, consultez le site des schémas JSON.
Pour relier l'implémentation de notre builder à son schéma et à son nom, vous devez créer un fichier de définition de builder, vers lequel vous pouvez pointer dans package.json.
Créez un fichier nommé builders.json qui ressemble à ceci :
builders.json
{
"builders": {
"copy": {
"implementation": "./dist/my-builder.js",
"schema": "./src/schema.json",
"description": "Copies a file."
}
}
}
Dans le fichier package.json, ajoutez une clé builders qui indique à l'outil Architect où trouver notre fichier de définition de 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"
}
}
Le nom officiel de notre builder est désormais @example/copy-file:copy.
La première partie correspond au nom du package et la seconde au nom du builder tel que défini dans le fichier builders.json.
Ces valeurs sont accessibles via options.source et options.destination.
src/my-builder.ts (signalement du statut)
await fs.copyFile(options.source, options.destination);
Configuration de la cible
Un builder doit avoir une cible définie qui l'associe à une configuration d'entrée et à un projet spécifiques.
Les cibles sont définies dans le fichier de configuration de la CLI angular.json.
Une cible indique le builder à utiliser, sa configuration d'options par défaut et des configurations alternatives nommées.
Architect, dans l'Angular CLI, utilise la définition de la cible pour résoudre les options d'entrée d'une exécution donnée.
Le fichier angular.json comporte une section pour chaque projet, et la section « architect » de chaque projet configure les cibles des builders utilisés par les commandes de la CLI comme « build », « test » et « serve ».
Par défaut, par exemple, la commande ng build exécute le builder @angular/build:application pour réaliser la tâche de build, et lui passe les valeurs d'options par défaut indiquées pour la cible build dans 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",
"...": "..."
}
}
},
"...": "..."
}
}
}
La commande passe au builder l'ensemble des options par défaut indiquées dans la section « options ».
Si vous passez l'option --configuration=production, elle utilise les valeurs de remplacement indiquées dans la configuration production.
Indiquez d'autres remplacements d'options individuellement sur la ligne de commande.
Chaînes de cible
La commande générique ng run de la CLI prend comme premier argument une chaîne de cible de la forme suivante.
project:target[:configuration]
| Détails | |
|---|---|
| project | Le nom du projet Angular CLI auquel la cible est associée. |
| target | Une configuration de builder nommée, issue de la section architect du fichier angular.json. |
| configuration | (facultatif) Le nom d'une configuration de remplacement spécifique pour la cible donnée, telle que définie dans le fichier angular.json. |
Si votre builder appelle un autre builder, il peut avoir besoin de lire une chaîne de cible qui lui est passée.
Analysez cette chaîne pour la convertir en objet à l'aide de la fonction utilitaire targetFromTargetString() de @angular-devkit/architect.
Planifier et exécuter
Architect exécute les builders de manière asynchrone. Pour invoquer un builder, vous planifiez une tâche qui sera exécutée une fois la résolution de toute la configuration terminée.
La fonction du builder n'est pas exécutée tant que le planificateur n'a pas renvoyé un objet de contrôle BuilderRun.
La CLI planifie généralement les tâches en appelant la fonction context.scheduleTarget(), puis résout les options d'entrée à l'aide de la définition de la cible dans le fichier angular.json.
Architect résout les options d'entrée d'une cible donnée en prenant l'objet des options par défaut, puis en écrasant ses valeurs avec celles de la configuration, puis en les écrasant à nouveau avec celles de l'objet de remplacements passé à context.scheduleTarget().
Pour l'Angular CLI, l'objet de remplacements est construit à partir des arguments de la ligne de commande.
Architect valide les valeurs d'options obtenues par rapport au schéma du builder. Si les entrées sont valides, Architect crée le contexte et exécute le builder.
Pour plus d'informations, consultez Configuration du workspace.
Utile : Vous pouvez aussi invoquer un builder directement depuis un autre builder ou un test en appelant context.scheduleBuilder().
Vous passez directement un objet options à la méthode, et ces valeurs d'options sont validées par rapport au schéma du builder sans autre ajustement.
Seule la méthode context.scheduleTarget() résout la configuration et les remplacements via le fichier angular.json.
Configuration architect par défaut
Créons un fichier angular.json simple qui met en contexte les configurations de cibles.
Vous pouvez publier le builder sur npm (consultez Publier votre library), et l'installer avec la commande suivante :
npm install @example/copy-file
Si vous créez un nouveau projet avec ng new builder-test, le fichier angular.json généré ressemble à ceci, avec uniquement les configurations de builders par défaut.
angular.json
{
"projects": {
"builder-test": {
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/builder-test",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json"
},
"configurations": {
"production": {
"optimization": true,
"aot": true
}
}
}
}
}
}
}
Ajouter une cible
Ajoutez une nouvelle cible qui exécutera notre builder pour copier un fichier.
Cette cible indique au builder de copier le fichier package.json.
- Nous allons ajouter une nouvelle section de cible à l'objet
architectde notre projet - La cible nommée
copy-packageutilise notre builder, que vous avez publié sous@example/copy-file. - L'objet options fournit des valeurs par défaut pour les deux entrées que vous avez définies.
source- Le fichier existant que vous copiez.destination- Le chemin vers lequel vous voulez copier.
angular.json
{
"projects": {
"builder-test": {
"architect": {
"copy-package": {
"builder": "@example/copy-file:copy",
"options": {
"source": "package.json",
"destination": "package-copy.json"
}
}
// Existing targets...
}
}
}
}
Exécuter le builder
Pour exécuter notre builder avec la configuration par défaut de la nouvelle cible, utilisez la commande CLI suivante.
ng run builder-test:copy-package
Cela copie le fichier package.json vers package-copy.json.
Utilisez des arguments de ligne de commande pour remplacer les valeurs par défaut configurées.
Par exemple, pour exécuter le builder avec une autre valeur de destination, utilisez la commande CLI suivante.
ng run builder-test:copy-package --destination=package-other.json
Cela copie le fichier vers package-other.json au lieu de package-copy.json.
Comme vous n'avez pas remplacé l'option source, la copie se fait toujours à partir du fichier par défaut package.json.
Tester un builder
Utilisez des tests d'intégration pour votre builder, afin de pouvoir utiliser le planificateur d'Architect pour créer un contexte, comme dans cet exemple.
Dans le répertoire source du builder, créez un nouveau fichier de test my-builder.spec.ts. Le test crée de nouvelles instances de JsonSchemaRegistry (pour la validation du schéma), TestingArchitectHost (une implémentation en mémoire de ArchitectHost) et Architect.
Voici un exemple de test qui exécute le builder de copie de fichier.
Le test utilise le builder pour copier le fichier package.json et vérifie que le contenu du fichier copié est identique à celui de la source.
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);
});
});
Utile : Pour exécuter ce test dans votre dépôt, vous avez besoin du package ts-node.
Vous pouvez éviter cela en renommant my-builder.spec.ts en my-builder.spec.js.
Mode watch
La plupart des builders s'exécutent une seule fois puis rendent la main. Toutefois, ce comportement n'est pas entièrement compatible avec un builder qui surveille les modifications (comme un serveur de développement, par exemple). Architect peut prendre en charge le mode watch, mais il faut faire attention à certains points.
Pour être utilisée avec le mode watch, la fonction de traitement d'un builder doit renvoyer un
Observable. Architect s'abonne à l'Observablejusqu'à ce qu'il se termine, et peut le réutiliser si le builder est de nouveau planifié avec les mêmes arguments.Le builder doit toujours émettre un objet
BuilderOutputaprès chaque exécution. Une fois exécuté, il peut passer en mode watch, pour être déclenché par un événement externe. Si un événement le relance, le builder doit exécuter la fonctioncontext.reportRunning()pour indiquer à Architect qu'il est de nouveau en cours d'exécution. Cela empêche Architect d'arrêter le builder si une autre exécution est planifiée.
Lorsque votre builder appelle BuilderRun.stop() pour quitter le mode watch, Architect se désabonne de l'Observable du builder et appelle la logique de libération du builder pour faire le nettoyage.
Ce comportement permet aussi d'arrêter et de nettoyer les builds de longue durée.
En général, si votre builder surveille un événement externe, vous devez séparer votre exécution en trois phases.
| Phases | Détails |
|---|---|
| Exécution | La tâche en cours, comme l'invocation d'un compilateur. Elle se termine lorsque le compilateur a fini et que votre builder émet un objet BuilderOutput. |
| Surveillance | Entre deux exécutions, surveiller un flux d'événements externes. Par exemple, surveiller les modifications du système de fichiers. Elle se termine lorsque le compilateur redémarre et que context.reportRunning() est appelée. |
| Achèvement | Soit la tâche est entièrement terminée, comme un compilateur qui doit s'exécuter un certain nombre de fois, soit l'exécution du builder a été arrêtée (avec BuilderRun.stop()). Architect exécute la logique de libération et se désabonne de l'Observable de votre builder. |
Résumé
L'API CLI Builder permet de modifier le comportement de l'Angular CLI en utilisant des builders pour exécuter une logique personnalisée.
- Les builders peuvent être synchrones ou asynchrones, s'exécuter une seule fois ou surveiller des événements externes, et peuvent planifier d'autres builders ou cibles.
- Les builders ont des options par défaut indiquées dans le fichier de configuration
angular.json, qui peuvent être écrasées par une configuration alternative de la cible, puis à nouveau par les options de la ligne de commande - L'équipe Angular vous recommande d'utiliser des tests d'intégration pour tester les builders Architect. Utilisez des tests unitaires pour valider la logique exécutée par le builder.
- Si votre builder renvoie un
Observable, il doit nettoyer le builder dans la logique de libération (teardown) de cetObservable.