Skip to content

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:

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

Next, install and build:

Terminal window
cd nx-plugin-for-aws
pnpm i
pnpm 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:

  1. Install the Nx Console VSCode Plugin if you haven't already
  2. Open the Nx Console in VSCode
  3. Click Generate (UI) in the "Common Nx Commands" section
  4. Search for @aws/nx-plugin - ts#nx-generator
  5. 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
  6. Click Generate

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"]
}

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:

  1. Create a TypeScript file for the new procedure
  2. 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:

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:

procedure/generator.ts
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.

procedure/generator.ts
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.

Terminal window
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:

Terminal window
npx create-nx-workspace@~20.6.3 trpc-generator-test --pm=pnpm --preset=ts --ci=skip --formatter=prettier

Next, let’s generate a tRPC API to add the procedure to:

  1. Install the Nx Console VSCode Plugin if you haven't already
  2. Open the Nx Console in VSCode
  3. Click Generate (UI) in the "Common Nx Commands" section
  4. Search for @aws/nx-plugin - ts#trpc-api
  5. Fill in the required parameters
    • apiName: test-api
  6. Click Generate

In your codebase, let’s link our local @aws/nx-plugin:

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

Run the new Generator

Let’s try the new generator:

  1. Install the Nx Console VSCode Plugin if you haven't already
  2. Open the Nx Console in VSCode
  3. Click Generate (UI) in the "Common Nx Commands" section
  4. Search for @aws/nx-plugin - ts#trpc-api#procedure
  5. Fill in the required parameters
    • Click Generate

    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:

    1. Create an empty workspace tree using createTreeUsingTsSolutionSetup()
    2. Add any files that should already exist in the tree (e.g. project.json and src/router.ts for a tRPC backend)
    3. Run the generator under test
    4. 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.