diff --git a/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap b/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap index a8dab6efd6..7ce56a4b0c 100644 --- a/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap +++ b/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap @@ -44,6 +44,52 @@ exports[`TaskHasher dependentTasksOutputFiles should work with dependent tasks w } `; +exports[`TaskHasher hashTarget should hash entire subtree in a deterministic way 1`] = ` +{ + "details": { + "command": "81188892120010785", + "implicitDeps": {}, + "nodes": { + "ProjectConfiguration": "8474322003863204060", + "TsConfig": "8767608672024750088", + "appA:{projectRoot}/**/*": "3244421341483603138", + "npm:@nx/webpack": "$@nx/webpack0.0.0$", + "npm:packageA": "$packageA0.0.0$", + "npm:packageB": "$packageB0.0.0$", + "npm:packageC": "$packageC0.0.0$", + "{workspaceRoot}/.gitignore": "3244421341483603138", + "{workspaceRoot}/.nxignore": "3244421341483603138", + "{workspaceRoot}/nx.json": "8942239360311677987", + }, + "runtime": {}, + }, + "value": "12756041818139421941", +} +`; + +exports[`TaskHasher hashTarget should hash entire subtree in a deterministic way 2`] = ` +{ + "details": { + "command": "9096392622609675764", + "implicitDeps": {}, + "nodes": { + "ProjectConfiguration": "17724470359684527282", + "TsConfig": "8767608672024750088", + "appB:{projectRoot}/**/*": "3244421341483603138", + "npm:@nx/webpack": "$@nx/webpack0.0.0$", + "npm:packageA": "$packageA0.0.0$", + "npm:packageB": "$packageB0.0.0$", + "npm:packageC": "$packageC0.0.0$", + "{workspaceRoot}/.gitignore": "3244421341483603138", + "{workspaceRoot}/.nxignore": "3244421341483603138", + "{workspaceRoot}/nx.json": "8942239360311677987", + }, + "runtime": {}, + }, + "value": "15326312070983573452", +} +`; + exports[`TaskHasher hashTarget should hash entire subtree of dependencies 1`] = ` { "details": { diff --git a/packages/nx/src/hasher/task-hasher.spec.ts b/packages/nx/src/hasher/task-hasher.spec.ts index 6524852c74..e04d8529d3 100644 --- a/packages/nx/src/hasher/task-hasher.spec.ts +++ b/packages/nx/src/hasher/task-hasher.spec.ts @@ -1257,6 +1257,9 @@ describe('TaskHasher', () => { expect(hashAppB1).toEqual(hashAppB2); expect(hashAppA1).toEqual(hashAppA2); + + expect(hashAppA1).toMatchSnapshot(); + expect(hashAppB1).toMatchSnapshot(); }); 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 f88cd7603c..da6641411e 100644 --- a/packages/nx/src/hasher/task-hasher.ts +++ b/packages/nx/src/hasher/task-hasher.ts @@ -20,6 +20,7 @@ import { getHashEnv } from './set-hash-env'; import { workspaceRoot } from '../utils/workspace-root'; import { join, relative } from 'path'; import { normalizePath } from '../utils/path'; +import { findAllProjectNodeDependencies } from '../utils/project-graph-utils'; type ExpandedSelfInput = | { fileset: string } @@ -429,60 +430,39 @@ class TaskHasherImpl { return combinedHash; } - private hashExternalDependency( - externalNodeName: string, - visited: Set - ): PartialHash[] { - // try to retrieve the hash from cache - if (this.externalDependencyHashes.has(externalNodeName)) { - return this.externalDependencyHashes.get(externalNodeName); - } - visited.add(externalNodeName); + private hashSingleExternalDependency(externalNodeName: string): PartialHash { const node = this.projectGraph.externalNodes[externalNodeName]; - const partialHashes: Set = new Set(); - if (node) { - if (node.data.hash) { - // we already know the hash of this dependency - partialHashes.add({ - value: node.data.hash, - details: { - [externalNodeName]: node.data.hash, - }, - }); - } else { - // we take version as a hash - partialHashes.add({ - value: node.data.version, - details: { - [externalNodeName]: node.data.version, - }, - }); - } - // we want to calculate the hash of the entire dependency tree - if (this.projectGraph.dependencies[externalNodeName]) { - this.projectGraph.dependencies[externalNodeName].forEach((d) => { - if (!visited.has(d.target)) { - for (const hash of this.hashExternalDependency(d.target, visited)) { - partialHashes.add(hash); - } - } - }); - } - } else { - // unknown dependency - // 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 - partialHashes.add({ - value: `__${externalNodeName}__`, + if (node.data.hash) { + // we already know the hash of this dependency + return { + value: node.data.hash, details: { - [externalNodeName]: `__${externalNodeName}__`, + [externalNodeName]: node.data.hash, }, - }); + }; + } else { + // we take version as a hash + return { + value: node.data.version, + details: { + [externalNodeName]: node.data.version, + }, + }; } - const partialHashArray = Array.from(partialHashes); - this.externalDependencyHashes.set(externalNodeName, partialHashArray); - return partialHashArray; + } + + private hashExternalDependency(externalNodeName: string) { + const partialHashes: Set = new Set(); + partialHashes.add(this.hashSingleExternalDependency(externalNodeName)); + const deps = findAllProjectNodeDependencies( + externalNodeName, + this.projectGraph, + true + ); + for (const dep of deps) { + partialHashes.add(this.hashSingleExternalDependency(dep)); + } + return Array.from(partialHashes); } private hashTarget( @@ -507,6 +487,13 @@ class TaskHasherImpl { const executorPackage = target.executor.split(':')[0]; const executorNodeName = this.findExternalDependencyNodeName(executorPackage); + + // This is either a local plugin or a non-existent executor + if (!executorNodeName) { + // TODO: This should not return null if it is a local plugin's executor + return null; + } + return this.getExternalDependencyHash(executorNodeName); } else { // use command external dependencies if available to construct the hash @@ -519,6 +506,12 @@ class TaskHasherImpl { const externalDependencies = input['externalDependencies']; for (let dep of externalDependencies) { dep = this.findExternalDependencyNodeName(dep); + if (!dep) { + throw new Error( + `The externalDependency "${dep}" for "${projectName}:${targetName}" could not be found` + ); + } + partialHashes.push(this.getExternalDependencyHash(dep)); } } @@ -543,7 +536,7 @@ class TaskHasherImpl { } } - private findExternalDependencyNodeName(packageName: string): string { + private findExternalDependencyNodeName(packageName: string): string | null { if (this.projectGraph.externalNodes[packageName]) { return packageName; } @@ -555,8 +548,8 @@ class TaskHasherImpl { return node.name; } } - // not found, just return the package name - return packageName; + // not found + return null; } private async hashSingleProjectInputs( @@ -768,7 +761,10 @@ class TaskHasherImpl { private calculateExternalDependencyHashes() { const keys = Object.keys(this.projectGraph.externalNodes); for (const externalNodeName of keys) { - this.hashExternalDependency(externalNodeName, new Set()); + this.externalDependencyHashes.set( + externalNodeName, + this.hashExternalDependency(externalNodeName) + ); } } } diff --git a/packages/nx/src/utils/project-graph-utils.ts b/packages/nx/src/utils/project-graph-utils.ts index 4bf89d30b5..e5cdd43ae7 100644 --- a/packages/nx/src/utils/project-graph-utils.ts +++ b/packages/nx/src/utils/project-graph-utils.ts @@ -75,21 +75,24 @@ export function getSourceDirOfDependentProjects( /** * Find all internal project dependencies. - * All the external (npm) dependencies will be filtered out + * All the external (npm) dependencies will be filtered out unless includeExternalDependencies is set to true * @param {string} parentNodeName * @param {ProjectGraph} projectGraph + * @param includeExternalDependencies * @returns {string[]} */ export function findAllProjectNodeDependencies( parentNodeName: string, - projectGraph = readCachedProjectGraph() + projectGraph = readCachedProjectGraph(), + includeExternalDependencies = false ): string[] { const dependencyNodeNames = new Set(); collectDependentProjectNodesNames( projectGraph as ProjectGraph, dependencyNodeNames, - parentNodeName + parentNodeName, + includeExternalDependencies ); return Array.from(dependencyNodeNames); @@ -99,7 +102,8 @@ export function findAllProjectNodeDependencies( function collectDependentProjectNodesNames( nxDeps: ProjectGraph, dependencyNodeNames: Set, - parentNodeName: string + parentNodeName: string, + includeExternalDependencies: boolean ) { const dependencies = nxDeps.dependencies[parentNodeName]; if (!dependencies) { @@ -111,23 +115,28 @@ function collectDependentProjectNodesNames( for (const dependency of dependencies) { const dependencyName = dependency.target; - // we're only interested in internal nodes, not external - if (nxDeps.externalNodes?.[dependencyName]) { - continue; - } - // skip dependencies already added (avoid circular dependencies) if (dependencyNodeNames.has(dependencyName)) { continue; } + // we're only interested in internal nodes, not external + if (nxDeps.externalNodes?.[dependencyName]) { + if (includeExternalDependencies) { + dependencyNodeNames.add(dependencyName); + } else { + continue; + } + } + dependencyNodeNames.add(dependencyName); // Get the dependencies of the dependencies collectDependentProjectNodesNames( nxDeps, dependencyNodeNames, - dependencyName + dependencyName, + includeExternalDependencies ); } }