🧩 Generator Patterns

Best practices for generator development.


Schema Patterns

Optional with Defaults

1
2
3
4
5
6
7
8
{
  "includeTests": {
    "type": "boolean",
    "description": "Include test files",
    "default": true,
    "x-prompt": "Would you like to include test files?"
  }
}

Enum Choices

1
2
3
4
5
6
7
{
  "style": {
    "type": "string",
    "enum": ["css", "scss", "styled-components", "none"],
    "default": "css"
  }
}

Conditional Properties

1
2
3
4
5
6
7
8
9
10
11
12
{
  "framework": {
    "type": "string",
    "enum": ["express", "fastify", "none"],
    "default": "none"
  },
  "port": {
    "type": "number",
    "default": 3000,
    "x-if": "framework !== 'none'"
  }
}

Template Patterns

Conditional File Generation

1
2
3
4
5
files/
├── src/
│   ├── index.ts__tmpl__
│   └── __fileName__.test.ts__tmpl__  # Always included
└── __tests__/  # Conditional directory

In generator:

1
2
3
if (options.includeTests) {
  generateFiles(tree, joinPathFragments(__dirname, './files/__tests__'), ...);
}

Dynamic Naming

1
2
3
4
// __fileName__.service.ts__tmpl__
export class <%= className %>Service {
  // ...
}

Implementation Patterns

Composable Generators

1
2
3
4
5
6
7
8
9
10
11
12
13
export default async function myGenerator(tree: Tree, options: Schema) {
  // Compose with other generators
  await libraryGenerator(tree, {
    name: options.name,
    directory: options.directory,
  });
  
  // Add custom files
  generateFiles(tree, ...);
  
  // Update configuration
  updateProjectConfiguration(tree, options.name, {...});
}

Post-Generation Tasks

1
2
3
4
5
6
7
8
9
export default async function myGenerator(tree: Tree, options: Schema) {
  generateFiles(tree, ...);
  
  // Return tasks to run after file changes
  return () => {
    installPackagesTask(tree);
    console.log('✅ Generator complete!');
  };
}

Project Configuration

Adding to project.json

1
2
3
4
5
6
7
8
9
10
11
import { updateProjectConfiguration, readProjectConfiguration } from '@nx/devkit';

const projectConfig = readProjectConfiguration(tree, options.name);
projectConfig.targets = {
  ...projectConfig.targets,
  'custom-build': {
    executor: '@my/plugin:build',
    options: {...}
  }
};
updateProjectConfiguration(tree, options.name, projectConfig);

Path Aliasing

1
2
3
4
5
6
7
8
import { updateJson } from '@nx/devkit';

updateJson(tree, 'tsconfig.base.json', (json) => {
  json.compilerOptions.paths[`@${workspaceName}/${options.name}`] = [
    `libs/${options.directory}/${options.name}/src/index.ts`
  ];
  return json;
});

Testing Patterns

Minimal Tree Setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

describe('my-generator', () => {
  let tree: Tree;
  
  beforeEach(() => {
    tree = createTreeWithEmptyWorkspace();
  });
  
  it('should create expected files', async () => {
    await myGenerator(tree, { name: 'test' });
    expect(tree.exists('libs/test/src/index.ts')).toBeTruthy();
  });
});

Snapshot Testing

1
2
3
4
it('should match snapshot', async () => {
  await myGenerator(tree, { name: 'test' });
  expect(tree.read('libs/test/src/index.ts', 'utf-8')).toMatchSnapshot();
});

Anti-Patterns

❌ Avoid

1
2
3
4
5
6
7
8
// Don't hardcode paths
generateFiles(tree, './files', '/absolute/path/libs/...');

// Don't mutate outside project
tree.write('/etc/config', '...');

// Don't skip formatting
// (always call formatFiles)

✅ Prefer

1
2
3
4
5
6
7
8
// Use joinPathFragments
const root = joinPathFragments(options.directory, options.name);

// Stay within workspace
generateFiles(tree, ..., `libs/${options.name}`, ...);

// Format generated code
await formatFiles(tree);