DEV Community

Ricardo Mello
Ricardo Mello

Posted on • Originally published at ricardo-mello.Medium on

Schematics: Criando um gerador de códigos com Angular — Parte 2

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"]
}
view raw schema.json hosted with ❤ by GitHub

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"
}
}
}
view raw collection.json hosted with ❤ by GitHub

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"
}
}
}
view raw collection.json hosted with ❤ by GitHub

Depois, vamos adicionar a chave cli com a nossa collection no arquivo angular.json do projeto:

// angular.json
...  
  "cli": {
    "defaultCollection": "my-schematics"
  }
Enter fullscreen mode Exit fullscreen mode

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)
};
}
view raw index.ts hosted with ❤ by GitHub

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
Enter fullscreen mode Exit fullscreen mode

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)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay