
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":
- 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 .
- 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!).
- 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:
- constructor : el nombre del recopilador que se utilizará para completar la tarea, en el formato PACKAGE_NAME: ASSEMBLY_NAME .
- opciones : configuración utilizada al iniciar una tarea de forma predeterminada.
- 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);
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).
- 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.
- 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:
- Cumplimiento Por ejemplo, compilación de Webpack. Este paso finaliza cuando Webpack termina de compilar y su recopilador envía BuilderOutput al Observable .
- 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.
- 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:
- 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.
- Los recopiladores pueden ser síncronos, asíncronos y receptivos a eventos externos. Pueden ser llamados varias veces, así como por otros coleccionistas.
- 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.
- 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.
- 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.