Einführung in CLI Builder

Einführung in CLI Builder

In diesem Artikel wird die neue Angular CLI-API vorgestellt, mit der Sie vorhandene CLI-Funktionen erweitern und neue hinzufügen können. Wir werden diskutieren, wie mit dieser API gearbeitet wird und welche Punkte ihrer Erweiterung vorhanden sind, die es ermöglichen, der CLI neue Funktionen hinzuzufügen.

Die Geschichte


Vor ungefähr einem Jahr haben wir die Arbeitsbereichsdatei ( angle.json ) in der Angular-CLI eingeführt und viele der Grundprinzipien für die Implementierung ihrer Befehle überarbeitet . Wir sind zu der Tatsache gekommen, dass wir die Teams in die „Boxen“ gestellt haben:

  1. Schematische Befehle - "Schematische Befehle". Inzwischen haben Sie wahrscheinlich bereits von Schematics gehört, der Bibliothek, mit der die CLI Ihren Code generiert und ändert. Es wurde in Version 5 veröffentlicht und wird derzeit in den meisten Befehlen verwendet, die Ihren Code betreffen, z. B. neu , generieren , hinzufügen und aktualisieren .
  2. Verschiedene Befehle - "Andere Teams" . Dies sind Befehle, die nicht direkt mit Ihrem Projekt zusammenhängen: Hilfe , Version , Konfiguration , Dokument . Vor kurzem sind auch Analysen erschienen, ebenso wie unsere Ostereier (Shhh! Kein Wort an irgendjemanden!).
  3. Aufgabenbefehle - "Aufgabenbefehle". Diese Kategorie "startet im Großen und Ganzen Prozesse, die mit dem Code anderer Personen ausgeführt werden". - Als Beispiel ist Build ein Projekt-Build, Lint debuggt und Test testet.

Wir haben vor langer Zeit angefangen, angular.json zu entwerfen. Ursprünglich wurde es als Ersatz für die Webpack-Konfiguration konzipiert. Darüber hinaus sollte es Entwicklern ermöglichen, die Implementierung der Projektassembly unabhängig zu wählen. Als Ergebnis erhielten wir ein grundlegendes Task-Startsystem, das für unsere Experimente einfach und bequem blieb. Wir haben diese API "Architekt" genannt.

Trotz der Tatsache, dass Architect nicht offiziell unterstützt wurde, war es bei Entwicklern beliebt, die die Zusammenstellung von Projekten anpassen wollten, sowie bei Bibliotheken von Drittanbietern, die ihren Workflow steuern mussten. Nx verwendete es, um Bazel-Befehle auszuführen, Ionic, um Unit-Tests für Jest auszuführen, und Benutzer konnten ihre Webpack-Konfigurationen mit Tools wie ngx-build-plus erweitern . Und das war erst der Anfang.

In Angular CLI Version 8 wird eine offiziell unterstützte, stabile und verbesserte Version dieser API verwendet.

Konzept


Die Architect-API bietet Tools zum Planen und Koordinieren von Aufgaben, mit denen die Angular CLI ihre Befehle implementiert. Es verwendet aufgerufene Funktionen
"Bauherren" - "Sammler", die als Aufgaben oder Planer anderer Sammler fungieren können. Darüber hinaus wird angle.json als Befehlssatz für die Kollektoren selbst verwendet.

Dies ist ein sehr allgemeines System, das flexibel und erweiterbar ist. Es enthält eine API zum Berichten, Protokollieren und Testen. Bei Bedarf kann das System für neue Aufgaben erweitert werden.

Pflücker


Assembler sind Funktionen, die Logik und Verhalten für eine Aufgabe implementieren, die den CLI-Befehl ersetzen kann. - Starten Sie zum Beispiel den Linter.

Die Collector-Funktion akzeptiert zwei Argumente: den Eingabewert (oder die Optionen) und den Kontext, der die Beziehung zwischen der CLI und dem Collector selbst bereitstellt. Die Aufteilung der Verantwortung ist hier dieselbe wie in Schema - der CLI-Benutzer legt die Optionen fest, die API ist für den Kontext verantwortlich und Sie (der Entwickler) legen das erforderliche Verhalten fest. Das Verhalten kann synchron, asynchron implementiert werden oder einfach eine bestimmte Anzahl von Werten anzeigen. Die Ausgabe muss vom Typ BuilderOutput sein , der den Erfolg des logischen Feldes und den optionalen Feldfehler enthält, der die Fehlermeldung enthält.

Arbeitsbereichsdatei und Aufgaben


Die Architect-API basiert auf angle.json , einer Arbeitsbereichsdatei zum Speichern von Aufgaben und deren Einstellungen.

angle.json unterteilt den Arbeitsbereich in Projekte und diese wiederum in Aufgaben. Beispielsweise ist Ihre mit dem Befehl ng new erstellte Anwendung ein solches Projekt. Eine der Aufgaben in diesem Projekt ist die Build- Task, die mit dem Befehl ng build gestartet werden kann. Standardmäßig hat diese Aufgabe drei Schlüssel:

  1. Builder - Der Name des Kollektors, mit dem die Aufgabe ausgeführt werden soll, im Format PACKAGE_NAME: ASSEMBLY_NAME .
  2. Optionen - Einstellungen, die standardmäßig beim Starten einer Aufgabe verwendet werden.
  3. Konfigurationen - Einstellungen, die angewendet werden, wenn eine Aufgabe mit der angegebenen Konfiguration gestartet wird.

Die Einstellungen werden wie folgt angewendet: Wenn die Aufgabe gestartet wird, werden die Einstellungen aus dem Optionsblock übernommen. Wenn eine Konfiguration angegeben wurde, werden deren Einstellungen über die vorhandenen Einstellungen geschrieben. Wenn danach zusätzliche Einstellungen an ScheduleTarget () - den Überschreibungsblock - übergeben wurden, werden sie zuletzt geschrieben. Bei Verwendung der Angular-CLI werden Befehlszeilenargumente an Überschreibungen übergeben . Nachdem alle Einstellungen an den Kollektor übertragen wurden, überprüft er sie gemäß seinem Schema. Nur wenn die Einstellungen diesem entsprechen, wird der Kontext erstellt und der Kollektor beginnt zu arbeiten.

Weitere Informationen zum Arbeitsbereich finden Sie hier .

Erstellen Sie Ihren eigenen Sammler


Als Beispiel erstellen wir einen Kollektor, der einen Befehl in der Befehlszeile ausführt. Verwenden Sie zum Erstellen eines Kollektors die createBuilder- Factory und geben Sie das BuilderOutput- Objekt zurück:

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

Fügen wir nun unserem Kollektor eine Logik hinzu: Wir möchten den Kollektor über die Einstellungen steuern, neue Prozesse erstellen, auf den Abschluss des Prozesses warten und, wenn der Prozess erfolgreich abgeschlossen wurde (dh den Rückkehrcode 0), dies dem Architekten signalisieren:

 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 }); }); }); }); 

Ausgabeverarbeitung


Jetzt übergibt die Spawn- Methode alle Daten an die Standardausgabe des Prozesses. Wir möchten sie möglicherweise auf den Logger übertragen - Logger. In diesem Fall wird zum einen das Debuggen während des Testens erleichtert, und zum anderen kann Architect selbst unseren Kollektor in einem separaten Prozess ausführen oder die Standardausgabe von Prozessen deaktivieren (z. B. in der Electron-Anwendung).

Zu diesem Zweck können wir den im Kontextobjekt verfügbaren Logger verwenden , mit dem wir die Ausgabe des Prozesses umleiten können:

 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 }); }); }); }); 

Leistungs- und Statusberichte


Der letzte Teil der API, der sich auf die Implementierung Ihres eigenen Kollektors bezieht, sind die Fortschritts- und aktuellen Statusberichte.

In unserem Fall wird der Befehl entweder ausgeführt oder ausgeführt, sodass es keinen Sinn macht, einen Fortschrittsbericht hinzuzufügen. Wir können dem übergeordneten Sammler jedoch unseren Status mitteilen, damit er versteht, was passiert.

 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 }); }); }); }); 

Verwenden Sie zum Übergeben eines Fortschrittsberichts die reportProgress- Methode mit aktuellen und (optional) zusammenfassenden Werten als Argumente. total kann eine beliebige Zahl sein. Wenn Sie beispielsweise wissen, wie viele Dateien Sie verarbeiten müssen, können Sie deren Anzahl auf insgesamt übertragen . Anschließend können Sie die Anzahl der bereits verarbeiteten Dateien auf aktuell übertragen. So berichtet der tslint-Sammler über seinen Fortschritt.

Eingabevalidierung


Das an den Collector übergebene Optionsobjekt wird mithilfe des JSON-Schemas überprüft. Dies ähnelt Schematics, wenn Sie wissen, was es ist.

In unserem Kollektorbeispiel erwarten wir, dass unsere Parameter ein Objekt sind, das zwei Schlüssel empfängt: Befehl - Befehl (Zeichenfolge) und Argumente - Argumente (Array von Zeichenfolgen). Unser Überprüfungsschema sieht folgendermaßen aus:

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

Schemata sind wirklich mächtige Werkzeuge, die eine große Anzahl von Überprüfungen durchführen können. Weitere Informationen zu JSON-Schemata finden Sie auf der offiziellen JSON-Schema-Website .

Erstellen Sie ein Build-Paket


Es gibt eine Schlüsseldatei, die wir für unseren eigenen Kollektor erstellen müssen, um ihn mit der Angular CLI kompatibel zu machen - builders.json , die für die Beziehung zwischen unserer Implementierung des Kollektors, seinem Namen und dem Überprüfungsschema verantwortlich ist. Die Datei selbst sieht folgendermaßen aus:

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

Dann fügen wir in der Datei package.json den Builders- Schlüssel hinzu, der auf die Datei builders.json verweist:

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

Dadurch wird Architect mitgeteilt, wo nach der Collector-Definitionsdatei gesucht werden soll.

Daher lautet der Name unseres Sammlers "@ example / command-running: command" . Der erste Teil des Namens vor dem Doppelpunkt (:) ist der Name des Pakets, das mit package.json definiert wurde. Der zweite Teil ist der Name des Kollektors, der mithilfe der Datei builders.json definiert wird.

Testen Sie Ihre eigenen Builder


Die empfohlene Methode zum Testen von Assemblern besteht in Integrationstests. Dies liegt daran, dass das Erstellen eines Kontexts nicht einfach ist. Verwenden Sie daher den Scheduler von Architect.

Um die Muster zu vereinfachen, haben wir uns eine einfache Möglichkeit zum Erstellen einer Architect-Instanz ausgedacht : Zuerst erstellen Sie eine JsonSchemaRegistry (um das Schema zu testen), dann TestingArchitectHost und schließlich eine Architect- Instanz. Jetzt können Sie die Konfigurationsdatei builders.json kompilieren.

Hier ist ein Beispiel für die Ausführung des Collectors, der den Befehl ls ausführt und überprüft, ob der Befehl erfolgreich ausgeführt wurde. Bitte beachten Sie, dass wir die Standardausgabe von Prozessen in Logger verwenden .

 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'); }); }); 

Um das obige Beispiel auszuführen, benötigen Sie das Paket ts-node . Wenn Sie Node verwenden möchten , benennen Sie index_spec.ts in index_spec.js um .

Verwenden des Kollektors in einem Projekt


Lassen Sie uns eine einfache angle.json erstellen, die alles demonstriert, was wir über Assembler gelernt haben. Angenommen, wir haben unseren Kollektor in Beispiel / Befehlsläufer gepackt und dann mit ng new builder-test eine neue Anwendung erstellt, könnte die Datei angle.json folgendermaßen aussehen (ein Teil des Inhalts wurde der Kürze halber entfernt):

 { // ...   . "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 } } } } 

Wenn wir uns entschließen, eine neue Aufgabe hinzuzufügen, um (zum Beispiel) den Befehl touch mit unserem Collector auf die Datei anzuwenden (aktualisiert das Änderungsdatum der Datei), würden wir npm install example / command- running ausführen und dann Änderungen an angle.json vornehmen :

 { "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 } } } } } } } 

Die Angular-CLI verfügt über einen Befehl run , der der Hauptbefehl zum Ausführen von Kollektoren ist. Als erstes Argument wird eine Zeichenfolge im Format PROJECT: TASK [: CONFIGURATION] verwendet . Um unsere Aufgabe auszuführen, können wir den Befehl ng run builder-test: touch verwenden .

Jetzt möchten wir vielleicht einige Argumente neu definieren. Leider können wir Arrays über die Befehlszeile noch nicht neu definieren, aber wir können den Befehl selbst zur Demonstration ändern: ng run builder-test: touch --command = ls . - Dadurch wird die Datei src / main.ts ausgegeben .

Watch-Modus


Standardmäßig wird davon ausgegangen, dass Kollektoren einmal aufgerufen und beendet werden. Sie können jedoch Observable zurückgeben , um ihren eigenen Beobachtungsmodus zu implementieren (wie dies der Webpack- Kollektor tut). Der Architekt abonniert das Observable, bis es beendet oder gestoppt ist, und kann den Collector erneut abonnieren, wenn der Collector mit denselben Parametern aufgerufen wird (wenn auch nicht garantiert).

  1. Der Kollektor sollte nach jeder Ausführung ein BuilderOutput- Objekt zurückgeben. Nach Abschluss kann es in den Beobachtungsmodus wechseln, der durch ein externes Ereignis verursacht wird. Wenn es erneut gestartet wird, muss es die Funktion context.reportRunning () aufrufen , um Architect zu benachrichtigen, dass der Kollektor wieder arbeitet. Dies schützt den Kollektor davor, ihn vom Architekten bei einem neuen Aufruf zu stoppen.
  2. Der Architekt selbst meldet sich von Observable ab, wenn der Kollektor stoppt (z. B. mit run.stop ()), und verwendet dabei die Teardown-Logik - den Zerstörungsalgorithmus. Auf diese Weise können Sie die Assembly stoppen und löschen, wenn dieser Prozess bereits ausgeführt wird.

Zusammenfassend lässt sich sagen, dass Ihr Sammler, wenn er externe Ereignisse beobachtet, in drei Schritten arbeitet:

  1. Erfüllung. Zum Beispiel die Kompilierung von Webpack. Dieser Schritt endet, wenn Webpack die Erstellung abgeschlossen hat und Ihr Kollektor BuilderOutput an Observable sendet.
  2. Beobachtung. - Zwischen zwei Starts werden externe Ereignisse überwacht. Beispielsweise überwacht Webpack das Dateisystem auf Änderungen. Dieser Schritt endet, wenn Webpack den Build fortsetzt und context.reportRunning () aufgerufen wird. Nach diesem Schritt beginnt Schritt 1 erneut.
  3. Fertigstellung. - Die Aufgabe ist vollständig abgeschlossen (es wurde beispielsweise erwartet, dass Webpack eine bestimmte Anzahl von Malen gestartet wird) oder der Start des Kollektors wurde gestoppt (mit run.stop () ). In diesem Fall wird der Observable- Zerstörungsalgorithmus ausgeführt und gelöscht.

Fazit


Hier ist eine Zusammenfassung dessen, was wir in dieser Veröffentlichung gelernt haben:

  1. Wir bieten eine neue API, mit der Entwickler das Verhalten von Angular CLI-Befehlen ändern und mithilfe von Assemblern, die die erforderliche Logik implementieren, neue hinzufügen können.
  2. Kollektoren können synchron, asynchron und auf externe Ereignisse reagieren. Sie können sowohl mehrfach als auch von anderen Sammlern aufgerufen werden.
  3. Die Parameter, die der Kollektor beim Start der Task empfängt, werden zuerst aus der Datei angle.json gelesen, dann von den Parametern aus der Konfiguration, falls vorhanden, überschrieben und dann von den Befehlszeilenflags überschrieben, wenn sie hinzugefügt wurden.
  4. Die empfohlene Methode zum Testen von Kollektoren besteht in Integrationstests. Sie können jedoch Unit-Tests getrennt von der Kollektorlogik durchführen.
  5. Wenn der Kollektor ein Observable zurückgibt, sollte es nach Durchlaufen des Zerstörungsalgorithmus gelöscht werden.

In naher Zukunft wird die Häufigkeit der Verwendung dieser APIs zunehmen. Zum Beispiel ist die Bazel-Implementierung stark mit ihnen verbunden.

Wir sehen bereits, wie die Community neue CLI-Kollektoren zur Verwendung erstellt, z. B. Jest und Cypress zum Testen.

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


All Articles