Experts in Angular

Angular CLIParte 2: Schematics para Bibliotecas no Angular
Empacotando as Ferramentas para a Jornada

Parte 2: Schematics para Bibliotecas no Angular

Construindo seus Schematics: Empacotando as Ferramentas para a Jornada

Após criar sua coleção de Schematics e implementar a lógica de cada um, é hora de empacotar tudo junto com sua biblioteca Angular, para que os usuários possam desfrutar de uma experiência de instalação e uso completa.

Configurando o TypeScript: O Manual de Instruções da Fábrica

Para empacotar seus Schematics junto com sua biblioteca, você precisa configurar o TypeScript para compilá-los separadamente e, em seguida, adicioná-los ao pacote final.

Criando o Arquivo tsconfig.schematics.json

Imagine o tsconfig.schematics.json como o projeto arquitetônico que orienta o processo de construção de nossos Schematics. Esse arquivo deve ser colocado ao lado do tsconfig.lib.json, o qual foi gerado pelo Angular CLI para configurar o build da biblioteca.

Exemplo de tsconfig.schematics.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "lib": [
      "es2018",
      "dom"
    ],
    "declaration": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "rootDir": "schematics",
    "outDir": "../../dist/my-lib/schematics",
    "skipDefaultLibCheck": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strictNullChecks": true,
    "target": "es6",
    "types": [
      "jasmine",
      "node"
    ]
  },
  "include": [
    "schematics/**/*"
  ],
  "exclude": [
    "schematics/*/files/**/*"
  ]
}
Os Fundamentos:
  • rootDir: Este é o alicerce sobre o qual nossos Schematics são construídos, definindo a pasta schematics como a fonte de entrada para os arquivos a serem compilados.
  • outDir: Como uma linha de montagem, direciona os produtos acabados para o diretório de saída da biblioteca, geralmente dist/my-lib/schematics.

Scripts de Construção no package.json

Tal como em um concerto bem orquestrado, onde cada nota tem seu momento exato, precisamos garantir que nossos Schematics sejam compilados e incluídos no pacote da biblioteca. Para isso, adicionamos scripts ao arquivo package.json do projeto.

Exemplo de package.json
{
  "name": "my-lib",
  "version": "0.0.1",
  "scripts": {
    "build": "tsc -p tsconfig.schematics.json",
    "postbuild": "copyfiles schematics/*/schema.json schematics/*/files/** schematics/collection.json ../../dist/my-lib/"
  },
  "peerDependencies": {
    "@angular/common": "^16.1.0",
    "@angular/core": "^16.1.0"
  },
  "schematics": "./schematics/collection.json",
  "ng-add": {
    "save": "devDependencies"
  },
  "devDependencies": {
    "copyfiles": "file:../../node_modules/copyfiles",
    "typescript": "file:../../node_modules/typescript"
  }
}
Sinfonia dos Scripts:
  • build: Este script é como um maestro que conduz a compilação de seus Schematics, utilizando o tsconfig.schematics.json como partitura.
  • postbuild: Após o último acorde da compilação, este script entra em cena para garantir que todos os arquivos sejam copiados para o lugar certo, completando a sinfonia.

Instalando as Dependências

Antes de qualquer apresentação, a orquestra deve estar afinada. Da mesma forma, antes de executar os scripts, é necessário garantir que as dependências copyfiles e typescript estejam devidamente instaladas. Navegue até o caminho definido em devDependencies e execute npm install para afinar seu projeto.

Fornecendo Suporte à Geração: Criando Schematics para Agilizar o Desenvolvimento

Com o Angular CLI, você pode ir além da simples instalação da sua biblioteca, oferecendo aos usuários a capacidade de gerar elementos específicos, como componentes, serviços e outros artefatos, diretamente do terminal. Essa funcionalidade é proporcionada pelos Schematics de geração, que automatizam a criação desses elementos, economizando tempo e garantindo a consistência do código.

Vamos supor que sua biblioteca define um serviço especial chamado my-service, que requer algumas configurações iniciais.
Você deseja que seus usuários possam gerá-lo facilmente usando o comando:

ng generate my-lib:my-service

Estruturando o Projeto: A Base do Schematic

  1. Criação do Subdiretório
    • Primeiro, vamos estabelecer um novo subdiretório para nosso schematic. Dentro da pasta schematics do seu projeto, crie uma nova pasta chamada my-service. É aqui que o coração do nosso schematic irá bater.
  2. Configurando o Schematic
    • Para adicionar um schematic à sua coleção, é necessário referenciá-lo no schema da coleção e fornecer arquivos de configuração para definir as opções que um usuário pode passar para o comando.
      • Edite o arquivo schematics/collection.json para apontar para a nova subpasta do schematic e inclua um ponteiro para um arquivo de schema que especifique as entradas para o novo schematic.
{
  "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "ng-add": {
      "description": "Add my library to the project.",
      "factory": "./ng-add/index#ngAdd",
      "schema": "./ng-add/schema.json"
    },
    "my-service": {
      "description": "Generate a service in the project.",
      "factory": "./my-service/index#myService",
      "schema": "./my-service/schema.json"
    }
  }
}

3. Definindo o Schema do Schematic

Dentro da pasta my-service, crie um arquivo schema.json e defina as opções disponíveis para o schematic. Este arquivo descreve os parâmetros que o usuário pode fornecer ao executar o comando de geração.

{
  "$schema": "http://json-schema.org/schema",
  "$id": "SchematicsMyService",
  "title": "My Service Schema",
  "type": "object",
  "properties": {
    "name": {
      "description": "The name of the service.",
      "type": "string"
    },
    "path": {
      "type": "string",
      "format": "path",
      "description": "The path to create the service.",
      "visible": false
    },
    "project": {
      "type": "string",
      "description": "The name of the project.",
      "$default": {
        "$source": "projectName"
      }
    }
  },
  "required": [
    "name"
  ]
}
Elementos Essenciais:
  • $id: Um identificador único para o schema dentro da coleção.
  • title: Uma descrição legível para humanos do schema.
  • properties: Define as opções disponíveis, cada uma com um tipo, descrição e, opcionalmente, um alias.

Exemplo de Interface Schema:

No mesmo diretório, crie um arquivo schema.ts para definir uma interface TypeScript que armazena os valores das opções definidas no schema.json.

export interface Schema {
  // The name of the service.
  name: string;
  // The path to create the service.
  path?: string;
  // The name of the project.
  project?: string;
}
Detalhes das Opções:
  • name: O nome do serviço que será criado.
  • path: Sobrescreve o caminho fornecido para o schematic. O valor padrão do caminho é baseado no diretório de trabalho atual.
  • project: Especifica um projeto específico para executar o schematic. No schematic, você pode fornecer um valor padrão se a opção não for fornecida pelo usuário.

Adicionando Arquivos de Template: Moldando a Estrutura da sua Criação

Agora que você configurou seu Schematic de geração, é hora de dar forma à sua criação. Para adicionar artefatos, como componentes, serviços ou módulos, ao projeto do usuário, seu Schematic precisa de seus próprios arquivos de template. Esses templates permitem que você defina a estrutura básica do código gerado, incluindo placeholders para variáveis que serão substituídas dinamicamente durante a execução do Schematic.

Passos para Criar Arquivos de Template

  1. Crie o Diretório de Templates

Dentro da pasta schematics/my-service, crie uma nova pasta chamada files. Essa pasta armazenará os templates que serão utilizados para gerar os arquivos do seu serviço.

projects/my-lib/schematics/my-service/files/

2. Crie o Arquivo de Template para o Serviço

No diretório files/, crie um arquivo com o nome __name@dasherize__.service.ts.template.
Este arquivo definirá o template usado para gerar o serviço.
O uso de expressões dinâmicas no nome do arquivo (__name@dasherize__) permite que o nome do serviço seja configurado de forma programática quando o schematic é executado.

// projects/my-lib/schematics/my-service/files/__name@dasherize__.service.ts.template

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class <%= classify(name) %>Service {
  constructor(private http: HttpClient) { }
}

Explicando a Magia: Transformações e Substituições

  • Funções Utilitárias de TransformaçãoNo template acima, você notará o uso de duas funções utilitárias: classify e dasherize. Estas funções ajudam a transformar o conteúdo do template, ajustando variáveis e nomes de arquivos de acordo com padrões específicos.
    • <%= classify(name) %>: Esta expressão transforma o nome fornecido em uma classe TypeScript, aplicando a capitalização adequada (PascalCase), o que é padrão para nomes de classes em TypeScript.
    • __name@dasherize__: Aqui, o nome do serviço é transformado em um formato de nome de arquivo adequado (kebab-case), substituindo espaços ou letras maiúsculas por hífens. Isso é útil para manter a consistência dos nomes de arquivos.
  • Injeção de DependênciasNo exemplo do template, a classe de serviço gerada já tem a injeção do HttpClient do Angular no construtor, demonstrando como você pode fornecer funcionalidades padrão que muitos desenvolvedores precisam.

A Variável name: O Coração da Personalização

A variável name é fornecida como uma propriedade da sua função de fábrica no arquivo index.ts. Ela corresponde ao valor da opção name definida no esquema JSON. Ao executar o Schematic, o Angular CLI substituirá <%= classify(name) %> e <%= dasherize(name) %> pelos valores correspondentes, gerando um arquivo de serviço com o nome e a classe personalizados.


Por que Usar __name@dasherize__ para Nomear Arquivos?

1. Automação e Dinamismo

A expressão __name@dasherize__ permite que o nome do arquivo seja determinado dinamicamente com base nas entradas fornecidas pelo usuário ao executar o schematic. Isso significa que, ao gerar novos artefatos, o nome do arquivo se adapta automaticamente ao nome especificado pelo usuário, mantendo consistência e eliminando a necessidade de renomear arquivos manualmente após a geração.

2. Consistência no Formato dos Nomes de Arquivo
Dasherize
  • dasherize: Esta função transforma strings em um formato de nome de arquivo com hífens, também conhecido como kebab-case. Isso é particularmente importante para nomes de arquivos no mundo JavaScript e TypeScript, onde a consistência no uso de letras minúsculas e hífens é uma prática comum para melhorar a legibilidade.
    • Exemplo: Se o usuário passar o nome UserProfile, a expressão __name@dasherize__ será transformada em user-profile.
Classify
  • classify: Embora classify não seja usada para nomear arquivos, ela é frequentemente usada dentro de templates para transformar nomes em PascalCase, adequado para nomes de classes em TypeScript.
3. Facilita o Naming Convention

Usar convenções de nomeação como __name@dasherize__ ajuda a impor uma regra consistente de nomeação em todos os projetos, o que é crucial em projetos colaborativos ou de larga escala.

4. Personalização e Reutilização

Este padrão de nomeação permite criar templates que podem ser reutilizados em diferentes contextos com o mínimo de alterações, apenas mudando as variáveis de entrada. Isso resulta em um desenvolvimento mais ágil e menos propenso a erros.

5. Simplificação do Workflow

Ao integrar funções como dasherize nos nomes de arquivos, os desenvolvedores reduzem a necessidade de lógica adicional para manipulação de strings em código. A simplicidade do uso de templates torna o fluxo de trabalho mais direto e menos sobrecarregado por detalhes de implementação.

Exemplo Prático

Vamos considerar um cenário em que um usuário executa um comando para gerar um serviço chamado MyExampleService:

ng generate my-lib:my-service --name=MyExampleService
Resultados Gerados
  • Nome do Arquivo: my-example-service.service.ts
  • Nome da Classe: MyExampleService

Aqui, __name@dasherize__ garante que o nome do arquivo seja formatado como my-example-service, enquanto <%= classify(name) %> no conteúdo do template formata o nome da classe como MyExampleService.


Para continuar o processo de criação de um schematic completo no Angular, vamos avançar na implementação da função de fábrica, responsável por realizar as modificações necessárias no projeto do usuário. Este passo é crucial para garantir que o código gerado esteja de acordo com as especificações e requisitos definidos na interface do esquema.

Adicionando a Função de Fábrica: Dando Vida à Lógica do Schematic

Agora que a infraestrutura está pronta, é hora de definir a função principal que realizará as modificações necessárias no projeto do usuário. Essa função, chamada de “fábrica de regras”, é o coração do seu Schematic, onde a mágica da automação acontece.

Estrutura de Templates e Funções Utilitárias

O framework de Schematics do Angular oferece um sistema robusto de templates que suporta tanto templates de caminho quanto de conteúdo. Esse sistema preenche automaticamente os placeholders definidos nos arquivos ou caminhos carregados na árvore de entrada (input Tree), utilizando valores passados para a Rule.

Passo a Passo para Adicionar a Função de Fábrica

1. Criação do Arquivo Principal

Comece criando o arquivo principal index.ts dentro da pasta my-service do seu schematic e adicione o código fonte para a função de fábrica do seu schematic.

2. Importação das Definições Necessárias

Primeiro, importe as definições de schematics que serão necessárias. O framework de Schematics oferece diversas funções utilitárias para criar e utilizar regras ao executar um schematic.

// projects/my-lib/schematics/my-service/index.ts (Imports)
import {
  Rule,
  Tree,
  SchematicsException,
  apply,
  url,
  applyTemplates,
  move,
  chain,
  mergeWith,
} from '@angular-devkit/schematics';
import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';

3. Importação da Interface do Esquema

Importe a interface do esquema que fornece as informações de tipo para as opções do seu schematic. Essa interface foi definida previamente no arquivo schema.ts.

import {Schema as MyServiceSchema} from './schema';

4. Inicialização da Função de Fábrica com uma Regra Vazia

Para começar, crie uma função de fábrica inicial que retorne a árvore (tree) sem modificações. As opções recebidas são os valores de opção passados pelo comando ng generate.

export function myService(options: MyServiceSchema): Rule {
  return (tree: Tree) => tree;
}

Essa fábrica de regras simplesmente retorna a árvore sem modificações. As opções (options) são os valores passados pelo comando ng generate.

Próximos Passos

Com a estrutura básica em vigor, o próximo passo é implementar a lógica que realizará as modificações desejadas no projeto do usuário. Isso envolverá o uso de funções utilitárias para manipular a árvore de arquivos, aplicar templates, e integrar a lógica específica do serviço ou componente que você está criando.

No próximo artigo, vamos explorar como preencher essa função de fábrica com lógica real que manipula o projeto conforme especificado no esquema. Isso incluirá:

  • Validação de opções de entrada.
  • Localização do caminho correto para a geração do artefato.
  • Aplicação de templates para gerar arquivos e conteúdo dinamicamente.
  • Integração das mudanças no projeto do usuário, garantindo que as dependências e configurações sejam ajustadas conforme necessário.