> ## 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.

# Project Graph Plugins

> Extend the Nx project graph by adding custom project nodes and dependencies using the createNodesV2 and createDependencies plugin APIs.

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.

<Note>
  Set `NX_DAEMON=false` during plugin development. The Nx daemon caches plugin code, so changes won't be reflected until the daemon restarts.
</Note>

## Registering a plugin

Add the plugin to the `plugins` array in `nx.json`:

```json theme={null}
// nx.json
{
  "plugins": [
    {
      "plugin": "my-plugin",
      "options": {
        "buildTargetName": "build"
      }
    }
  ]
}
```

For a local (unpublished) plugin, reference its entry point by path:

```json theme={null}
{
  "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:

```typescript theme={null}
// 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:

```typescript theme={null}
// 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:

| Property                            | Merge strategy                                                   |
| ----------------------------------- | ---------------------------------------------------------------- |
| `name`, `sourceRoot`, `projectType` | Overwritten by the later plugin                                  |
| `tags`                              | Merged and deduplicated                                          |
| `implicitDependencies`              | Merged (later plugin's entries appended)                         |
| `targets`                           | Merged; compatible targets have their `options` shallowly merged |
| `generators`                        | Merged                                                           |
| `namedInputs`                       | Later plugin's value overwrites earlier                          |

<Note>
  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.
</Note>

## Passing options to a plugin

Options defined in `nx.json` are forwarded as the `options` parameter:

```json theme={null}
// nx.json
{
  "plugins": [
    {
      "plugin": "my-plugin",
      "options": {
        "tagName": "team:platform"
      }
    }
  ]
}
```

```typescript theme={null}
// 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`:

```typescript theme={null}
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

<Tabs>
  <Tab title="Static">
    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.

    ```typescript theme={null}
    import { DependencyType } from '@nx/devkit';

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

  <Tab title="Dynamic">
    Dynamic dependencies are loaded conditionally at runtime (e.g. lazy-loaded routes). Tracking them separately lets Nx detect when a static import accidentally breaks lazy loading.

    ```typescript theme={null}
    {
      source: 'my-app',
      target: 'my-feature',
      sourceFile: 'apps/my-app/src/routes.ts',
      dependencyType: DependencyType.dynamic,
    }
    ```
  </Tab>

  <Tab title="Implicit">
    An implicit dependency has no associated source file. Nx recomputes it on every invocation.

    ```typescript theme={null}
    {
      source: 'my-app',
      target: 'shared-config',
      dependencyType: DependencyType.implicit,
    }
    ```
  </Tab>
</Tabs>

### 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:

```typescript theme={null}
// 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:

```bash theme={null}
nx graph
```

During development, disable the project graph cache so changes to your plugin are reflected immediately:

```bash theme={null}
NX_CACHE_PROJECT_GRAPH=false nx graph
```

## Testing project graph plugins

The fastest way to verify the computed graph is `nx show project`:

```bash theme={null}
nx show project my-app --json
```

In an e2e test, assert on the computed configuration:

```typescript theme={null}
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'],
  });
});
```

<Tip>
  Set `NX_CACHE_PROJECT_GRAPH=false` in your e2e test environment to ensure the graph is recomputed from scratch during each test run.
</Tip>

## Key project graph APIs

| API                         | Description                                                                            |
| --------------------------- | -------------------------------------------------------------------------------------- |
| `CreateNodesV2`             | Tuple type for the `[glob, fn]` export that adds project nodes                         |
| `createNodesFromFiles`      | Helper that processes each matched file individually and handles batching              |
| `CreateNodesContextV2`      | Context passed to each `createNodesV2` call; includes `workspaceRoot`                  |
| `CreateDependencies`        | Function type for the export that adds dependency edges                                |
| `CreateDependenciesContext` | Context passed to `createDependencies`; includes projects, files, and `filesToProcess` |
| `DependencyType`            | Enum with values `static`, `dynamic`, and `implicit`                                   |
| `validateDependency`        | Throws if a candidate dependency references a project that doesn't exist               |
