Skip to content

Commit

Permalink
feat(install): Implement a very basic hook system (#5293)
Browse files Browse the repository at this point in the history
* feat(install): Implement a very basic hook system

This PR adds a very basic and undocumented hook system. I plan to use it internally to get better
stats on how Yarn performs, and how much time is spent on the linking step.

* Adds tests

* Improves the typing of callThroughHook

* Lints
  • Loading branch information
arcanis committed Feb 2, 2018
1 parent d68b6c9 commit aee005a
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 46 deletions.
43 changes: 43 additions & 0 deletions __tests__/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* @flow */

import {runInstall} from './commands/_helpers.js';

test('install should call the resolveStep hook', async () => {
global.experimentalYarnHooks = {
resolveStep: jest.fn(cb => cb()),
};
await runInstall({}, 'install-production', config => {
expect(global.experimentalYarnHooks.resolveStep.mock.calls.length).toEqual(1);
});
delete global.experimentalYarnHooks;
});

test('install should call the fetchStep hook', async () => {
global.experimentalYarnHooks = {
fetchStep: jest.fn(cb => cb()),
};
await runInstall({}, 'install-production', config => {
expect(global.experimentalYarnHooks.fetchStep.mock.calls.length).toEqual(1);
});
delete global.experimentalYarnHooks;
});

test('install should call the linkStep hook', async () => {
global.experimentalYarnHooks = {
linkStep: jest.fn(cb => cb()),
};
await runInstall({}, 'install-production', config => {
expect(global.experimentalYarnHooks.linkStep.mock.calls.length).toEqual(1);
});
delete global.experimentalYarnHooks;
});

test('install should call the buildStep hook', async () => {
global.experimentalYarnHooks = {
buildStep: jest.fn(cb => cb()),
};
await runInstall({}, 'install-production', config => {
expect(global.experimentalYarnHooks.buildStep.mock.calls.length).toEqual(1);
});
delete global.experimentalYarnHooks;
});
101 changes: 55 additions & 46 deletions src/cli/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {Manifest, DependencyRequestPatterns} from '../../types.js';
import type Config from '../../config.js';
import type {RegistryNames} from '../../registries/index.js';
import type {LockfileObject} from '../../lockfile';
import {callThroughHook} from '../../util/hooks.js';
import normalizeManifest from '../../util/normalize-manifest/index.js';
import {MessageError} from '../../errors.js';
import InstallationIntegrityChecker from '../../integrity-checker.js';
Expand Down Expand Up @@ -531,56 +532,64 @@ export class Install {
});
}

steps.push(async (curr: number, total: number) => {
this.reporter.step(curr, total, this.reporter.lang('resolvingPackages'), emoji.get('mag'));
this.resolutionMap.setTopLevelPatterns(rawPatterns);
await this.resolver.init(this.prepareRequests(depRequests), {
isFlat: this.flags.flat,
isFrozen: this.flags.frozenLockfile,
workspaceLayout,
});
topLevelPatterns = this.preparePatterns(rawPatterns);
flattenedTopLevelPatterns = await this.flatten(topLevelPatterns);
return {bailout: await this.bailout(topLevelPatterns, workspaceLayout)};
});
steps.push((curr: number, total: number) =>
callThroughHook('resolveStep', async () => {
this.reporter.step(curr, total, this.reporter.lang('resolvingPackages'), emoji.get('mag'));
this.resolutionMap.setTopLevelPatterns(rawPatterns);
await this.resolver.init(this.prepareRequests(depRequests), {
isFlat: this.flags.flat,
isFrozen: this.flags.frozenLockfile,
workspaceLayout,
});
topLevelPatterns = this.preparePatterns(rawPatterns);
flattenedTopLevelPatterns = await this.flatten(topLevelPatterns);
return {bailout: await this.bailout(topLevelPatterns, workspaceLayout)};
}),
);

steps.push(async (curr: number, total: number) => {
this.markIgnored(ignorePatterns);
this.reporter.step(curr, total, this.reporter.lang('fetchingPackages'), emoji.get('truck'));
const manifests: Array<Manifest> = await fetcher.fetch(this.resolver.getManifests(), this.config);
this.resolver.updateManifests(manifests);
await compatibility.check(this.resolver.getManifests(), this.config, this.flags.ignoreEngines);
});
steps.push((curr: number, total: number) =>
callThroughHook('fetchStep', async () => {
this.markIgnored(ignorePatterns);
this.reporter.step(curr, total, this.reporter.lang('fetchingPackages'), emoji.get('truck'));
const manifests: Array<Manifest> = await fetcher.fetch(this.resolver.getManifests(), this.config);
this.resolver.updateManifests(manifests);
await compatibility.check(this.resolver.getManifests(), this.config, this.flags.ignoreEngines);
}),
);

steps.push(async (curr: number, total: number) => {
// remove integrity hash to make this operation atomic
await this.integrityChecker.removeIntegrityFile();
this.reporter.step(curr, total, this.reporter.lang('linkingDependencies'), emoji.get('link'));
flattenedTopLevelPatterns = this.preparePatternsForLinking(
flattenedTopLevelPatterns,
manifest,
this.config.lockfileFolder === this.config.cwd,
);
await this.linker.init(flattenedTopLevelPatterns, workspaceLayout, {
linkDuplicates: this.flags.linkDuplicates,
ignoreOptional: this.flags.ignoreOptional,
});
});
steps.push((curr: number, total: number) =>
callThroughHook('linkStep', async () => {
// remove integrity hash to make this operation atomic
await this.integrityChecker.removeIntegrityFile();
this.reporter.step(curr, total, this.reporter.lang('linkingDependencies'), emoji.get('link'));
flattenedTopLevelPatterns = this.preparePatternsForLinking(
flattenedTopLevelPatterns,
manifest,
this.config.lockfileFolder === this.config.cwd,
);
await this.linker.init(flattenedTopLevelPatterns, workspaceLayout, {
linkDuplicates: this.flags.linkDuplicates,
ignoreOptional: this.flags.ignoreOptional,
});
}),
);

steps.push(async (curr: number, total: number) => {
this.reporter.step(
curr,
total,
this.flags.force ? this.reporter.lang('rebuildingPackages') : this.reporter.lang('buildingFreshPackages'),
emoji.get('page_with_curl'),
);
steps.push((curr: number, total: number) =>
callThroughHook('buildStep', async () => {
this.reporter.step(
curr,
total,
this.flags.force ? this.reporter.lang('rebuildingPackages') : this.reporter.lang('buildingFreshPackages'),
emoji.get('page_with_curl'),
);

if (this.flags.ignoreScripts) {
this.reporter.warn(this.reporter.lang('ignoredScripts'));
} else {
await this.scripts.init(flattenedTopLevelPatterns);
}
});
if (this.flags.ignoreScripts) {
this.reporter.warn(this.reporter.lang('ignoredScripts'));
} else {
await this.scripts.init(flattenedTopLevelPatterns);
}
}),
);

if (this.flags.har) {
steps.push(async (curr: number, total: number) => {
Expand Down
23 changes: 23 additions & 0 deletions src/util/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* @flow */

export type YarnHook = 'resolveStep' | 'fetchStep' | 'linkStep' | 'buildStep';

const YARN_HOOKS_KEY = 'experimentalYarnHooks';

export function callThroughHook<T>(type: YarnHook, fn: () => T): T {
if (typeof global === 'undefined') {
return fn();
}

if (typeof global[YARN_HOOKS_KEY] !== 'object' || !global[YARN_HOOKS_KEY]) {
return fn();
}

const hook: (() => T) => T = global[YARN_HOOKS_KEY][type];

if (!hook) {
return fn();
}

return hook(fn);
}

0 comments on commit aee005a

Please sign in to comment.