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|');
|
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 () => {
|
it('should not hash when nx:run-commands executor', async () => {
|
||||||
const hasher = new InProcessTaskHasher(
|
const hasher = new InProcessTaskHasher(
|
||||||
{},
|
{},
|
||||||
|
|||||||
@ -332,7 +332,7 @@ class TaskHasherImpl {
|
|||||||
visited
|
visited
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const hash = this.hashExternalDependency(d.target);
|
const hash = this.hashExternalDependency(d.source, d.target);
|
||||||
return {
|
return {
|
||||||
value: hash,
|
value: hash,
|
||||||
details: {
|
details: {
|
||||||
@ -408,16 +408,29 @@ class TaskHasherImpl {
|
|||||||
return partialHashes;
|
return partialHashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeExternalDependencyIdentifier(
|
||||||
|
sourceProjectName: string,
|
||||||
|
targetProjectName: string
|
||||||
|
): `${string}->${string}` {
|
||||||
|
return `${sourceProjectName}->${targetProjectName}`;
|
||||||
|
}
|
||||||
|
|
||||||
private hashExternalDependency(
|
private hashExternalDependency(
|
||||||
projectName: string,
|
sourceProjectName: string,
|
||||||
|
targetProjectName: string,
|
||||||
visited = new Set<string>()
|
visited = new Set<string>()
|
||||||
): string {
|
): string {
|
||||||
// try to retrieve the hash from cache
|
// try to retrieve the hash from cache
|
||||||
if (this.externalDepsHashCache[projectName]) {
|
if (this.externalDepsHashCache[targetProjectName]) {
|
||||||
return this.externalDepsHashCache[projectName];
|
return this.externalDepsHashCache[targetProjectName];
|
||||||
}
|
}
|
||||||
visited.add(projectName);
|
visited.add(
|
||||||
const node = this.projectGraph.externalNodes[projectName];
|
this.computeExternalDependencyIdentifier(
|
||||||
|
sourceProjectName,
|
||||||
|
targetProjectName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const node = this.projectGraph.externalNodes[targetProjectName];
|
||||||
let partialHash: string;
|
let partialHash: string;
|
||||||
if (node) {
|
if (node) {
|
||||||
const partialHashes: string[] = [];
|
const partialHashes: string[] = [];
|
||||||
@ -429,22 +442,32 @@ class TaskHasherImpl {
|
|||||||
partialHashes.push(node.data.version);
|
partialHashes.push(node.data.version);
|
||||||
}
|
}
|
||||||
// we want to calculate the hash of the entire dependency tree
|
// we want to calculate the hash of the entire dependency tree
|
||||||
if (this.projectGraph.dependencies[projectName]) {
|
if (this.projectGraph.dependencies[targetProjectName]) {
|
||||||
this.projectGraph.dependencies[projectName].forEach((d) => {
|
this.projectGraph.dependencies[targetProjectName].forEach((d) => {
|
||||||
if (!visited.has(d.target)) {
|
if (
|
||||||
partialHashes.push(this.hashExternalDependency(d.target, visited));
|
!visited.has(
|
||||||
|
this.computeExternalDependencyIdentifier(
|
||||||
|
targetProjectName,
|
||||||
|
d.target
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
partialHashes.push(
|
||||||
|
this.hashExternalDependency(targetProjectName, d.target, visited)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
partialHash = hashArray(partialHashes);
|
partialHash = hashArray(partialHashes);
|
||||||
} else {
|
} else {
|
||||||
// unknown dependency
|
// unknown dependency
|
||||||
// this may occur if dependency is not an npm package
|
// 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
|
// 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
|
// 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;
|
return partialHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,7 +493,7 @@ class TaskHasherImpl {
|
|||||||
const executorPackage = target.executor.split(':')[0];
|
const executorPackage = target.executor.split(':')[0];
|
||||||
const executorNodeName =
|
const executorNodeName =
|
||||||
this.findExternalDependencyNodeName(executorPackage);
|
this.findExternalDependencyNodeName(executorPackage);
|
||||||
hash = this.hashExternalDependency(executorNodeName);
|
hash = this.hashExternalDependency(projectName, executorNodeName);
|
||||||
} else {
|
} else {
|
||||||
// use command external dependencies if available to construct the hash
|
// use command external dependencies if available to construct the hash
|
||||||
const partialHashes: string[] = [];
|
const partialHashes: string[] = [];
|
||||||
@ -482,7 +505,7 @@ class TaskHasherImpl {
|
|||||||
const externalDependencies = input['externalDependencies'];
|
const externalDependencies = input['externalDependencies'];
|
||||||
for (let dep of externalDependencies) {
|
for (let dep of externalDependencies) {
|
||||||
dep = this.findExternalDependencyNodeName(dep);
|
dep = this.findExternalDependencyNodeName(dep);
|
||||||
partialHashes.push(this.hashExternalDependency(dep));
|
partialHashes.push(this.hashExternalDependency(projectName, dep));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user