Comprehensive guide for creating and managing Nx plugins including generators, inferred tasks, migrations, and best practices for extending Nx workspaces
Overall
score
100%
Does it follow best practices?
Validation for skill structure
Source: Nx Documentation - Extending Nx
Last Updated: December 2024
Nx Version: 22
npx create-nx-plugin my-pluginScaffolds a complete workspace with: plugin package structure, generator templates, testing setup, and build configuration.
# Install plugin capability
npx nx add @nx/plugin
# Generate a new plugin
npx nx g plugin tools/my-pluginCreates a plugin package in tools/my-plugin with generator scaffolding and plugin registration files.
| Component | Purpose |
|---|---|
| Generators | Automate code scaffolding and project creation |
| Inferred Tasks | Auto-configure targets (build, test, etc.) from project structure |
| Migrations | Update dependencies, configs, and code patterns across versions |
| Presets | Starting-point templates for new repositories |
npx nx g generator my-plugin/src/generators/library-with-readmeCreates: generator implementation file, schema definition, template files folder, and registration in generators.json.
Basic Generator Implementation:
import { Tree, addProjectConfiguration, generateFiles, formatFiles } from '@nx/devkit';
import * as path from 'path';
export async function libraryWithReadmeGenerator(
tree: Tree,
options: LibraryWithReadmeGeneratorSchema
) {
const projectRoot = `libs/${options.name}`;
// Create project configuration
addProjectConfiguration(tree, options.name, {
root: projectRoot,
projectType: 'library',
sourceRoot: `${projectRoot}/src`,
targets: {},
});
// Generate files from templates
generateFiles(
tree,
path.join(__dirname, 'files'),
projectRoot,
options
);
// Format generated files
await formatFiles(tree);
}Key Functions:
addProjectConfiguration - Register new projectgenerateFiles - Create files from templatesformatFiles - Apply code formattingTemplates use EJS syntax for variable injection:
Example README Template:
<!-- files/README.md.template -->
# <%= name %>
This was generated by the `library-with-readme` generator!Template Variables:
<%= name %> - Inject generator options<% if (condition) { %> - Conditional logic<% for (item of items) { %> - IterationFile Naming:
.template suffix is removed during generation__name__ is replaced with actual valuesDry Run Mode (always run first):
# Preview changes without writing files
npx nx g my-plugin:library-with-readme mylib --dry-runReview the dry-run output carefully:
Actual Execution:
npx nx g my-plugin:library-with-readme mylibWith Options:
npx nx g my-plugin:library-with-readme mylib --directory=shared --tags=utilsPost-Generation Validation:
After running the generator, verify correctness:
# Confirm the project is registered in the graph
npx nx show project mylib
# Run the project's tasks to ensure configuration is valid
npx nx build mylib
npx nx test mylibCommon Errors and Fixes:
Project 'mylib' does not exist → Check addProjectConfiguration was called and generators.json is registered correctly in package.jsongenerateFilesawait formatFiles(tree) is called at the end of the generatorschema.json match what you pass when invoking the generatorFor a full Nx Devkit API reference, see Nx Devkit API Reference.
import {
generateFiles,
readProjectConfiguration,
updateProjectConfiguration,
joinPathFragments
} from '@nx/devkit';
// Generate files from templates
generateFiles(tree, templatePath, targetPath, variables);
// Read project config
const config = readProjectConfiguration(tree, projectName);
// Update project config
updateProjectConfiguration(tree, projectName, updatedConfig);import {
addProjectConfiguration,
removeProjectConfiguration,
getProjects
} from '@nx/devkit';
// Add new project
addProjectConfiguration(tree, name, config);
// Remove project
removeProjectConfiguration(tree, name);
// Get all projects
const projects = getProjects(tree);import {
addDependenciesToPackageJson,
removeDependenciesFromPackageJson
} from '@nx/devkit';
// Add dependencies
addDependenciesToPackageJson(
tree,
{ 'lodash': '^4.17.21' }, // dependencies
{ '@types/lodash': '^4.14.0' } // devDependencies
);import { names } from '@nx/devkit';
const result = names('my-awesome-lib');
// {
// name: 'my-awesome-lib',
// className: 'MyAwesomeLib',
// propertyName: 'myAwesomeLib',
// constantName: 'MY_AWESOME_LIB',
// fileName: 'my-awesome-lib'
// }Leverage built-in functions instead of manual file operations:
// Good: Use Nx utilities
import { updateJson } from '@nx/devkit';
updateJson(tree, 'package.json', (json) => {
json.scripts = { ...json.scripts, test: 'jest' };
return json;
});
// Bad: Manual file manipulation
const content = tree.read('package.json').toString();
const json = JSON.parse(content);
json.scripts.test = 'jest';
tree.write('package.json', JSON.stringify(json));{
"cli": "nx",
"id": "library-with-readme",
"description": "Generate a library with README",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": {
"$source": "argv",
"index": 0
}
},
"directory": {
"type": "string",
"description": "Directory where library is created"
}
},
"required": ["name"]
}import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { libraryWithReadmeGenerator } from './generator';
describe('library-with-readme', () => {
it('should create README file', async () => {
const tree = createTreeWithEmptyWorkspace();
await libraryWithReadmeGenerator(tree, { name: 'test' });
expect(tree.exists('libs/test/README.md')).toBeTruthy();
});
});import { formatFiles } from '@nx/devkit';
export async function myGenerator(tree: Tree, options: Schema) {
// ... generator logic
// Format all modified files
await formatFiles(tree);
}import { libraryGenerator } from '@nx/js';
export async function enhancedLibraryGenerator(
tree: Tree,
options: Schema
) {
// Generate base library
await libraryGenerator(tree, {
name: options.name,
directory: 'libs'
});
// Add custom files
generateFiles(
tree,
path.join(__dirname, 'files'),
`libs/${options.name}`,
options
);
await formatFiles(tree);
}export async function myGenerator(tree: Tree, options: Schema) {
// Always generate base files
generateFiles(tree, baseFilesPath, projectRoot, options);
// Conditionally generate test files
if (options.includeTests) {
generateFiles(tree, testFilesPath, projectRoot, options);
}
await formatFiles(tree);
}import { updateProjectConfiguration, readProjectConfiguration } from '@nx/devkit';
export async function myGenerator(tree: Tree, options: Schema) {
const projectConfig = readProjectConfiguration(tree, options.project);
// Add new target
projectConfig.targets.myTarget = {
executor: 'nx:run-commands',
options: {
command: 'echo "Hello"'
}
};
updateProjectConfiguration(tree, options.project, projectConfig);
}Always maintain compatibility with supported Nx versions and publish breaking changes as major version bumps.
--dry-run preview.writeFileSync("libs/my-lib/README.md", content) in generator.tree.write("libs/my-lib/README.md", content) via Nx Devkit Tree API."apps/" or "packages/" break when directories change.const projectRoot = "libs/" + options.name; assumes libs/ convention.readProjectConfiguration(tree, projectName).root.schema: anyschema: any and no required fields."required": ["name"], explicit types, descriptions, and defaults.project.json deletes existing targets and tags that other generators added.updateProjectConfiguration(tree, name, { root, targets: { build: {...} } }) (replaces entire config).const config = readProjectConfiguration(tree, name); config.targets.build = {...}; updateProjectConfiguration(tree, name, config);.@frontend scope writes imports to @backend scope without checking tags.ensureProjectBoundaries or tag rules before generating imports.Install with Tessl CLI
npx tessl i pantheon-ai/extending-nx-plugins