CtrlK
BlogDocsLog inGet started
Tessl Logo

pantheon-ai/extending-nx-plugins

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

Overview
Skills
Evals
Files

SKILL.md

name:
extending-nx-plugins
description:
Creates Nx plugins, builds custom generators, configures inferred tasks, and writes version migrations for Nx workspaces. Use when the user wants to create a custom Nx plugin, scaffold a generator with nx generate, add inferred tasks, write workspace migrations, build a custom executor, develop a plugin preset, or extend an Nx workspace with custom tooling. Covers nx plugin development, generator templates, Nx Devkit API usage, schema definitions, testing generators, and publishing plugins.

Extending Nx with Plugins

Source: Nx Documentation - Extending Nx
Last Updated: December 2024
Nx Version: 22

Creating a Plugin

New Workspace with Plugin

npx create-nx-plugin my-plugin

Scaffolds a complete workspace with: plugin package structure, generator templates, testing setup, and build configuration.

Add Plugin to Existing Workspace

# Install plugin capability
npx nx add @nx/plugin

# Generate a new plugin
npx nx g plugin tools/my-plugin

Creates a plugin package in tools/my-plugin with generator scaffolding and plugin registration files.

Plugin Components

ComponentPurpose
GeneratorsAutomate code scaffolding and project creation
Inferred TasksAuto-configure targets (build, test, etc.) from project structure
MigrationsUpdate dependencies, configs, and code patterns across versions
PresetsStarting-point templates for new repositories

Creating Your First Generator

Generate a Generator

npx nx g generator my-plugin/src/generators/library-with-readme

Creates: generator implementation file, schema definition, template files folder, and registration in generators.json.

Generator Structure

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 project
  • generateFiles - Create files from templates
  • formatFiles - Apply code formatting

Template Files

Templates 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) { %> - Iteration

File Naming:

  • .template suffix is removed during generation
  • __name__ is replaced with actual values

Running and Validating Generators

Dry Run Mode (always run first):

# Preview changes without writing files
npx nx g my-plugin:library-with-readme mylib --dry-run

Review the dry-run output carefully:

  • Confirm all expected files appear in the list
  • Check that no unexpected files are modified
  • Verify file paths match your intended project root

Actual Execution:

npx nx g my-plugin:library-with-readme mylib

With Options:

npx nx g my-plugin:library-with-readme mylib --directory=shared --tags=utils

Post-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 mylib

Common Errors and Fixes:

  • Project 'mylib' does not exist → Check addProjectConfiguration was called and generators.json is registered correctly in package.json
  • Template variable undefined → Ensure all template variables are passed as the fourth argument to generateFiles
  • Files not formatted → Confirm await formatFiles(tree) is called at the end of the generator
  • Schema validation failure → Cross-check that required fields in schema.json match what you pass when invoking the generator

Helper Functions

For a full Nx Devkit API reference, see Nx Devkit API Reference.

File Operations

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);

Project Management

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);

Dependency Management

import {
  addDependenciesToPackageJson,
  removeDependenciesFromPackageJson
} from '@nx/devkit';

// Add dependencies
addDependenciesToPackageJson(
  tree,
  { 'lodash': '^4.17.21' },  // dependencies
  { '@types/lodash': '^4.14.0' }  // devDependencies
);

String Utilities

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'
// }

Best Practices

1. Use Nx Devkit Utilities

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));

2. Provide Clear Schema Definitions

{
  "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"]
}

3. Test Your Generators

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();
  });
});

4. Format Generated Code

import { formatFiles } from '@nx/devkit';

export async function myGenerator(tree: Tree, options: Schema) {
  // ... generator logic
  
  // Format all modified files
  await formatFiles(tree);
}

Common Patterns

Composing Generators

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);
}

Conditional File Generation

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);
}

Updating Existing Projects

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.

Anti-Patterns

NEVER modify files outside the Tree API

  • WHY: direct filesystem operations bypass dry-run mode and change tracking, breaking --dry-run preview.
  • BAD: writeFileSync("libs/my-lib/README.md", content) in generator.
  • GOOD: tree.write("libs/my-lib/README.md", content) via Nx Devkit Tree API.

NEVER hardcode workspace structure assumptions

  • WHY: monorepo layouts evolve; hardcoded paths like "apps/" or "packages/" break when directories change.
  • BAD: const projectRoot = "libs/" + options.name; assumes libs/ convention.
  • GOOD: derive paths from workspace layout: readProjectConfiguration(tree, projectName).root.

NEVER skip schema validation or use schema: any

  • WHY: untyped options cause runtime errors when invalid inputs are passed; users get cryptic failures.
  • BAD: generator options interface with schema: any and no required fields.
  • GOOD: typed schema.json with "required": ["name"], explicit types, descriptions, and defaults.

NEVER mutate project configuration blindly

  • WHY: overwriting full project.json deletes existing targets and tags that other generators added.
  • BAD: updateProjectConfiguration(tree, name, { root, targets: { build: {...} } }) (replaces entire config).
  • GOOD: const config = readProjectConfiguration(tree, name); config.targets.build = {...}; updateProjectConfiguration(tree, name, config);.

NEVER generate across project boundaries without dependency checks

  • WHY: adding imports from restricted scopes introduces circular dependencies or violates module boundary rules.
  • BAD: generator in @frontend scope writes imports to @backend scope without checking tags.
  • GOOD: read target project tags, verify allowed dependencies via ensureProjectBoundaries or tag rules before generating imports.

Learning Resources

Official Tutorials

Reference Documentation

Community and Support

Install with Tessl CLI

npx tessl i pantheon-ai/extending-nx-plugins@0.2.0

SKILL.md

tile.json