Contribute a Generator
Let’s create a new generator to contribute to @aws/nx-plugin
. Our objective will be to generate a new procedure for a tRPC API.
Check Out the Plugin
First, let’s clone the plugin:
git clone git@github.com:awslabs/nx-plugin-for-aws.git
Next, install and build:
cd nx-plugin-for-awspnpm ipnpm nx run-many --target build --all
Create an Empty Generator
Let’s create the new generator in packages/nx-plugin/src/trpc/procedure
.
We provide a generator for creating new generators so you can quickly scaffold your new generator! You can run this generator as follows:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)
in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#nx-generator
- Fill in the required parameters
- pluginProject: @aws/nx-plugin
- name: ts#trpc-api#procedure
- directory: trpc/procedure
- description: Adds a procedure to a tRPC API
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API
yarn nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API
npx nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API
bunx nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API
You can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API --dry-run
yarn nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API --dry-run
npx nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API --dry-run
bunx nx g @aws/nx-plugin:ts#nx-generator --pluginProject=@aws/nx-plugin --name=ts#trpc-api#procedure --directory=trpc/procedure --description=Adds a procedure to a tRPC API --dry-run
You will notice the following files have been generated for you:
Directorypackages/nx-plugin/src/trpc/procedure
- schema.json Defines the input for the generator
- schema.d.ts A typescript interface which matches the schema
- generator.ts Function which Nx runs as the generator
- generator.spec.ts Tests for the generator
Directorydocs/src/content/docs/guides/
- trpc-procedure.mdx Documentation for the generator
- packages/nx-plugin/generators.json Updated to include the generator
Let’s update the schema to add the properties we’ll need for the generator:
{ "$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"]}
export interface TrpcProcedureSchema { project: string; procedure: string; type: 'query' | 'mutation';}
You will notice the generator has already been hooked up in 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" } },...
Implement the Generator
To add a procedure to a tRPC API, we need to do two things:
- Create a TypeScript file for the new procedure
- Add the procedure to the router
Create the new Procedure
To create the TypeScript file for the new procedure, we’ll use a utility called generateFiles
. Using this, we can define an EJS template which we can render in our generator with variables based on the options selected by the user.
First, we’ll define the template in packages/nx-plugin/src/trpc/procedure/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 {}; });
In the template, we referenced three variables:
procedureNameCamelCase
procedureNameKebabCase
procedureType
So we’ll need to make sure we pass those to generateFiles
, as well as the directory to generate files into, namely the location of source files (i.e. sourceRoot
) for the tRPC project the user selected as input for the generator, which we can extract from the project configuration.
Let’s update the generator to do that:
import { generateFiles, joinPathFragments, readProjectConfiguration, Tree,} from '@nx/devkit';import { TrpcProcedureSchema } from './schema';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(__dirname, 'files'), projectConfig.sourceRoot, { procedureNameCamelCase, procedureNameKebabCase, procedureType: options.type, }, );
await formatFilesInSubtree(tree);};
export default trpcProcedureGenerator;
Add the Procedure to the Router
Next, we want the generator to hook up the new procedure to the router. This means reading and updating the user’s source code!
We use TypeScript AST manipulation to update the relevant parts of the TypeScript source file. There are some helpers called replace
and destructuredImport
to make this a little easier.
import { generateFiles, joinPathFragments, readProjectConfiguration, Tree,} from '@nx/devkit';import { TrpcProcedureSchema } from './schema';import { formatFilesInSubtree } from '../../utils/format';import camelCase from 'lodash.camelcase';import kebabCase from 'lodash.kebabcase';import { destructuredImport, replace } from '../../utils/ast';import { factory, ObjectLiteralExpression } from 'typescript';
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(__dirname, 'files'), projectConfig.sourceRoot, { procedureNameCamelCase, procedureNameKebabCase, procedureType: options.type, }, );
const routerPath = joinPathFragments(projectConfig.sourceRoot, 'router.ts');
destructuredImport( tree, routerPath, [procedureNameCamelCase], `./procedures/${procedureNameKebabCase}.js`, );
replace( tree, routerPath, 'CallExpression[expression.name="router"] > ObjectLiteralExpression', (node) => factory.createObjectLiteralExpression([ ...(node as ObjectLiteralExpression).properties, factory.createShorthandPropertyAssignment(procedureNameCamelCase), ]), );
await formatFilesInSubtree(tree);};
export default trpcProcedureGenerator;
Now that we’ve implemented the generator, let’s compile it to make sure it’s available for us to test it out in our dungeon adventure project.
pnpm nx run @aws/nx-plugin:compile
Testing the Generator
To test the generator, we’ll link our local Nx Plugin for AWS to an existing codebase.
Create a Test Project with a tRPC API
In a separate directory, create a new test workspace:
npx create-nx-workspace@~20.6.3 trpc-generator-test --pm=pnpm --preset=ts --ci=skip --formatter=prettier
npx create-nx-workspace@~20.6.3 trpc-generator-test --pm=yarn --preset=ts --ci=skip --formatter=prettier
npx create-nx-workspace@~20.6.3 trpc-generator-test --pm=npm --preset=ts --ci=skip --formatter=prettier
npx create-nx-workspace@~20.6.3 trpc-generator-test --pm=bun --preset=ts --ci=skip --formatter=prettier
Next, let’s generate a tRPC API to add the procedure to:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)
in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#trpc-api
- Fill in the required parameters
- apiName: test-api
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive
yarn nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive
npx nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive
bunx nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive
You can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api --apiName=test-api --no-interactive --dry-run
Link our local Nx Plugin for AWS
In your codebase, let’s link our local @aws/nx-plugin
:
cd path/to/trpc-generator-testpnpm link path/to/nx-plugin-for-aws/dist/packages/nx-plugin
cd path/to/trpc-generator-testyarn link path/to/nx-plugin-for-aws/dist/packages/nx-plugin
cd path/to/trpc-generator-testnpm link path/to/nx-plugin-for-aws/dist/packages/nx-plugin
cd path/to/nx-plugin-for-aws/dist/packages/nx-pluginbun linkcd path/to/trpc-generator-testbun link @aws/nx-plugin
Run the new Generator
Let’s try the new generator:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)
in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#trpc-api#procedure
- Fill in the required parameters
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api#procedure
yarn nx g @aws/nx-plugin:ts#trpc-api#procedure
npx nx g @aws/nx-plugin:ts#trpc-api#procedure
bunx nx g @aws/nx-plugin:ts#trpc-api#procedure
You can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#trpc-api#procedure --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api#procedure --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api#procedure --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api#procedure --dry-run
If successful, we should have generated a new procedure and added the procedure to our router in router.ts
.
Exercises
If you’ve got this far and still have some time to experiment with Nx generators, here are some suggestions of features to add to the procedure generator:
1. Nested Operations
Try updating the generator to support nested routers by:
- Accepting dot notation for the
procedure
input (e.g.games.query
) - Generating a procedure with a name based on reversed dot notation (e.g.
queryGames
) - Adding the appropriate nested router (or updating it if it already exists!)
2. Validation
Our generator should defend against potential issues, such as a user selecting a project
which isn’t a tRPC API. Take a look at the api-connection
generator for an example of this.
3. Unit Tests
Write some unit tests for the generator. These are quite straightforward to implement, and most follow the general flow:
- Create an empty workspace tree using
createTreeUsingTsSolutionSetup()
- Add any files that should already exist in the tree (e.g.
project.json
andsrc/router.ts
for a tRPC backend) - Run the generator under test
- Validate the expected changes are made to the tree
4. End to End Tests
At present, we have a single “smoke test” which runs all the generators and makes sure that the build succeeds. This should be updated to include the new generator.