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.
Usage
Generate a Generator
You can generate a generator in two ways:
- 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
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#nx-generator
yarn nx g @aws/nx-plugin:ts#nx-generator
npx nx g @aws/nx-plugin:ts#nx-generator
bunx nx g @aws/nx-plugin:ts#nx-generator
You can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#nx-generator --dry-run
yarn nx g @aws/nx-plugin:ts#nx-generator --dry-run
npx nx g @aws/nx-plugin:ts#nx-generator --dry-run
bunx nx g @aws/nx-plugin:ts#nx-generator --dry-run
Options
Parameter | Type | Default | Description |
---|---|---|---|
pluginProject Required | string | - | TypeScript project to add the generator to. We recommend creating a ts#project in a top-level 'tools' directory. |
name Required | string | - | Generator name |
description | string | - | A description of your generator |
directory | string | - | The directory within the plugin project's source folder to add the generator to (default: <name>) |
Generator Output
The generator will create the following project files within the given pluginProject
:
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
- generators.json Nx configuration to define your generators
- package.json Created or updated to add a “generators” entry
- tsconfig.json Updated to use CommonJS
This generator will update the selected pluginProject
to use CommonJS, as Nx Generators only support CommonJS at present (refer to this GitHub issue for ESM support).
Local Generators
Select your local nx-plugin
project when running the ts#nx-generator
generator, and specify a name and optional directory and description.
Defining the Schema
The schema.json
file defines the options that your generator accepts. It follows the JSON Schema format with Nx-specific extensions.
Basic Structure
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"]}
Simple Example
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"]}
Interactive Prompts (CLI)
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?"}
Dropdown Selections
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"}
Project Selection Dropdown
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.
Positional Arguments
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
.
Setting Priorities
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.
Default Values
You can provide default values for options:
"directory": { "type": "string", "description": "Directory where the component will be created", "default": "src/components"}
More Information
For more details on schemas, refer to the Nx Generator Options documentation.
TypeScript Types with schema.d.ts
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; // ...}
Implementing a Generator
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.
Here are some common operations you might want to perform in your generator:
Reading and Writing Files
// Read a fileconst content = tree.read('path/to/file.ts', 'utf-8');
// Write a filetree.write('path/to/new-file.ts', 'export const hello = "world";');
// Check if a file existsif (tree.exists('path/to/file.ts')) { // Do something}
Generating Files from Templates
import { generateFiles, joinPathFragments } from '@nx/devkit';
// Generate files from templatesgenerateFiles( tree, joinPathFragments(__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 },);
TypeScript AST (Abstract Syntax Tree) Manipulation
We recommend installing TSQuery to help with AST manipulation.
import { tsquery } from '@phenomnomnominal/tsquery';import * as ts from 'typescript';
// Example: Increment version number in a file
// Parse the file content into a TypeScript ASTconst sourceFile = tsquery.ast(tree.read('path/to/version.ts', 'utf-8'));
// Find nodes matching the selectorconst nodes = tsquery.query( sourceFile, 'VariableDeclaration:has(Identifier[name="VERSION"]) NumericLiteral',);
if (nodes.length > 0) { // Get the numeric literal node const numericNode = nodes[0] as ts.NumericLiteral;
// Get the current version number and increment it const currentVersion = Number(numericNode.text); const newVersion = currentVersion + 1;
// Replace the node in the AST const result = tsquery.replace( sourceFile, 'VariableDeclaration:has(Identifier[name="VERSION"]) NumericLiteral', () => ts.factory.createNumericLiteral(newVersion), );
// Write the updated content back to the tree tree.write( 'path/to/version.ts', ts .createPrinter({ newLine: ts.NewLineKind.LineFeed, }) .printNode(ts.EmitHint.Unspecified, result, sourceFile), );}
Adding Dependencies
import { addDependenciesToPackageJson } from '@nx/devkit';
// Add dependencies to package.jsonaddDependenciesToPackageJson( tree, { 'new-dependency': '^1.0.0', }, { 'new-dev-dependency': '^2.0.0', },);
Formatting Generated Files
import { formatFiles } from '@nx/devkit';
// Format all files that were modifiedawait formatFiles(tree);
Reading and Updating JSON Files
import { readJson, updateJson } from '@nx/devkit';
// Read a JSON fileconst packageJson = readJson(tree, 'package.json');
// Update a JSON fileupdateJson(tree, 'tsconfig.json', (json) => { json.compilerOptions = { ...json.compilerOptions, strict: true, }; return json;});
Running Your Generator
You can run your generator in two ways:
- 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
@my-project/nx-plugin - my-generator
- Fill in the required parameters
- Click
Generate
pnpm nx g @my-project/nx-plugin:my-generator
yarn nx g @my-project/nx-plugin:my-generator
npx nx g @my-project/nx-plugin:my-generator
bunx nx g @my-project/nx-plugin:my-generator
You can also perform a dry-run to see what files would be changed
pnpm nx g @my-project/nx-plugin:my-generator --dry-run
yarn nx g @my-project/nx-plugin:my-generator --dry-run
npx nx g @my-project/nx-plugin:my-generator --dry-run
bunx nx g @my-project/nx-plugin:my-generator --dry-run
Testing Your Generator
Unit tests for generators are straightforward to implement. Here’s a typical pattern:
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';import { yourGenerator } from './generator';
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'); });});
Key points for testing generators:
- Use
createTreeWithEmptyWorkspace()
to create a virtual file system - 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
Contributing Generators to @aws/nx-plugin
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
You can then start to implement your generator.