Apresentando o CLI Builder

Apresentando o CLI Builder

Neste artigo, examinaremos a nova API da CLI angular, que permitirá estender os recursos existentes da CLI e adicionar novos. Discutiremos como trabalhar com essa API e quais pontos de sua extensão existem, o que permite adicionar novas funcionalidades à CLI.

A história


Há cerca de um ano, introduzimos o arquivo da área de trabalho ( angular.json ) na CLI angular e repensamos muitos dos princípios básicos para implementar seus comandos. Chegamos ao fato de termos colocado as equipes nas “caixas”:

  1. Comandos esquemáticos - "Comandos esquemáticos" . Até agora, você provavelmente já ouviu falar sobre o Schematics, a biblioteca usada pela CLI para gerar e modificar seu código. Ele apareceu na versão 5 e atualmente é usado na maioria dos comandos relacionados ao seu código, como novo , gerar , adicionar e atualizar .
  2. Comandos diversos - "Outras equipes" . Estes são comandos que não estão diretamente relacionados ao seu projeto: ajuda , versão , configuração , doc . Recentemente, a análise também apareceu, bem como nossos ovos de Páscoa (Shhh! Nem uma palavra para ninguém!).
  3. Comandos de tarefas - "Comandos de tarefas" . Essa categoria, em geral, "inicia processos executados no código de outras pessoas". - Por exemplo, build é uma projeto, lint está depurando e test está testando.

Começamos a projetar o angular.json há muito tempo. Inicialmente, foi concebido como um substituto para a configuração do Webpack. Além disso, deveria permitir que os desenvolvedores escolhessem independentemente a implementação da montagem do projeto. Como resultado, obtivemos um sistema básico de iniciação de tarefas, que permaneceu simples e conveniente para nossos experimentos. Chamamos essa API de "arquiteto".

Apesar do fato de o Architect não ser oficialmente suportado, ele era popular entre os desenvolvedores que desejavam personalizar a montagem de projetos, bem como entre as bibliotecas de terceiros que precisavam controlar seu fluxo de trabalho. O Nx o usou para executar comandos do Bazel, a Ionic para executar testes de unidade no Jest, e os usuários podem estender suas configurações de Webpack usando ferramentas como o ngx-build-plus . E isso foi apenas o começo.

Uma versão oficialmente suportada, estável e aprimorada dessa API é usada na CLI Angular versão 8.

Conceito


A API do arquiteto oferece ferramentas para agendar e coordenar tarefas que a CLI Angular usa para implementar seus comandos. Ele usa funções chamadas
“Construtores” - “colecionadores” que podem atuar como tarefas ou planejadores de outros colecionadores. Além disso, ele usa angular.json como um conjunto de instruções para os próprios coletores.

Este é um sistema muito geral projetado para ser flexível e extensível. Ele contém uma API para relatórios, registros e testes. Se necessário, o sistema pode ser expandido para novas tarefas.

Pickers


Montadores são funções que implementam lógica e comportamento para uma tarefa que pode substituir o comando da CLI. - Por exemplo, inicie o linter.

A função do coletor usa dois argumentos: o valor de entrada (ou opções) e o contexto que fornece o relacionamento entre a CLI e o próprio coletor. A divisão de responsabilidade aqui é a mesma que em Esquema - o usuário da CLI define as opções, a API é responsável pelo contexto e você (o desenvolvedor) define o comportamento necessário. O comportamento pode ser implementado de forma síncrona, assíncrona ou simplesmente exibir um determinado número de valores. A saída deve ser do tipo BuilderOutput , que contém o sucesso do campo lógico e o erro opcional do campo, que contém a mensagem de erro.

Arquivo e tarefas da área de trabalho


A API do Architect conta com angular.json , um arquivo da área de trabalho para armazenar tarefas e suas configurações.

angular.json divide o espaço de trabalho em projetos e eles, por sua vez, em tarefas. Por exemplo, seu aplicativo criado com o comando ng new é um desses projetos. Uma das tarefas deste projeto será a tarefa de construção , que pode ser iniciada usando o comando ng build . Por padrão, esta tarefa possui três chaves:

  1. construtor - o nome do coletor a ser usado para concluir a tarefa, no formato PACKAGE_NAME: ASSEMBLY_NAME .
  2. opções - configurações usadas ao iniciar uma tarefa por padrão.
  3. configurações - configurações que serão aplicadas ao iniciar uma tarefa com a configuração especificada.

As configurações são aplicadas da seguinte maneira: quando a tarefa é iniciada, as configurações são obtidas do bloco de opções e, se uma configuração foi especificada, suas configurações são gravadas sobre as existentes. Depois disso, se configurações adicionais forem passadas para scheduleTarget () - o bloco de substituições , elas serão gravadas por último. Ao usar a CLI Angular, os argumentos da linha de comando são transmitidos para substituições . Depois que todas as configurações são transferidas para o coletor, ele as verifica de acordo com seu esquema e, somente se as configurações corresponderem a ele, o contexto será criado e o coletor começará a funcionar.

Mais informações sobre o espaço de trabalho aqui .

Crie seu próprio colecionador


Como exemplo, vamos criar um coletor que executará um comando na linha de comando. Para criar um coletor, use a fábrica createBuilder e retorne o objeto BuilderOutput :

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

Agora, vamos adicionar um pouco de lógica ao nosso coletor: queremos controlar o coletor através das configurações, criar novos processos, aguardar a conclusão do processo e, se o processo for concluído com êxito (ou seja, código de retorno 0), sinalize isso ao arquiteto:

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

Processamento de saída


Agora, o método de geração passa todos os dados para a saída padrão do processo. Podemos querer transferi-los para o logger - logger. Nesse caso, primeiro, a depuração durante o teste será facilitada e, segundo, o próprio Architect pode executar nosso coletor em um processo separado ou desativar a saída padrão de processos (por exemplo, no aplicativo Electron).

Para fazer isso, podemos usar o Logger , disponível no objeto de contexto , o que nos permitirá redirecionar a saída do processo:

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

Relatórios de desempenho e status


A parte final da API relacionada à implementação de seu próprio coletor é o progresso e os relatórios de status atuais.

No nosso caso, o comando é concluído ou executado, portanto, não faz sentido adicionar um relatório de progresso. No entanto, podemos comunicar nosso status ao coletor pai, para que ele entenda o que está acontecendo.

 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 passar um relatório de progresso , use o método reportProgress com valores de resumo atuais e (opcionalmente) como argumentos. total pode ser qualquer número. Por exemplo, se você souber quantos arquivos precisa processar, poderá transferir o número deles para o total , e para o atual, poderá transferir o número de arquivos já processados. É assim que o coletor tslint relata seu progresso.

Validação de entrada


O objeto de opções passado para o coletor é verificado usando o esquema JSON. Isso é semelhante ao esquema, se você souber o que é.

Em nosso exemplo de coletor, esperamos que nossos parâmetros sejam um objeto que receba duas chaves: command - command (string) e args - argumentos (array de strings). Nosso esquema de verificação ficará assim:

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

Os esquemas são ferramentas realmente poderosas que podem realizar um grande número de verificações. Para obter mais informações sobre esquemas JSON, consulte o site oficial do esquema JSON .

Crie um pacote de compilação


Há um arquivo-chave que precisamos criar para nosso próprio coletor, a fim de torná-lo compatível com a CLI Angular - builders.json , responsável pela relação entre nossa implementação do coletor, seu nome e o esquema de verificação. O arquivo em si é assim:

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

Em seguida, no arquivo package.json , adicionamos a chave builders , apontando para o arquivo builders.json :

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

Isso informará ao Architect onde procurar o arquivo de definição do coletor.

Portanto, o nome do nosso coletor é "@ example / command-runner: command" . A primeira parte do nome, antes dos dois pontos (:), é o nome do pacote, definido usando package.json . A segunda parte é o nome do coletor, definido usando o arquivo builders.json .

Testando seus próprios construtores


A maneira recomendada de testar montadores é através do teste de integração. Isso ocorre porque a criação de um contexto não é fácil, portanto, você deve usar o agendador do Architect.

Para simplificar os padrões, pensamos em uma maneira simples de criar uma instância do Architect: primeiro, você cria um JsonSchemaRegistry (para testar o esquema), depois TestingArchitectHost e, finalmente, uma instância do Architect . Agora você pode compilar o arquivo de configuração builders.json .

Aqui está um exemplo de execução do coletor, que executa o comando ls e verifica se o comando foi concluído com êxito. Observe que usaremos a saída padrão dos processos no logger .

 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 executar o exemplo acima, você precisa do pacote ts-node . Se você pretende usar o Nó, renomeie index_spec.ts para index_spec.js .

Usando o coletor em um projeto


Vamos criar um angular.json simples que demonstre tudo o que aprendemos sobre montadores. Supondo que empacotamos nosso coletor em example / command-runner e, em seguida, criamos um novo aplicativo usando o ng builder-test , o arquivo angular.json pode ter a seguinte aparência (parte do conteúdo foi removida por questões de brevidade):

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

Se decidimos adicionar uma nova tarefa para aplicar (por exemplo) o comando touch ao arquivo (atualiza a data de modificação do arquivo) usando nosso coletor, executaríamos o npm install example / command-runner e, em seguida, faríamos alterações em 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 } } } } } } } 

A CLI Angular possui um comando de execução , que é o principal comando para executar coletores. Como primeiro argumento, ele usa uma sequência do formato PROJECT: TASK [: CONFIGURATION] . Para executar nossa tarefa, podemos usar o comando ng run builder-test: touch .

Agora, podemos querer redefinir alguns argumentos. Infelizmente, não podemos redefinir matrizes a partir da linha de comando até o momento, no entanto, podemos alterar o próprio comando para demonstração: ng execute o builder-test: touch --command = ls . - Isso produzirá o arquivo src / main.ts.

Modo de exibição


Por padrão, supõe-se que os coletores serão chamados uma vez e encerrados; no entanto, eles podem retornar Observable para implementar seu próprio modo de observação (como o coletor Webpack ). O Architect assinará o Observable até que ele termine ou pare e possa assinar o coletor novamente se o coletor for chamado com os mesmos parâmetros (embora não seja garantido).

  1. O coletor deve retornar um objeto BuilderOutput após cada execução. Após a conclusão, ele pode entrar no modo de observação causado por um evento externo e, se reiniciar, precisará chamar a função context.reportRunning () para notificar o Architect de que o coletor está trabalhando novamente. Isso protegerá o coletor de impedi-lo pelo arquiteto em uma nova chamada.
  2. O próprio arquiteto cancela a assinatura de Observable quando o coletor para (usando run.stop (), por exemplo), usando a lógica Teardown - o algoritmo de destruição. Isso permitirá que você pare e limpe a montagem se esse processo já estiver em execução.

Resumindo o acima, se o seu coletor assistir a eventos externos, ele funcionará em três estágios:

  1. Cumprimento. Por exemplo, compilação do Webpack. Esta etapa termina quando o Webpack termina a construção e seu coletor envia o BuilderOutput ao Observable .
  2. Observação. - Entre dois lançamentos, eventos externos são monitorados. Por exemplo, o Webpack monitora o sistema de arquivos para quaisquer alterações. Esta etapa termina quando o Webpack retoma a construção e context.reportRunning () é chamado. Após esta etapa, a etapa 1 começa novamente.
  3. Conclusão. - A tarefa está totalmente concluída (por exemplo, era esperado que o Webpack iniciasse um certo número de vezes) ou o início do coletor foi interrompido (usando run.stop () ). Nesse caso, o algoritmo de destruição observável é executado e limpo.

Conclusão


Aqui está um resumo do que aprendemos nesta publicação:

  1. Fornecemos uma nova API que permitirá que os desenvolvedores alterem o comportamento dos comandos da CLI angular e adicionem novos usando assemblers que implementam a lógica necessária.
  2. Os coletores podem ser síncronos, assíncronos e responsivos a eventos externos. Eles podem ser chamados várias vezes, assim como por outros colecionadores.
  3. Os parâmetros que o coletor recebe quando a tarefa é iniciada são lidos primeiro a partir do arquivo angular.json e, em seguida, são substituídos pelos parâmetros da configuração, se houver, e substituídos pelos sinalizadores da linha de comando, se eles foram adicionados.
  4. A maneira recomendada de testar coletores é através de testes de integração, no entanto, você pode executar testes de unidade separadamente da lógica do coletor.
  5. Se o coletor retornar um Observable, ele deverá ser limpo depois de passar pelo algoritmo de destruição.

Num futuro próximo, a frequência de uso dessas APIs aumentará. Por exemplo, a implementação do Bazel está fortemente associada a eles.

Já vimos como a comunidade cria novos coletores de CLI para uso, por exemplo, brincadeiras e ciprestes para teste.

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


All Articles