Schematics: Criando um gerador de códigos com Angular — Parte 2
Este post é o segundo de uma série sobre o Schematics, que aborda desde os conceitos até termos um gerador de código funcional. Leia o primeiro post aqui.
No post anterior nós aprendemos os principais conceitos do Schematics e criamos a nossa primeira Collection. Agora, vamos estruturar a nossa Collection e criar o nosso gerador.
Criando o Schema
O Schema contém os metadados com as opções que o schematic permite e/ou precisa para funcionar. Pra adicionar um Schema ao nosso schematic padrão, vamos criar o arquivo schema.json
dentro da pasta my-schematics
com o seguinte conteúdo:
{ | |
"$schema": "http://json-schema.org/schema", | |
"id": "my-schematics", | |
"type": "object", | |
"properties": { | |
"path": { | |
"description": "The path to create the module.", | |
"type": "string", | |
"format": "path", | |
"visible": false | |
}, | |
"project": { | |
"type": "string", | |
"description": "The name of the project.", | |
"$default": { | |
"$source": "projectName" | |
} | |
}, | |
"name": { | |
"type": "string", | |
"description": "Custom module name", | |
"$default": { | |
"$source": "argv", | |
"index": 0 | |
}, | |
"x-prompt": "What name would you like to use for the module?" | |
} | |
}, | |
"required": ["name"] | |
} |
Nele, nós definimos alguns atributos padrões, como o path
, que é fornecido pelo CLI, e o atributo name
, que será o nome do nosso módulo customizado.
Nós conseguimos obter o nome do módulo pelo flag --name
e pelo argv[0]
que é o primeiro argumento passado pro comando (ex.: ng g my-schematics hello). Caso nenhum nome seja passado, o x-prompt
perguntará o nome do módulo. Lembrando que o nome está marcado como required
, tornando ele um parâmetro obrigatório.
Esses parâmetros estarão disponíveis como atributos do no nosso objeto _options
, que a gente usou no hello world.
Feito isso, vamos alterar o collection.json
novamente para que ele referencie o nosso schema:
{ | |
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", | |
"schematics": { | |
"my-schematics": { | |
"description": "A custom module generator.", | |
"factory": "./my-schematics/index#mySchematics", | |
"schema": "./my-schematics/schema.json" | |
} | |
} | |
} |
Definindo a Collection padrão
Lembra que eu disse que é legal estender a collection do Angular pra gente não precisar ficar passando o nome da nossa collection o tempo todo? Pra isso, Vamos adicionar a linha ["extends": "@schematics/angular"]
ao arquivo collection.json
. Ele vai ficar assim:
{ | |
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", | |
"extends": ["@schematics/angular"], | |
"schematics": { | |
"my-schematics": { | |
"description": "A custom module generator.", | |
"factory": "./my-schematics/index#mySchematics", | |
"schema": "./my-schematics/schema.json" | |
} | |
} | |
} |
Depois, vamos adicionar a chave cli
com a nossa collection no arquivo angular.json
do projeto:
// angular.json
...
"cli": {
"defaultCollection": "my-schematics"
}
Agora, você pode executar o seu schematic da mesma forma que executa os comandos do Angular!
Configurando o gerador
Vamos configurar a RuleFactory
para gerar o nosso módulo conforme o template que ainda iremos definir. Para isso, substitua o conteúdo do arquivo index.ts
pelo arquivo abaixo:
import { | |
apply, | |
branchAndMerge, | |
chain, | |
mergeWith, | |
move, | |
renameTemplateFiles, | |
Rule, | |
SchematicContext, | |
template, | |
Tree, | |
url | |
} from '@angular-devkit/schematics'; | |
import { | |
basename, | |
dirname, | |
experimental, | |
normalize, | |
Path, | |
strings | |
} from '@angular-devkit/core'; | |
export function mySchematics(_options: any): Rule { | |
return (tree: Tree, context: SchematicContext) => { | |
const workspaceConfig = tree.read('/angular.json'); | |
if (!workspaceConfig) { | |
throw new Error('Could not find Angular workspace configuration'); | |
} | |
// convert workspace settings to string | |
const workspaceContent = workspaceConfig.toString(); | |
// parse workspace string into JSON object | |
const workspace: experimental.workspace.WorkspaceSchema = JSON.parse( | |
workspaceContent | |
); | |
// get project name | |
if (!_options.project) { | |
_options.project = workspace.defaultProject; | |
} | |
const projectName = _options.project as string; | |
const project = workspace.projects[projectName]; | |
const projectType = project.projectType === 'application' ? 'app' : 'lib'; | |
// Get the path to create files | |
if (_options.path === undefined) { | |
_options.path = `${project.sourceRoot}/${projectType}`; | |
} | |
const parsedPath = parseName(_options.path, _options.name); | |
_options.name = parsedPath.name; | |
_options.path = parsedPath.path; | |
// Parse template files | |
const templateSource = apply(url('./files'), [ | |
renameTemplateFiles(), | |
template({ | |
...strings, | |
..._options, | |
classify: strings.classify, | |
dasherize: strings.dasherize | |
}), | |
move(normalize((_options.path + '/' + _options.name) as string)) | |
]); | |
// Return Rule chain | |
return chain([branchAndMerge(chain([mergeWith(templateSource)]))])( | |
tree, | |
context | |
); | |
}; | |
} | |
export function parseName( | |
path: string, | |
name: string | |
): { name: string; path: Path } { | |
const nameWithoutPath = basename(name as Path); | |
const namePath = dirname((path + '/' + name) as Path); | |
return { | |
name: nameWithoutPath, | |
path: normalize('/' + namePath) | |
}; | |
} |
A nossa Rule
pode ser resumida em três etapas:
- Obtenção os dados do projeto a partir da configuração do workspace;
- Montagem do
path
em que os arquivo serão criados; - Parse dos arquivos de template.
É necessário dar algumas voltas pra ter um schematic consistente e que funcione tanto dentro do projeto quanto a partir da raiz, assim como os schematics do próprio Angular. No final de tudo, o resultado é bem satisfatório.
Criando o Template
Nós podemos trabalhar de várias formas com o schematics. Seja executando os schematics padrão para gerar uma estrutura de módulo personalizada ou alterando arquivos da tree. Como estamos falando em geração de código, vamos gerar o nosso módulo via template.
Nosso template vai conter um módulo com dois componentes. O template do schematics não é nada diferente do que nós já estamos acostumados. A sintaxe lembra muito um cara old school chamado ASP.
Como já temos o nosso index.ts
pronto pra compilar o nosso template, vamos ao trabalho!
Template do módulo
O primeiro arquivo a ser criado é o nosso módulo. Vamos criar uma pasta chamada files
dentro do diretório do nosso schematics e nela o arquivo: __name@dasherize__.module.ts.template
. Esse nome esquisito é necessário para que o parser gere o nome do arquivo utilizando a função dasherize
que irá converter o atributo name
para kebab-case.
import { NgModule } from '@angular/core'; | |
import { CommonModule } from '@angular/common'; | |
import { <%= classify(name) %>ListComponent } from './<%= name %>-list.component'; | |
import { <%= classify(name) %>FormComponent } from './<%= name %>-form.component'; | |
@NgModule({ | |
imports: [CommonModule], | |
declarations: [<%= classify(name) %>ListComponent, <%= classify(name) %>FormComponent], | |
}) | |
export class <%= classify(name) %>Module {} |
No nosso template, nós utilizamos a função classify
para converter o atributo name
para PascalCase, e com isso gerar os nomes das classes dinamicamente.
Template dos componentes
No caso dos componentes, a lógica é a mesma. Você pode criar um ou vários componentes no mesmo diretório ou em pastas separadas que o schematics respeitará a sua estrutura. No nosso caso, vamos criar os dois componentes (list e form) no mesmo diretório do módulo:
import { Component, OnInit } from '@angular/core'; | |
@Component({ | |
selector: 'app-<%= dasherize(name) %>-list', | |
template: ` | |
<p> | |
list works! | |
</p> | |
`, | |
styles: [] | |
}) | |
export class <%= classify(name) %>ListComponent implements OnInit { | |
constructor() { } | |
ngOnInit() { | |
} | |
} |
import { Component, OnInit } from '@angular/core'; | |
@Component({ | |
selector: 'app-<%= dasherize(name) %>-form', | |
template: ` | |
<p> | |
form works! | |
</p> | |
`, | |
styles: [] | |
}) | |
export class <%= classify(name) %>FormComponent implements OnInit { | |
constructor() { } | |
ngOnInit() { | |
} | |
} |
E agora, José?
Vamos ver se esse treco todo funciona? Primeiro vamos rodar o npm run build
do projeto my-schematics
para compilar o projeto, navegar para o projeto my-project
e ficar feliz porque agora é hora da verdade! Vamos executar o nosso gerador:
ng g my-schematics awesome

RODOOOUU!! Geramos um módulo com dois componentes básicos dentro do nosso projeto. Agora basta definir as suas rotas e testar os seus componentes que acabaram de sair do forno. OU, fica a lição de casa: adicionar as rotas ao seu template do schematics também.
Eu preferi padronizar tudo. Módulos, rotas, esqueletos de página e até mesmo os testes unitários foram adicionados ao template. Uma dica é: Crie um módulo completamente funcional do jeito que você quer padronizar e converta ele pra template somente quando estiver tudo pronto. Assim fica mais fácil de testar enquanto você está desenvolvendo.
Até a próxima!
Top comments (0)