Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nrwl/nx/llms.txt

Use this file to discover all available pages before exploring further.

The Nx project graph is a representation of all projects in your workspace and the dependencies between them. By default, Nx builds this graph by analyzing JavaScript and TypeScript source files. Project graph plugins let you extend the graph with additional projects, targets, and dependencies — including for languages and tools Nx doesn’t understand natively. Two exported members define the plugin API:
  • createNodesV2 — adds project nodes (and their targets) to the graph.
  • createDependencies — adds dependency edges between projects.
Set NX_DAEMON=false during plugin development. The Nx daemon caches plugin code, so changes won’t be reflected until the daemon restarts.

Registering a plugin

Add the plugin to the plugins array in nx.json:
// nx.json
{
  "plugins": [
    {
      "plugin": "my-plugin",
      "options": {
        "buildTargetName": "build"
      }
    }
  ]
}
For a local (unpublished) plugin, reference its entry point by path:
{
  "plugins": ["./tools/my-plugin/src/index.ts"]
}

Adding nodes with createNodesV2

createNodesV2 is a tuple of [globPattern, asyncFunction]. Nx finds all files matching the glob and calls the function with the full list. Use createNodesFromFiles to process each file individually:
// my-plugin/index.ts
import {
  CreateNodesContextV2,
  CreateNodesV2,
  createNodesFromFiles,
} from '@nx/devkit';
import { readJsonFile } from '@nx/devkit';
import { dirname } from 'path';

export interface MyPluginOptions {
  buildTargetName?: string;
}

export const createNodesV2: CreateNodesV2<MyPluginOptions> = [
  // Glob pattern — Nx calls this plugin for every matching file
  '**/project.json',
  async (configFiles, options, context) => {
    return await createNodesFromFiles(
      (configFile, options, context) =>
        createNodesInternal(configFile, options ?? {}, context),
      configFiles,
      options,
      context
    );
  },
];

async function createNodesInternal(
  configFilePath: string,
  options: MyPluginOptions,
  context: CreateNodesContextV2
) {
  const projectConfiguration = readJsonFile(configFilePath);
  const root = dirname(configFilePath);

  // Return a project node to be merged into the graph
  return {
    projects: {
      [root]: projectConfiguration,
    },
  };
}

Adding inferred targets to existing projects

A common pattern is to check for the presence of a tool’s config file and add a target to the project if found:
// my-plugin/index.ts
import {
  CreateNodesContextV2,
  CreateNodesV2,
  createNodesFromFiles,
} from '@nx/devkit';
import { existsSync } from 'fs';
import { dirname, join } from 'path';

export const createNodesV2: CreateNodesV2<MyPluginOptions> = [
  '**/tsconfig.json',
  async (configFiles, options, context) => {
    return await createNodesFromFiles(
      (configFile, options, context) =>
        createNodesInternal(configFile, options ?? {}, context),
      configFiles,
      options,
      context
    );
  },
];

async function createNodesInternal(
  configFilePath: string,
  options: MyPluginOptions,
  context: CreateNodesContextV2
) {
  const projectRoot = dirname(configFilePath);

  // Only add a target if this is an Nx project
  const isProject =
    existsSync(join(context.workspaceRoot, projectRoot, 'project.json')) ||
    existsSync(join(context.workspaceRoot, projectRoot, 'package.json'));

  if (!isProject) {
    return {};
  }

  return {
    projects: {
      [projectRoot]: {
        targets: {
          [options.buildTargetName ?? 'build']: {
            command: `tsc -p ${configFilePath}`,
            cache: true,
            inputs: ['{projectRoot}/**/*.ts', '{projectRoot}/tsconfig.json'],
            outputs: ['{projectRoot}/dist'],
          },
        },
      },
    },
  };
}

How project configurations are merged

When multiple plugins identify the same project (same root directory), Nx merges their configurations:
PropertyMerge strategy
name, sourceRoot, projectTypeOverwritten by the later plugin
tagsMerged and deduplicated
implicitDependenciesMerged (later plugin’s entries appended)
targetsMerged; compatible targets have their options shallowly merged
generatorsMerged
namedInputsLater plugin’s value overwrites earlier
Nx’s built-in plugins (which read project.json and package.json) run after plugins listed in nx.json. A project.json file will overwrite any conflicting configuration added by your plugin.

Passing options to a plugin

Options defined in nx.json are forwarded as the options parameter:
// nx.json
{
  "plugins": [
    {
      "plugin": "my-plugin",
      "options": {
        "tagName": "team:platform"
      }
    }
  ]
}
// my-plugin/index.ts
type MyPluginOptions = { tagName?: string };

export const createNodesV2: CreateNodesV2<MyPluginOptions> = [
  '**/tsconfig.json',
  async (configFiles, options, context) => {
    return await createNodesFromFiles(
      (configFile, options, context) => {
        const root = dirname(configFile);
        return {
          projects: {
            [root]: {
              tags: options?.tagName ? [options.tagName] : [],
            },
          },
        };
      },
      configFiles,
      options,
      context
    );
  },
];

Adding dependencies with createDependencies

createDependencies lets a plugin add dependency edges to the project graph. Export it from the same index.ts as createNodesV2:
export type CreateDependencies<T> = (
  opts: T,
  context: CreateDependenciesContext
) => CandidateDependency[] | Promise<CandidateDependency[]>;
The CreateDependenciesContext provides:
  • projectsConfigurations — every project in the workspace.
  • externalNodes — external npm packages in the graph.
  • fileMap — all files in the workspace, indexed by project.
  • filesToProcess — only files changed since the last invocation (use this for incremental analysis).
  • nxJsonConfiguration — the contents of nx.json.

Dependency types

A static dependency is associated with a specific source file. Nx tracks which file defines the dependency and skips re-analysis when the file hasn’t changed.
import { DependencyType } from '@nx/devkit';

{
  source: 'my-app',
  target: 'my-lib',
  sourceFile: 'apps/my-app/src/main.ts',
  dependencyType: DependencyType.static,
}

Full createDependencies example

This plugin reads each project’s package.json and creates static dependencies for any dependencies entries that resolve to other Nx projects:
// my-plugin/index.ts
import {
  CreateDependencies,
  DependencyType,
  validateDependency,
} from '@nx/devkit';
import { existsSync } from 'fs';
import { join } from 'path';

export const createDependencies: CreateDependencies = (opts, ctx) => {
  // Build a map from package.json name → Nx project name
  const packageJsonProjectMap = new Map<string, string>();
  const nxProjects = Object.values(ctx.projectsConfigurations.projects);

  for (const project of nxProjects) {
    const pkgJsonPath = join(ctx.workspaceRoot, project.root, 'package.json');
    if (existsSync(pkgJsonPath)) {
      const json = JSON.parse(require('fs').readFileSync(pkgJsonPath, 'utf8'));
      if (json.name) {
        packageJsonProjectMap.set(json.name, project.name);
      }
    }
  }

  const results = [];

  for (const project of nxProjects) {
    const pkgJsonPath = join(ctx.workspaceRoot, project.root, 'package.json');
    if (!existsSync(pkgJsonPath)) continue;

    const json = JSON.parse(require('fs').readFileSync(pkgJsonPath, 'utf8'));
    const deps = Object.keys(json.dependencies ?? {});

    for (const dep of deps) {
      if (!packageJsonProjectMap.has(dep)) continue;

      const newDependency = {
        source: project.name,
        target: packageJsonProjectMap.get(dep),
        sourceFile: join(project.root, 'package.json'),
        dependencyType: DependencyType.static,
      };

      // Throws if source or target project does not exist in the graph
      validateDependency(newDependency, ctx);
      results.push(newDependency);
    }
  }

  return results;
};

Visualizing the project graph

After modifying your plugin, inspect the resulting graph:
nx graph
During development, disable the project graph cache so changes to your plugin are reflected immediately:
NX_CACHE_PROJECT_GRAPH=false nx graph

Testing project graph plugins

The fastest way to verify the computed graph is nx show project:
nx show project my-app --json
In an e2e test, assert on the computed configuration:
import { execSync } from 'child_process';

it('should infer the build target from my-tool.config.js', () => {
  const projectDetails = JSON.parse(
    execSync('nx show project my-app --json', {
      cwd: projectDirectory,
    }).toString()
  );

  expect(projectDetails.targets.build).toMatchObject({
    cache: true,
    executor: 'nx:run-commands',
    inputs: expect.arrayContaining(['{projectRoot}/my-tool.config.js']),
    outputs: ['{projectRoot}/dist'],
  });
});
Set NX_CACHE_PROJECT_GRAPH=false in your e2e test environment to ensure the graph is recomputed from scratch during each test run.

Key project graph APIs

APIDescription
CreateNodesV2Tuple type for the [glob, fn] export that adds project nodes
createNodesFromFilesHelper that processes each matched file individually and handles batching
CreateNodesContextV2Context passed to each createNodesV2 call; includes workspaceRoot
CreateDependenciesFunction type for the export that adds dependency edges
CreateDependenciesContextContext passed to createDependencies; includes projects, files, and filesToProcess
DependencyTypeEnum with values static, dynamic, and implicit
validateDependencyThrows if a candidate dependency references a project that doesn’t exist

Build docs developers (and LLMs) love