Skip to content

Nx Generator Generator

Adds an Nx Generator to a TypeScript project, to help you automate repetitive tasks such as scaffolding components or enforcing particular project structures.

You can generate a generator in two ways:

Run this generator@aws/nx-plugin:ts#nx-generator

pnpm nx g @aws/nx-plugin:ts#nx-generator
Build your command5

Required

Required

Generator Options5 options
projectRequiredstring

TypeScript project to add the generator to. We recommend using the ts#nx-plugin generator to create this.

nameRequiredstring

Generator name

descriptionstring

A description of your generator

directorystring

The directory within the plugin project's source folder to add the generator to (default: <name>)

preferInstallDependenciesbooleanDefault: true

Whether to prefer installing dependencies after the generator runs. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end.

The generator will create the following project files within the given project:

  • Directorysrc/<name>/
    • schema.json Schema for input to your generator
    • schema.d.ts TypeScript types for your schema
    • generator.ts Stub generator implementation
    • generator.spec.ts Tests for your generator
    • README.md Documentation for your generator
    • Directoryfiles/
      • hello.ts.template Example template rendered by the stub generator
  • src/index.ts Updated to export your generator
  • generators.json Created or updated to define your generators
  • project.json Updated to record the generator against the project
  • package.json Created if absent, and pointed at generators.json

Select your local nx-plugin project when running the ts#nx-generator generator, and specify a name and optional directory and description.

The schema.json file defines the options that your generator accepts. It follows the JSON Schema format with Nx-specific extensions.

A schema.json file has the following basic structure:

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

Here’s a simple example with a few basic options:

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

You can customise the prompts displayed when running your generator via the CLI by adding the x-prompt property:

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

For boolean options, you can use a yes/no prompt:

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

For options with a fixed set of choices, use enum so that users can select from one of the options.

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

A common pattern is to let users select from existing projects in the 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"
}

The x-dropdown: "projects" property tells Nx to populate the dropdown with all projects in the workspace.

You can configure options to be passed as positional arguments when running the generator from the command line:

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

This allows users to run your generator like nx g your-generator my-component instead of nx g your-generator --name=my-component.

Use the x-priority property to indicate which options are most important:

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

Options can have priorities of "important" or "internal". This helps Nx to order properties in the Nx VSCode extension and Nx CLI.

You can provide default values for options:

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

For more details on schemas, refer to the Nx Generator Options documentation.

Along with schema.json, the generator creates a schema.d.ts file that provides TypeScript types for your generator options:

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

This interface is used in your generator implementation to provide type safety and code completion:

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;
// ...
}

After creating the new generator as above, you can write your implementation in generator.ts.

A generator is a function which mutates a virtual filesystem (the Tree), reading and writing files to make the desired changes. Changes from the Tree are only written to disk once the generator finishes executing, unless it is run in “dry-run” mode. An empty generator looks as follows:

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

Here are some common operations you might want to perform in your generator:

// 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
}

You can generate files with the generateFiles utility from @nx/devkit. This allows you to define templates in EJS syntax, and substitute variables.

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
},
);

You can use GritQL to declaratively search and transform source code in your generators. GritQL supports multiple languages including TypeScript, JavaScript, Python, HCL (Terraform), and more — so you can use the same pattern syntax across your entire stack.

The Nx Plugin for AWS exposes two helpers:

  • applyGritQL(tree, filePath, pattern) — applies a GritQL rewrite pattern to a file and returns Promise<boolean> indicating whether changes were made
  • matchGritQL(tree, filePath, pattern) — checks whether a GritQL pattern matches anywhere in a file and returns 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
}

GritQL patterns also work on non-TypeScript files. Prefix your pattern with language <name> to target other languages:

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

GritQL patterns use backtick-delimited code snippets with $metavariables as wildcards. Use => for rewrites and where clauses for conditions.

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;
});

Extending a Generator from the Nx Plugin for AWS

Section titled “Extending a Generator from the Nx Plugin for AWS”

You can import generators from the Nx Plugin for AWS, and extend or compose them as you wish, for example you might wish to create a generator which builds on top of a TypeScript project:

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;
};

You can use and extend the generators we use for TypeScript clients and hooks in a similar way to the above:

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

We also expose a method which allows you to build a data structure that can be used to iterate over operations in an OpenAPI specification and therefore instrument your own code generation, for example:

import { buildOpenApiCodeGenerationData } from '@aws/nx-plugin/sdk/open-api';
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,
);
};

Which then allows you to write templates such as:

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

Refer to the codebase on GitHub for more complex example templates.

You can run your generator in two ways:

Run this generator@my-project/nx-plugin:my-generator

pnpm nx g @my-project/nx-plugin:my-generator

Unit tests for generators are straightforward to implement. Here’s a typical pattern:

import { createTreeUsingTsSolutionSetup } from '@aws/nx-plugin/sdk/utils/test';
import { yourGenerator } from './generator.js';
describe('your generator', () => {
let tree;
beforeEach(() => {
// Create a workspace tree matching the shape our preset generates
tree = createTreeUsingTsSolutionSetup();
// 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');
});
});

Key points for testing generators:

  • Use createTreeUsingTsSolutionSetup() from @aws/nx-plugin/sdk/utils/test to create a virtual file system. It seeds the workspace markers our preset writes (tsconfig.base.json, pnpm-workspace.yaml, biome.json, a type: module root package.json), which any generator composing an @aws/nx-plugin generator needs
  • Set up any prerequisite files before running the generator
  • Test both the creation of new files and updates to existing files
  • Use snapshots for complex file content
  • Test error conditions to ensure your generator fails gracefully

You can also use ts#nx-generator to scaffold a generator within @aws/nx-plugin.

When this generator is run in our repository, it’ll generate the following files for you:

  • Directorypackages/nx-plugin/src/<name>/
    • schema.json Schema for input to your generator
    • schema.d.ts TypeScript types for your schema
    • generator.ts Generator implementation
    • generator.spec.ts Tests for your generator
  • Directorydocs/src/content/docs/guides/
    • <name>.mdx Documentation page for your generator
  • packages/nx-plugin/generators.json Updated to include your generator
  • packages/nx-plugin/sdk/<prefix>.ts Updated to expose your generator from the SDK (for ts# and py# generators)

You can then start to implement your generator.