Introduciendo CLI Builder

Introduciendo CLI Builder

En este artículo, veremos la nueva API de CLI angular, que le permitirá ampliar las características de CLI existentes y agregar otras nuevas. Analizaremos cómo trabajar con esta API y qué puntos de su extensión existen, lo que permite agregar nuevas funcionalidades a la CLI.

La historia


Hace aproximadamente un año, presentamos el archivo de espacio de trabajo ( angular.json ) en la CLI angular y repensamos muchos de los principios básicos para implementar sus comandos. Llegamos al hecho de que colocamos los equipos en las "cajas":

  1. Comandos esquemáticos - "Comandos esquemáticos" . A estas alturas, probablemente ya haya escuchado sobre Schematics, la biblioteca utilizada por la CLI para generar y modificar su código. Apareció en la versión 5 y actualmente se usa en la mayoría de los comandos relacionados con su código, como nuevo , generar , agregar y actualizar .
  2. Comandos misceláneos - "Otros equipos" . Estos son comandos que no están directamente relacionados con su proyecto: ayuda , versión , configuración , doc . Recientemente, también ha aparecido el análisis , así como nuestros huevos de Pascua (¡Shhh! ¡Ni una palabra para nadie!).
  3. Comandos de tareas: "Comandos de tareas" . Esta categoría, en general, "inicia procesos ejecutados en el código de otras personas". - Como ejemplo, build es una construcción de proyecto, lint está depurando y test está probando.

Comenzamos a diseñar angular.json hace mucho tiempo. Inicialmente, fue concebido como un reemplazo para la configuración de Webpack. Además, se suponía que permitiría a los desarrolladores elegir independientemente la implementación del ensamblaje del proyecto. Como resultado, obtuvimos un sistema básico de inicio de tareas, que seguía siendo simple y conveniente para nuestros experimentos. Llamamos a esta API "Arquitecto".

A pesar de que Architect no recibió soporte oficial, fue popular entre los desarrolladores que querían personalizar el ensamblaje de proyectos, así como entre las bibliotecas de terceros que necesitaban controlar su flujo de trabajo. Nx lo usó para ejecutar los comandos de Bazel, Ionic lo usó para ejecutar pruebas unitarias en Jest, y los usuarios podían extender sus configuraciones de Webpack usando herramientas como ngx-build-plus . Y eso fue solo el comienzo.

Se utiliza una versión oficialmente mejorada, estable y mejorada de esta API en Angular CLI versión 8.

Concepto


La API de Architect ofrece herramientas para programar y coordinar tareas que la CLI Angular usa para implementar sus comandos. Utiliza funciones llamadas
“Constructores”: “recolectores” que pueden actuar como tareas o planificadores de otros recolectores. Además, utiliza angular.json como un conjunto de instrucciones para los propios recopiladores.

Este es un sistema muy general diseñado para ser flexible y extensible. Contiene una API para informes, registros y pruebas. Si es necesario, el sistema se puede ampliar para nuevas tareas.

Recolectores


Los ensambladores son funciones que implementan lógica y comportamiento para una tarea que puede reemplazar el comando CLI. - Por ejemplo, inicie el linter.

La función de recopilador toma dos argumentos: el valor de entrada (u opciones) y el contexto que proporciona la relación entre la CLI y el recopilador en sí. La división de responsabilidad aquí es la misma que en Schematics: el usuario de CLI establece las opciones, la API es responsable del contexto y usted (el desarrollador) establece el comportamiento necesario. El comportamiento puede implementarse sincrónicamente, asincrónicamente o simplemente mostrar un cierto número de valores. La salida debe ser del tipo BuilderOutput , que contiene el éxito del campo lógico y el error de campo opcional, que contiene el mensaje de error.

Archivo de espacio de trabajo y tareas


La API de Architect se basa en angular.json , un archivo de espacio de trabajo para almacenar tareas y su configuración.

angular.json divide el espacio de trabajo en proyectos y, a su vez, en tareas. Por ejemplo, su aplicación creada con el comando ng new es uno de esos proyectos. Una de las tareas en este proyecto será la tarea de compilación , que se puede iniciar utilizando el comando ng build . Por defecto, esta tarea tiene tres claves:

  1. constructor : el nombre del recopilador que se utilizará para completar la tarea, en el formato PACKAGE_NAME: ASSEMBLY_NAME .
  2. opciones : configuración utilizada al iniciar una tarea de forma predeterminada.
  3. configuraciones : configuraciones que se aplicarán al iniciar una tarea con la configuración especificada.

La configuración se aplica de la siguiente manera: cuando se inicia la tarea, la configuración se toma del bloque de opciones, luego, si se ha especificado una configuración, sus configuraciones se escriben encima de las existentes. Después de eso, si se pasaron configuraciones adicionales a scheduleTarget () - el bloque de anulaciones , se escribirán en último lugar. Cuando se usa la CLI angular, los argumentos de la línea de comandos se pasan a las anulaciones . Después de que todas las configuraciones se transfieran al recopilador, las verifica de acuerdo con su esquema, y ​​solo si las configuraciones corresponden, se creará el contexto y el recopilador comenzará a funcionar.

Más información sobre el espacio de trabajo aquí .

Crea tu propio coleccionista


Como ejemplo, creemos un recopilador que ejecute un comando en la línea de comando. Para crear un recopilador, use la fábrica createBuilder y devuelva el objeto BuilderOutput :

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

Ahora, agreguemos algo de lógica a nuestro recopilador: queremos controlar el recopilador a través de la configuración, crear nuevos procesos, esperar a que se complete el proceso, y si el proceso se completó con éxito (es decir, el código de retorno 0), envíelo a 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 }); }); }); }); 

Procesamiento de salida


Ahora, el método de generación pasa todos los datos a la salida estándar del proceso. Es posible que queramos transferirlos al registrador - registrador. En este caso, en primer lugar, se facilitará la depuración durante las pruebas y, en segundo lugar, el arquitecto mismo puede ejecutar nuestro recopilador en un proceso separado o deshabilitar la salida estándar de procesos (por ejemplo, en la aplicación Electron).

Para hacer esto, podemos usar Logger , disponible en el objeto de contexto , que nos permitirá redirigir la salida del proceso:

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

Informes de rendimiento y estado


La parte final de la API relacionada con la implementación de su propio recopilador son los informes de progreso y estado actual.

En nuestro caso, el comando se completa o ejecuta, por lo que no tiene sentido agregar un informe de progreso. Sin embargo, podemos comunicar nuestro estado al colector principal para que comprenda lo que está sucediendo.

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

Para pasar un informe de progreso , use el método reportProgress con valores de resumen actuales y (opcionalmente) como argumentos. El total puede ser cualquier número. Por ejemplo, si sabe cuántos archivos necesita procesar, puede transferir su número al total , luego al actual puede transferir el número de archivos ya procesados. Así es como el recolector tslint informa sobre su progreso.

Validación de entrada


El objeto de opciones pasado al recopilador se verifica utilizando el esquema JSON. Esto es similar a Schematics si sabes lo que es.

En nuestro ejemplo de recopilador, esperamos que nuestros parámetros sean un objeto que reciba dos claves: comando - comando (cadena) y argumentos - argumentos (matriz de cadenas). Nuestro esquema de verificación se verá así:

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

Los esquemas son herramientas realmente poderosas que pueden llevar a cabo una gran cantidad de controles. Para obtener más información sobre los esquemas JSON, puede consultar el sitio web oficial del esquema JSON .

Crear un paquete de compilación


Hay un archivo clave que debemos crear para nuestro propio recopilador para que sea compatible con la CLI angular: builders.json , que es responsable de la relación entre nuestra implementación del recopilador, su nombre y el esquema de verificación. El archivo en sí se ve así:

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

Luego, en el archivo package.json , agregamos la clave de constructores , apuntando al archivo builders.json :

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

Esto le dirá al arquitecto dónde buscar el archivo de definición del recopilador.

Por lo tanto, el nombre de nuestro recopilador es "@ example / command-runner: command" . La primera parte del nombre, antes de los dos puntos (:) es el nombre del paquete, definido usando package.json . La segunda parte es el nombre del recopilador, definido mediante el archivo builders.json .

Probar tus propios constructores


La forma recomendada de probar los ensambladores es a través de pruebas de integración. Esto se debe a que crear un contexto no es fácil, por lo que debe usar el programador de Architect.

Para simplificar los patrones, pensamos en una forma simple de crear una instancia de Architect: primero crea un JsonSchemaRegistry (para probar el esquema), luego TestingArchitectHost y, finalmente, una instancia de Architect . Ahora puede compilar el archivo de configuración builders.json .

Aquí hay un ejemplo de cómo ejecutar el recopilador, que ejecuta el comando ls y verifica que el comando se haya completado correctamente. Tenga en cuenta que utilizaremos la salida estándar de procesos en el registrador .

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

Para ejecutar el ejemplo anterior, necesita el paquete ts-node . Si tiene la intención de usar Node, cambie el nombre de index_spec.ts a index_spec.js .

Usando el colector en un proyecto


Creemos un simple angular.json que demuestre todo lo que aprendimos sobre ensambladores. Suponiendo que empaquetamos nuestro recopilador en example / command-runner y luego creamos una nueva aplicación usando ng new builder-test , el archivo angular.json podría verse así (parte del contenido se ha eliminado por brevedad):

 { // ...   . "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 decidimos agregar una nueva tarea para aplicar (por ejemplo) el comando táctil al archivo (actualiza la fecha de modificación del archivo) usando nuestro recopilador, ejecutaremos npm install example / command-runner y luego haremos cambios en 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 angular tiene un comando de ejecución , que es el comando principal para ejecutar los recopiladores. Como primer argumento, toma una cadena del formato PROYECTO: TAREA [: CONFIGURACIÓN] . Para ejecutar nuestra tarea, podemos usar el comando ng run builder-test: touch .

Ahora podemos querer redefinir algunos argumentos. Desafortunadamente, hasta ahora no podemos redefinir las matrices desde la línea de comando, sin embargo, podemos cambiar el comando en sí para la demostración: ng run builder-test: touch --command = ls . - Esto generará el archivo src / main.ts.

Modo de reloj


De forma predeterminada, se supone que los recopiladores serán llamados una vez y terminados, sin embargo, pueden devolver Observable para implementar su propio modo de observación (como lo hace el recopilador Webpack ). El arquitecto se suscribirá al Observable hasta que finalice o se detenga y puede suscribirse nuevamente al recopilador si se llama al recopilador con los mismos parámetros (aunque no está garantizado).

  1. El recopilador debe devolver un objeto BuilderOutput después de cada ejecución. Una vez completado, puede ingresar al modo de observación causado por un evento externo y, si comienza de nuevo, tendrá que llamar a la función context.reportRunning () para notificar a Architect que el recopilador está trabajando nuevamente. Esto protegerá al recolector de que el Arquitecto lo detenga en una nueva llamada.
  2. El propio arquitecto se da de baja de Observable cuando el colector se detiene (usando run.stop (), por ejemplo), usando la lógica Teardown , el algoritmo de destrucción. Esto le permitirá detener y borrar el ensamblaje si este proceso ya se está ejecutando.

Resumiendo lo anterior, si su coleccionista mira eventos externos, funciona en tres etapas:

  1. Cumplimiento Por ejemplo, compilación de Webpack. Este paso finaliza cuando Webpack termina de compilar y su recopilador envía BuilderOutput al Observable .
  2. Observación - Entre dos lanzamientos, se supervisan los eventos externos. Por ejemplo, Webpack monitorea el sistema de archivos en busca de cambios. Este paso finaliza cuando Webpack reanuda la compilación y se llama a context.reportRunning () . Después de este paso, el paso 1 comienza de nuevo.
  3. Terminación - La tarea se completó por completo (por ejemplo, se esperaba que Webpack se iniciara un cierto número de veces) o se detuvo el inicio del recopilador (usando run.stop () ). En este caso, se ejecuta el algoritmo de destrucción Observable y se borra.

Conclusión


Aquí hay un resumen de lo que aprendimos en esta publicación:

  1. Proporcionamos una nueva API que permitirá a los desarrolladores cambiar el comportamiento de los comandos de CLI angular y agregar nuevos usando ensambladores que implementen la lógica necesaria.
  2. Los recopiladores pueden ser síncronos, asíncronos y receptivos a eventos externos. Pueden ser llamados varias veces, así como por otros coleccionistas.
  3. Los parámetros que recibe el recopilador cuando se inicia la tarea se leen primero desde el archivo angular.json , luego se sobrescriben con los parámetros de la configuración, si los hay, y luego se sobrescriben con los indicadores de la línea de comandos si se agregaron.
  4. La forma recomendada de probar los recopiladores es a través de pruebas de integración, sin embargo, puede realizar pruebas unitarias por separado de la lógica del recopilador.
  5. Si el colector devuelve un Observable, debe borrarse después de pasar por el algoritmo de destrucción.

En un futuro próximo, aumentará la frecuencia de uso de estas API. Por ejemplo, la implementación de Bazel está fuertemente asociada con ellos.

Ya vemos cómo la comunidad crea nuevos colectores CLI para su uso, por ejemplo, broma y ciprés para pruebas.

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


All Articles