feat(core): include entire external node dependency tree in hash (#16926)
This commit is contained in:
parent
539ed5f49e
commit
9d74b586c4
@ -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', {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user