| name | nx-generators |
| description | Create Nx generators to automate code scaffolding and enforce best practices in Nx workspaces. Covers the Tree API, schema validation, composing generators, template files, and testing with TypeScript. |
| license | MIT |
| compatibility | opencode |
| metadata | {"category":"nx-development","audience":"nx-developers"} |
Creating Nx Generators
Automate code scaffolding and enforce best practices in Nx workspaces using custom generators.
Quick Start
1. Create Local Plugin
nx add @nx/plugin
nx g @nx/plugin:plugin plugins/<namespace>
2. Generate Generator Scaffold
nx generate @nx/plugin:generator plugins/<namespace>/src/generators/my-generator
Generated Structure:
plugins/
└── <namespace>/
└── src/
└── generators/
└── my-generator/
├── generator.ts # Main logic
├── generator.spec.ts # Unit tests
├── schema.d.ts # TypeScript types
└── schema.json # Configuration
Generator Implementation
Basic Generator
import { Tree, formatFiles, installPackagesTask } from "@nx/devkit"
import { libraryGenerator } from "@nx/js"
export default async function (tree: Tree, schema: MyGeneratorSchema) {
await libraryGenerator(tree, { name: schema.name })
await formatFiles(tree)
return () => {
installPackagesTask(tree)
}
}
Schema Definition
export interface MyGeneratorSchema {
name: string
directory?: string
style?: "css" | "scss" | "less"
tags?: string
}
{
"cli": "nx",
"id": "my-generator",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": { "$source": "argv", "index": 0 }
},
"directory": {
"type": "string",
"description": "Directory where library is created"
},
"style": {
"type": "string",
"description": "Style format",
"enum": ["css", "scss", "less"],
"default": "css"
}
},
"required": ["name"]
}
The Tree API
The Tree represents an in-memory file system:
- All changes are staged until generator completes
- Changes are batched for performance
- Supports read/write without touching actual files until commit
File Operations
import {
generateFiles,
readProjectConfiguration,
updateProjectConfiguration,
joinPathFragments,
readJson,
updateJson
} from "@nx/devkit"
export default async function (tree: Tree, schema: MyGeneratorSchema) {
generateFiles(
tree,
joinPathFragments(__dirname, "./files"),
`./libs/${schema.name}`,
{ name: schema.name, tmpl: "" }
)
const config = readProjectConfiguration(tree, schema.name)
updateProjectConfiguration(tree, schema.name, {
...config,
tags: ["scope:shared", "type:util"]
})
const packageJson = readJson(tree, "package.json")
updateJson(tree, "nx.json", (json) => ({
...json,
targetDefaults: {
...json.targetDefaults,
"custom-target": { cache: true }
}
}))
}
Composing Generators
Call other generators from your generator:
import { libraryGenerator } from "@nx/js"
import { componentGenerator } from "@nx/react"
export default async function (tree: Tree, schema: MyGeneratorSchema) {
await libraryGenerator(tree, {
name: schema.name,
directory: "libs"
})
await componentGenerator(tree, {
name: "MyComponent",
project: schema.name
})
await formatFiles(tree)
}
Template Files
Use EJS syntax for variable injection:
<!-- files/README.md.template -->
# <%= name %>
Generated on <%= new Date().toISOString() %>
## Description
<%= description || "No description" %>
Template Variables:
<%= name %> - Inject option values
<% if (condition) { %> - Conditional logic
<% for (item of items) { %> - Iteration
File Naming:
.template suffix removed during generation
__name__ replaced with actual values
generateFiles(
tree,
joinPathFragments(__dirname, "./files"),
`./libs/${schema.name}`,
{
name: schema.name,
description: schema.description || "No description",
tmpl: ""
}
)
Running Generators
nx generate @myorg/my-plugin:my-generator mylib
nx g @myorg/my-plugin:my-generator mylib --directory=shared --style=scss
nx g @myorg/my-plugin:my-generator mylib --dry-run
Important: Use name from package.json, not folder name.
Utility Functions
String Manipulation
import { names } from "@nx/devkit"
const options = names("my-awesome-lib")
Logging
import { logger } from "@nx/devkit"
logger.info("Starting generator...")
logger.warn("This might take a while")
logger.error("Something went wrong!")
Dependencies
import { addDependenciesToPackageJson } from "@nx/devkit"
export default async function (tree: Tree, schema: MyGeneratorSchema) {
addDependenciesToPackageJson(
tree,
{ "lodash": "^4.17.21" },
{ "@types/lodash": "^4.14.0" }
)
return () => {
installPackagesTask(tree)
}
}
Advanced Patterns
Conditional Logic
export default async function (tree: Tree, schema: MyGeneratorSchema) {
const projects = getProjects(tree)
if (projects.has(schema.name)) {
throw new Error(`Project ${schema.name} already exists`)
}
if (schema.includeTests) {
generateFiles(tree, "./test-files", "./tests", schema)
}
}
Interactive Prompts
import { prompt } from "enquirer"
export default async function (tree: Tree, schema: MyGeneratorSchema) {
if (!schema.style) {
const response = await prompt<{ style: string }>({
type: "select",
name: "style",
message: "Which style format?",
choices: ["css", "scss", "less"]
})
schema.style = response.style
}
}
Component Generator Example
export default async function (tree: Tree, schema: ComponentSchema) {
const project = readProjectConfiguration(tree, schema.project)
const componentDir = `${project.sourceRoot}/components/${schema.name}`
generateFiles(
tree,
joinPathFragments(__dirname, "./files"),
componentDir,
{
...schema,
fileName: names(schema.name).fileName,
className: names(schema.name).className,
tmpl: ""
}
)
await formatFiles(tree)
}
Testing Generators
import { createTreeWithEmptyWorkspace } from "@nx/devkit/testing"
import myGenerator from "./generator"
describe("my-generator", () => {
it("should generate a library", async () => {
const tree = createTreeWithEmptyWorkspace()
await myGenerator(tree, { name: "test-lib" })
expect(tree.exists("libs/test-lib/src/index.ts")).toBeTruthy()
})
})
Debugging
VS Code
- Open Command Palette →
Debug: Create JavaScript Debug Terminal
- Set breakpoints in generator code
- Run generator:
nx g my-generator
- Execution pauses at breakpoints
Console Logging
export default async function (tree: Tree, schema: MyGeneratorSchema) {
console.log("Schema:", schema)
console.log("Tree files:", tree.listChanges())
}
Best Practices
-
Validate Input Early
if (!schema.name.match(/^[a-z][a-z0-9-]*$/)) {
throw new Error("Name must be kebab-case")
}
-
Use TypeScript for Schema
- Define proper types in
schema.d.ts
- Import into generator for type safety
-
Always Format Files
await formatFiles(tree)
-
Write Tests
- Test file generation
- Test config updates
- Test error cases
-
Document Your Generator
{
"cli": "nx",
"id": "my-generator",
"description": "Generate a new library",
"examples": [{
"command": "nx g @myorg/my-plugin:my-generator mylib",
"description": "Generate library named 'mylib'"
}]
}
Troubleshooting
TsConfig Issues
- Ensure
tsconfig.base.json has correct paths
- Follow Node 16+ recommendations
Package Name Mismatch
Use name from package.json, not folder name:
nx g @myorg/my-plugin:my-generator
nx g my-plugin:my-generator
Generator Not Found
- Check
tsconfig.base.json paths
- Rebuild plugin:
nx build my-plugin
- Verify
package.json exports
Complete Example
import {
Tree,
formatFiles,
generateFiles,
joinPathFragments,
addProjectConfiguration,
names
} from "@nx/devkit"
export interface MyGeneratorSchema {
name: string
directory?: string
tags?: string
}
export default async function (tree: Tree, schema: MyGeneratorSchema) {
if (!schema.name.match(/^[a-z][a-z0-9-]*$/)) {
throw new Error("Name must be kebab-case")
}
const projectRoot = `libs/${schema.name}`
addProjectConfiguration(tree, schema.name, {
root: projectRoot,
projectType: "library",
sourceRoot: `${projectRoot}/src`,
targets: {}
})
generateFiles(
tree,
joinPathFragments(__dirname, "./files"),
projectRoot,
{
...schema,
...names(schema.name),
tmpl: ""
}
)
await formatFiles(tree)
}
Resources