Nx Plugin Authoring
Create Nx plugins with custom generators and executors for TypeScript monorepos. Covers plugin scaffolding, Tree API usage, schema-driven options, ExecutorContext API, template generation, project-graph-safe updates, cache-aware outputs, and testable workflows. Use when creating Nx plugins, building custom generators, implementing executors, enforcing conventions, or extending Nx workspace automation.
npx skills add github:pantheon-org/tekhne --skill repository-mgmt--nx--nx-plugin-authoringNavigation hub for creating custom Nx plugins, generators, and executors.
When to Use
- You need to scaffold files/projects with repeatable conventions (generators).
- You need custom task execution logic (executors).
- You are building reusable plugin automation for Nx workspaces.
- You want to enforce organization-specific standards and patterns.
When Not to Use
- The task is generic Nx workspace setup unrelated to custom plugin development.
- You only need to use existing plugins, not create new ones.
Principles
- Keep plugins as orchestration tools, not business-logic containers.
- Prefer small, composable generation steps over large mutation passes.
- Keep executors deterministic and cache-aware.
- Use Tree API exclusively for filesystem operations in generators.
Workflow
1. Create Plugin Structure
Start new workspace with plugin:
npx create-nx-plugin my-plugin
Add plugin to existing workspace:
npx nx add @nx/plugin
npx nx g plugin tools/my-plugin
2. Build Generators
Create a generator:
npx nx g generator my-plugin/src/generators/library-with-readme
Implement with Tree API:
- Use
tree.write,generateFiles,updateJsonfor filesystem edits - Define typed schema with required fields
- Add templates with EJS syntax
- Validate with
--dry-runbefore execution
Validate: Run npx nx g my-plugin:library-with-readme mylib --dry-run
Stop if: Unexpected files appear or required options fail validation
3. Build Executors
Create executor structure:
mkdir -p tools/executors/my-executor
Implement with ExecutorContext:
- Define schema.json with required fields and defaults
- Return
{ success: boolean }from executor - Use package notation for registration
- Declare outputs for cache awareness
Validate: Run npx nx run my-project:my-target --help
Stop if: Executor fails to resolve or schema is invalid
4. Test and Publish
Run tests:
npx nx test my-plugin
Validate in real workspace:
npx nx g my-plugin:my-generator sample --dry-run
npx nx run my-project:my-target
Quick Commands
npx create-nx-plugin my-plugin
npx nx add @nx/plugin
npx nx g generator my-plugin/src/generators/my-gen
npx nx g my-plugin:my-gen sample --dry-run
npx nx run my-project:my-target --skip-nx-cache
npx nx reset
rg -n "generateFiles|updateJson|readProjectConfiguration" plugins tools
Anti-Patterns
NEVER modify files outside the Tree API
- WHY: direct filesystem writes bypass dry-run and change tracking.
- BAD:
writeFileSync("libs/my-lib/src/index.ts", content). - GOOD:
tree.write("libs/my-lib/src/index.ts", content).
NEVER hardcode project paths in generator logic
- WHY: brittle paths fail when workspace layout evolves.
- BAD: fixed
libs/my-lib/...writes. - GOOD: derive paths from
readProjectConfiguration(tree, name).root.
NEVER skip schema validation and typed options
- WHY: invalid inputs fail late with unclear errors.
- BAD:
schema: anyand no guardrails. - GOOD: typed schema.json with required fields, types, defaults, descriptions.
NEVER generate across project boundaries without explicit checks
- WHY: hidden boundary violations introduce circular dependencies.
- BAD: generator writes imports into disallowed scopes without verification.
- GOOD: verify tags and dependency direction before cross-scope imports.
NEVER mutate project configuration blindly
- WHY: naive updates remove existing targets/tags.
- BAD: overwrite full
project.json. - GOOD:
const config = readProjectConfiguration(tree, name); config.targets.build = {...}; updateProjectConfiguration(tree, name, config);.
NEVER put business logic directly inside executor entrypoints
- WHY: bloated executors are hard to test and reuse.
- BAD: 200 lines of transformation logic in
executor.ts. - GOOD: delegate to composable library functions.
NEVER use relative executor references in targets
- WHY: relative paths are fragile across workspace changes.
- BAD:
"executor": "tools/executors:task"(a workspace-relative path instead of a package name). - GOOD:
"executor": "@scope/tools:task".
NEVER omit outputs and dependencies semantics for cacheable work
- WHY: Nx cache quality depends on deterministic inputs and outputs.
- BAD: no outputs declared, implicit file writes.
- GOOD: declare outputs in target config:
"outputs": ["{options.outputPath}"].
NEVER block executor runs with synchronous file I/O in hot paths
- WHY: sync I/O hurts parallel throughput.
- BAD:
fs.readFileSyncin main execution flow. - GOOD:
await fs.promises.readFilewith structured error handling.
NEVER skip formatFiles after generating code
- WHY: inconsistent formatting causes noisy diffs and CI failures.
- BAD:
generateFiles()without callingformatFiles(tree). - GOOD:
await generateFiles(...); await formatFiles(tree);.
NEVER ignore existing project structure when generating files
- WHY: overwriting existing files destroys user work.
- BAD:
tree.write(path, content)without checkingtree.exists(path). - GOOD:
if (tree.exists(path)) { throw new Error('File already exists'); }.
NEVER forget to export generator/executor from plugin index
- WHY: unregistered functions cannot be invoked by Nx CLI.
- BAD: generator implemented but missing from
src/index.tsandgenerators.json. - GOOD: export in
src/index.tsand register ingenerators.jsonwith factory path.
Constraint vs Flexibility Guidelines
Hard Constraints (MUST)
Generators:
- MUST use Tree API for filesystem edits
- MUST validate generator options via schema and explicit guards
- MUST respect Nx project graph boundaries and existing tags
- MUST test with
--dry-runbefore broad rollouts
Executors:
- MUST return
{ success: boolean }from executor function - MUST use package notation for executor references in targets
- MUST declare outputs for cacheable tasks
- MUST implement schema validation with required fields
Flexible Choices (CAN)
- CAN choose template style (EJS-heavy vs minimal placeholders)
- CAN choose option naming conventions if documented in schema
- CAN compose other generators to reduce duplication
- CAN delegate executor work to external libraries
Fallback Behaviors
| Missing Input | Fallback |
|---|---|
directory omitted in generator |
Use workspace default location |
| optional flags omitted | Apply safe schema defaults |
| custom template not provided | Generate minimal boilerplate |
| executor outputs not declared | No cache, always re-run |
References
Getting Started
| Topic | Reference |
|---|---|
| Plugin scaffolding and structure | references/plugin-scaffolding.md |
| Core concepts | knowledge-base/concepts.md |
Generators
| Topic | Reference |
|---|---|
| Generator implementation guide | references/generators-guide.md |
| Tree API patterns | references/tree-api-reference.md |
| Template engine guidance | references/template-engine-guide.md |
| Template system details | knowledge-base/template-system.md |
Executors
| Topic | Reference |
|---|---|
| Executor implementation guide | references/executors-guide.md |
| ExecutorContext API usage | references/executor-context-api.md |
| Executor schema design | references/executor-schema-design.md |
Shared Resources
| Topic | Reference |
|---|---|
| Schema design patterns | references/schema-design-patterns.md |
| Utility helpers | knowledge-base/utilities.md |
| Testing and troubleshooting | references/testing-and-troubleshooting.md |
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
No eval scenarios available.
ExecutorContext API Reference
Use this reference when implementing executor behavior with Nx context metadata.
Commonly Used Fields
context.root: workspace root path.context.projectName: active project name.context.targetName: current target key.context.configurationName: active configuration, if provided.context.projectsConfigurations: full project graph metadata.
Example Usage
import type { ExecutorContext } from "@nx/devkit";
export default async (_options: { outputPath: string }, context: ExecutorContext) => {
const project = context.projectName ?? "unknown-project";
const target = context.targetName ?? "unknown-target";
console.log(`Running ${project}:${target}`);
return { success: true };
};
Practical Patterns
- Log project/target identity for debuggability.
- Resolve workspace-relative paths via
context.root. - Keep context reads defensive (
??) to avoid brittle assumptions.
Executor Schema Design
Use this reference when designing schema.json for custom Nx executors.
Baseline Schema Example
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"properties": {
"outputPath": {
"type": "string",
"description": "Where generated artifacts are written"
},
"watch": {
"type": "boolean",
"default": false,
"description": "Enable incremental watch mode"
}
},
"required": ["outputPath"]
}
Validation Patterns
- Use
requiredfor mandatory operational parameters. - Add defaults for safe optional flags.
- Prefer descriptive option text; it becomes CLI help content.
Conditional/Dependent Example
{
"type": "object",
"properties": {
"mode": { "enum": ["dev", "prod"] },
"minify": { "type": "boolean" }
},
"allOf": [
{
"if": { "properties": { "mode": { "const": "prod" } } },
"then": { "required": ["minify"] }
}
]
}
Nx Executors Guide
What Are Executors?
Executors define reusable build, test, and development tasks. They encapsulate task logic that can be shared across multiple projects in a workspace with cache-aware execution.
Executor Implementation
Basic structure:
import { ExecutorContext } from '@nx/devkit';
import { ExecutorSchema } from './schema';
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
console.log('Executing task with options:', options);
console.log('Project:', context.projectName);
try {
// Implementation logic here
return { success: true };
} catch (error) {
console.error('Execution failed:', error);
return { success: false };
}
}
ExecutorContext API
Available properties:
interface ExecutorContext {
root: string; // Workspace root
cwd: string; // Current working directory
projectName: string; // Current project name
targetName: string; // Current target name
configurationName?: string; // Active configuration
workspace: WorkspaceJsonConfiguration;
projectsConfigurations: ProjectsConfigurations;
nxJsonConfiguration: NxJsonConfiguration;
isVerbose: boolean;
}
Common usage:
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
const project = context.projectsConfigurations.projects[context.projectName];
const projectRoot = project.root;
const workspaceRoot = context.root;
console.log(`Running ${context.targetName} for ${context.projectName}`);
console.log(`Project root: ${projectRoot}`);
console.log(`Workspace root: ${workspaceRoot}`);
// Implementation
return { success: true };
}
Schema Definition
schema.json:
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"title": "My Executor",
"description": "Execute something useful",
"type": "object",
"properties": {
"outputPath": {
"type": "string",
"description": "Output directory"
},
"watch": {
"type": "boolean",
"description": "Watch for changes",
"default": false
},
"port": {
"type": "number",
"description": "Port number",
"default": 3000
}
},
"required": ["outputPath"]
}
schema.d.ts:
export interface ExecutorSchema {
outputPath: string;
watch?: boolean;
port?: number;
}
Executor Registration
executors.json:
{
"executors": {
"build": {
"implementation": "./src/executors/build/executor",
"schema": "./src/executors/build/schema.json",
"description": "Build the project"
},
"serve": {
"implementation": "./src/executors/serve/executor",
"schema": "./src/executors/serve/schema.json",
"description": "Serve the project"
}
}
}
Target Configuration
project.json:
{
"name": "my-app",
"targets": {
"build": {
"executor": "@my-org/my-plugin:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/my-app"
},
"configurations": {
"production": {
"optimization": true,
"sourceMap": false
}
}
}
}
}
Cache Configuration
Declare outputs for caching:
{
"targets": {
"build": {
"executor": "@my-org/my-plugin:build",
"outputs": [
"{options.outputPath}",
"{workspaceRoot}/dist/{projectRoot}"
],
"options": {
"outputPath": "dist/apps/my-app"
}
}
}
}
Declare inputs for cache invalidation:
{
"targets": {
"build": {
"executor": "@my-org/my-plugin:build",
"inputs": [
"default",
"{workspaceRoot}/tsconfig.base.json"
],
"outputs": ["{options.outputPath}"]
}
}
}
Async File Operations
Use async I/O:
import { promises as fs } from 'fs';
import { join } from 'path';
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
const project = context.projectsConfigurations.projects[context.projectName];
const filePath = join(context.root, project.root, 'package.json');
try {
const content = await fs.readFile(filePath, 'utf-8');
const pkg = JSON.parse(content);
// Process package.json
await fs.writeFile(
join(options.outputPath, 'manifest.json'),
JSON.stringify(pkg, null, 2)
);
return { success: true };
} catch (error) {
console.error('File operation failed:', error);
return { success: false };
}
}
Watch Mode Support
Implement watch mode:
import { watch } from 'fs';
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
const project = context.projectsConfigurations.projects[context.projectName];
const watchPath = join(context.root, project.root, 'src');
if (options.watch) {
console.log(`Watching ${watchPath} for changes...`);
watch(watchPath, { recursive: true }, async (eventType, filename) => {
console.log(`File ${filename} changed, rebuilding...`);
await build(options, context);
});
return { success: true };
}
return await build(options, context);
}
async function build(options: ExecutorSchema, context: ExecutorContext) {
// Build logic
return { success: true };
}
Workspace Setup
Enable package references:
- Add to root
package.json:
{
"workspaces": [
"packages/*",
"tools/*"
]
}
- Add to
tsconfig.base.json:
{
"compilerOptions": {
"paths": {
"@my-org/tools": ["tools/src/index.ts"]
}
}
}
- Create entry point
tools/src/index.ts:
export * from './executors/build/executor';
export * from './executors/serve/executor';
- Set main field in
tools/package.json:
{
"name": "@my-org/tools",
"main": "./src/index.ts",
"executors": "./executors.json"
}
Testing
Unit test example:
import { ExecutorContext } from '@nx/devkit';
import executor from './executor';
import { ExecutorSchema } from './schema';
const options: ExecutorSchema = {
outputPath: 'dist/test'
};
const context: ExecutorContext = {
root: '/workspace',
cwd: '/workspace',
projectName: 'test-project',
targetName: 'build',
workspace: { version: 2, projects: {} },
projectsConfigurations: {
version: 2,
projects: {
'test-project': {
root: 'apps/test-project'
}
}
},
isVerbose: false
};
describe('Build Executor', () => {
it('should execute successfully', async () => {
const result = await executor(options, context);
expect(result.success).toBe(true);
});
});
Best Practices
- Return success boolean - always return
{ success: boolean } - Use package notation - use
@scope/package:executornot relative paths - Declare outputs - enable cache for deterministic tasks
- Use async I/O - avoid blocking operations
- Delegate logic - keep executors thin, delegate to library functions
- Validate options - check required fields early
- Log clearly - provide actionable output for debugging
References
Nx Generators Guide
What Are Generators?
Generators automate file and project scaffolding with repeatable conventions. They use the Tree API for filesystem operations and schema-driven options for type-safe inputs.
Generator Implementation
Basic structure:
import { Tree, formatFiles, generateFiles, joinPathSegments } from '@nx/devkit';
import * as path from 'path';
import { GeneratorSchema } from './schema';
export async function myGenerator(tree: Tree, options: GeneratorSchema) {
const projectRoot = `libs/${options.name}`;
generateFiles(
tree,
path.join(__dirname, 'files'),
projectRoot,
options
);
await formatFiles(tree);
}
export default myGenerator;
Tree API Operations
Write files:
tree.write('path/to/file.ts', 'content');
Read files:
const content = tree.read('path/to/file.ts', 'utf-8');
Check existence:
if (tree.exists('path/to/file.ts')) {
// file exists
}
Delete files:
tree.delete('path/to/file.ts');
List files:
tree.children('libs/my-lib/src');
Update JSON:
import { updateJson } from '@nx/devkit';
updateJson(tree, 'package.json', (json) => {
json.scripts.build = 'nx build';
return json;
});
Project Configuration
Read project:
import { readProjectConfiguration } from '@nx/devkit';
const project = readProjectConfiguration(tree, 'my-lib');
console.log(project.root); // libs/my-lib
console.log(project.targets); // { build: {...}, test: {...} }
Update project:
import { updateProjectConfiguration } from '@nx/devkit';
const project = readProjectConfiguration(tree, 'my-lib');
project.targets.lint = {
executor: '@nx/linter:eslint',
options: {
lintFilePatterns: [`${project.root}/**/*.ts`]
}
};
updateProjectConfiguration(tree, 'my-lib', project);
Add new project:
import { addProjectConfiguration } from '@nx/devkit';
addProjectConfiguration(tree, 'my-lib', {
root: 'libs/my-lib',
projectType: 'library',
sourceRoot: 'libs/my-lib/src',
targets: {
build: {
executor: '@nx/js:tsc',
outputs: ['{options.outputPath}'],
options: {
outputPath: 'dist/libs/my-lib',
main: 'libs/my-lib/src/index.ts',
tsConfig: 'libs/my-lib/tsconfig.lib.json'
}
}
}
});
Template Generation
Directory structure:
src/generators/my-generator/
├── generator.ts
├── schema.json
├── schema.d.ts
└── files/
├── src/
│ └── index.ts.template
├── README.md.template
└── __name__/
└── __fileName__.ts.template
Template files use EJS syntax:
// files/src/index.ts.template
export * from './<%= name %>';
export function <%= propertyName %>(): string {
return '<%= name %> works!';
}
File name substitution:
__name__→ replaced withoptions.name__fileName__→ replaced with custom option values__directory__→ replaced withoptions.directory
Generate from templates:
import { generateFiles } from '@nx/devkit';
import * as path from 'path';
generateFiles(
tree,
path.join(__dirname, 'files'),
`libs/${options.name}`,
{
...options,
template: '', // removes .template extension
propertyName: names(options.name).propertyName
}
);
Schema-Driven Options
schema.json:
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"id": "my-generator",
"title": "My Generator",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?"
},
"directory": {
"type": "string",
"description": "Directory where the library is placed"
},
"tags": {
"type": "string",
"description": "Tags to add to project (comma-delimited)"
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files",
"default": false
}
},
"required": ["name"]
}
schema.d.ts:
export interface GeneratorSchema {
name: string;
directory?: string;
tags?: string;
skipFormat?: boolean;
}
Utility Helpers
Name transformations:
import { names } from '@nx/devkit';
const result = names('my-lib');
// {
// name: 'my-lib',
// className: 'MyLib',
// propertyName: 'myLib',
// constantName: 'MY_LIB',
// fileName: 'my-lib'
// }
Path utilities:
import { joinPathSegments, normalizePath } from '@nx/devkit';
const fullPath = joinPathSegments('libs', options.name, 'src');
const normalized = normalizePath('libs\\my-lib\\src'); // libs/my-lib/src
Dependency management:
import { addDependenciesToPackageJson } from '@nx/devkit';
addDependenciesToPackageJson(
tree,
{ '@my-org/shared': '^1.0.0' },
{ '@my-org/build-tools': '^1.0.0' }
);
Validation and Testing
Dry run validation:
npx nx g @my-org/my-plugin:library test --dry-run
Unit test example:
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readProjectConfiguration } from '@nx/devkit';
import { libraryGenerator } from './generator';
describe('library generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should create library', async () => {
await libraryGenerator(tree, { name: 'test' });
const config = readProjectConfiguration(tree, 'test');
expect(config.root).toBe('libs/test');
expect(tree.exists('libs/test/src/index.ts')).toBeTruthy();
expect(tree.exists('libs/test/README.md')).toBeTruthy();
});
it('should add tags', async () => {
await libraryGenerator(tree, { name: 'test', tags: 'scope:shared,type:util' });
const config = readProjectConfiguration(tree, 'test');
expect(config.tags).toEqual(['scope:shared', 'type:util']);
});
});
Best Practices
- Use Tree API exclusively - never direct filesystem writes
- Read before mutate - always
readProjectConfigurationbeforeupdateProjectConfiguration - Validate boundaries - check tags and dependency rules before cross-scope imports
- Format generated files - call
formatFiles(tree)after generation - Test with dry-run - validate before broad rollouts
- Compose generators - call other generators to reduce duplication
References
Nx Plugin Scaffolding
Creating a New Plugin Workspace
Start from scratch:
npx create-nx-plugin my-plugin
cd my-plugin
This creates:
- Nx workspace with plugin package pre-configured
- Sample generator and executor
- Testing infrastructure with Jest
- E2E testing setup
- TypeScript configuration
Adding Plugin to Existing Workspace
Install plugin package:
npx nx add @nx/plugin
Generate plugin:
npx nx g @nx/plugin:plugin tools/my-plugin
Generated structure:
tools/my-plugin/
├── src/
│ ├── generators/
│ │ └── my-plugin/
│ │ ├── generator.ts
│ │ ├── schema.json
│ │ └── schema.d.ts
│ ├── executors/
│ └── index.ts
├── package.json
├── project.json
├── tsconfig.json
└── tsconfig.lib.json
Plugin Directory Structure
Minimal plugin:
my-plugin/
├── src/
│ ├── generators/
│ │ ├── generator-name/
│ │ │ ├── generator.ts
│ │ │ ├── schema.json
│ │ │ ├── schema.d.ts
│ │ │ └── files/ # Optional templates
│ │ │ └── __name__.txt.template
│ │ └── generators.json
│ ├── executors/
│ │ ├── executor-name/
│ │ │ ├── executor.ts
│ │ │ ├── schema.json
│ │ │ └── schema.d.ts
│ │ └── executors.json
│ └── index.ts
├── package.json
└── project.json
Package Configuration
tools/my-plugin/package.json:
{
"name": "@my-org/my-plugin",
"version": "0.0.1",
"main": "./src/index.ts",
"generators": "./generators.json",
"executors": "./executors.json",
"dependencies": {
"@nx/devkit": "^22.0.0",
"tslib": "^2.3.0"
}
}
Registry Files
generators.json:
{
"generators": {
"my-generator": {
"factory": "./src/generators/my-generator/generator",
"schema": "./src/generators/my-generator/schema.json",
"description": "Generate something useful"
}
}
}
executors.json:
{
"executors": {
"my-executor": {
"implementation": "./src/executors/my-executor/executor",
"schema": "./src/executors/my-executor/schema.json",
"description": "Execute something useful"
}
}
}
Workspace Integration
Enable package references:
- Add to root
package.json:
{
"workspaces": [
"packages/*",
"tools/*"
]
}
- Add to
tsconfig.base.json:
{
"compilerOptions": {
"paths": {
"@my-org/my-plugin": ["tools/my-plugin/src/index.ts"]
}
}
}
- Create entry point
tools/my-plugin/src/index.ts:
export * from './generators/my-generator/generator';
export * from './executors/my-executor/executor';
Testing Setup
Unit tests:
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readProjectConfiguration } from '@nx/devkit';
import { myGenerator } from './generator';
describe('my-generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should generate files', async () => {
await myGenerator(tree, { name: 'test' });
expect(tree.exists('libs/test/README.md')).toBeTruthy();
});
});
E2E tests:
import { execSync } from 'child_process';
import { rmSync } from 'fs';
describe('my-plugin e2e', () => {
beforeAll(() => {
execSync('npx create-nx-workspace test-workspace --preset=empty --no-interactive');
});
afterAll(() => {
rmSync('test-workspace', { recursive: true, force: true });
});
it('should generate library', () => {
execSync('npx nx g @my-org/my-plugin:library test', {
cwd: 'test-workspace',
stdio: 'inherit'
});
});
});
Publishing
Local testing:
npm link
cd ../test-workspace
npm link @my-org/my-plugin
Registry publishing:
npm publish --access public
Nx plugin registry: Submit to nx.dev/plugin-registry after publishing to npm.
References
Schema Design Patterns
Use this reference to design generator schemas that fail early and guide users clearly.
Baseline Schema Pattern
{
"cli": "nx",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Artifact name"
},
"directory": {
"type": "string",
"description": "Target directory"
}
},
"required": ["name"]
}
Conditional Schema Example
{
"type": "object",
"properties": {
"publishable": { "type": "boolean" },
"importPath": { "type": "string" }
},
"allOf": [
{
"if": { "properties": { "publishable": { "const": true } } },
"then": { "required": ["importPath"] }
}
]
}
Design Guidance
- Use
requiredfor mandatory values. - Add clear
descriptionfields for CLI help readability. - Provide sane defaults for optional flags.
- Avoid ambiguous option names across generators.
Template Engine Guide
Use this reference when creating and maintaining generator template files.
Template Basics
- Store templates under
files/in the generator directory. - Use
.templatefile suffix for rendered outputs. - Include
tmpl: ""in template variables to strip.templatesuffix.
EJS Variable Examples
const templateVars = {
...schema,
...names(schema.name),
tmpl: "",
};
export const <%= propertyName %> = "<%= name %>";
Reusable Template Patterns
- Keep shared snippets small and composable.
- Prefer explicit variable names over implicit assumptions.
- Separate structure templates (directories/files) from configuration mutation logic.
Troubleshooting
- Unreplaced variables usually mean missing keys in
templateVars. - Incorrect filenames often mean missing
tmpl: ""mapping.
Testing and Troubleshooting Nx Plugins
Testing Generators
Unit Tests
Basic generator test:
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readProjectConfiguration } from '@nx/devkit';
import { libraryGenerator } from './generator';
describe('library generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should create library', async () => {
await libraryGenerator(tree, { name: 'test' });
const config = readProjectConfiguration(tree, 'test');
expect(config.root).toBe('libs/test');
expect(tree.exists('libs/test/src/index.ts')).toBeTruthy();
});
it('should handle directory option', async () => {
await libraryGenerator(tree, {
name: 'test',
directory: 'shared'
});
const config = readProjectConfiguration(tree, 'shared-test');
expect(config.root).toBe('libs/shared/test');
});
});
Dry-Run Validation
Test before applying:
npx nx g @my-org/my-plugin:library test --dry-run
Validate output structure:
npx nx g @my-org/my-plugin:library test --dry-run | grep -E "CREATE|UPDATE"
Testing Executors
Unit Tests
Basic executor test:
import { ExecutorContext } from '@nx/devkit';
import executor from './executor';
import { ExecutorSchema } from './schema';
const mockContext: ExecutorContext = {
root: '/workspace',
cwd: '/workspace',
projectName: 'test-project',
targetName: 'build',
workspace: { version: 2, projects: {} },
projectsConfigurations: {
version: 2,
projects: {
'test-project': {
root: 'apps/test-project'
}
}
},
isVerbose: false
};
describe('Build Executor', () => {
it('should execute successfully', async () => {
const options: ExecutorSchema = {
outputPath: 'dist/test'
};
const result = await executor(options, mockContext);
expect(result.success).toBe(true);
});
it('should fail with invalid options', async () => {
const options: ExecutorSchema = {
outputPath: ''
};
const result = await executor(options, mockContext);
expect(result.success).toBe(false);
});
});
Integration Tests
Test executor in workspace:
npx nx run my-project:my-target
Skip cache for testing:
npx nx run my-project:my-target --skip-nx-cache
Verify outputs:
npx nx run my-project:build
ls -la dist/apps/my-project
Common Issues
“Unable to resolve executor package reference”
Error:
NX Cannot find executor '@my-org/tools:build'
Unable to resolve @my-org/tools:build
Causes:
- Package not in workspaces
- Missing tsconfig path mapping
- No entry point file
- Missing main field
Solution:
- Check workspace inclusion in root
package.json:
{
"workspaces": [
"packages/*",
"tools/*"
]
}
- Verify path mapping in
tsconfig.base.json:
{
"compilerOptions": {
"paths": {
"@my-org/tools": ["tools/src/index.ts"]
}
}
}
- Create entry point
tools/src/index.ts:
export * from './executors/build/executor';
- Set main field in
tools/package.json:
{
"main": "./src/index.ts"
}
- Reset Nx daemon:
npx nx reset
npx nx run my-project:build
“Generator not found”
Error:
Cannot find generator '@my-org/my-plugin:library'
Solution:
- Check
generators.jsonregistration:
{
"generators": {
"library": {
"factory": "./src/generators/library/generator",
"schema": "./src/generators/library/schema.json"
}
}
}
- Verify
package.jsonpoints to registry:
{
"generators": "./generators.json"
}
- Check generator export:
export default async function libraryGenerator(tree: Tree, options: Schema) {
// ...
}
Schema validation errors
Error:
Required property 'name' is missing
Solution:
- Check schema.json required fields:
{
"required": ["name"]
}
- Provide default values:
{
"properties": {
"name": {
"type": "string",
"$default": {
"$source": "argv",
"index": 0
}
}
}
}
- Add prompts for interactive mode:
{
"properties": {
"name": {
"type": "string",
"x-prompt": "What name would you like to use?"
}
}
}
Cache not working
Symptoms:
- Executor runs every time
- No cache hits reported
Solution:
- Declare outputs in target config:
{
"targets": {
"build": {
"outputs": ["{options.outputPath}"]
}
}
}
- Ensure outputs are deterministic
- Check inputs configuration
- Verify executor returns
{ success: true }
Tree API violations
Error:
Cannot write file outside of workspace
Solution:
- Use Tree API exclusively:
tree.write()notfs.writeFileSync() - Keep all paths relative to workspace root
- Never bypass Tree with direct filesystem writes
Debugging Tips
Enable verbose output
npx nx run my-project:build --verbose
Reset Nx cache
npx nx reset
Check project configuration
npx nx show project my-project
Validate workspace
npx nx workspace-lint
Inspect task graph
npx nx graph
Search for implementation patterns
rg -n "generateFiles|tree.write|ExecutorContext" tools plugins
E2E Testing
Create test workspace:
import { execSync } from 'child_process';
import { rmSync } from 'fs';
describe('plugin e2e', () => {
const testWorkspace = 'test-workspace';
beforeAll(() => {
execSync(`npx create-nx-workspace ${testWorkspace} --preset=empty --no-interactive`);
execSync(`npm install @my-org/my-plugin`, { cwd: testWorkspace });
});
afterAll(() => {
rmSync(testWorkspace, { recursive: true, force: true });
});
it('should generate library', () => {
execSync(`npx nx g @my-org/my-plugin:library test`, {
cwd: testWorkspace,
stdio: 'inherit'
});
// Verify generated files
});
});
Performance Testing
Measure generator performance:
time npx nx g @my-org/my-plugin:library test --dry-run
Measure executor performance:
time npx nx run my-project:build --skip-nx-cache
References
Tree API Reference
Use this reference for safe file and configuration mutations in generators.
Common Methods
tree.read(path)reads file contents.tree.write(path, content)writes/creates files tracked by dry-run.tree.exists(path)checks whether a path is present in the virtual tree.tree.delete(path)removes files safely in generator context.
High-Value Devkit Helpers
generateFiles(tree, src, dest, vars)for template-based generation.updateJson(tree, path, updater)for structured JSON mutations.readProjectConfiguration(tree, project)andupdateProjectConfiguration(...)for project settings.
Example Pattern
import { Tree, updateJson, generateFiles, joinPathFragments } from "@nx/devkit";
export default async function generator(tree: Tree, schema: { name: string }) {
const root = joinPathFragments("libs", schema.name);
generateFiles(tree, joinPathFragments(__dirname, "files"), root, { ...schema, tmpl: "" });
updateJson(tree, "nx.json", (json) => {
json.generators = json.generators ?? {};
return json;
});
}
Safety Notes
- Prefer Tree operations over Node
fsin generator runtime. - Keep updates idempotent where possible.