
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”:
- 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 .
- 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!).
- 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:
- construtor - o nome do coletor a ser usado para concluir a tarefa, no formato PACKAGE_NAME: ASSEMBLY_NAME .
- opções - configurações usadas ao iniciar uma tarefa por padrão.
- 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);
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).
- 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.
- 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:
- 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 .
- 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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.