feat(core): add hash details to the task object (#2943)
This commit is contained in:
parent
4341328dd4
commit
c4e33123fd
@ -6,7 +6,14 @@ jest.mock('hasha');
|
||||
jest.mock('fs');
|
||||
|
||||
describe('Hasher', () => {
|
||||
let hashes = { 'yarn.lock': 'yarn.lock.hash' };
|
||||
let hashes = {
|
||||
'yarn.lock': 'yarn.lock.hash',
|
||||
'nx.json': 'nx.json.hash',
|
||||
'package-lock.json': 'package-lock.json.hash',
|
||||
'package.json': 'package.json.hash',
|
||||
'tsconfig.json': 'tsconfig.json.hash',
|
||||
'workspace.json': 'workspace.json.hash',
|
||||
};
|
||||
beforeEach(() => {
|
||||
hasha.mockImplementation((values) => values.join('|'));
|
||||
hasha.fromFile.mockImplementation((path) => Promise.resolve(hashes[path]));
|
||||
@ -43,13 +50,30 @@ describe('Hasher', () => {
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
|
||||
expect(hash).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hash).toContain('file.hash'); //project files
|
||||
expect(hash).toContain('prop-value'); //overrides
|
||||
expect(hash).toContain('proj'); //project
|
||||
expect(hash).toContain('build'); //target
|
||||
expect(hash).toContain('runtime123'); //target
|
||||
expect(hash).toContain('runtime456'); //target
|
||||
expect(hash.value).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hash.value).toContain('file.hash'); //project files
|
||||
expect(hash.value).toContain('prop-value'); //overrides
|
||||
expect(hash.value).toContain('proj'); //project
|
||||
expect(hash.value).toContain('build'); //target
|
||||
expect(hash.value).toContain('runtime123'); //target
|
||||
expect(hash.value).toContain('runtime456'); //target
|
||||
|
||||
expect(hash.details.command).toEqual('proj|build||{"prop":"prop-value"}');
|
||||
expect(hash.details.sources).toEqual({
|
||||
proj: 'file.hash',
|
||||
});
|
||||
expect(hash.details.implicitDeps).toEqual({
|
||||
'yarn.lock': 'yarn.lock.hash',
|
||||
'nx.json': 'nx.json.hash',
|
||||
'package-lock.json': 'package-lock.json.hash',
|
||||
'package.json': 'package.json.hash',
|
||||
'tsconfig.json': 'tsconfig.json.hash',
|
||||
'workspace.json': 'workspace.json.hash',
|
||||
});
|
||||
expect(hash.details.runtime).toEqual({
|
||||
'echo runtime123': 'runtime123',
|
||||
'echo runtime456': 'runtime456',
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
@ -82,6 +106,46 @@ describe('Hasher', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should hash projects with dependencies', async (done) => {
|
||||
hashes['/filea'] = 'a.hash';
|
||||
hashes['/fileb'] = 'b.hash';
|
||||
const hasher = new Hasher(
|
||||
{
|
||||
nodes: {
|
||||
parent: {
|
||||
name: 'parent',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/filea', ext: '.ts', mtime: 1 }] },
|
||||
},
|
||||
child: {
|
||||
name: 'child',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/fileb', ext: '.ts', mtime: 1 }] },
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
parent: [{ source: 'parent', target: 'child', type: 'static' }],
|
||||
},
|
||||
},
|
||||
{} as any,
|
||||
{}
|
||||
);
|
||||
|
||||
const hasha = await hasher.hash({
|
||||
target: { project: 'parent', target: 'build' },
|
||||
id: 'parent-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
|
||||
// note that the parent hash is based on parent source files only!
|
||||
expect(hasha.details.sources).toEqual({
|
||||
parent: 'a.hash',
|
||||
child: 'b.hash',
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should hash when circular dependencies', async (done) => {
|
||||
hashes['/filea'] = 'a.hash';
|
||||
hashes['/fileb'] = 'b.hash';
|
||||
@ -114,12 +178,13 @@ describe('Hasher', () => {
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
|
||||
expect(hasha).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hasha).toContain('a.hash'); //project files
|
||||
expect(hasha).toContain('b.hash'); //project files
|
||||
expect(hasha).toContain('prop-value'); //overrides
|
||||
expect(hasha).toContain('proj'); //project
|
||||
expect(hasha).toContain('build'); //target
|
||||
expect(hasha.value).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hasha.value).toContain('a.hash'); //project files
|
||||
expect(hasha.value).toContain('b.hash'); //project files
|
||||
expect(hasha.value).toContain('prop-value'); //overrides
|
||||
expect(hasha.value).toContain('proj'); //project
|
||||
expect(hasha.value).toContain('build'); //target
|
||||
expect(hasha.details.sources).toEqual({ proja: 'a.hash', projb: 'b.hash' });
|
||||
|
||||
const hashb = await hasher.hash({
|
||||
target: { project: 'projb', target: 'build' },
|
||||
@ -127,12 +192,13 @@ describe('Hasher', () => {
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
|
||||
expect(hashb).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hashb).toContain('a.hash'); //project files
|
||||
expect(hashb).toContain('b.hash'); //project files
|
||||
expect(hashb).toContain('prop-value'); //overrides
|
||||
expect(hashb).toContain('proj'); //project
|
||||
expect(hashb).toContain('build'); //target
|
||||
expect(hashb.value).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hashb.value).toContain('a.hash'); //project files
|
||||
expect(hashb.value).toContain('b.hash'); //project files
|
||||
expect(hashb.value).toContain('prop-value'); //overrides
|
||||
expect(hashb.value).toContain('proj'); //project
|
||||
expect(hashb.value).toContain('build'); //target
|
||||
expect(hashb.details.sources).toEqual({ proja: 'a.hash', projb: 'b.hash' });
|
||||
|
||||
done();
|
||||
});
|
||||
@ -158,11 +224,13 @@ describe('Hasher', () => {
|
||||
{}
|
||||
);
|
||||
|
||||
const hash = await hasher.hash({
|
||||
target: { project: 'proja', target: 'build' },
|
||||
id: 'proja-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
const hash = (
|
||||
await hasher.hash({
|
||||
target: { project: 'proja', target: 'build' },
|
||||
id: 'proja-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
})
|
||||
).value;
|
||||
|
||||
expect(hash).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hash).toContain('5000001'); //project files
|
||||
|
||||
@ -7,10 +7,35 @@ import { execSync } from 'child_process';
|
||||
|
||||
const hasha = require('hasha');
|
||||
|
||||
export interface Hash {
|
||||
value: string;
|
||||
details: {
|
||||
command: string;
|
||||
sources: { [projectName: string]: string };
|
||||
implicitDeps: { [key: string]: string };
|
||||
runtime: { [input: string]: string };
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectHashResult {
|
||||
value: string;
|
||||
sources: { [projectName: string]: string };
|
||||
}
|
||||
|
||||
interface ImplicitHashResult {
|
||||
value: string;
|
||||
sources: { [fileName: string]: string };
|
||||
}
|
||||
|
||||
interface RuntimeHashResult {
|
||||
value: string;
|
||||
runtime: { [input: string]: string };
|
||||
}
|
||||
|
||||
export class Hasher {
|
||||
static version = '1.0';
|
||||
implicitDependencies: Promise<string>;
|
||||
runtimeInputs: Promise<string>;
|
||||
implicitDependencies: Promise<ImplicitHashResult>;
|
||||
runtimeInputs: Promise<RuntimeHashResult>;
|
||||
fileHashes = new FileHashes();
|
||||
projectHashes = new ProjectHashes(this.projectGraph, this.fileHashes);
|
||||
|
||||
@ -20,13 +45,16 @@ export class Hasher {
|
||||
private readonly options: any
|
||||
) {}
|
||||
|
||||
async hash(task: Task): Promise<string> {
|
||||
const args = [
|
||||
task.target.project || '',
|
||||
task.target.target || '',
|
||||
task.target.configuration || '',
|
||||
JSON.stringify(task.overrides),
|
||||
];
|
||||
async hash(task: Task): Promise<Hash> {
|
||||
const command = hasha(
|
||||
[
|
||||
task.target.project || '',
|
||||
task.target.target || '',
|
||||
task.target.configuration || '',
|
||||
JSON.stringify(task.overrides),
|
||||
],
|
||||
{ algorithm: 'sha256' }
|
||||
);
|
||||
|
||||
const values = await Promise.all([
|
||||
this.projectHashes.hashProject(task.target.project, [
|
||||
@ -36,10 +64,25 @@ export class Hasher {
|
||||
this.runtimeInputsHash(),
|
||||
]);
|
||||
|
||||
return hasha([Hasher.version, ...args, ...values], { algorithm: 'sha256' });
|
||||
const value = hasha(
|
||||
[Hasher.version, command, ...values.map((v) => v.value)],
|
||||
{
|
||||
algorithm: 'sha256',
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
value,
|
||||
details: {
|
||||
command,
|
||||
sources: values[0].sources,
|
||||
implicitDeps: values[1].sources,
|
||||
runtime: values[2].runtime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async runtimeInputsHash() {
|
||||
private async runtimeInputsHash(): Promise<RuntimeHashResult> {
|
||||
if (this.runtimeInputs) return this.runtimeInputs;
|
||||
|
||||
const inputs =
|
||||
@ -48,34 +91,65 @@ export class Hasher {
|
||||
: [];
|
||||
if (inputs.length > 0) {
|
||||
try {
|
||||
const values = await Promise.all(
|
||||
inputs.map(async (i) => execSync(i).toString().trim())
|
||||
const values = (await Promise.all(
|
||||
inputs.map(async (input) => {
|
||||
const value = execSync(input).toString().trim();
|
||||
return { input, value };
|
||||
})
|
||||
)) as any;
|
||||
|
||||
const value = await hasha(
|
||||
values.map((v) => v.value),
|
||||
{
|
||||
algorithm: 'sha256',
|
||||
}
|
||||
);
|
||||
this.runtimeInputs = hasha(values, { algorithm: 'sha256' });
|
||||
const runtime = values.reduce(
|
||||
(m, c) => ((m[c.input] = c.value), m),
|
||||
{}
|
||||
);
|
||||
return { value, runtime };
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Nx failed to execute runtimeCacheInputs defined in nx.json failed:\n${e.message}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.runtimeInputs = Promise.resolve('');
|
||||
this.runtimeInputs = Promise.resolve({ value: '', runtime: {} });
|
||||
}
|
||||
|
||||
return this.runtimeInputs;
|
||||
}
|
||||
|
||||
private async implicitDepsHash() {
|
||||
private async implicitDepsHash(): Promise<ImplicitHashResult> {
|
||||
if (this.implicitDependencies) return this.implicitDependencies;
|
||||
|
||||
const values = await Promise.all([
|
||||
...Object.keys(this.nxJson.implicitDependencies || {}).map((r) =>
|
||||
this.fileHashes.hashFile(r)
|
||||
),
|
||||
...rootWorkspaceFileNames().map((r) => this.fileHashes.hashFile(r)),
|
||||
this.fileHashes.hashFile('package-lock.json'),
|
||||
this.fileHashes.hashFile('yarn.lock'),
|
||||
]);
|
||||
this.implicitDependencies = hasha(values, { algorithm: 'sha256' });
|
||||
const fileNames = [
|
||||
...Object.keys(this.nxJson.implicitDependencies || {}),
|
||||
...rootWorkspaceFileNames(),
|
||||
'package-lock.json',
|
||||
'yarn.lock',
|
||||
];
|
||||
|
||||
this.implicitDependencies = Promise.resolve().then(async () => {
|
||||
const fileHashes = await Promise.all(
|
||||
fileNames.map(async (file) => {
|
||||
const hash = await this.fileHashes.hashFile(file);
|
||||
return { file, hash };
|
||||
})
|
||||
);
|
||||
|
||||
const combinedHash = await hasha(
|
||||
fileHashes.map((v) => v.hash),
|
||||
{
|
||||
algorithm: 'sha256',
|
||||
}
|
||||
);
|
||||
return {
|
||||
value: combinedHash,
|
||||
sources: fileHashes.reduce((m, c) => ((m[c.file] = c.hash), m), {}),
|
||||
};
|
||||
});
|
||||
return this.implicitDependencies;
|
||||
}
|
||||
}
|
||||
@ -88,20 +162,36 @@ export class ProjectHashes {
|
||||
private readonly fileHashes: FileHashes
|
||||
) {}
|
||||
|
||||
async hashProject(projectName: string, visited: string[]) {
|
||||
async hashProject(
|
||||
projectName: string,
|
||||
visited: string[]
|
||||
): Promise<ProjectHashResult> {
|
||||
return Promise.resolve().then(async () => {
|
||||
const deps = (this.projectGraph.dependencies[projectName] || []).map(
|
||||
(t) => {
|
||||
if (visited.indexOf(t.target) > -1) {
|
||||
return '';
|
||||
} else {
|
||||
visited.push(t.target);
|
||||
return this.hashProject(t.target, visited);
|
||||
}
|
||||
}
|
||||
const deps = this.projectGraph.dependencies[projectName] || [];
|
||||
const depHashes = (
|
||||
await Promise.all(
|
||||
deps.map(async (d) => {
|
||||
if (visited.indexOf(d.target) > -1) {
|
||||
return null;
|
||||
} else {
|
||||
visited.push(d.target);
|
||||
return await this.hashProject(d.target, visited);
|
||||
}
|
||||
})
|
||||
)
|
||||
).filter((r) => !!r);
|
||||
const projectHash = await this.hashProjectNodeSource(projectName);
|
||||
const sources = depHashes.reduce(
|
||||
(m, c) => {
|
||||
return { ...m, ...c.sources };
|
||||
},
|
||||
{ [projectName]: projectHash }
|
||||
);
|
||||
const sources = this.hashProjectNodeSource(projectName);
|
||||
return hasha(await Promise.all([...deps, sources]));
|
||||
const value = await hasha([
|
||||
...depHashes.map((d) => d.value),
|
||||
projectHash,
|
||||
]);
|
||||
return { value, sources };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -44,7 +44,9 @@ export async function runCommand<T extends RunArgs>(
|
||||
const hasher = new Hasher(projectGraph, nxJson, tasksOptions);
|
||||
await Promise.all(
|
||||
tasks.map(async (t) => {
|
||||
t.hash = await hasher.hash(t);
|
||||
const hash = await hasher.hash(t);
|
||||
t.hash = hash.value;
|
||||
t.hashDetails = hash.details;
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@ -9,6 +9,12 @@ export interface Task {
|
||||
target: Target;
|
||||
overrides: Object;
|
||||
hash?: string;
|
||||
hashDetails?: {
|
||||
command: string;
|
||||
sources: { [projectName: string]: string };
|
||||
implicitDeps: { [key: string]: string };
|
||||
runtime: { [input: string]: string };
|
||||
};
|
||||
}
|
||||
|
||||
export enum AffectedEventType {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user