Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions astro-docs/src/content/docs/features/CI Features/affected.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,30 @@ By default, Nx will mark all projects as affected whenever your package manager'

The flag `projectsAffectedByDependencyUpdates` can be set to `auto`, `all`, or an array that contains project specifiers. The default value is `all`.

## Reduce affected fan-out for JavaScript and TypeScript projects

If your workspace uses `@nx/js` source analysis, you can also enable dependency narrowing to reduce affected fan-out from source-level imports.

```json
// nx.json
{
"pluginsConfig": {
"@nx/js": {
"dependencyNarrowing": {
"affectedNarrowing": true
}
}
}
}
```

This is different from `projectsAffectedByDependencyUpdates`.

- `projectsAffectedByDependencyUpdates` controls what happens when the lock file changes.
- `dependencyNarrowing.affectedNarrowing` makes `nx affected` more precise for JavaScript and TypeScript source changes.

See [Narrow project graph dependencies](/docs/technologies/typescript/guides/dependency-narrowing) for the full workflow.

## Not using git

If you aren't using Git, you can pass `--files` to any affected command to indicate what files have been changed.
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ If you want to disable detecting dependencies from source code and want to only
## Default

The default setting for Nx repos is `"analyzeSourceFiles": true`. The assumption is that if there is a real link in the code between projects, you want to know about it. For Lerna repos, the default value is `false` in order to maintain backward compatibility with the way Lerna has always calculated dependencies.

If you still want Nx to analyze source files but you want the project graph to keep fewer edges, enable [`@nx/js` dependency narrowing](/docs/technologies/typescript/guides/dependency-narrowing) instead of turning source analysis off entirely.
23 changes: 23 additions & 0 deletions astro-docs/src/content/docs/reference/nx-json.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,29 @@ This will exclude all e2e projects except `toolkit-workspace-e2e`.
- The last matching pattern determines if a file is included
- If the first pattern is a negation, all files are matched initially

### Plugin-specific configuration with `pluginsConfig`

Use `pluginsConfig` for plugin-specific settings that Nx reads from `nx.json` outside of the `plugins` array.

Keys are plugin package names such as `@nx/js`.

For example:

```json
// nx.json
{
"pluginsConfig": {
"@nx/js": {
"dependencyNarrowing": {
"affectedNarrowing": true
}
}
}
}
```

For `@nx/js` dependency narrowing behavior and options, see [Narrow project graph dependencies](/docs/technologies/typescript/guides/dependency-narrowing).

## Task options

The following properties affect the way Nx runs tasks and can be set at the root of `nx.json`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: Narrow project graph dependencies
description: Use @nx/js dependency narrowing to remove safe project graph edges and reduce the number of projects marked as affected by a change.
sidebar:
label: Narrow project graph dependencies
filter: 'type:Guides'
---

If you use `@nx/js` to analyze source imports, you can ask Nx to remove project graph edges that are not needed at runtime.

This is useful when a project imports a symbol from another project but never uses that symbol in emitted code. In that case, keeping the edge makes the project graph denser than the runtime dependency graph, which can also make `nx affected` less precise.

## Enable dependency narrowing

Add `dependencyNarrowing` under `pluginsConfig["@nx/js"]` in `nx.json`:

```json
// nx.json
{
"pluginsConfig": {
"@nx/js": {
"dependencyNarrowing": {}
}
}
}
```

An empty object enables the feature with the default settings.

If you want to be explicit about the main behavior switches, start with this shape:

```json
// nx.json
{
"pluginsConfig": {
"@nx/js": {
"dependencyNarrowing": {
"respectSideEffects": true,
"removeTypeOnlyEdges": true,
"fallbackToStaticGraph": true,
"affectedNarrowing": true
}
}
}
}
```

## What changes when you enable it

Dependency narrowing changes the computed project graph for JavaScript and TypeScript imports discovered by `@nx/js` source analysis.

- `nx graph` shows the narrowed graph because it renders the computed project graph.
- `nx affected` can mark fewer downstream projects as affected when a change only touches exports that consumers do not use.
- Task pipelines that depend on the project graph use the narrowed edges too.

That is the intended behavior. Nx is not hiding edges in the UI. It is building a more precise graph.

## When Nx keeps an edge

Nx only removes an edge when it can do so conservatively.

It keeps edges for cases such as:

- side-effect imports
- dynamic imports
- namespace imports unless you opt into resolving accessed properties
- re-export cases that still matter to downstream consumers
- targets that may have side effects when `respectSideEffects` is enabled

If Nx cannot prove that removing an edge is safe, it keeps the edge.

## Common options

These are the settings you are most likely to tune first:

| Property | Default | What it changes |
| ----------------------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
| `concurrency` | `50` | Number of files Nx analyzes in parallel. |
| `respectSideEffects` | `true` | Keeps edges for projects that may have side effects. |
| `removeTypeOnlyEdges` | `true` | Allows type-only imports to be removed from the project graph. |
| `treatMissingPackageJsonAsSideEffectFree` | `false` | Treats projects without a `package.json` as side-effect-free when side effects are being checked. |
| `resolveNamespaceImports` | `false` | Tracks accessed properties on namespace imports instead of keeping the whole edge by default. |
| `fallbackToStaticGraph` | `true` | Falls back to the normal static graph when aggressive narrowing cannot use bundler signals. |
| `affectedNarrowing` | `true` | Reduces affected fan-out when a change only touches exports that consumers do not use. |

The `dependencyNarrowing` object also includes advanced fields such as `mode`, `bundlerAdapters`, and `debug`. Leave those at their defaults unless you are testing a specific narrowing workflow.

## Compare it with disabling source analysis

Dependency narrowing is different from disabling source analysis.

- If you set `analyzeSourceFiles` to `false`, Nx stops creating source-based edges from JavaScript and TypeScript imports.
- If you enable dependency narrowing, Nx still analyzes source imports but removes only the edges it can prove are unnecessary.

Use dependency narrowing when you still want the project graph to reflect real source relationships, but you want that graph to be closer to runtime behavior.

## Use it with affected

Dependency narrowing is especially useful on CI because it can reduce the number of downstream projects that `nx affected` needs to run.

See [Run Only Tasks Affected by a PR](/docs/features/ci-features/affected) for the `affected` workflow, and see [nx.json Reference](/docs/reference/nx-json#plugin-specific-configuration-with-pluginsconfig) for the configuration shape.
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ nx affected -t build test typecheck lint
This uses the [project graph](/docs/features/explore-graph) to determine which projects are affected by your changes and only runs tasks for those.
Read more about [the benefits of `nx affected`](/docs/features/ci-features/affected).

If you want Nx to keep the project graph closer to runtime behavior, enable [dependency narrowing](/docs/technologies/typescript/guides/dependency-narrowing). It can remove safe edges from the `@nx/js` project graph and reduce affected fan-out for JavaScript and TypeScript projects.

### Remote caching

Share build and typecheck cache results across your team and CI with [remote caching](/docs/features/ci-features/remote-cache):
Expand All @@ -341,6 +343,7 @@ For large monorepos with many TypeScript projects, [TSC batch mode](/docs/techno
{% linkcard title="Learn Nx Tutorial" description="Build a TypeScript monorepo step-by-step with Nx." href="/docs/getting-started/tutorials/crafting-your-workspace" /%}
{% linkcard title="Switch to Workspaces & Project References" description="Migrate from path aliases to the modern monorepo setup." href="/docs/technologies/typescript/guides/switch-to-workspaces-project-references" /%}
{% linkcard title="Compile to Multiple Formats" description="Build libraries to both ESM and CommonJS with Rollup." href="/docs/technologies/typescript/guides/compile-multiple-formats" /%}
{% linkcard title="Narrow project graph dependencies" description="Remove safe `@nx/js` project graph edges and reduce affected fan-out." href="/docs/technologies/typescript/guides/dependency-narrowing" /%}
{% linkcard title="Generators Reference" description="Full API reference for @nx/js generators." href="/docs/technologies/typescript/generators" /%}
{% linkcard title="Executors Reference" description="Full API reference for @nx/js executors." href="/docs/technologies/typescript/executors" /%}
{% linkcard title="Migrations Reference" description="Full reference for @nx/js migrations." href="/docs/technologies/typescript/migrations" /%}
Expand Down
67 changes: 67 additions & 0 deletions e2e/nx/src/affected-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
newProject,
readFile,
readJson,
updateJson,
cleanupProject,
runCLI,
runCLIAsync,
Expand Down Expand Up @@ -606,6 +607,72 @@ describe('show projects --affected', () => {
});
}, 120000);

it('should reduce affected fan-out for imports not used in emitted code when dependency narrowing is enabled', async () => {
const unusedConsumer = uniq('unused-consumer');
const usedConsumer = uniq('used-consumer');
const sharedLib = uniq('shared-lib');

updateJson('nx.json', (json) => ({
...json,
pluginsConfig: {
...json.pluginsConfig,
'@nx/js': {
...json.pluginsConfig?.['@nx/js'],
dependencyNarrowing: {
...json.pluginsConfig?.['@nx/js']?.dependencyNarrowing,
affectedNarrowing: true,
respectSideEffects: false,
},
},
},
}));
runCLI('reset');

runCLI(
`generate @nx/web:app ${unusedConsumer} --directory=apps/${unusedConsumer} --unitTestRunner=vitest`
);
runCLI(
`generate @nx/web:app ${usedConsumer} --directory=apps/${usedConsumer} --unitTestRunner=vitest`
);
runCLI(`generate @nx/js:lib ${sharedLib} --directory=libs/${sharedLib}`);

updateFile(
`libs/${sharedLib}/src/index.ts`,
`export const unusedValue = 'unused';\nexport const usedValue = 'used';\n`
);

updateFile(
`apps/${unusedConsumer}/src/app/app.element.spec.ts`,
`import { unusedValue } from '@${proj}/${sharedLib}';\n\n` +
`describe('unused import consumer', () => {\n` +
` it('uses the import only in a type position', () => {\n` +
` type ImportedValue = typeof unusedValue;\n` +
` const value: ImportedValue = 'unused';\n` +
` expect(value).toEqual('unused');\n` +
` });\n` +
`});\n`
);

updateFile(
`apps/${usedConsumer}/src/app/app.element.spec.ts`,
`import { usedValue } from '@${proj}/${sharedLib}';\n\n` +
`describe('used import consumer', () => {\n` +
` it('uses the imported value', () => {\n` +
` expect(usedValue).toEqual('used');\n` +
` });\n` +
`});\n`
);

const { stdout } = await runCLIAsync(
`show projects --affected --files=libs/${sharedLib}/src/index.ts --exclude=${unusedConsumer}-e2e,${usedConsumer}-e2e`
);

const affectedProjects = stdout.split('\n').filter(Boolean);
expect(affectedProjects).toContain(sharedLib);
expect(affectedProjects).toContain(usedConsumer);
expect(affectedProjects).not.toContain(unusedConsumer);
}, 120000);

function compareTwoArrays(a: string[], b: string[]) {
expect(a.sort((x, y) => x.localeCompare(y))).toEqual(
b.sort((x, y) => x.localeCompare(y))
Expand Down
39 changes: 39 additions & 0 deletions packages/node/src/generators/init/init.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
addDependenciesToPackageJson,
readJson,
readNxJson,
Tree,
updateJson,
} from '@nx/devkit';
Expand Down Expand Up @@ -33,10 +34,21 @@ describe('init', () => {
await initGenerator(tree, {});

const packageJson = readJson(tree, 'package.json');
const nxJson = readNxJson(tree);

expect(packageJson.dependencies['@nx/node']).toBeUndefined();
expect(packageJson.dependencies[existing]).toBeDefined();
expect(packageJson.devDependencies['@nx/node']).toBeDefined();
expect(packageJson.devDependencies[existing]).toBeDefined();
expect(nxJson.plugins).toBeUndefined();
expect(nxJson.pluginsConfig?.['@nx/js']).toMatchObject({
dependencyNarrowing: {
respectSideEffects: true,
removeTypeOnlyEdges: true,
fallbackToStaticGraph: true,
affectedNarrowing: true,
},
});
});

it('should not fail when dependencies is missing from package.json and no other init generators are invoked', async () => {
Expand All @@ -47,4 +59,31 @@ describe('init', () => {

await expect(initGenerator(tree, {})).resolves.toBeTruthy();
});

it('should preserve existing @nx/js plugin config when configuring dependency narrowing', async () => {
updateJson(tree, 'nx.json', (json) => {
json.pluginsConfig = {
'@nx/js': {
analyzeLockfile: true,
dependencyNarrowing: {
debug: true,
},
},
};
return json;
});

await initGenerator(tree, {});

expect(readNxJson(tree).pluginsConfig?.['@nx/js']).toMatchObject({
analyzeLockfile: true,
dependencyNarrowing: {
debug: true,
respectSideEffects: true,
removeTypeOnlyEdges: true,
fallbackToStaticGraph: true,
affectedNarrowing: true,
},
});
});
});
27 changes: 27 additions & 0 deletions packages/node/src/generators/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
readNxJson,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { nxVersion } from '../../utils/versions';
import { Schema } from './schema';
Expand All @@ -25,12 +27,37 @@ function updateDependencies(tree: Tree, options: Schema) {
return runTasksInSerial(...tasks);
}

function addProjectGraphPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.pluginsConfig ??= {};
const jsPluginConfig =
(nxJson.pluginsConfig['@nx/js'] as Record<string, unknown> | undefined) ??
{};

nxJson.pluginsConfig['@nx/js'] = {
...jsPluginConfig,
dependencyNarrowing: {
respectSideEffects: true,
removeTypeOnlyEdges: true,
fallbackToStaticGraph: true,
affectedNarrowing: true,
...(jsPluginConfig.dependencyNarrowing as
| Record<string, unknown>
| undefined),
},
};

updateNxJson(tree, nxJson);
}

export async function initGenerator(tree: Tree, options: Schema) {
let installTask: GeneratorCallback = () => {};
if (!options.skipPackageJson) {
installTask = updateDependencies(tree, options);
}

addProjectGraphPlugin(tree);

if (!options.skipFormat) {
await formatFiles(tree);
}
Expand Down
Loading