fix(core): ensure external dependency hashes are resolved in a deterministic way (#17926)
This commit is contained in:
parent
eaebcc34f9
commit
65adb94bf6
@ -1161,6 +1161,144 @@ describe('TaskHasher', () => {
|
||||
expect(hash.value).toContain('|5.0.0|');
|
||||
});
|
||||
|
||||
it('should hash entire subtree of dependencies', async () => {
|
||||
const createHasher = () =>
|
||||
new InProcessTaskHasher(
|
||||
{},
|
||||
allWorkspaceFiles,
|
||||
{
|
||||
nodes: {
|
||||
appA: {
|
||||
name: 'appA',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'apps/appA',
|
||||
targets: { build: { executor: '@nx/webpack:webpack' } },
|
||||
},
|
||||
},
|
||||
appB: {
|
||||
name: 'appB',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'apps/appB',
|
||||
targets: { build: { executor: '@nx/webpack:webpack' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
externalNodes: {
|
||||
'npm:packageA': {
|
||||
name: 'npm:packageA',
|
||||
type: 'npm',
|
||||
data: {
|
||||
packageName: 'packageA',
|
||||
version: '0.0.0',
|
||||
hash: '$packageA0.0.0$',
|
||||
},
|
||||
},
|
||||
'npm:packageB': {
|
||||
name: 'npm:packageB',
|
||||
type: 'npm',
|
||||
data: {
|
||||
packageName: 'packageB',
|
||||
version: '0.0.0',
|
||||
hash: '$packageB0.0.0$',
|
||||
},
|
||||
},
|
||||
'npm:packageC': {
|
||||
name: 'npm:packageC',
|
||||
type: 'npm',
|
||||
data: {
|
||||
packageName: 'packageC',
|
||||
version: '0.0.0',
|
||||
hash: '$packageC0.0.0$',
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
appA: [
|
||||
{
|
||||
source: 'app',
|
||||
target: 'npm:packageA',
|
||||
type: DependencyType.static,
|
||||
},
|
||||
{
|
||||
source: 'app',
|
||||
target: 'npm:packageB',
|
||||
type: DependencyType.static,
|
||||
},
|
||||
{
|
||||
source: 'app',
|
||||
target: 'npm:packageC',
|
||||
type: DependencyType.static,
|
||||
},
|
||||
],
|
||||
appB: [
|
||||
{
|
||||
source: 'app',
|
||||
target: 'npm:packageC',
|
||||
type: DependencyType.static,
|
||||
},
|
||||
],
|
||||
'npm:packageC': [
|
||||
{
|
||||
source: 'app',
|
||||
target: 'npm:packageA',
|
||||
type: DependencyType.static,
|
||||
},
|
||||
{
|
||||
source: 'app',
|
||||
target: 'npm:packageB',
|
||||
type: DependencyType.static,
|
||||
},
|
||||
],
|
||||
'npm:packageB': [
|
||||
{
|
||||
source: 'app',
|
||||
target: 'npm:packageA',
|
||||
type: DependencyType.static,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
roots: ['app-build'],
|
||||
tasks: {
|
||||
'app-build': {
|
||||
id: 'app-build',
|
||||
target: { project: 'app', target: 'build' },
|
||||
overrides: {},
|
||||
},
|
||||
},
|
||||
dependencies: {},
|
||||
},
|
||||
{} as any,
|
||||
{},
|
||||
fileHasher
|
||||
);
|
||||
|
||||
const computeTaskHash = async (hasher, appName) => {
|
||||
const hashAppA = await hasher.hashTask({
|
||||
target: { project: appName, target: 'build' },
|
||||
id: `${appName}-build`,
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
|
||||
return hashAppA.value;
|
||||
};
|
||||
|
||||
const hasher1 = createHasher();
|
||||
|
||||
await computeTaskHash(hasher1, 'appA');
|
||||
const hashAppB1 = await computeTaskHash(hasher1, 'appB');
|
||||
|
||||
const hasher2 = createHasher();
|
||||
|
||||
const hashAppB2 = await computeTaskHash(hasher2, 'appB');
|
||||
await computeTaskHash(hasher2, 'appA');
|
||||
|
||||
expect(hashAppB1).toEqual(hashAppB2);
|
||||
});
|
||||
|
||||
it('should not hash when nx:run-commands executor', async () => {
|
||||
const hasher = new InProcessTaskHasher(
|
||||
{},
|
||||
|
||||
@ -332,7 +332,7 @@ class TaskHasherImpl {
|
||||
visited
|
||||
);
|
||||
} else {
|
||||
const hash = this.hashExternalDependency(d.target);
|
||||
const hash = this.hashExternalDependency(d.source, d.target);
|
||||
return {
|
||||
value: hash,
|
||||
details: {
|
||||
@ -408,16 +408,29 @@ class TaskHasherImpl {
|
||||
return partialHashes;
|
||||
}
|
||||
|
||||
private computeExternalDependencyIdentifier(
|
||||
sourceProjectName: string,
|
||||
targetProjectName: string
|
||||
): `${string}->${string}` {
|
||||
return `${sourceProjectName}->${targetProjectName}`;
|
||||
}
|
||||
|
||||
private hashExternalDependency(
|
||||
projectName: string,
|
||||
sourceProjectName: string,
|
||||
targetProjectName: string,
|
||||
visited = new Set<string>()
|
||||
): string {
|
||||
// try to retrieve the hash from cache
|
||||
if (this.externalDepsHashCache[projectName]) {
|
||||
return this.externalDepsHashCache[projectName];
|
||||
if (this.externalDepsHashCache[targetProjectName]) {
|
||||
return this.externalDepsHashCache[targetProjectName];
|
||||
}
|
||||
visited.add(projectName);
|
||||
const node = this.projectGraph.externalNodes[projectName];
|
||||
visited.add(
|
||||
this.computeExternalDependencyIdentifier(
|
||||
sourceProjectName,
|
||||
targetProjectName
|
||||
)
|
||||
);
|
||||
const node = this.projectGraph.externalNodes[targetProjectName];
|
||||
let partialHash: string;
|
||||
if (node) {
|
||||
const partialHashes: string[] = [];
|
||||
@ -429,22 +442,32 @@ class TaskHasherImpl {
|
||||
partialHashes.push(node.data.version);
|
||||
}
|
||||
// we want to calculate the hash of the entire dependency tree
|
||||
if (this.projectGraph.dependencies[projectName]) {
|
||||
this.projectGraph.dependencies[projectName].forEach((d) => {
|
||||
if (!visited.has(d.target)) {
|
||||
partialHashes.push(this.hashExternalDependency(d.target, visited));
|
||||
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)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
partialHash = hashArray(partialHashes);
|
||||
} 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
|
||||
partialHash = `__${projectName}__`;
|
||||
partialHash = `__${targetProjectName}__`;
|
||||
}
|
||||
this.externalDepsHashCache[projectName] = partialHash;
|
||||
this.externalDepsHashCache[targetProjectName] = partialHash;
|
||||
return partialHash;
|
||||
}
|
||||
|
||||
@ -470,7 +493,7 @@ class TaskHasherImpl {
|
||||
const executorPackage = target.executor.split(':')[0];
|
||||
const executorNodeName =
|
||||
this.findExternalDependencyNodeName(executorPackage);
|
||||
hash = this.hashExternalDependency(executorNodeName);
|
||||
hash = this.hashExternalDependency(projectName, executorNodeName);
|
||||
} else {
|
||||
// use command external dependencies if available to construct the hash
|
||||
const partialHashes: string[] = [];
|
||||
@ -482,7 +505,7 @@ class TaskHasherImpl {
|
||||
const externalDependencies = input['externalDependencies'];
|
||||
for (let dep of externalDependencies) {
|
||||
dep = this.findExternalDependencyNodeName(dep);
|
||||
partialHashes.push(this.hashExternalDependency(dep));
|
||||
partialHashes.push(this.hashExternalDependency(projectName, dep));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user