feat(core): add hash details to the task object (#2943)

This commit is contained in:
Jason Jean 2020-04-30 11:17:48 -04:00 committed by GitHub
parent 4341328dd4
commit c4e33123fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 229 additions and 63 deletions

View File

@ -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

View File

@ -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 };
});
}

View File

@ -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;
})
);

View File

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