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
- Função
createHost
: Esta função cria umWorkspaceHost
que permite a leitura e escrita de arquivos noTree
. Isso é necessário para usar o métodoreadWorkspace
da biblioteca@angular-devkit/core
. - Leitura da Configuração do Workspace: Utilizamos
workspaces.readWorkspace
para obter a configuração do workspace a partir do arquivoangular.json
. Este passo é crucial para identificar qual projeto precisa ser modificado. - 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.
- 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. - 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
- 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.
- Utilizamos
- 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.
- Verificamos a propriedade
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
, etest
. - prefix: Prefixo padrão para seletores de componentes gerados.
- projectType: Indica se o projeto é uma
application
ou umalib
.
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
- 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 seoptions.path
éundefined
.
- O código verifica se a opção
- 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 arquivoangular.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 dosourceRoot
onde os arquivos serão colocados.
- Se
- 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.
- O caminho padrão é então concatenado como
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:
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.
- Source: A origem dos arquivos que serão transformados, que no nosso caso é o retorno de
- Propósito: Este método aplica múltiplas regras a uma fonte (
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 templatesfiles/
dentro do schematic.
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
edasherize
) 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.
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óriofiles/
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çãopath
.
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
edasherize
, 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 umWorkspaceHost
a partir doTree
, 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
.
- Utiliza
- Verificação do Tipo de Projeto:
- Identifica se o projeto é um aplicativo (
app
) ou uma biblioteca (lib
) usando a propriedadeprojectType
.
- Identifica se o projeto é um aplicativo (
- 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 osourceRoot
do projeto e o tipo de projeto como base.
- Define o caminho de destino (
- Transformação dos Templates:
apply()
,url()
,applyTemplates()
emove()
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.