Présentation de CLI Builder

Présentation de CLI Builder

Dans cet article, nous examinerons la nouvelle API CLI angulaire, qui vous permettra d'étendre les fonctionnalités CLI existantes et d'en ajouter de nouvelles. Nous verrons comment travailler avec cette API et quels sont les points de son extension qui permettent d'ajouter de nouvelles fonctionnalités à la CLI.

L'histoire


Il y a environ un an, nous avons introduit le fichier d'espace de travail ( angular.json ) dans la CLI angulaire et repensé de nombreux principes de base pour implémenter ses commandes. Nous sommes arrivés au fait d'avoir placé les équipes dans les «cases»:

  1. Commandes schématiques - "Commandes schématiques" . À ce jour, vous avez probablement déjà entendu parler de Schematics, la bibliothèque utilisée par la CLI pour générer et modifier votre code. Il est apparu dans la version 5 et est actuellement utilisé dans la plupart des commandes qui concernent votre code, comme nouveau , générer , ajouter et mettre à jour .
  2. Commandes diverses - "Autres équipes" . Ce sont des commandes qui ne sont pas directement liées à votre projet: aide , version , config , doc . Récemment, des analyses ont également fait leur apparition, ainsi que nos œufs de Pâques (Chut! Pas un mot à personne!).
  3. Commandes de tâche - "Commandes de tâche" . Cette catégorie, dans l'ensemble, «lance les processus exécutés sur le code d'autres personnes». - Par exemple, build est une construction de projet, lint débogue et test teste.

Nous avons commencé à concevoir angular.json il y a longtemps. Initialement, il a été conçu en remplacement de la configuration Webpack. En outre, il était censé permettre aux développeurs de choisir indépendamment la mise en œuvre de l'assemblage du projet. En conséquence, nous avons obtenu un système de lancement de tâche de base, qui est resté simple et pratique pour nos expériences. Nous avons appelé cette API "Architecte".

Malgré le fait que Architect n'était pas officiellement pris en charge, il était populaire parmi les développeurs qui souhaitaient personnaliser l'assemblage des projets, ainsi que parmi les bibliothèques tierces qui devaient contrôler leur flux de travail. Nx l'a utilisé pour exécuter les commandes Bazel, Ionic l'a utilisé pour exécuter des tests unitaires sur Jest, et les utilisateurs ont pu étendre leurs configurations Webpack en utilisant des outils comme ngx-build-plus . Et ce n'était que le début.

Une version officiellement prise en charge, stable et améliorée de cette API est utilisée dans Angular CLI version 8.

Concept


L'API Architect propose des outils de planification et de coordination des tâches que la CLI angulaire utilise pour implémenter ses commandes. Il utilise des fonctions appelées
«Constructeurs» - «collectionneurs» qui peuvent agir en tant que tâches ou planificateurs d'autres collectionneurs. De plus, il utilise angular.json comme un ensemble d'instructions pour les collectionneurs eux-mêmes.

Il s'agit d'un système très général conçu pour être flexible et extensible. Il contient une API pour les rapports, la journalisation et les tests. Si nécessaire, le système peut être étendu pour de nouvelles tâches.

Pickers


Les assembleurs sont des fonctions qui implémentent la logique et le comportement d'une tâche pouvant remplacer la commande CLI. - Par exemple, démarrez le linter.

La fonction de collecteur prend deux arguments: la valeur d'entrée (ou les options) et le contexte qui fournit la relation entre la CLI et le collecteur lui-même. La répartition des responsabilités ici est la même que dans les schémas - l'utilisateur CLI définit les options, l'API est responsable du contexte et vous (le développeur) définissez le comportement nécessaire. Le comportement peut être implémenté de manière synchrone, asynchrone ou afficher simplement un certain nombre de valeurs. La sortie doit être de type BuilderOutput , qui contient le succès du champ logique et l' erreur de champ facultatif, qui contient le message d'erreur.

Fichier et tâches de l'espace de travail


L'API Architect repose sur angular.json , un fichier d'espace de travail pour stocker les tâches et leurs paramètres.

angular.json divise l'espace de travail en projets et ceux-ci, à leur tour, en tâches. Par exemple, votre application créée avec la commande ng new est l'un de ces projets. L'une des tâches de ce projet sera la tâche de génération , qui peut être lancée à l'aide de la commande ng build . Par défaut, cette tâche a trois clés:

  1. constructeur - le nom du collecteur à utiliser pour terminer la tâche, au format PACKAGE_NAME: ASSEMBLY_NAME .
  2. options - paramètres utilisés lors du démarrage d'une tâche par défaut.
  3. configurations - paramètres qui seront appliqués lors du démarrage d'une tâche avec la configuration spécifiée.

Les paramètres sont appliqués comme suit: lorsque la tâche démarre, les paramètres sont extraits du bloc d'options, puis, si une configuration a été spécifiée, ses paramètres sont écrits par-dessus ceux existants. Après cela, si des paramètres supplémentaires ont été transmis à scheduleTarget () - le bloc de remplacement , ils seront écrits en dernier. Lors de l'utilisation de la CLI angulaire, les arguments de ligne de commande sont passés à des remplacements . Une fois tous les paramètres transférés au collecteur, il les vérifie selon son schéma, et uniquement si les paramètres y correspondent, le contexte sera créé et le collecteur commencera à fonctionner.

Plus d'informations sur l'espace de travail ici .

Créez votre propre collectionneur


Par exemple, créons un collecteur qui exécutera une commande sur la ligne de commande. Pour créer un collecteur, utilisez la fabrique createBuilder et renvoyez l'objet BuilderOutput :

import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; export default createBuilder((options, context) => { return new Promise<BuilderOutput>(resolve => { resolve({ success: true }); }); }); 

Maintenant, ajoutons une logique à notre collecteur: nous voulons contrôler le collecteur via les paramètres, créer de nouveaux processus, attendre que le processus se termine, et si le processus s'est terminé avec succès (c'est-à-dire, renvoyer le code 0), signalez-le à Architect:

 import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; import * as childProcess from 'child_process'; export default createBuilder((options, context) => { const child = childProcess.spawn(options.command, options.args); return new Promise<BuilderOutput>(resolve => { child.on('close', code => { resolve({ success: code === 0 }); }); }); }); 

Traitement de sortie


Maintenant, la méthode d' apparition transmet toutes les données à la sortie standard du processus. Nous pouvons vouloir les transférer à l' enregistreur - enregistreur. Dans ce cas, d'une part, le débogage pendant les tests sera facilité, et d'autre part, Architect lui-même peut exécuter notre collecteur dans un processus séparé ou désactiver la sortie standard des processus (par exemple, dans l'application Electron).

Pour ce faire, nous pouvons utiliser Logger , disponible dans l'objet contextuel , qui nous permettra de rediriger la sortie du processus:

 import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; import * as childProcess from 'child_process'; export default createBuilder((options, context) => { const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' }); child.stdout.on('data', (data) => { context.logger.info(data.toString()); }); child.stderr.on('data', (data) => { context.logger.error(data.toString()); }); return new Promise<BuilderOutput>(resolve => { child.on('close', code => { resolve({ success: code === 0 }); }); }); }); 

Rapports sur le rendement et l'état


La dernière partie de l'API liée à la mise en œuvre de votre propre collecteur est la progression et les rapports d'état actuels.

Dans notre cas, la commande est terminée ou exécutée, il est donc inutile d'ajouter un rapport de progression. Cependant, nous pouvons communiquer notre statut au collectionneur parent afin qu'il comprenne ce qui se passe.

 import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; import * as childProcess from 'child_process'; export default createBuilder((options, context) => { context.reportStatus(`Executing "${options.command}"...`); const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' }); child.stdout.on('data', (data) => { context.logger.info(data.toString()); }); child.stderr.on('data', (data) => { context.logger.error(data.toString()); }); return new Promise<BuilderOutput>(resolve => { context.reportStatus(`Done.`); child.on('close', code => { resolve({ success: code === 0 }); }); }); }); 

Pour passer un rapport de progression , utilisez la méthode reportProgress avec les valeurs actuelles et (facultativement) récapitulatives comme arguments. le total peut être n'importe quel nombre. Par exemple, si vous savez combien de fichiers vous devez traiter, vous pouvez transférer leur nombre au total , puis à courant, vous pouvez transférer le nombre de fichiers déjà traités. C'est ainsi que le collecteur tslint rend compte de ses progrès.

Validation des entrées


L'objet d' options passé au collecteur est vérifié à l'aide du schéma JSON. Ceci est similaire aux schémas si vous savez ce que c'est.

Dans notre exemple de collecteur, nous nous attendons à ce que nos paramètres soient un objet qui reçoit deux clés: commande - commande (chaîne) et args - arguments (tableau de chaînes). Notre schéma de vérification ressemblera à ceci:

 { "$schema": "http://json-schema.org/schema", "type": "object", "properties": { "command": { "type": "string" }, "args": { "type": "array", "items": { "type": "string" } } } 

Les schémas sont des outils vraiment puissants qui peuvent effectuer un grand nombre de contrôles. Pour plus d'informations sur les schémas JSON, vous pouvez vous référer au site Web officiel du schéma JSON .

Créer un package de construction


Il y a un fichier clé que nous devons créer pour notre propre collecteur afin de le rendre compatible avec la CLI angulaire - builders.json , qui est responsable de la relation entre notre implémentation du collecteur, son nom et le schéma de vérification. Le fichier lui-même ressemble à ceci:

 { "builders": { "command": { "implementation": "./command", "schema": "./command/schema.json", "description": "Runs any command line in the operating system." } } } 

Ensuite, dans le fichier package.json , nous ajoutons la clé builders , pointant vers le fichier builders.json :

 { "name": "@example/command-runner", "version": "1.0.0", "description": "Builder for Architect", "builders": "builders.json", "devDependencies": { "@angular-devkit/architect": "^1.0.0" } } 

Cela indiquera à Architect où rechercher le fichier de définition du collecteur.

Ainsi, le nom de notre collecteur est "@ example / command-runner: command" . La première partie du nom, avant les deux-points (:) est le nom du package, défini à l'aide de package.json . La deuxième partie est le nom du collecteur, défini à l'aide du fichier builders.json .

Tester vos propres constructeurs


La méthode recommandée pour tester les assembleurs consiste à effectuer des tests d'intégration. En effet, la création d'un contexte n'est pas facile, vous devez donc utiliser le planificateur d'Architect.

Pour simplifier les modèles, nous avons pensé à un moyen simple de créer une instance Architect: vous créez d'abord un JsonSchemaRegistry (pour tester le schéma), puis TestingArchitectHost et, enfin, une instance Architect . Vous pouvez maintenant compiler le fichier de configuration builders.json .

Voici un exemple d'exécution du collecteur, qui exécute la commande ls et vérifie que la commande s'est terminée avec succès. Veuillez noter que nous utiliserons la sortie standard des processus dans l' enregistreur .

 import { Architect, ArchitectHost } from '@angular-devkit/architect'; import { TestingArchitectHost } from '@angular-devkit/architect/testing'; import { logging, schema } from '@angular-devkit/core'; describe('Command Runner Builder', () => { let architect: Architect; let architectHost: ArchitectHost; beforeEach(async () => { const registry = new schema.CoreSchemaRegistry(); registry.addPostTransform(schema.transforms.addUndefinedDefaults); //  TestingArchitectHost –    . //     ,   . architectHost = new TestingArchitectHost(__dirname, __dirname); architect = new Architect(architectHost, registry); //      NPM-, //    package.json  . await architectHost.addBuilderFromPackage('..'); }); //      Windows it('can run ls', async () => { //  ,     . const logger = new logging.Logger(''); const logs = []; logger.subscribe(ev => logs.push(ev.message)); // "run"    ,       . const run = await architect.scheduleBuilder('@example/command-runner:command', { command: 'ls', args: [__dirname], }, { logger }); // "result" –    . //    "BuilderOutput". const output = await run.result; //  . Architect     //   ,    ,    . await run.stop(); //   . expect(output.success).toBe(true); // ,     . // `ls $__dirname`. expect(logs).toContain('index_spec.ts'); }); }); 

Pour exécuter l'exemple ci-dessus, vous avez besoin du package ts-node . Si vous avez l'intention d'utiliser Node, renommez index_spec.ts en index_spec.js .

Utilisation du collecteur dans un projet


Créons un simple angular.json qui montre tout ce que nous avons appris sur les assembleurs. En supposant que nous avons compressé notre collecteur dans exemple / commande-runner puis créé une nouvelle application en utilisant ng new builder-test , le fichier angular.json pourrait ressembler à ceci (une partie du contenu a été supprimé pour plus de brièveté):

 { // ...   . "projects": { // ... "builder-test": { // ... "architect": { // ... "build": { "builder": "@angular-devkit/build-angular:browser", "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, "buildOptimizer": true } } } } 

Si nous décidions d'ajouter une nouvelle tâche pour appliquer (par exemple) la commande tactile au fichier (met à jour la date de modification du fichier) à l'aide de notre collecteur, nous exécuterions npm install example / command-runner , puis apporterions des modifications à angular.json :

 { "projects": { "builder-test": { "architect": { "touch": { "builder": "@example/command-runner:command", "options": { "command": "touch", "args": [ "src/main.ts" ] } }, "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/builder-test", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json" }, "configurations": { "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "optimization": true, "aot": true, "buildOptimizer": true } } } } } } } 

La CLI angulaire possède une commande d' exécution , qui est la commande principale pour exécuter les collecteurs. Comme premier argument, il prend une chaîne de format PROJECT: TASK [: CONFIGURATION] . Pour exécuter notre tâche, nous pouvons utiliser la commande ng run builder-test: touch .

Maintenant, nous pouvons vouloir redéfinir certains arguments. Malheureusement, nous ne pouvons pas redéfinir les tableaux à partir de la ligne de commande jusqu'à présent, mais nous pouvons modifier la commande elle-même pour la démonstration: ng exécutez builder-test: touch --command = ls . - Cela produira le fichier src / main.ts.

Mode montre


Il est supposé que, par défaut, les collecteurs seront appelés une fois et terminés, cependant, ils peuvent retourner Observable pour implémenter leur propre mode d'observation (comme le fait le collecteur Webpack ). Architect s'abonnera à l' Observable jusqu'à ce qu'il se termine ou s'arrête et peut à nouveau s'abonner au collecteur si le collecteur est appelé avec les mêmes paramètres (bien que cela ne soit pas garanti).

  1. Le collecteur doit retourner un objet BuilderOutput après chaque exécution. Une fois terminé, il peut entrer en mode d'observation provoqué par un événement externe et, s'il recommence, il devra appeler la fonction context.reportRunning () pour informer l'architecte que le collecteur fonctionne à nouveau. Cela empêchera le collecteur de l'arrêter par l'architecte lors d'un nouvel appel.
  2. L'architecte lui-même se désabonne d' Observable lorsque le collecteur s'arrête (en utilisant run.stop (), par exemple), en utilisant la logique Teardown - l'algorithme de destruction. Cela vous permettra d'arrêter et d'effacer l'assembly si ce processus est déjà en cours d'exécution.

Pour résumer ce qui précède, si votre collectionneur regarde des événements externes, cela fonctionne en trois étapes:

  1. Accomplissement. Par exemple, compilation de Webpack. Cette étape se termine lorsque Webpack a terminé la construction et que votre collecteur envoie BuilderOutput à l' Observable .
  2. Observation. - Entre deux lancements, les événements externes sont surveillés. Par exemple, Webpack surveille le système de fichiers pour toute modification. Cette étape se termine lorsque Webpack reprend la génération et context.reportRunning () est appelé. Après cette étape, l'étape 1 recommence.
  3. Achèvement. - La tâche est entièrement terminée (par exemple, il était prévu que Webpack démarre un certain nombre de fois) ou le démarrage du collecteur a été arrêté (à l'aide de run.stop () ). Dans ce cas, l'algorithme de destruction observable est exécuté et il est effacé.

Conclusion


Voici un résumé de ce que nous avons appris dans cette publication:

  1. Nous fournissons une nouvelle API qui permettra aux développeurs de modifier le comportement des commandes CLI angulaires et d'en ajouter de nouvelles à l'aide d'assembleurs qui implémentent la logique nécessaire.
  2. Les collecteurs peuvent être synchrones, asynchrones et sensibles aux événements externes. Ils peuvent être appelés plusieurs fois, ainsi que par d'autres collectionneurs.
  3. Les paramètres que le collecteur reçoit au démarrage de la tâche sont d'abord lus dans le fichier angular.json , puis ils sont remplacés par les paramètres de la configuration, le cas échéant, puis remplacés par les indicateurs de ligne de commande s'ils ont été ajoutés.
  4. La méthode recommandée pour tester les collecteurs consiste à effectuer des tests d'intégration, mais vous pouvez effectuer des tests unitaires séparément de la logique du collecteur.
  5. Si le collecteur renvoie un observable, il doit être effacé après avoir traversé l'algorithme de destruction.

Dans un avenir proche, la fréquence d'utilisation de ces API augmentera. Par exemple, l'implémentation Bazel leur est fortement associée.

Nous voyons déjà comment la communauté crée de nouveaux collecteurs CLI à utiliser, par exemple, jest et cypress pour les tests.

Source: https://habr.com/ru/post/fr450746/


All Articles