Complete Nx plugin development toolkit: create custom generators, executors, and extend Nx workspaces with reusable automation
93
94%
Does it follow best practices?
Impact
92%
1.00xAverage score across 5 eval scenarios
Passed
No known issues
Template-driven file generation using EJS syntax and Nx conventions.
Generators use template files to create workspace files with variable substitution:
__name__ → actual value).template suffix (removed during generation)generators/
└── my-generator/
├── generator.ts
├── schema.json
└── files/ # Template directory
├── README.md.template # .template removed
├── src/
│ ├── __name__.ts.template
│ └── index.ts.template
└── __name__/ # __name__ replaced
└── config.json.templateimport { generateFiles, joinPathFragments } from "@nx/devkit"
export default async function (tree: Tree, schema: MyGeneratorSchema) {
generateFiles(
tree, // Tree instance
joinPathFragments(__dirname, "./files"), // Template source dir
`./libs/${schema.name}`, // Destination path
{ // Template variables
name: schema.name,
description: schema.description || "No description",
author: "MyOrg",
tmpl: "" // Controls .template extension removal
}
)
}__dirname)<!-- README.md.template -->
# <%= name %>
Author: <%= author %>
Created: <%= new Date().toISOString() %>// config.ts.template
export const config = {
name: "<%= name %>",
<% if (includeTests) { %>
testDir: "tests",
<% } %>
version: "1.0.0"
}// tags.ts.template
export const tags = [
<% tags.forEach((tag, i) => { %>
"<%= tag %>"<%= i < tags.length - 1 ? ',' : '' %>
<% }) %>
]Template file names support token replacement:
files/
├── __name__.ts.template # Replaced with schema.name
├── __directory__/ # Replaced with schema.directory
└── README.md.template # No replacementExample:
generateFiles(tree, srcDir, "./libs/my-lib", {
name: "my-lib",
directory: "shared",
tmpl: ""
})Result: __name__.ts.template → my-lib.ts, __directory__/ → shared/
The tmpl variable controls .template extension removal:
// Remove .template extension (common practice)
generateFiles(tree, srcDir, destDir, { tmpl: "" })import { names } from "@nx/devkit"
const nameVariants = names(schema.name)
// { className, propertyName, constantName, fileName }
generateFiles(tree, srcDir, destDir, {
...nameVariants,
tmpl: ""
})Template:
// __fileName__.ts.template
export class <%= className %> {
private <%= propertyName %>: string
public static <%= constantName %> = "<%= name %>"
}// tsconfig.json.template
{
"extends": "<%= relativePathToRoot %>tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
<% if (enableDecorators) { %>
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
<% } %>
<% if (strict) { %>
"strict": true,
"noImplicitAny": true,
<% } %>
"outDir": "./dist"
}
}my-generator/
├── generator.ts
└── files/
└── src/index.ts.templatemy-generator/
├── generator.ts
├── files-base/
├── files-react/
└── files-angular/// Generate base + conditional framework files
generateFiles(tree, joinPathFragments(__dirname, "./files-base"), root, schema)
if (schema.framework === "react") {
generateFiles(tree, joinPathFragments(__dirname, "./files-react"), root, schema)
}{ ...schema, ...names(schema.name), tmpl: "" }description: schema.description || "No description"generateFiles()await formatFiles(tree) after generation// index.ts.template
<% if (hasDescription) { %>
/** <%= description %> */
<% } %>
export const <%= propertyName %> = () => "<%= name %>"// config.json.template
{
"name": "<%= name %>",
"features": <%= JSON.stringify(features || []) %>
}// __fileName__.spec.ts.template
import { <%= className %> } from "./<%= fileName %>"
describe("<%= className %>", () => {
it("should be defined", () => {
expect(<%= className %>).toBeDefined()
})
})// Log generated files
tree.listChanges().forEach((change) => {
logger.info(`Generated: ${change.path}`)
})
// Validate template variables
console.log("Template vars:", JSON.stringify(templateVars, null, 2))Dry run: nx g my-generator mylib --dry-run (shows generated files without writing)