Experts in Angular

Angular CLIParte 3: Schematics para Bibliotecas no Angular
Schematic de geração que utiliza templates

Parte 3: Schematics para Bibliotecas no Angular

Para prosseguirmos com a série de artigos sobre Schematics do Angular, vamos definir a regra de geração. Este é um passo essencial que permite a criação de código que modifica a aplicação do usuário, configurando-a para utilizar o serviço definido em sua biblioteca.

Com a infraestrutura e os templates prontos, é hora de dar vida à lógica do seu Schematic.

Identificando o Projeto Alvo: O Destino da Missão

O workspace Angular onde o usuário instalou sua biblioteca pode conter múltiplos projetos (aplicações e bibliotecas). O usuário pode especificar o projeto na linha de comando ou deixá-lo como padrão. Em ambos os casos, seu código precisa identificar o projeto específico ao qual o Schematic está sendo aplicado, para que você possa recuperar informações da configuração do projeto.

Para isso, utilize o objeto Tree que é passado para a função de fábrica. Os métodos da Tree fornecem acesso completo à árvore de arquivos do workspace, permitindo que você leia e escreva arquivos durante a execução do Schematic.

1. Obter a Configuração do Projeto

Para determinar o projeto de destino, utilizamos o método workspaces.readWorkspace para ler o conteúdo do arquivo de configuração do workspace, angular.json. Precisamos criar um workspaces.WorkspaceHost a partir do Tree.
Veja como fazer isso na função de fábrica.

import {
  Rule,
  Tree,
  SchematicsException,
  apply,
  url,
  applyTemplates,
  move,
  chain,
  mergeWith,
} from '@angular-devkit/schematics';

import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';

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

function createHost(tree: Tree): workspaces.WorkspaceHost {
  return {
    async readFile(path: string): Promise<string> {
      const data = tree.read(path);
      if (!data) {
        throw new SchematicsException('File not found.');
      }
      return virtualFs.fileBufferToString(data);
    },
    async writeFile(path: string, data: string): Promise<void> {
      return tree.overwrite(path, data);
    },
    async isDirectory(path: string): Promise<boolean> {
      return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
    },
    async isFile(path: string): Promise<boolean> {
      return tree.exists(path);
    },
  };
}

export function myService(options: MyServiceSchema): Rule {
  return async (tree: Tree) => {
    const host = createHost(tree);
    const { workspace } = await workspaces.readWorkspace('/', host);

    // Verificar se o projeto existe
    const project = options.project != null ? workspace.projects.get(options.project) : null;
    if (!project) {
      throw new SchematicsException(`Invalid project name: ${options.project}`);
    }

    // Determinar o tipo de projeto
    const projectType = project.extensions.projectType === 'application' ? 'app' : 'lib';

    // Determinar o caminho para aplicar os templates
    if (options.path === undefined) {
      options.path = `${project.sourceRoot}/${projectType}`;
    }

    // A partir daqui, adicione a lógica para modificar o projeto
  };
}
Explicação do Código
  1. Função createHost: Esta função cria um WorkspaceHost que permite a leitura e escrita de arquivos no Tree. Isso é necessário para usar o método readWorkspace da biblioteca @angular-devkit/core.
  2. Leitura da Configuração do Workspace: Utilizamos workspaces.readWorkspace para obter a configuração do workspace a partir do arquivo angular.json. Este passo é crucial para identificar qual projeto precisa ser modificado.
  3. Validação do Projeto: Verificamos se o projeto especificado nas opções realmente existe no workspace. Se não existir, lançamos uma exceção.
  4. Determinação do Tipo de Projeto: Identificamos se o projeto é uma aplicação (app) ou uma biblioteca (lib). Isso nos ajuda a determinar o caminho correto para aplicar os templates.
  5. Definição do Caminho de Destino: Se o caminho não foi especificado, utilizamos o sourceRoot do projeto e o tipo de projeto para definir onde os arquivos de template serão movidos após a aplicação do schematic.

Agora que você identificou o nome do projeto, pode usá-lo para recuperar as informações específicas de configuração desse projeto. Esta etapa é crucial para garantir que o schematic seja aplicado corretamente ao projeto correto dentro do workspace Angular do usuário.

2. Recuperando a Configuração do Projeto

O objeto workspace.projects contém todas as informações de configuração específicas de cada projeto no workspace. Utilizamos este objeto para acessar detalhes sobre o projeto alvo, o que nos permite determinar como e onde o schematic deve aplicar suas modificações.

Código para Recuperar Configuração do Projeto
const project = options.project != null ? workspace.projects.get(options.project) : null;
if (!project) {
  throw new SchematicsException(`Invalid project name: ${options.project}`);
}
const projectType = project.extensions.projectType === 'application' ? 'app' : 'lib';

Explicação do Código

  1. Obtenção do Projeto Específico:
    • Utilizamos workspace.projects.get(options.project) para obter o objeto de configuração do projeto especificado nas opções.
    • Caso o projeto não seja encontrado, lançamos uma exceção SchematicsException com uma mensagem indicando que o nome do projeto é inválido.
  2. Determinação do Tipo de Projeto:
    • Verificamos a propriedade project.extensions.projectType para identificar se o projeto é do tipo ‘application’ ou ‘lib’.
    • Essa informação é armazenada na variável projectType, que pode ser utilizada posteriormente para decidir o comportamento do schematic em função do tipo de projeto.

Uso do Objeto workspace.projects

O objeto workspace.projects é uma coleção de todos os projetos no workspace Angular, cada um representado por um objeto que contém diversas propriedades de configuração, tais como:

  • sourceRoot: O diretório raiz dos arquivos de origem do projeto.
  • architect: Configurações para tarefas de construção, como build, serve, e test.
  • prefix: Prefixo padrão para seletores de componentes gerados.
  • projectType: Indica se o projeto é uma application ou uma lib.

Ao desenvolver um schematic, um aspecto crítico é determinar o local correto onde os arquivos de template serão inseridos no projeto do usuário. O caminho onde os arquivos do schematic serão colocados é especificado pela opção path dentro das opções do schematic. Se esta opção não for explicitamente definida pelo usuário, é importante calcular um caminho padrão baseado na configuração do projeto.

3. Determinando o Caminho para os Arquivos de Template

A seguir, vamos explorar como calcular o caminho padrão para a aplicação dos arquivos de template, utilizando a configuração do projeto:

Código para Determinar o Caminho dos Arquivos de Template
if (options.path === undefined) {
  options.path = `${project.sourceRoot}/${projectType}`;
}
Explicação do Código
  1. Verificação da Opção path:
    • O código verifica se a opção path foi fornecida nas opções do schematic. Isso é feito verificando se options.path é undefined.
  2. Definição do Caminho Padrão:
    • Se path não foi especificado, calculamos um caminho padrão utilizando a configuração do projeto:
      • project.sourceRoot: Esta propriedade contém o diretório raiz dos arquivos de origem do projeto, conforme definido na configuração do projeto dentro do arquivo angular.json.
      • projectType: Esta variável, que determina se o projeto é uma aplicação (app) ou uma biblioteca (lib), é usada para definir o subdiretório dentro do sourceRoot onde os arquivos serão colocados.
  3. Construção do Caminho:
    • O caminho padrão é então concatenado como ${project.sourceRoot}/${projectType}, garantindo que os arquivos do schematic sejam colocados na estrutura correta dentro do projeto do usuário.

Por Que Isso é Importante?

Definir corretamente o caminho onde os arquivos de template serão aplicados é fundamental para garantir que o schematic funcione como esperado e que os arquivos gerados estejam localizados no lugar correto:

  • Organização do Projeto: Garante que os arquivos sejam integrados ao projeto de acordo com as convenções padrão, evitando desorganização e confusão.
  • Compatibilidade: Assegura que o schematic funcione com diferentes configurações de projeto, seja ele uma aplicação ou uma biblioteca.
  • Facilidade de Uso: Simplifica a experiência do usuário, não requerendo que ele forneça explicitamente o caminho, a menos que deseje sobrepor o padrão.

Definindo a Regra de Geração: A Magia da Criação de Código

Na implementação de um schematic no Angular, uma parte fundamental é definir a lógica que irá transformar os arquivos de template e inseri-los no projeto do usuário. Isso é feito utilizando o conceito de regras (Rule) que aplicam transformações em fontes de arquivos, resultando em um novo conjunto de arquivos modificados. Vamos explorar como configurar essa regra para aplicar os templates do schematic.

1. Construindo a Regra: Aplicando Templates e Movendo Arquivos

Vamos adicionar o seguinte código à sua função de fábrica para definir a regra de geração:

// projects/my-lib/schematics/my-service/index.ts (Template transform)
const templateSource = apply(url('./files'), [
  applyTemplates({
    classify: strings.classify,
    dasherize: strings.dasherize,
    name: options.name,
  }),
  move(normalize(options.path as string)),
]);
Detalhes dos Métodos Utilizados

Vamos detalhar cada um dos métodos utilizados na construção dessa regra:

  1. apply():
    • Propósito: Este método aplica múltiplas regras a uma fonte (source) e retorna a fonte transformada.
    • Uso: Recebe dois argumentos:
      • Source: A origem dos arquivos que serão transformados, que no nosso caso é o retorno de url('./files').
      • Array de Regras: Um array de funções Rule que serão aplicadas sequencialmente à fonte.
  2. url():
    • Propósito: Lê arquivos de origem do sistema de arquivos, relativos ao diretório do schematic.
    • Uso: No exemplo, url('./files') aponta para o diretório de templates files/ dentro do schematic.
  3. applyTemplates():
    • Propósito: Fornece métodos e propriedades que estarão disponíveis tanto para o conteúdo dos templates quanto para os nomes dos arquivos do template.
    • Uso: Recebe um objeto com métodos de transformação (classify e dasherize) e propriedades (name) que podem ser utilizadas nos templates:
      • classify(): Transforma uma string em Title Case. Por exemplo, “meu serviço” se torna “MeuServiço”.
      • dasherize(): Transforma uma string em kebab-case (letras minúsculas separadas por hífen). Por exemplo, “MeuServiço” se torna “meu-servico”.
      • name: Representa o nome passado nas opções do schematic, que pode ser utilizado nos templates.
  4. move():
    • Propósito: Move os arquivos de origem transformados para o diretório de destino especificado, quando o schematic é aplicado.
    • Uso: Recebe como argumento o caminho normalizado (normalize(options.path as string)) onde os arquivos devem ser colocados.
Como Funciona o Fluxo
  • Leitura dos Arquivos: O url('./files') coleta todos os arquivos do diretório files/ que foram preparados como templates.
  • Transformação dos Templates: O applyTemplates executa transformações nas strings presentes nos arquivos de template e nos seus nomes de arquivo, utilizando os métodos de string utilitários e as opções fornecidas.
  • Movimentação dos Arquivos: Finalmente, o move() reposiciona os arquivos transformados para o local correto no projeto do usuário, conforme determinado pela opção path.
Importância da Transformação de Templates
  • Customização: Permite a criação de arquivos altamente customizados, ajustados às necessidades do usuário final, baseado nas opções que ele fornece.
  • Flexibilidade: Utilizando métodos de transformação de string, como classify e dasherize, podemos adaptar os nomes dos arquivos e o conteúdo para atender às convenções de nomeação específicas do projeto.
  • Integração: O uso do sistema de templating permite que os desenvolvedores integrem suas bibliotecas Angular de maneira consistente, automatizando partes do processo de setup.

Finalizando a Fábrica de Regras: Integrando as Peças e Dando o Toque Final

Com a regra de geração definida e os templates prontos para serem transformados, é hora de finalizar a função de fábrica do seu Schematic, integrando todas as peças e garantindo que o código gerado seja inserido corretamente no projeto do usuário.

2. Encadeando as Regras: A Montagem da Nave

A função chain() do Angular DevKit permite combinar múltiplas regras em uma única regra composta. Isso é útil para realizar várias operações em um único Schematic, como gerar arquivos, atualizar módulos e aplicar outras transformações.

No nosso exemplo, vamos encadear a regra de geração de templates (mergeWith(templateSource)) com outras regras que podem ser necessárias para configurar o serviço corretamente.

// projects/my-lib/schematics/my-service/index.ts (Chain Rule)
return chain([
  mergeWith(templateSource),
  // ... outras regras (opcional)
]);

O Código Completo: A Nave Espacial Pronta para o Lançamento

Agora, vamos reunir todas as peças que construímos ao longo deste artigo e apresentar o código completo do nosso Schematic my-service:

import {
  Rule,
  Tree,
  SchematicsException,
  apply,
  url,
  applyTemplates,
  move,
  chain,
  mergeWith,
} from '@angular-devkit/schematics';
import {strings, normalize, virtualFs, workspaces} from '@angular-devkit/core';
import {Schema as MyServiceSchema} from './schema';

function createHost(tree: Tree): workspaces.WorkspaceHost {
  return {
    async readFile(path: string): Promise<string> {
      const data = tree.read(path);
      if (!data) {
        throw new SchematicsException('File not found.');
      }
      return virtualFs.fileBufferToString(data);
    },
    async writeFile(path: string, data: string): Promise<void> {
      return tree.overwrite(path, data);
    },
    async isDirectory(path: string): Promise<boolean> {
      return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
    },
    async isFile(path: string): Promise<boolean> {
      return tree.exists(path);
    },
  };
}

export function myService(options: MyServiceSchema): Rule {
  return async (tree: Tree) => {
    const host = createHost(tree);
    const {workspace} = await workspaces.readWorkspace('/', host);

    const project = options.project != null ? workspace.projects.get(options.project) : null;
    if (!project) {
      throw new SchematicsException(`Invalid project name: ${options.project}`);
    }

    const projectType = project.extensions.projectType === 'application' ? 'app' : 'lib';
    if (options.path === undefined) {
      options.path = `${project.sourceRoot}/${projectType}`;
    }

    const templateSource = apply(url('./files'), [
      applyTemplates({
        classify: strings.classify,
        dasherize: strings.dasherize,
        name: options.name,
      }),
      move(normalize(options.path as string)),
    ]);

    return chain([mergeWith(templateSource)]);
  };
}

Componentes da Implementação

  • createHost(): Esta função cria um WorkspaceHost a partir do Tree, fornecendo métodos para ler e escrever arquivos, assim como verificar se um caminho é um diretório ou arquivo.
  • Leitura da Configuração do Projeto:
    • Utiliza workspaces.readWorkspace() para acessar a configuração do workspace (angular.json).
    • Determina o projeto específico no qual a regra será aplicada com base na opção project.
  • Verificação do Tipo de Projeto:
    • Identifica se o projeto é um aplicativo (app) ou uma biblioteca (lib) usando a propriedade projectType.
  • Configuração do Caminho de Destino:
    • Define o caminho de destino (options.path) para onde os arquivos de template devem ser movidos. Se não for fornecido, utiliza o sourceRoot do projeto e o tipo de projeto como base.
  • Transformação dos Templates:
    • apply(), url(), applyTemplates() e move() são usados para transformar e mover os templates para o local apropriado no projeto do usuário.
  • Cadeia de Regras:
    • chain() combina múltiplas regras, permitindo a execução de várias operações. Aqui, mergeWith(templateSource) aplica as transformações e insere os arquivos no projeto.

Conclusão

A função myService é um exemplo robusto de como criar e aplicar um schematic no Angular. Combinando a leitura da configuração do workspace, manipulação de arquivos e aplicação de templates, este código exemplifica a poderosa funcionalidade dos schematics para automatizar e simplificar a integração de bibliotecas e funcionalidades em projetos Angular.