diff --git a/packages/nx/src/hasher/task-hasher.spec.ts b/packages/nx/src/hasher/task-hasher.spec.ts index 6ba82a56e424a..80be6b2ce2609 100644 --- a/packages/nx/src/hasher/task-hasher.spec.ts +++ b/packages/nx/src/hasher/task-hasher.spec.ts @@ -1161,7 +1161,7 @@ describe('TaskHasher', () => { expect(hash.value).toContain('|5.0.0|'); }); - it('should hash entire subtree of dependencies', async () => { + it('should hash entire subtree in a deterministic way', async () => { const createHasher = () => new InProcessTaskHasher( {}, @@ -1217,47 +1217,54 @@ describe('TaskHasher', () => { dependencies: { appA: [ { - source: 'app', + source: 'appA', target: 'npm:packageA', type: DependencyType.static, }, { - source: 'app', + source: 'appA', target: 'npm:packageB', type: DependencyType.static, }, { - source: 'app', + source: 'appA', target: 'npm:packageC', type: DependencyType.static, }, ], appB: [ { - source: 'app', + source: 'appB', target: 'npm:packageC', type: DependencyType.static, }, ], 'npm:packageC': [ { - source: 'app', + source: 'npm:packageC', target: 'npm:packageA', type: DependencyType.static, }, { - source: 'app', + source: 'npm:packageC', target: 'npm:packageB', type: DependencyType.static, }, ], 'npm:packageB': [ { - source: 'app', + source: 'npm:packageB', target: 'npm:packageA', type: DependencyType.static, }, ], + 'npm:packageA': [ + { + source: 'npm:packageA', + target: 'npm:packageC', + type: DependencyType.static, + }, + ], }, }, { @@ -1288,15 +1295,16 @@ describe('TaskHasher', () => { const hasher1 = createHasher(); - await computeTaskHash(hasher1, 'appA'); + const hasAppA1 = await computeTaskHash(hasher1, 'appA'); const hashAppB1 = await computeTaskHash(hasher1, 'appB'); const hasher2 = createHasher(); const hashAppB2 = await computeTaskHash(hasher2, 'appB'); - await computeTaskHash(hasher2, 'appA'); + const hasAppA2 = await computeTaskHash(hasher2, 'appA'); expect(hashAppB1).toEqual(hashAppB2); + expect(hasAppA1).toEqual(hasAppA2); }); it('should not hash when nx:run-commands executor', async () => { diff --git a/packages/nx/src/hasher/task-hasher.ts b/packages/nx/src/hasher/task-hasher.ts index 4b40e50a03337..9b3d5808bc1a7 100644 --- a/packages/nx/src/hasher/task-hasher.ts +++ b/packages/nx/src/hasher/task-hasher.ts @@ -332,7 +332,7 @@ class TaskHasherImpl { visited ); } else { - const hash = this.hashExternalDependency(d.source, d.target); + const { hash } = this.hashExternalDependency(d.target); return { value: hash, details: { @@ -408,30 +408,21 @@ class TaskHasherImpl { return partialHashes; } - private computeExternalDependencyIdentifier( - sourceProjectName: string, - targetProjectName: string - ): `${string}->${string}` { - return `${sourceProjectName}->${targetProjectName}`; - } - private hashExternalDependency( - sourceProjectName: string, - targetProjectName: string, - visited = new Set() - ): string { + projectName: string, + parentProjects = new Set() + ): { fullyResolved: boolean; hash: string } { // try to retrieve the hash from cache - if (this.externalDepsHashCache[targetProjectName]) { - return this.externalDepsHashCache[targetProjectName]; + if (this.externalDepsHashCache[projectName]) { + return { + fullyResolved: true, + hash: this.externalDepsHashCache[projectName], + }; } - visited.add( - this.computeExternalDependencyIdentifier( - sourceProjectName, - targetProjectName - ) - ); - const node = this.projectGraph.externalNodes[targetProjectName]; + parentProjects.add(projectName); + const node = this.projectGraph.externalNodes[projectName]; let partialHash: string; + let fullyResolved = true; if (node) { const partialHashes: string[] = []; if (node.data.hash) { @@ -442,19 +433,20 @@ class TaskHasherImpl { partialHashes.push(node.data.version); } // we want to calculate the hash of the entire dependency tree - if (this.projectGraph.dependencies[targetProjectName]) { - this.projectGraph.dependencies[targetProjectName].forEach((d) => { - if ( - !visited.has( - this.computeExternalDependencyIdentifier( - targetProjectName, - d.target - ) - ) - ) { - partialHashes.push( - this.hashExternalDependency(targetProjectName, d.target, visited) + if (this.projectGraph.dependencies[projectName]) { + this.projectGraph.dependencies[projectName].forEach((d) => { + if (!parentProjects.has(d.target)) { + const hashResult = this.hashExternalDependency( + d.target, + new Set(parentProjects) ); + partialHashes.push(hashResult.hash); + if (!hashResult.fullyResolved) { + fullyResolved = false; + } + } else { + // NOTE: do not store hash to cache since it is only a partial hash + fullyResolved = false; } }); } @@ -465,10 +457,14 @@ class TaskHasherImpl { // this may occur if dependency is not an npm package // but rather symlinked in node_modules or it's pointing to a remote git repo // in this case we have no information about the versioning of the given package - partialHash = `__${targetProjectName}__`; + partialHash = `__${projectName}__`; + } + + if (fullyResolved) { + this.externalDepsHashCache[projectName] = partialHash; } - this.externalDepsHashCache[targetProjectName] = partialHash; - return partialHash; + + return { fullyResolved, hash: partialHash }; } private hashTarget( @@ -483,7 +479,7 @@ class TaskHasherImpl { return; } - let hash; + let hash: string; // we can only vouch for @nx packages's executor dependencies // if it's "run commands" or third-party we skip traversing since we have no info what this command depends on if ( @@ -493,7 +489,7 @@ class TaskHasherImpl { const executorPackage = target.executor.split(':')[0]; const executorNodeName = this.findExternalDependencyNodeName(executorPackage); - hash = this.hashExternalDependency(projectName, executorNodeName); + hash = this.hashExternalDependency(executorNodeName).hash; } else { // use command external dependencies if available to construct the hash const partialHashes: string[] = []; @@ -505,7 +501,7 @@ class TaskHasherImpl { const externalDependencies = input['externalDependencies']; for (let dep of externalDependencies) { dep = this.findExternalDependencyNodeName(dep); - partialHashes.push(this.hashExternalDependency(projectName, dep)); + partialHashes.push(this.hashExternalDependency(dep).hash); } } }