Skip to content

Comments

fix(core): stabilizes project references in dependsOn and inputs when later plugins rename a project#34332

Open
AgentEnder wants to merge 6 commits intomasterfrom
fix/stable-project-refs-in-deps-and-inputs
Open

fix(core): stabilizes project references in dependsOn and inputs when later plugins rename a project#34332
AgentEnder wants to merge 6 commits intomasterfrom
fix/stable-project-refs-in-deps-and-inputs

Conversation

@AgentEnder
Copy link
Member

Current Behavior

There's a bug currently where is a plugin returns a dependsOn dependency or input that directly references another project by name, and a later plugin renames that project, the dependsOn or input entry is left stale and pointing at a now non-existent project.

Expected Behavior

The old refs are kept up to date as the nodes get merged together

AI Summary

This pull request introduces a new mechanism to handle project name substitutions in the Nx project graph, ensuring that references to project names in inputs and dependsOn blocks remain accurate even if a plugin changes a project's name during graph construction. The main addition is the ProjectNameInNodePropsManager, which tracks and updates references when project names change. Several related refactorings and improvements were made to integrate this manager into the project configuration merging process.

Project name substitution and consistency:

  • Added a new ProjectNameInNodePropsManager class to manage and apply project name substitutions when project names change, ensuring that all references in inputs and dependsOn blocks remain consistent.
  • Integrated the ProjectNameInNodePropsManager into the mergeCreateNodesResults function, registering substitutors for node results, marking roots as dirty when names change, and applying substitutions after merging. [1] [2] [3]

API and function changes:

  • Modified mergeProjectConfigurationIntoRootMap to return an object indicating whether a project name was changed, instead of just returning void. [1] [2]

Code organization and import cleanup:

  • Refactored imports in project-configuration-utils.ts for better organization and to accommodate the new manager. [1] [2]

@vercel
Copy link

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nx-dev Ready Ready Preview Feb 4, 2026 8:52pm

Request Review

@nx-cloud
Copy link
Contributor

nx-cloud bot commented Feb 4, 2026

View your CI Pipeline Execution ↗ for commit 1f167a9

Command Status Duration Result
nx affected --targets=lint,test,test-kt,build,e... ❌ Failed 48m 3s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 2m 16s View ↗
nx-cloud record -- nx-cloud conformance:check ✅ Succeeded 7s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 2s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-23 06:51:03 UTC

@netlify
Copy link

netlify bot commented Feb 4, 2026

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 1f167a9
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/699bec8fb4607c000828f2e0
😎 Deploy Preview https://deploy-preview-34332--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Feb 4, 2026

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 1f167a9
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/699bec8fc1c89d00081eb967
😎 Deploy Preview https://deploy-preview-34332--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

nx-cloud[bot]

This comment was marked as outdated.

@AgentEnder AgentEnder force-pushed the fix/stable-project-refs-in-deps-and-inputs branch from ba435cc to 615e53a Compare February 11, 2026 19:39
@AgentEnder AgentEnder force-pushed the fix/stable-project-refs-in-deps-and-inputs branch from 615e53a to 71a0422 Compare February 13, 2026 01:43
@AgentEnder AgentEnder marked this pull request as ready for review February 13, 2026 20:49
@AgentEnder AgentEnder requested a review from a team as a code owner February 13, 2026 20:49
Comment on lines 51 to 64
registerSubstitutorsForNodeResults(
pluginResultProjects?: Record<
string,
Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>
>,
mergedConfigurations?: Record<string, ProjectConfiguration>
) {
const nameMap = new AggregateMap(
rootMapToNameMap(pluginResultProjects),
new LazyMap(() => rootMapToNameMap(mergedConfigurations))
);

for (const root in pluginResultProjects) {
const project = pluginResultProjects[root];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null/undefined check for pluginResultProjects parameter before iteration. The test on line 88-91 of the spec file shows this method should handle undefined inputs gracefully, but the code will throw a runtime error when trying to iterate over undefined.

registerSubstitutorsForNodeResults(
  pluginResultProjects?: Record<...>,
  mergedConfigurations?: Record<string, ProjectConfiguration>
) {
  // Add this guard
  if (!pluginResultProjects) {
    return;
  }
  
  const nameMap = new AggregateMap(
    rootMapToNameMap(pluginResultProjects),
    new LazyMap(() => rootMapToNameMap(mergedConfigurations))
  );
  
  for (const root in pluginResultProjects) {
    // ...
  }
}
Suggested change
registerSubstitutorsForNodeResults(
pluginResultProjects?: Record<
string,
Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>
>,
mergedConfigurations?: Record<string, ProjectConfiguration>
) {
const nameMap = new AggregateMap(
rootMapToNameMap(pluginResultProjects),
new LazyMap(() => rootMapToNameMap(mergedConfigurations))
);
for (const root in pluginResultProjects) {
const project = pluginResultProjects[root];
registerSubstitutorsForNodeResults(
pluginResultProjects?: Record<
string,
Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>
>,
mergedConfigurations?: Record<string, ProjectConfiguration>
) {
if (!pluginResultProjects) {
return;
}
const nameMap = new AggregateMap(
rootMapToNameMap(pluginResultProjects),
new LazyMap(() => rootMapToNameMap(mergedConfigurations))
);
for (const root in pluginResultProjects) {
const project = pluginResultProjects[root];

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines 195 to 200
get(key: K): V | null {
if (!this.data) {
this.data = this.builder();
}
return this.data.get(key);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type mismatch between declared return type and actual return value. The method declares return type V | null but this.data.get(key) returns V | undefined. This will cause undefined to be returned when a key is not found, not null as declared.

get(key: K): V | undefined {  // Fix return type
  if (!this.data) {
    this.data = this.builder();
  }
  return this.data.get(key);
}
Suggested change
get(key: K): V | null {
if (!this.data) {
this.data = this.builder();
}
return this.data.get(key);
}
get(key: K): V | undefined {
if (!this.data) {
this.data = this.builder();
}
return this.data.get(key);
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

const project = {
root: node,
...projectNodes[node],
root: root,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
root: root,
root,

private projectNameSubstitutors = new Map<string, ProjectNameSubstitutor[]>();
private dirtyRoots = new Set<string>();

constructor() {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
constructor() {}

string,
Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>
>,
mergedConfigurations?: Record<string, ProjectConfiguration>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mergedConfigurations?: Record<string, ProjectConfiguration>
mergedRootMap?: Record<string, ProjectConfiguration>


if (project.targets) {
for (const target in project.targets) {
const config = project.targets[target];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const config = project.targets[target];
const targetConfig = project.targets[target];

if (config.inputs) {
for (const input of config.inputs) {
if (typeof input === 'object' && 'projects' in input) {
const projects = input['projects'];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const projects = input['projects'];
const inputProjectNames = input.projects;

// names.
for (let i = 0; i < dep.projects.length; i++) {
const projectName = dep.projects[i];
if (!isGlobPattern(projectName)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!isGlobPattern(projectName)) {

if (project.targets) {
for (const target in project.targets) {
const config = project.targets[target];
if (config.inputs) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract this into registerSubstitutorsForInputs

}
}

if (config.dependsOn) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract this into registerSubstitutorsForDependsOn

}

markDirty(root: string) {
// It can't be dirty yet if there's no substitutors pointing at it
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels incorrect? What is the downside of saying it's always dirty?

if (project) {
this.registerProjectNameSubstitutor(
project.root,
(name) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(name) => {
(finalName) => {

nx-cloud[bot]

This comment was marked as outdated.

@AgentEnder AgentEnder force-pushed the fix/stable-project-refs-in-deps-and-inputs branch from c45117e to 45deaa9 Compare February 19, 2026 21:39
nx-cloud[bot]

This comment was marked as outdated.

Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
Comment on lines 242 to 266
} else if (Array.isArray(inputProjectNames)) {
for (let j = 0; j < inputProjectNames.length; j++) {
const projectName = inputProjectNames[j];
const referencedProject = nameMap.get(projectName);
if (referencedProject) {
const arrayIndex = j; // Capture j by value
this.registerProjectNameSubstitutor(
referencedProject.root,
ownerRoot,
arrayKey,
i,
(finalName, ownerConfig) => {
const finalInput =
ownerConfig.targets?.[targetName]?.inputs?.[i];
if (
finalInput &&
typeof finalInput === 'object' &&
'projects' in finalInput
) {
(finalInput['projects'] as string[])[arrayIndex] = finalName;
}
}
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bug: When inputProjectNames is an array containing multiple project names (e.g., ['project-b', 'project-c']), all substitutors are registered with the same arrayKey and index i. This causes each subsequent call to registerProjectNameSubstitutor to invoke clearSubstitutorAtIndex(arrayKey, i), which removes the previous substitutor for that index.

For example, if inputs[0].projects = ['project-b', 'project-c']:

  • First iteration (j=0): registers substitutor for 'project-b' at index 0
  • Second iteration (j=1): calls clearSubstitutorAtIndex(arrayKey, 0), removing the 'project-b' substitutor, then registers 'project-c'

Result: Only the last project name in the array gets a substitutor, so earlier projects won't be updated when renamed.

Fix: The tracking key should include both i and j to uniquely identify each project reference:

const trackingKey = `${arrayKey}.${i}.projects.${j}`;
this.registerProjectNameSubstitutor(
  referencedProject.root,
  ownerRoot,
  trackingKey,  // Use unique key per project in array
  i,
  (finalName, ownerConfig) => { /* ... */ }
);

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Contributor

@nx-cloud nx-cloud bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ The fix from Nx Cloud was applied

We've fixed a bug in the ProjectNameInNodePropsManager where multiple project names within a single inputs or dependsOn array (e.g., ['project-b', 'project-c']) would overwrite each other's substitutors, causing only the last project name to be tracked. The fix introduces a three-level tracking key (arrayKey, index, subIndex) to ensure each project name in an array maintains its own substitutor, allowing all project references to be correctly updated when plugins rename projects.

Tip

We verified this fix by re-running nx:test.

Warning

The suggested diff is too large to display here, but you can view it on Nx Cloud ↗


Revert fix via Nx Cloud  

View interactive diff ↗

➡️ This fix was applied by Craigory Coppola

🎓 Learn more about Self-Healing CI on nx.dev

nx-cloud bot and others added 2 commits February 23, 2026 03:51
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants