Best practices for generator development.
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?"
}
}
1
2
3
4
5
6
7
{
"style": {
"type": "string",
"enum": ["css", "scss", "styled-components", "none"],
"default": "css"
}
}
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'"
}
}
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__'), ...);
}
1
2
3
4
// __fileName__.service.ts__tmpl__
export class <%= className %>Service {
// ...
}
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, {...});
}
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!');
};
}
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);
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;
});
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();
});
});
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();
});
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)
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);