feat(core): include entire external node dependency tree in hash (#16926)

This commit is contained in:
Miroslav Jonaš 2023-05-11 23:17:15 +02:00 committed by GitHub
parent 539ed5f49e
commit 9d74b586c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 241 additions and 22 deletions

View File

@ -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', {

View File

@ -179,6 +179,7 @@ class TaskHasher {
private runtimeHashes: {
[runtime: string]: Promise<PartialHash>;
} = {};
private externalDepsHashCache: { [packageName: string]: PartialHash } = {};
constructor(
private readonly nxJson: NxJsonConfiguration,
@ -321,26 +322,59 @@ 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,
visited = new Set<string>()
): PartialHash {
// try to retrieve the hash from cache
if (this.externalDepsHashCache[projectName]) {
return this.externalDepsHashCache[projectName];
}
visited.add(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) => {
if (!visited.has(d.target)) {
partialHashes.push(this.hashExternalDependency(d.target, visited));
}
});
}
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 {