Skip to content

Commit 3144108

Browse files
fix(core): resolve name refs copied into pattern-matched target arrays (#36359)
## Current Behavior Name-ref sentinels (used to keep `dependsOn`/`inputs` project references correct across renames) are written back through a pointer to the array they were created in. Merges copy sentinels **by reference** into fresh arrays — most visibly when a project.json pattern target (e.g. `e2e-ci--**/**`) with a `"..."` spread is applied to every matching atomized target. The copies never get resolved, leaving raw internal objects (`RootRef { value, parent, targetPart }`) in the final project configuration, and task graph creation crashes with: ``` NX pattern is not iterable ``` ## Expected Behavior Every name ref resolves to its project name wherever it ends up. `applySubstitutions` now sweeps the merged rootMap and resolves each sentinel in place, covering arrays a sentinel was copied into. Since write-back no longer depends on back-references, the `parent`/`key` fields, the `allRefs` registry, and the parent-rebinding branches are removed. The new integration test reproduces the exact corruption on the previous implementation (raw `RootRef` objects in the atomized targets' `dependsOn`) and passes with the sweep. ## Related Issue(s) Found while using `"..."` in the `dependsOn` of atomized e2e pattern targets in this repo (see `e2e/gradle/project.json` / `e2e/maven/project.json`); that cleanup was reverted from #36302 and can be re-applied once this fix ships. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Qjm3xvLS2vLRsaxt4vv56d <!-- polygraph-session-start --> --- [View session information ↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/PR-for-pnpm-11-ERR_PNPM_IGNORED_BUILDS-fix-f71af2eb) <!-- polygraph-session-end --> --------- Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> (cherry picked from commit 4a05b10)
1 parent 144918b commit 3144108

5 files changed

Lines changed: 235 additions & 82 deletions

File tree

packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ describe('project-configuration-utils', () => {
172172
});
173173

174174
// Mirror of the dependsOn P2 regression for the inputs path:
175-
// processInputs and processDependsOn share writeReplacement /
176-
// createRef plumbing, but the top-level walk is separate. Locks in
175+
// processInputs and processDependsOn share the createRef plumbing,
176+
// but the substitution sweep walks each array separately. Locks in
177177
// that default-plugin inputs references get sentinel treatment too.
178178
it('should resolve inputs refs owned by default plugins when the referenced project is renamed during the default apply', () => {
179179
const specifiedResults: CreateNodesResultEntry[][] = [
@@ -608,6 +608,113 @@ describe('project-configuration-utils', () => {
608608
expect(buildTarget.inputs).toEqual(['explicit', 'from-defaults']);
609609
});
610610

611+
it('should resolve name refs in every pattern-matched target when a project.json pattern target spreads targetDefaults', () => {
612+
// Regression: the spread copies the targetDefaults dependsOn entries
613+
// (already sentinelized as name refs) by reference into a fresh array
614+
// for every target matching the pattern. Write-back through each
615+
// sentinel's original array left raw sentinel objects in the copies,
616+
// which later crashed task graph creation ("pattern is not iterable").
617+
const specifiedResults = [
618+
[
619+
[
620+
'fake-atomizer-plugin',
621+
'e2e/maven/jest.config.ts',
622+
{
623+
projects: {
624+
'e2e/maven': {
625+
name: 'e2e-maven',
626+
targets: {
627+
'e2e-ci--src/a.test.ts': { command: 'echo a' },
628+
'e2e-ci--src/b.test.ts': { command: 'echo b' },
629+
},
630+
},
631+
},
632+
},
633+
],
634+
],
635+
] as const;
636+
637+
const defaultResults = [
638+
[
639+
[
640+
'nx/core/project-json',
641+
'project.json',
642+
{
643+
projects: {
644+
'.': {
645+
name: '@nx/nx-source',
646+
root: '.',
647+
targets: {
648+
'populate-storage': { command: 'echo populate' },
649+
'local-registry': { command: 'echo registry' },
650+
},
651+
},
652+
},
653+
},
654+
],
655+
[
656+
'nx/core/project-json',
657+
'e2e/maven/project.json',
658+
{
659+
projects: {
660+
'e2e/maven': {
661+
name: 'e2e-maven',
662+
root: 'e2e/maven',
663+
targets: {
664+
'e2e-ci--**/**': {
665+
dependsOn: ['...', 'maven-plugin:install'],
666+
},
667+
},
668+
},
669+
},
670+
},
671+
],
672+
[
673+
'nx/core/project-json',
674+
'libs/maven-plugin/project.json',
675+
{
676+
projects: {
677+
'libs/maven-plugin': {
678+
name: 'maven-plugin',
679+
root: 'libs/maven-plugin',
680+
targets: {
681+
install: { command: 'echo install' },
682+
},
683+
},
684+
},
685+
},
686+
],
687+
],
688+
] as const;
689+
690+
const errors = [];
691+
const result = mergeCreateNodesResults(
692+
specifiedResults as any,
693+
defaultResults as any,
694+
{
695+
targetDefaults: {
696+
'e2e-ci--**/**': {
697+
dependsOn: [
698+
'@nx/nx-source:populate-storage',
699+
'@nx/nx-source:local-registry',
700+
],
701+
},
702+
},
703+
},
704+
'/tmp/test',
705+
errors
706+
);
707+
708+
const targets = result.projectRootMap['e2e/maven'].targets!;
709+
const expected = [
710+
'@nx/nx-source:populate-storage',
711+
'@nx/nx-source:local-registry',
712+
'maven-plugin:install',
713+
];
714+
expect(targets['e2e-ci--src/a.test.ts'].dependsOn).toEqual(expected);
715+
expect(targets['e2e-ci--src/b.test.ts'].dependsOn).toEqual(expected);
716+
});
717+
611718
it('should resolve spread tokens from package.json script target augmentation against target defaults', () => {
612719
// https://github.com/nrwl/nx/issues/36235 — `nx.targets.build` augments
613720
// the script-derived target inside the package.json reader. The `'...'`

packages/nx/src/project-graph/utils/project-configuration-utils.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -432,12 +432,10 @@ export function mergeCreateNodesResults(
432432
}
433433
}
434434

435-
// The intermediate apply may have rebuilt dependsOn / inputs arrays
436-
// via spread merges, leaving sentinels inserted against the
437-
// intermediate rootMap pointing at now-orphaned arrays. Re-walking
438-
// the final merged targets rebinds each encountered sentinel's
439-
// `parent` to the current array (see
440-
// ProjectNameInNodePropsManager#processInputs / processDependsOn).
435+
// The intermediate apply may have rebuilt dependsOn / inputs arrays via
436+
// spread merges, introducing name-ref strings that weren't visible in any
437+
// single plugin result. Re-walking the final merged targets sentinelizes
438+
// them so the final substitution sweep resolves them too.
441439
nodesManager.registerNameRefs(intermediateDefaultRootMap);
442440

443441
// Overlay default-plugin attribution onto the main source maps using

packages/nx/src/project-graph/utils/project-configuration/name-substitution-manager.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2187,5 +2187,51 @@ describe('ProjectNameInNodePropsManager', () => {
21872187
expect(typeof mergedDependsOn[0]).toBe('string');
21882188
expect(typeof mergedDependsOn[1]).toBe('string');
21892189
});
2190+
2191+
// Regression: applying a pattern target (e.g. `e2e-ci--**/**`) copies
2192+
// its dependsOn entries by reference into a fresh array on every
2193+
// matching target, and those arrays are never re-registered. Write-back
2194+
// through the sentinel's original array left the copies holding raw
2195+
// sentinel objects, which later crashed task graph creation
2196+
// ("pattern is not iterable").
2197+
it('should resolve sentinels copied into arrays that are never re-registered', () => {
2198+
const manager = createManager();
2199+
2200+
const projectA = createProject('project-a', 'libs/a', {
2201+
targets: {
2202+
'e2e-ci--**/**': {
2203+
dependsOn: ['project-b:build'],
2204+
},
2205+
},
2206+
});
2207+
const projectB = createProject('project-b', 'libs/b');
2208+
const baseResult = createPluginResult([projectA, projectB]);
2209+
identifyProjects(manager, baseResult);
2210+
manager.registerNameRefs(baseResult);
2211+
2212+
// Simulate pattern-target application: the matching atomized target
2213+
// gets a fresh dependsOn array holding the same sentinel reference.
2214+
// Nothing re-registers this array.
2215+
const templateDependsOn = projectA.targets['e2e-ci--**/**']
2216+
.dependsOn as unknown[];
2217+
const atomizedDependsOn: unknown[] = [templateDependsOn[0]];
2218+
projectA.targets['e2e-ci--src/a.test.ts'] = {
2219+
dependsOn: atomizedDependsOn,
2220+
};
2221+
2222+
const rootMap = createRootMap([
2223+
{
2224+
name: 'project-a',
2225+
root: 'libs/a',
2226+
targets: projectA.targets,
2227+
},
2228+
{ name: 'project-b', root: 'libs/b' },
2229+
]);
2230+
2231+
manager.applySubstitutions(rootMap);
2232+
2233+
expect(atomizedDependsOn[0]).toBe('project-b:build');
2234+
expect(templateDependsOn[0]).toBe('project-b:build');
2235+
});
21902236
});
21912237
});

0 commit comments

Comments
 (0)