๐Ÿ˜ตโ€ Why is this untouched project affected?

This is a question I hear every day! A question that has led me many times into a debugging session of the Nx Affected process to find an answer.

In this article, I aim to provide you with all the necessary insights to understand how the affected process of Nx works, helping you answer that very question.

๐Ÿค“ Affected Reminder

When working on a large code base in a Monorepo, youโ€™ll have a repository that contains multiple Applications and Libraries.

As your Monorepo grows, rebuilding all apps/libs can become slow on the CI. The ability to re-execute only the impacted apps/libs will drastically reduce your Software Development Cycle time.

To be able to compute the affected projects, Nx will generate an Affected Graph with all affected projects and their dependencies. More info in this clip made by Zack

Affected Projects

Any change applied to app/lib will cascade affected flags on all other apps/libs that depend on it:

To understand the dependencies between apps/libs, Nx generates a Project Graph with all nodes (apps/libs), the external nodes (npm), and all dependencies between them.

Affected Tasks

Considering the impact of a modification on the entire app/lib is not enough. For instance, if you change a test within an app, it does not mean you should re-build that app entirely. Only the test should be re-run:

To understand the task dependencies between apps/libs, Nx generates a Task Graph with nodes representing an association of the app/lib by tasks.

๐Ÿคฉ Affected Commands

Nx provides multiple ways to identify which projects/tasks are affected.

Affected Run

The main command used typically on the CI is the Nx affected command:

nx affected -t lint test build

With this command, youโ€™ll be able to run only the list of tasks that are affected.

Show Command

Another useful command to get a direct overview is the Nx show command:

nx show projects --affected

This command allows you to see the affected projects/tasks directly in the console and also export the result to a JSON file, for example.

Nx Graph

If you need a UI visualization and a way to track the path of the affected projects/tasks, you can open the Nx graph by using the command:

nx graph --affected

It will open a web page where you can see a graph similar to this:

๐Ÿ˜ถโ€๐ŸŒซ๏ธ Affected Rules

The Nx affected process goes through several steps and considers various files and configurations to decide which project can be affected:

Step 1 - Find Touched Files

Before computing the list of affected projects, Nx loads the list of modified/touched files:

Nx computes that list by taking the modified files since the targeted Affected Base.

By default, the base will be your main branch, but you can modify it using the -base and -head options.

All modified files not yetcommitted or tracked will also be added. You can change the behavior using the -uncommitted or -untracked options.

If you don't want Nx to compute the list of files, you can provide your list using the -files option.

Files matching a pattern in .gitignore or .nxignore will be ignored.

Step 2.1 - Find Affected Nodes from Paths

When all touched files are defined, Nx checks how they can affect the projects:

The most common rule is to check if the file path matches the project root path.

Step 2.2 - Find Affected Nodes from Tasks

Nx will also implicitly affect some projects if one of their tasks is impacted by a touched file:

For example, if you modify a global Jest configuration file like jest.preset.ts, because it is specified in the inputs related to that target, you will also affect all projects using that executor:

"targetDefaults": {
  "@nx/jest:jest": {
    "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.ts"],
  },
},

Each time a project contains a target with an input affected by a touched file, it will be impacted.

For more detailed information about how Nx handles target caching through InputsNamed Inputs and Outputs, refer to the Nx documentation

Step 2.3 - Find Affected Nodes from Plugins

Since the Nx Project Crystal and the generalization of Nx Plugins with inferred configurations, Nx also checks if a plugin pattern is affected by the list of touched files.

For example, if you delete or move a file, Nx assumes the project is deleted and marks all projects as affected.

Step 2.4 - Find Affected Nodes from Npm Dependencies

If the package.json is modified, Nx uses a smart approach to understand what exactly changed.

If you modify an npm library, Nx finds all projects using that library and marks them affected. If it is a @types/*, Nx extracts the related library and applies the same principle as if you were modifying the library.

If you modify a library used in the nx.json plugins or delete a library, all projects will be considered affected:

By default, modifying the package manager lock file affects all projects:

This behavior can be modified using the projectsAffectedByDependencyUpdates in your nx.json:

"pluginsConfig": {
    "@nx/js": { 
        "projectsAffectedByDependencyUpdates": "auto"
    }
}

Options:

  • all: Affect all projects
  • auto: Affect only projects related to the modified dependencies
  • string[]: Define a list of projects

Step 2.5 - Find Affected Nodes From Typescript Configuration

Modifying the global TypeScript configuration can also impact the list of affected nodes:

If a path is modified, Nx affects the related project that matches the root path. However, modifying a global config or deleting a path affects all projects.

Step 2.6 - Find Affected Nodes From Global Files

By default, modifying nx.json affects all projects.

Step 3 - Generate Affected Graph

After identifying all affected nodes, Nx generates an affected graph to determine which nodesexternalNodes and dependencies are finally affected.

Nx takes the affected nodes and recursively searches for all dependencies in the Project Graph:

For example, if the affected node lib10 is used by lib4, which is used by app1, all of these nodes will be added to the affected project graph.

Nx applies the same principles to the externalNodes:

For example, if the affected npm library enquirer which is used by the npm library nx which is also used by the internal library tools.

To be sure the Affected Graph is complete, Nx will also add the related dependencies.

๐Ÿง Affected Investigation

If you still donโ€™t understand why some projects are affected on a branch, you can always debug the Nx affected process.

Nx Graph Visualizer

If you open the Nx graph with the affected command, youโ€™ll be able to see all affected projects as specified in the ๐Ÿค“ Affected Reminder section.

You can then explore your workspace and use multiple features like the Project Focus or the Dependency Tracker.

Debugging

However, on big repositories, the graph is often hard to use for debugging. I prefer to debug the Nx-affected process to see exactly which step is responsible.

You can start by putting a breakpoint in packages/nx/src/command-line/affected/affected.ts and run nx show project --affected in debug mode.

๐Ÿค• Affected Fixes

Customizing the affected process is not straightforward. If you think too many projects are affected with each modification, here are some recommendations:

1. Good Splitting of Apps/Libs

Ensure the split of your apps/libs is correctly done.

Often, shared libraries are used for many projects with utilities needed by only one project. In such cases, touching that library affects all projects.

2. Strict Named Inputs

Ensure your Named Inputs are correctly configured. Named inputs define if modifying a file can impact the output of a target.

For example, modifying a spec file can impact the test but not the build. If you use the default named input, touching one file affects all targets of your project.

3. Affected Customization

Currently, there is limited customization for the affected process. You can customize it when updating a dependency by using the configuration projectsAffectedByDependencyUpdates (see Step 2.4โ€Šโ€”โ€ŠFind Affected Nodes from Npm Dependencies).

4. Patch Nx

โš ๏ธ Only use this as a last resort!

This is a hacky solution, but I use it to customize the affected process. Patching your Nx library using your package managerโ€™s patch system allows you to change the rules.

For example, if fixes take time to implement, you can disable the โ€œAffected Allโ€ use cases with:

diff --git a/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js b/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js
index 72e78e7..7793bea 100644
--- a/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js
+++ b/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js
@@ -20,7 +20,8 @@ const getTouchedNpmPackages = (touchedFiles, _, nxJson, packageJson, projectGrap
             c.path.length === 2) {
             // A package was deleted so mark all workspace projects as touched.
             if (c.type === json_diff_1.JsonDiffType.Deleted) {
-                touched = Object.keys(projectGraph.nodes);
+                // PATCH TO NOT AFFECTED ALL WHEN PACKAGE IS DELETED
+                // touched = Object.keys(projectGraph.nodes);
                 break;
             }
             else {
diff --git a/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js b/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js
index bac7008..37ae136 100644
--- a/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js
+++ b/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js
@@ -24,7 +24,8 @@ const getTouchedProjectsFromTsConfig = (touchedFiles, _a, _b, _c, graph) => {
         }
         // If a path is deleted, everything is touched
         if (change.type === json_diff_1.JsonDiffType.Deleted) {
-            return Object.keys(graph.nodes);
+            // PATCH TO NOT AFFECTED ALL WHEN PATH IS DELETED
+            // return Object.keys(graph.nodes);
         }
         touched.push(...getProjectsAffectedByPaths(change, Object.values(graph.nodes)));
     }
diff --git a/node_modules/nx/src/project-graph/affected/affected-project-graph.js b/node_modules/nx/src/project-graph/affected/affected-project-graph.js
index 5665c8d..d5a69aa 100644
--- a/node_modules/nx/src/project-graph/affected/affected-project-graph.js
+++ b/node_modules/nx/src/project-graph/affected/affected-project-graph.js
@@ -12,7 +12,8 @@ async function filterAffected(graph, touchedFiles, nxJson = (0, configuration_1.
     const touchedProjectLocators = [
         workspace_projects_1.getTouchedProjects,
         workspace_projects_1.getImplicitlyTouchedProjects,
-        project_glob_changes_1.getTouchedProjectsFromProjectGlobChanges,
+        // PATCH TO NOT AFFECTED ALL WHEN PLUGIN PATTERN MATCHING CHANGED FILE
+        // project_glob_changes_1.getTouchedProjectsFromProjectGlobChanges,
         touched_projects_1.getTouchedProjects,
     ];
     const touchedProjects = [];
diff --git a/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js b/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js
index c5aec64..edaa989 100644
--- a/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js
+++ b/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js
@@ -16,7 +16,8 @@ const getTouchedProjects = (touchedFiles, projectGraphNodes) => {
 exports.getTouchedProjects = getTouchedProjects;
 const getImplicitlyTouchedProjects = (fileChanges, projectGraphNodes, nxJson) => {
     const implicits = {
-        'nx.json': '*',
+        // PATCH TO NOT AFFECTED ALL WHEN nx.json CHANGED
+        // 'nx.json': '*',
     };
     Object.values(projectGraphNodes || {}).forEach((node) => {
         const namedInputs = {

๐Ÿ™‚ Last Thoughts

As you can see, the Nx affected process not only considers the list of modified files but also computes the list of projects based on various other factors.

This makes the investigation not always straightforward and can often lead to an affected-all situation.

I hope I clarified some parts and provided you with the keys for a better understanding of the affected process.

In the future, we should have more customization options for the affected process by generalizing the list of options like projectsAffectedByDependencyUpdates.

Thanks for your attention and Stay Tuned ๐Ÿš€


Tagged in:

nx

Last Update: August 21, 2024