Pular para o conteúdo

Contribuir com um Gerador

Vamos criar um novo gerador para contribuir com o @aws/nx-plugin. Nosso objetivo será gerar um novo procedimento para uma API tRPC.

Primeiro, vamos clonar o plugin:

Terminal window
git clone git@github.com:awslabs/nx-plugin-for-aws.git

Em seguida, instale e compile:

Terminal window
cd nx-plugin-for-aws
pnpm i
pnpm nx run-many --target build --all

Vamos criar o novo gerador em packages/nx-plugin/src/trpc/procedure.

Fornecemos um gerador para criar novos geradores para que você possa rapidamente estruturar seu novo gerador! Você pode executar este gerador da seguinte forma:

Terminal window
pnpm nx g @aws/nx-plugin:ts#nx-generator --project=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API
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 --project=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API --dry-run

Você notará que os seguintes arquivos foram gerados para você:

  • Directorypackages/nx-plugin/src/trpc/procedure
    • schema.json Define a entrada para o gerador
    • schema.d.ts Uma interface typescript que corresponde ao schema
    • generator.ts Função que o Nx executa como o gerador
    • generator.spec.ts Testes para o gerador
  • Directorydocs/src/content/docs/guides/
    • trpc-procedure.mdx Documentação para o gerador
  • packages/nx-plugin/generators.json Atualizado para incluir o gerador

Vamos atualizar o schema para adicionar as propriedades que precisaremos para o gerador:

{
"$schema": "https://json-schema.org/schema",
"$id": "tRPCProcedure",
"title": "Adds a procedure to a tRPC API",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "tRPC API project",
"x-prompt": "Select the tRPC API project to add the procedure to",
"x-dropdown": "projects",
"x-priority": "important"
},
"procedure": {
"description": "The name of the new procedure",
"type": "string",
"x-prompt": "What would you like to call your new procedure?",
"x-priority": "important"
},
"type": {
"description": "The type of procedure to generate",
"type": "string",
"x-prompt": "What type of procedure would you like to generate?",
"x-priority": "important",
"default": "query",
"enum": ["query", "mutation"]
}
},
"required": ["project", "procedure"]
}

Você notará que o gerador já foi conectado em packages/nx-plugin/generators.json:

...
"generators": {
...
"ts#trpc-api#procedure": {
"factory": "./src/trpc/procedure/generator",
"schema": "./src/trpc/procedure/schema.json",
"description": "Adds a procedure to a tRPC API"
}
},
...

Para adicionar um procedimento a uma API tRPC, precisamos fazer duas coisas:

  1. Criar um arquivo TypeScript para o novo procedimento
  2. Adicionar o procedimento ao roteador

Para criar o arquivo TypeScript para o novo procedimento, usaremos um utilitário chamado generateFiles. Usando isso, podemos definir um template EJS que podemos renderizar em nosso gerador com variáveis baseadas nas opções selecionadas pelo usuário.

Primeiro, definiremos o template em packages/nx-plugin/src/trpc/procedure/files/procedures/__procedureNameKebabCase__.ts.template:

files/procedures/__procedureNameKebabCase__.ts.template
import { publicProcedure } from '../init.js';
import { z } from 'zod';
export const <%- procedureNameCamelCase %> = publicProcedure
.input(z.object({
// TODO: define input
}))
.output(z.object({
// TODO: define output
}))
.<%- procedureType %>(async ({ input, ctx }) => {
// TODO: implement!
return {};
});

No template, referenciamos três variáveis:

  • procedureNameCamelCase
  • procedureNameKebabCase
  • procedureType

Então precisaremos garantir que passamos essas para generateFiles, assim como o diretório para gerar arquivos, ou seja, a localização dos arquivos fonte (i.e. sourceRoot) para o projeto tRPC que o usuário selecionou como entrada para o gerador, que podemos extrair da configuração do projeto.

Vamos atualizar o gerador para fazer isso:

procedure/generator.ts
import {
generateFiles,
joinPathFragments,
readProjectConfiguration,
type Tree,
} from '@nx/devkit';
import type { TrpcProcedureSchema } from './schema.js';
import { formatFilesInSubtree } from '../../utils/format';
import camelCase from 'lodash.camelcase';
import kebabCase from 'lodash.kebabcase';
export const trpcProcedureGenerator = async (
tree: Tree,
options: TrpcProcedureSchema,
) => {
const projectConfig = readProjectConfiguration(tree, options.project);
const procedureNameCamelCase = camelCase(options.procedure);
const procedureNameKebabCase = kebabCase(options.procedure);
generateFiles(
tree,
joinPathFragments(import.meta.dirname, 'files'),
projectConfig.sourceRoot,
{
procedureNameCamelCase,
procedureNameKebabCase,
procedureType: options.type,
},
);
await formatFilesInSubtree(tree);
};
export default trpcProcedureGenerator;

Em seguida, queremos que o gerador conecte o novo procedimento ao roteador. Isso significa ler e atualizar o código fonte do usuário!

Usamos GritQL para buscar e transformar código fonte de forma declarativa. O helper addDestructuredImport adiciona imports nomeados, e applyGritQL aplica um padrão GritQL para adicionar o procedimento ao objeto literal do roteador.

procedure/generator.ts
import {
generateFiles,
joinPathFragments,
readProjectConfiguration,
type Tree,
} from '@nx/devkit';
import type { TrpcProcedureSchema } from './schema.js';
import { formatFilesInSubtree } from '../../utils/format';
import camelCase from 'lodash.camelcase';
import kebabCase from 'lodash.kebabcase';
import { addDestructuredImport, applyGritQL } from '../../utils/ast';
export const trpcProcedureGenerator = async (
tree: Tree,
options: TrpcProcedureSchema,
) => {
const projectConfig = readProjectConfiguration(tree, options.project);
const procedureNameCamelCase = camelCase(options.procedure);
const procedureNameKebabCase = kebabCase(options.procedure);
generateFiles(
tree,
joinPathFragments(import.meta.dirname, 'files'),
projectConfig.sourceRoot,
{
procedureNameCamelCase,
procedureNameKebabCase,
procedureType: options.type,
},
);
const routerPath = joinPathFragments(projectConfig.sourceRoot, 'router.ts');
await addDestructuredImport(
tree,
routerPath,
[procedureNameCamelCase],
`./procedures/${procedureNameKebabCase}.js`,
);
await applyGritQL(
tree,
routerPath,
`\`router({ $props })\` => \`router({ $props, ${procedureNameCamelCase} })\` where { $props <: not contains \`${procedureNameCamelCase}\` }`,
);
await formatFilesInSubtree(tree);
};
export default trpcProcedureGenerator;

Agora que implementamos o gerador, vamos compilá-lo para garantir que esteja disponível para testarmos em nosso projeto de aventura na masmorra.

Terminal window
pnpm nx compile @aws/nx-plugin

Para testar o gerador, vamos vincular nosso Nx Plugin for AWS local a uma base de código existente.

Em um diretório separado, crie um novo workspace de teste:

Terminal window
pnpm create @aws/nx-workspace trpc-generator-test

Em seguida, vamos gerar uma API tRPC para adicionar o procedimento:

Terminal window
pnpm nx g @aws/nx-plugin:ts#api --name=test-api --framework=trpc --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @aws/nx-plugin:ts#api --name=test-api --framework=trpc --no-interactive --dry-run

Em sua base de código, vamos vincular nosso @aws/nx-plugin local:

Terminal window
cd path/to/trpc-generator-test
pnpm link path/to/nx-plugin-for-aws/dist/packages/nx-plugin

Vamos experimentar o novo gerador:

Terminal window
pnpm nx g @aws/nx-plugin:ts#trpc-api#procedure
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @aws/nx-plugin:ts#trpc-api#procedure --dry-run

Se bem-sucedido, devemos ter gerado um novo procedimento e adicionado o procedimento ao nosso roteador em router.ts.

Se você chegou até aqui e ainda tem algum tempo para experimentar com geradores Nx, aqui estão algumas sugestões de recursos para adicionar ao gerador de procedimentos:

Tente atualizar o gerador para suportar roteadores aninhados:

  • Aceitando notação de ponto para a entrada procedure (por exemplo, games.query)
  • Gerando um procedimento com um nome baseado em notação de ponto reversa (por exemplo, queryGames)
  • Adicionando o roteador aninhado apropriado (ou atualizando-o se já existir!)

Nosso gerador deve se defender contra problemas potenciais, como um usuário selecionando um project que não é uma API tRPC. Dê uma olhada no gerador connection para um exemplo disso.

Escreva alguns testes unitários para o gerador. Eles são bastante diretos de implementar, e a maioria segue o fluxo geral:

  1. Criar uma árvore de workspace vazia usando createTreeUsingTsSolutionSetup()
  2. Adicionar quaisquer arquivos que já devem existir na árvore (por exemplo, project.json e src/router.ts para um backend tRPC)
  3. Executar o gerador em teste
  4. Validar que as alterações esperadas são feitas na árvore

Temos uma suíte de “smoke tests” que executam geradores em um workspace novo e garantem que tudo compila. No mínimo, seu novo gerador deve ser adicionado a ambas as matrizes de geradores para que seja exercitado pelos smoke tests:

  • e2e/src/smoke-tests/generator-matrix.ts — executa cada gerador através da CLI, uma invocação por vez, exatamente como um usuário faria.
  • packages/nx-plugin/src/internal/test-matrix/generator.ts — um gerador oculto que compõe todos os outros para testar migrações entre versões.

Observe que a matriz de geradores apenas executa os geradores e compila o workspace — ela não instancia nenhuma infraestrutura. Para geradores que implantam infraestrutura, considere estender os testes e2e de implantação (e2e/src/smoke-tests/cdk-deploy.spec.ts e terraform-deploy.spec.ts) para realmente implantar seus recursos (via cdk deploy / terraform apply), depois adicione uma asserção a deploy-invocations.ts que invoca o recurso implantado e verifica se ele se comporta como esperado.

Se seu gerador fornece um servidor de desenvolvimento local, considere adicioná-lo ao teste e2e de desenvolvimento local (e2e/src/smoke-tests/local-dev.spec.ts), que inicia o target dev e exercita o servidor em execução.

Esta seção contém algumas orientações gerais que podem ajudar ao trabalhar no Nx Plugin for AWS.

Trabalhe de trás para frente a partir de um projeto real

Seção intitulada “Trabalhe de trás para frente a partir de um projeto real”

Uma maneira útil de construir novos geradores ou adicionar recursos/correções a um gerador existente é construí-lo de verdade primeiro. Dessa forma, você pode validar suas ideias e iterar rapidamente para alcançar a funcionalidade que precisa. Depois de ter decidido sobre o resultado desejado, você pode então atualizar o gerador.

Na prática, este processo pode parecer com:

  1. Criar um novo workspace

    Terminal window
    pnpm create @aws/nx-workspace my-project
  2. Executar quaisquer geradores que possam ser pré-requisitos para seu novo gerador/recurso/correção

  3. Fazer commit de suas alterações (git commit)

  4. Fazer suas alterações desejadas e testá-las conforme necessário

  5. Usar o git diff de suas alterações para informar quais alterações devem ser feitas no Nx Plugin for AWS

  6. Realizar um teste final end to end (vinculando seu @aws/nx-plugin) para garantir que seu gerador forneça as alterações que você precisa