Pular para o conteúdo

Gerador de Gerador Nx

Adiciona um Nx Generator a um projeto TypeScript, para ajudá-lo a automatizar tarefas repetitivas, como scaffolding de componentes ou aplicação de estruturas de projeto específicas.

Você pode gerar um gerador de duas maneiras:

Terminal window
pnpm nx g @aws/nx-plugin:ts#nx-generator
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @aws/nx-plugin:ts#nx-generator --dry-run
ParâmetroTipoPadrãoDescrição
project Obrigatóriostring-Projeto TypeScript para adicionar o gerador. Recomendamos usar o gerador ts#nx-plugin para criar isso.
name Obrigatóriostring-Nome do gerador
description string-Uma descrição do seu gerador
directory string-O diretório dentro da pasta de origem do projeto do plugin para adicionar o gerador (padrão: <name>)
preferInstallDependencies booleantrueSe deve preferir instalar dependências após a execução do gerador. Defina como false para adiar a instalação ao executar múltiplos geradores em lote (uma instalação ainda é executada se necessário para que os geradores subsequentes possam calcular o grafo de projetos Nx); instale uma vez no final.

O gerador criará os seguintes arquivos de projeto dentro do project fornecido:

  • Directorysrc/<name>/
    • schema.json Schema para entrada do seu gerador
    • schema.d.ts Tipos TypeScript para seu schema
    • generator.ts Implementação stub do gerador
    • generator.spec.ts Testes para seu gerador
    • README.md Documentação para seu gerador
  • generators.json Configuração Nx para definir seus geradores
  • package.json Criado ou atualizado para adicionar uma entrada “generators”
  • tsconfig.json Atualizado para usar CommonJS

Modificação do Projeto

Este gerador atualizará o project selecionado para usar CommonJS, pois os Nx Generators atualmente suportam apenas CommonJS (consulte este issue do GitHub para suporte ESM).

Selecione seu projeto nx-plugin local ao executar o gerador ts#nx-generator, e especifique um nome e opcionalmente um diretório e descrição.

O arquivo schema.json define as opções que seu gerador aceita. Ele segue o formato JSON Schema com extensões específicas do Nx.

Um arquivo schema.json tem a seguinte estrutura básica:

{
"$schema": "https://json-schema.org/schema",
"$id": "YourGeneratorName",
"title": "Your Generator Title",
"description": "Description of what your generator does",
"type": "object",
"properties": {
// Your generator options go here
},
"required": ["requiredOption1", "requiredOption2"]
}

Aqui está um exemplo simples com algumas opções básicas:

{
"$schema": "https://json-schema.org/schema",
"$id": "ComponentGenerator",
"title": "Create a Component",
"description": "Creates a new React component",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Component name",
"x-priority": "important"
},
"directory": {
"type": "string",
"description": "Directory where the component will be created",
"default": "src/components"
},
"withTests": {
"type": "boolean",
"description": "Whether to generate test files",
"default": true
}
},
"required": ["name"]
}

Você pode personalizar os prompts exibidos ao executar seu gerador via CLI adicionando a propriedade x-prompt:

"name": {
"type": "string",
"description": "Component name",
"x-prompt": "What is the name of your component?"
}

Para opções booleanas, você pode usar um prompt sim/não:

"withTests": {
"type": "boolean",
"description": "Whether to generate test files",
"x-prompt": "Would you like to generate test files?"
}

Para opções com um conjunto fixo de escolhas, use enum para que os usuários possam selecionar uma das opções.

"style": {
"type": "string",
"description": "The styling approach to use",
"enum": ["css", "scss", "styled-components", "none"],
"default": "css"
}

Um padrão comum é permitir que os usuários selecionem entre projetos existentes no workspace:

"project": {
"type": "string",
"description": "The project to add the component to",
"x-prompt": "Which project would you like to add the component to?",
"x-dropdown": "projects"
}

A propriedade x-dropdown: "projects" diz ao Nx para preencher o dropdown com todos os projetos no workspace.

Você pode configurar opções para serem passadas como argumentos posicionais ao executar o gerador a partir da linha de comando:

"name": {
"type": "string",
"description": "Component name",
"x-priority": "important",
"$default": {
"$source": "argv",
"index": 0
}
}

Isso permite que os usuários executem seu gerador como nx g your-generator my-component em vez de nx g your-generator --name=my-component.

Use a propriedade x-priority para indicar quais opções são mais importantes:

"name": {
"type": "string",
"description": "Component name",
"x-priority": "important"
}

As opções podem ter prioridades de "important" ou "internal". Isso ajuda o Nx a ordenar propriedades na extensão Nx VSCode e no Nx CLI.

Você pode fornecer valores padrão para opções:

"directory": {
"type": "string",
"description": "Directory where the component will be created",
"default": "src/components"
}

Para mais detalhes sobre schemas, consulte a documentação de Opções de Gerador Nx.

Junto com schema.json, o gerador cria um arquivo schema.d.ts que fornece tipos TypeScript para as opções do seu gerador:

export interface YourGeneratorSchema {
name: string;
directory?: string;
withTests?: boolean;
}

Esta interface é usada na implementação do seu gerador para fornecer segurança de tipo e autocompletar:

import { YourGeneratorSchema } from './schema';
export default async function (tree: Tree, options: YourGeneratorSchema) {
// TypeScript knows the types of all your options
const { name, directory = 'src/components', withTests = true } = options;
// ...
}

Após criar o novo gerador como acima, você pode escrever sua implementação em generator.ts.

Um gerador é uma função que modifica um sistema de arquivos virtual (a Tree), lendo e escrevendo arquivos para fazer as alterações desejadas. As alterações da Tree só são gravadas no disco quando o gerador termina de executar, a menos que seja executado no modo “dry-run”. Um gerador vazio se parece com o seguinte:

export const myGenerator = async (tree: Tree, options: MyGeneratorSchema) => {
// Use the tree to apply changes
};
export default myGenerator;

Aqui estão algumas operações comuns que você pode querer realizar no seu gerador:

// Read a file
const content = tree.read('path/to/file.ts', 'utf-8');
// Write a file
tree.write('path/to/new-file.ts', 'export const hello = "world";');
// Check if a file exists
if (tree.exists('path/to/file.ts')) {
// Do something
}

Você pode gerar arquivos com o utilitário generateFiles do @nx/devkit. Isso permite que você defina templates na sintaxe EJS e substitua variáveis.

import { generateFiles, joinPathFragments } from '@nx/devkit';
// Generate files from templates
generateFiles(
tree,
joinPathFragments(import.meta.dirname, 'files'), // Template directory
'path/to/output', // Output directory
{
// Variables to replace in templates
name: options.name,
nameCamelCase: camelCase(options.name),
nameKebabCase: kebabCase(options.name),
// Add more variables as needed
},
);

Você pode usar GritQL para buscar e transformar código-fonte declarativamente em seus geradores. GritQL suporta múltiplas linguagens incluindo TypeScript, JavaScript, Python, HCL (Terraform) e mais — então você pode usar a mesma sintaxe de padrão em toda a sua stack.

O Nx Plugin for AWS expõe dois helpers:

  • applyGritQL(tree, filePath, pattern) — aplica um padrão de reescrita GritQL a um arquivo e retorna Promise<boolean> indicando se foram feitas alterações
  • matchGritQL(tree, filePath, pattern) — verifica se um padrão GritQL corresponde em algum lugar em um arquivo e retorna Promise<boolean>
import { applyGritQL, matchGritQL } from '@aws/nx-plugin/sdk/utils/ast';
// Replace a function call
await applyGritQL(
tree,
'src/app.ts',
'`console.log($msg)` => `logger.info($msg)`',
);
// Add an element to an array only if not already present
await applyGritQL(
tree,
'src/plugins.ts',
'`plugins: [$items]` => `plugins: [$items, myPlugin()]` where { $items <: not contains `myPlugin` }',
);
// Check if a pattern exists before making changes
if (!(await matchGritQL(tree, filePath, '`import { Auth } from "./auth"`'))) {
// Add the import
}

Padrões GritQL também funcionam em arquivos não-TypeScript. Prefixe seu padrão com language <name> para direcionar outras linguagens:

// Python: replace print statements with logging calls
await applyGritQL(
tree,
'src/handler.py',
'language python\n`print($msg)` => `logger.info($msg)`',
);

Padrões GritQL usam snippets de código delimitados por backtick com $metavariables como curingas. Use => para reescritas e cláusulas where para condições.

import { addDependenciesToPackageJson } from '@nx/devkit';
// Add dependencies to package.json
addDependenciesToPackageJson(
tree,
{
'new-dependency': '^1.0.0',
},
{
'new-dev-dependency': '^2.0.0',
},
);
import { formatFilesInSubtree } from '@aws/nx-plugin/sdk/utils/format';
// Format all files that were modified
await formatFilesInSubtree(tree, 'optional/path/to/format');
import { readJson, updateJson } from '@nx/devkit';
// Read a JSON file
const packageJson = readJson(tree, 'package.json');
// Update a JSON file
updateJson(tree, 'tsconfig.json', (json) => {
json.compilerOptions = {
...json.compilerOptions,
strict: true,
};
return json;
});

Você pode importar geradores do Nx Plugin for AWS e estendê-los ou compô-los como desejar, por exemplo, você pode querer criar um gerador que se baseia em um projeto TypeScript:

import { tsProjectGenerator } from '@aws/nx-plugin/sdk/ts';
export const myGenerator = async (tree: Tree, schema: MyGeneratorSchema) => {
const callback = await tsProjectGenerator(tree, { ... });
// Extend the TypeScript project generator here
// Return the callback to ensure dependencies are installed.
// You can wrap the callback if you wish to perform additional operations in the generator callback.
return callback;
};

Você pode usar e estender os geradores que usamos para clientes e hooks TypeScript de maneira similar ao acima:

import { openApiTsClientGenerator } from '@aws/nx-plugin/sdk/open-api';
export const myGenerator = async (tree: Tree, schema: MyGeneratorSchema) => {
await openApiTsClientGenerator(tree, { ... });
// Add additional files here
};

Também expomos um método que permite construir uma estrutura de dados que pode ser usada para iterar sobre operações em uma especificação OpenAPI e, portanto, instrumentar sua própria geração de código, por exemplo:

import { buildOpenApiCodeGenerationData } from '@aws/nx-plugin/sdk/open-api.js';
export const myGenerator = async (tree: Tree, schema: MyGeneratorSchema) => {
const data = await buildOpenApiCodeGenerationData(tree, 'path/to/spec.json');
generateFiles(
tree,
joinPathFragments(import.meta.dirname, 'files'), // Template directory
'path/to/output', // Output directory
data,
);
};

O que então permite que você escreva templates como:

files/my-operations.ts.template
export const myOperationNames = [
<%_ allOperations.forEach((op) => { _%>
'<%- op.name %>',
<%_ }); _%>
];

Consulte o código-fonte no GitHub para exemplos de templates mais complexos.

Você pode executar seu gerador de duas maneiras:

Terminal window
pnpm nx g @my-project/nx-plugin:my-generator
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @my-project/nx-plugin:my-generator --dry-run

Testes unitários para geradores são diretos de implementar. Aqui está um padrão típico:

import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { yourGenerator } from './generator.js';
describe('your generator', () => {
let tree;
beforeEach(() => {
// Create an empty workspace tree
tree = createTreeWithEmptyWorkspace();
// Add any files that should already exist in the tree
tree.write(
'project.json',
JSON.stringify({
name: 'test-project',
sourceRoot: 'src',
}),
);
tree.write('src/existing-file.ts', 'export const existing = true;');
});
it('should generate expected files', async () => {
// Run the generator
await yourGenerator(tree, {
name: 'test',
// Add other required options
});
// Check that files were created
expect(tree.exists('src/test/file.ts')).toBeTruthy();
// Check file content
const content = tree.read('src/test/file.ts', 'utf-8');
expect(content).toContain('export const test');
// You can also use snapshots
expect(tree.read('src/test/file.ts', 'utf-8')).toMatchSnapshot();
});
it('should update existing files', async () => {
// Run the generator
await yourGenerator(tree, {
name: 'test',
// Add other required options
});
// Check that existing files were updated
const content = tree.read('src/existing-file.ts', 'utf-8');
expect(content).toContain('import { test } from');
});
it('should handle errors', async () => {
// Expect the generator to throw an error in certain conditions
await expect(
yourGenerator(tree, {
name: 'invalid',
// Add options that should cause an error
}),
).rejects.toThrow('Expected error message');
});
});

Pontos-chave para testar geradores:

  • Use createTreeWithEmptyWorkspace() para criar um sistema de arquivos virtual
  • Configure quaisquer arquivos pré-requisitos antes de executar o gerador
  • Teste tanto a criação de novos arquivos quanto atualizações em arquivos existentes
  • Use snapshots para conteúdo de arquivo complexo
  • Teste condições de erro para garantir que seu gerador falhe graciosamente

Você também pode usar ts#nx-generator para fazer scaffold de um gerador dentro de @aws/nx-plugin.

Quando este gerador é executado em nosso repositório, ele gerará os seguintes arquivos para você:

  • Directorypackages/nx-plugin/src/<name>/
    • schema.json Schema para entrada do seu gerador
    • schema.d.ts Tipos TypeScript para seu schema
    • generator.ts Implementação do gerador
    • generator.spec.ts Testes para seu gerador
  • Directorydocs/src/content/docs/guides/
    • <name>.mdx Página de documentação para seu gerador
  • packages/nx-plugin/generators.json Atualizado para incluir seu gerador
  • packages/nx-plugin/sdk/<prefix>.ts Atualizado para expor seu gerador do SDK (para geradores ts# e py#)

Você pode então começar a implementar seu gerador.