From 671e64ad6ba0247baa31847fa317329c6c005497 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 10 May 2023 22:08:24 +0200 Subject: [PATCH] feat(core): include entire external node dependency tree in hash --- packages/nx/src/hasher/hasher.spec.ts | 201 +++++++++++++++++++++++++- packages/nx/src/hasher/hasher.ts | 56 +++++-- 2 files changed, 235 insertions(+), 22 deletions(-) diff --git a/packages/nx/src/hasher/hasher.spec.ts b/packages/nx/src/hasher/hasher.spec.ts index 833e45ee6b032..e4155ec57392e 100644 --- a/packages/nx/src/hasher/hasher.spec.ts +++ b/packages/nx/src/hasher/hasher.spec.ts @@ -459,7 +459,7 @@ describe('Hasher', () => { data: { root: 'libs/parent', targets: { - build: { executor: '@nx/workspace:run-commands' }, + build: { executor: 'nx:run-commands' }, }, files: [ { file: 'libs/parent/filea.ts', hash: 'a.hash' }, @@ -472,7 +472,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/child', - targets: { build: { executor: '@nx/workspace:run-commands' } }, + targets: { build: { executor: 'nx:run-commands' } }, files: [ { file: 'libs/child/fileb.ts', hash: 'b.hash' }, { file: 'libs/child/fileb.spec.ts', hash: 'b.spec.hash' }, @@ -526,7 +526,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parent', - targets: { build: { executor: '@nx/workspace:run-commands' } }, + targets: { build: { executor: 'nx:run-commands' } }, files: [{ file: '/file', hash: 'file.hash' }], }, }, @@ -575,7 +575,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parent', - targets: { build: { executor: '@nx/workspace:run-commands' } }, + targets: { build: { executor: 'nx:run-commands' } }, files: [{ file: '/filea.ts', hash: 'a.hash' }], }, }, @@ -584,7 +584,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/child', - targets: { build: { executor: '@nx/workspace:run-commands' } }, + targets: { build: { executor: 'nx:run-commands' } }, files: [{ file: '/fileb.ts', hash: 'b.hash' }], }, }, @@ -656,7 +656,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parent', - targets: { build: { executor: '@nx/workspace:run-commands' } }, + targets: { build: { executor: 'nx:run-commands' } }, files: [{ file: '/file', hash: 'some-hash' }], }, }, @@ -698,7 +698,7 @@ describe('Hasher', () => { type: 'app', data: { root: 'apps/app', - targets: { build: { executor: '@nx/workspace:run-commands' } }, + targets: { build: { executor: 'nx:run-commands' } }, files: [{ file: '/filea.ts', hash: 'a.hash' }], }, }, @@ -747,7 +747,7 @@ describe('Hasher', () => { type: 'app', data: { root: 'apps/app', - targets: { build: { executor: '@nx/workspace:run-commands' } }, + targets: { build: { executor: 'nx:run-commands' } }, files: [{ file: '/filea.ts', hash: 'a.hash' }], }, }, @@ -782,6 +782,191 @@ describe('Hasher', () => { }); }); + describe('hashTarget', () => { + it('should hash executor dependencies of @nx packages', async () => { + const hasher = new Hasher( + { + nodes: { + app: { + name: 'app', + type: 'app', + data: { + root: 'apps/app', + targets: { build: { executor: '@nx/webpack:webpack' } }, + files: [{ file: '/filea.ts', hash: 'a.hash' }], + }, + }, + }, + externalNodes: { + 'npm:@nx/webpack': { + name: 'npm:@nx/webpack', + type: 'npm', + data: { + packageName: '@nx/webpack', + version: '16.0.0', + }, + }, + }, + dependencies: {}, + allWorkspaceFiles, + }, + {} as any, + {}, + createHashing() + ); + + const hash = await hasher.hashTask({ + target: { project: 'app', target: 'build' }, + id: 'app-build', + overrides: { prop: 'prop-value' }, + }); + + assertFilesets(hash, { + 'npm:@nx/webpack': { contains: '16.0.0' }, + }); + }); + + it('should hash entire subtree of dependencies', async () => { + const hasher = new Hasher( + { + nodes: { + app: { + name: 'app', + type: 'app', + data: { + root: 'apps/app', + targets: { build: { executor: '@nx/webpack:webpack' } }, + files: [{ file: '/filea.ts', hash: 'a.hash' }], + }, + }, + }, + externalNodes: { + 'npm:@nx/webpack': { + name: 'npm:@nx/webpack', + type: 'npm', + data: { + packageName: '@nx/webpack', + version: '16.0.0', + hash: '$nx/webpack16$', + }, + }, + 'npm:@nx/devkit': { + name: 'npm:@nx/devkit', + type: 'npm', + data: { + packageName: '@nx/devkit', + version: '16.0.0', + hash: '$nx/devkit16$', + }, + }, + 'npm:nx': { + name: 'npm:nx', + type: 'npm', + data: { + packageName: 'nx', + version: '16.0.0', + hash: '$nx16$', + }, + }, + 'npm:webpack': { + name: 'npm:webpack', + type: 'npm', + data: { + packageName: 'webpack', + version: '5.0.0', // no hash intentionally + }, + }, + }, + dependencies: { + 'npm:@nx/webpack': [ + { + source: 'npm:@nx/webpack', + target: 'npm:@nx/devkit', + type: DependencyType.static, + }, + { + source: 'npm:@nx/webpack', + target: 'npm:nx', + type: DependencyType.static, + }, + { + source: 'npm:@nx/webpack', + target: 'npm:webpack', + type: DependencyType.static, + }, + ], + 'npm:@nx/devkit': [ + { + source: 'npm:@nx/devkit', + target: 'npm:nx', + type: DependencyType.static, + }, + ], + }, + allWorkspaceFiles, + }, + {} as any, + {}, + createHashing() + ); + + const hash = await hasher.hashTask({ + target: { project: 'app', target: 'build' }, + id: 'app-build', + overrides: { prop: 'prop-value' }, + }); + + assertFilesets(hash, { + 'npm:@nx/webpack': { contains: '$nx/webpack16$' }, + 'npm:@nx/devkit': { contains: '$nx/devkit16$' }, + 'npm:nx': { contains: '$nx16$' }, + 'npm:webpack': { contains: '5.0.0' }, + }); + }); + + it('should not hash when nx:run-commands executor', async () => { + const hasher = new Hasher( + { + nodes: { + app: { + name: 'app', + type: 'app', + data: { + root: 'apps/app', + targets: { build: { executor: 'nx:run-commands' } }, + files: [{ file: '/filea.ts', hash: 'a.hash' }], + }, + }, + }, + externalNodes: { + 'npm:nx': { + name: 'npm:nx', + type: 'npm', + data: { + packageName: 'nx', + version: '16.0.0', + }, + }, + }, + dependencies: {}, + allWorkspaceFiles, + }, + {} as any, + {}, + createHashing() + ); + + const hash = await hasher.hashTask({ + target: { project: 'app', target: 'build' }, + id: 'app-build', + overrides: { prop: 'prop-value' }, + }); + + expect(hash.details.nodes['npm:nx']).not.toBeDefined(); + expect(hash.details.nodes['app']).toEqual('nx:run-commands'); + }); + }); + describe('expandNamedInput', () => { it('should expand named inputs', () => { const expanded = expandNamedInput('c', { diff --git a/packages/nx/src/hasher/hasher.ts b/packages/nx/src/hasher/hasher.ts index 1174462e7cab7..be50620424dd9 100644 --- a/packages/nx/src/hasher/hasher.ts +++ b/packages/nx/src/hasher/hasher.ts @@ -179,6 +179,7 @@ class TaskHasher { private runtimeHashes: { [runtime: string]: Promise; } = {}; + private externalDepsHashCache: { [packageName: string]: PartialHash } = {}; constructor( private readonly nxJson: NxJsonConfiguration, @@ -321,26 +322,53 @@ class TaskHasher { .filter((r) => !!r); } - private hashExternalDependency(projectName: string) { - const n = this.projectGraph.externalNodes[projectName]; - const version = n?.data?.version; - let hash: string; - if (n?.data?.hash) { - // we already know the hash of this dependency - hash = n.data.hash; + private hashExternalDependency(projectName: string): PartialHash { + // try to retrieve the hash from cache + if (this.externalDepsHashCache[projectName]) { + return this.externalDepsHashCache[projectName]; + } + const node = this.projectGraph.externalNodes[projectName]; + let partialHash; + if (node) { + let hash; + if (node.data.hash) { + // we already know the hash of this dependency + hash = node.data.hash; + } else { + // we take version as a hash + hash = node.data.version; + } + // we want to calculate the hash of the entire dependency tree + const partialHashes: PartialHash[] = []; + if (this.projectGraph.dependencies[projectName]) { + this.projectGraph.dependencies[projectName].forEach((d) => { + partialHashes.push(this.hashExternalDependency(d.target)); + }); + } + partialHash = { + value: this.hashing.hashArray([ + hash, + ...partialHashes.map((p) => p.value), + ]), + details: { + [projectName]: hash, + ...partialHashes.reduce((m, c) => ({ ...m, ...c.details }), {}), + }, + }; } 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 - hash = version ? `__${projectName}@${version}__` : `__${projectName}__`; + partialHash = { + value: `__${projectName}__`, + details: { + [projectName]: `__${projectName}__`, + }, + }; } - return { - value: hash, - details: { - [projectName]: version || hash, - }, - }; + this.externalDepsHashCache[projectName] = partialHash; + return partialHash; } private hashTarget(projectName: string, targetName: string): PartialHash {