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:
| 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 |
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,
}
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.{
source: 'my-app',
target: 'my-feature',
sourceFile: 'apps/my-app/src/routes.ts',
dependencyType: DependencyType.dynamic,
}
An implicit dependency has no associated source file. Nx recomputes it on every invocation.{
source: 'my-app',
target: 'shared-config',
dependencyType: DependencyType.implicit,
}
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:
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
| 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 |