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'); jest.mock('fs');
describe('Hasher', () => { 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(() => { beforeEach(() => {
hasha.mockImplementation((values) => values.join('|')); hasha.mockImplementation((values) => values.join('|'));
hasha.fromFile.mockImplementation((path) => Promise.resolve(hashes[path])); hasha.fromFile.mockImplementation((path) => Promise.resolve(hashes[path]));
@ -43,13 +50,30 @@ describe('Hasher', () => {
overrides: { prop: 'prop-value' }, overrides: { prop: 'prop-value' },
}); });
expect(hash).toContain('yarn.lock.hash'); //implicits expect(hash.value).toContain('yarn.lock.hash'); //implicits
expect(hash).toContain('file.hash'); //project files expect(hash.value).toContain('file.hash'); //project files
expect(hash).toContain('prop-value'); //overrides expect(hash.value).toContain('prop-value'); //overrides
expect(hash).toContain('proj'); //project expect(hash.value).toContain('proj'); //project
expect(hash).toContain('build'); //target expect(hash.value).toContain('build'); //target
expect(hash).toContain('runtime123'); //target expect(hash.value).toContain('runtime123'); //target
expect(hash).toContain('runtime456'); //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(); 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) => { it('should hash when circular dependencies', async (done) => {
hashes['/filea'] = 'a.hash'; hashes['/filea'] = 'a.hash';
hashes['/fileb'] = 'b.hash'; hashes['/fileb'] = 'b.hash';
@ -114,12 +178,13 @@ describe('Hasher', () => {
overrides: { prop: 'prop-value' }, overrides: { prop: 'prop-value' },
}); });
expect(hasha).toContain('yarn.lock.hash'); //implicits expect(hasha.value).toContain('yarn.lock.hash'); //implicits
expect(hasha).toContain('a.hash'); //project files expect(hasha.value).toContain('a.hash'); //project files
expect(hasha).toContain('b.hash'); //project files expect(hasha.value).toContain('b.hash'); //project files
expect(hasha).toContain('prop-value'); //overrides expect(hasha.value).toContain('prop-value'); //overrides
expect(hasha).toContain('proj'); //project expect(hasha.value).toContain('proj'); //project
expect(hasha).toContain('build'); //target expect(hasha.value).toContain('build'); //target
expect(hasha.details.sources).toEqual({ proja: 'a.hash', projb: 'b.hash' });
const hashb = await hasher.hash({ const hashb = await hasher.hash({
target: { project: 'projb', target: 'build' }, target: { project: 'projb', target: 'build' },
@ -127,12 +192,13 @@ describe('Hasher', () => {
overrides: { prop: 'prop-value' }, overrides: { prop: 'prop-value' },
}); });
expect(hashb).toContain('yarn.lock.hash'); //implicits expect(hashb.value).toContain('yarn.lock.hash'); //implicits
expect(hashb).toContain('a.hash'); //project files expect(hashb.value).toContain('a.hash'); //project files
expect(hashb).toContain('b.hash'); //project files expect(hashb.value).toContain('b.hash'); //project files
expect(hashb).toContain('prop-value'); //overrides expect(hashb.value).toContain('prop-value'); //overrides
expect(hashb).toContain('proj'); //project expect(hashb.value).toContain('proj'); //project
expect(hashb).toContain('build'); //target expect(hashb.value).toContain('build'); //target
expect(hashb.details.sources).toEqual({ proja: 'a.hash', projb: 'b.hash' });
done(); done();
}); });
@ -158,11 +224,13 @@ describe('Hasher', () => {
{} {}
); );
const hash = await hasher.hash({ const hash = (
target: { project: 'proja', target: 'build' }, await hasher.hash({
id: 'proja-build', target: { project: 'proja', target: 'build' },
overrides: { prop: 'prop-value' }, id: 'proja-build',
}); overrides: { prop: 'prop-value' },
})
).value;
expect(hash).toContain('yarn.lock.hash'); //implicits expect(hash).toContain('yarn.lock.hash'); //implicits
expect(hash).toContain('5000001'); //project files expect(hash).toContain('5000001'); //project files

View File

@ -7,10 +7,35 @@ import { execSync } from 'child_process';
const hasha = require('hasha'); 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 { export class Hasher {
static version = '1.0'; static version = '1.0';
implicitDependencies: Promise<string>; implicitDependencies: Promise<ImplicitHashResult>;
runtimeInputs: Promise<string>; runtimeInputs: Promise<RuntimeHashResult>;
fileHashes = new FileHashes(); fileHashes = new FileHashes();
projectHashes = new ProjectHashes(this.projectGraph, this.fileHashes); projectHashes = new ProjectHashes(this.projectGraph, this.fileHashes);
@ -20,13 +45,16 @@ export class Hasher {
private readonly options: any private readonly options: any
) {} ) {}
async hash(task: Task): Promise<string> { async hash(task: Task): Promise<Hash> {
const args = [ const command = hasha(
task.target.project || '', [
task.target.target || '', task.target.project || '',
task.target.configuration || '', task.target.target || '',
JSON.stringify(task.overrides), task.target.configuration || '',
]; JSON.stringify(task.overrides),
],
{ algorithm: 'sha256' }
);
const values = await Promise.all([ const values = await Promise.all([
this.projectHashes.hashProject(task.target.project, [ this.projectHashes.hashProject(task.target.project, [
@ -36,10 +64,25 @@ export class Hasher {
this.runtimeInputsHash(), 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; if (this.runtimeInputs) return this.runtimeInputs;
const inputs = const inputs =
@ -48,34 +91,65 @@ export class Hasher {
: []; : [];
if (inputs.length > 0) { if (inputs.length > 0) {
try { try {
const values = await Promise.all( const values = (await Promise.all(
inputs.map(async (i) => execSync(i).toString().trim()) 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) { } catch (e) {
throw new Error( throw new Error(
`Nx failed to execute runtimeCacheInputs defined in nx.json failed:\n${e.message}` `Nx failed to execute runtimeCacheInputs defined in nx.json failed:\n${e.message}`
); );
} }
} else { } else {
this.runtimeInputs = Promise.resolve(''); this.runtimeInputs = Promise.resolve({ value: '', runtime: {} });
} }
return this.runtimeInputs; return this.runtimeInputs;
} }
private async implicitDepsHash() { private async implicitDepsHash(): Promise<ImplicitHashResult> {
if (this.implicitDependencies) return this.implicitDependencies; if (this.implicitDependencies) return this.implicitDependencies;
const values = await Promise.all([ const fileNames = [
...Object.keys(this.nxJson.implicitDependencies || {}).map((r) => ...Object.keys(this.nxJson.implicitDependencies || {}),
this.fileHashes.hashFile(r) ...rootWorkspaceFileNames(),
), 'package-lock.json',
...rootWorkspaceFileNames().map((r) => this.fileHashes.hashFile(r)), 'yarn.lock',
this.fileHashes.hashFile('package-lock.json'), ];
this.fileHashes.hashFile('yarn.lock'),
]); this.implicitDependencies = Promise.resolve().then(async () => {
this.implicitDependencies = hasha(values, { algorithm: 'sha256' }); 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; return this.implicitDependencies;
} }
} }
@ -88,20 +162,36 @@ export class ProjectHashes {
private readonly fileHashes: FileHashes private readonly fileHashes: FileHashes
) {} ) {}
async hashProject(projectName: string, visited: string[]) { async hashProject(
projectName: string,
visited: string[]
): Promise<ProjectHashResult> {
return Promise.resolve().then(async () => { return Promise.resolve().then(async () => {
const deps = (this.projectGraph.dependencies[projectName] || []).map( const deps = this.projectGraph.dependencies[projectName] || [];
(t) => { const depHashes = (
if (visited.indexOf(t.target) > -1) { await Promise.all(
return ''; deps.map(async (d) => {
} else { if (visited.indexOf(d.target) > -1) {
visited.push(t.target); return null;
return this.hashProject(t.target, visited); } 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); const value = await hasha([
return hasha(await Promise.all([...deps, sources])); ...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); const hasher = new Hasher(projectGraph, nxJson, tasksOptions);
await Promise.all( await Promise.all(
tasks.map(async (t) => { 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; target: Target;
overrides: Object; overrides: Object;
hash?: string; hash?: string;
hashDetails?: {
command: string;
sources: { [projectName: string]: string };
implicitDeps: { [key: string]: string };
runtime: { [input: string]: string };
};
} }
export enum AffectedEventType { export enum AffectedEventType {