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');
|
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
|
||||||
|
|||||||
@ -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 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user