Skip to content

feat(hooks): project persistent hooks #5597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 181 additions & 113 deletions lib/common/services/hooks-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
IProjectHelper,
IStringDictionary,
} from "../declarations";
import {
INsConfigHooks,
IProjectConfigService,
} from "../../definitions/project";
import { IInjector } from "../definitions/yok";
import { injector } from "../yok";

Expand All @@ -38,7 +42,8 @@ export class HooksService implements IHooksService {
private $injector: IInjector,
private $projectHelper: IProjectHelper,
private $options: IOptions,
private $performanceService: IPerformanceService
private $performanceService: IPerformanceService,
private $projectConfigService: IProjectConfigService
) {}

public get hookArgsName(): string {
Expand All @@ -61,6 +66,12 @@ export class HooksService implements IHooksService {
this.$logger.trace(
"Hooks directories: " + util.inspect(this.hooksDirectories)
);

const customHooks = this.$projectConfigService.getValue("hooks") || [];

if (customHooks.length) {
this.$logger.trace("Custom hooks: " + util.inspect(customHooks));
}
}

private static formatHookName(commandName: string): string {
Expand Down Expand Up @@ -118,6 +129,19 @@ export class HooksService implements IHooksService {
)
);
}

const customHooks = this.getCustomHooksByName(hookName);

for (const hook of customHooks) {
results.push(
await this.executeHook(
this.$projectHelper.projectDir,
hookName,
hook,
hookArguments
)
);
}
} catch (err) {
this.$logger.trace(`Failed during hook execution ${hookName}.`);
this.$errors.fail(err.message || err);
Expand All @@ -126,142 +150,186 @@ export class HooksService implements IHooksService {
return _.flatten(results);
}

private async executeHooksInDirectory(
private async executeHook(
directoryPath: string,
hookName: string,
hook: IHook,
hookArguments?: IDictionary<any>
): Promise<any[]> {
): Promise<any> {
hookArguments = hookArguments || {};
const results: any[] = [];
const hooks = this.getHooksByName(directoryPath, hookName);

for (let i = 0; i < hooks.length; ++i) {
const hook = hooks[i];
const relativePath = path.relative(directoryPath, hook.fullPath);
const trackId = relativePath.replace(
new RegExp("\\" + path.sep, "g"),
AnalyticsEventLabelDelimiter
);
let command = this.getSheBangInterpreter(hook);
let inProc = false;
if (!command) {
command = hook.fullPath;
if (path.extname(hook.fullPath).toLowerCase() === ".js") {
command = process.argv[0];
inProc = this.shouldExecuteInProcess(
this.$fs.readText(hook.fullPath)
);
}
let result;

const relativePath = path.relative(directoryPath, hook.fullPath);
const trackId = relativePath.replace(
new RegExp("\\" + path.sep, "g"),
AnalyticsEventLabelDelimiter
);
let command = this.getSheBangInterpreter(hook);
let inProc = false;
if (!command) {
command = hook.fullPath;
if (path.extname(hook.fullPath).toLowerCase() === ".js") {
command = process.argv[0];
inProc = this.shouldExecuteInProcess(this.$fs.readText(hook.fullPath));
}
}

const startTime = this.$performanceService.now();
if (inProc) {
this.$logger.trace(
"Executing %s hook at location %s in-process",
hookName,
hook.fullPath
);
const hookEntryPoint = require(hook.fullPath);
const startTime = this.$performanceService.now();
if (inProc) {
this.$logger.trace(
"Executing %s hook at location %s in-process",
hookName,
hook.fullPath
);
const hookEntryPoint = require(hook.fullPath);

this.$logger.trace(`Validating ${hookName} arguments.`);
this.$logger.trace(`Validating ${hookName} arguments.`);

const invalidArguments = this.validateHookArguments(
hookEntryPoint,
hook.fullPath
);
const invalidArguments = this.validateHookArguments(
hookEntryPoint,
hook.fullPath
);

if (invalidArguments.length) {
this.$logger.warn(
`${
hook.fullPath
} will NOT be executed because it has invalid arguments - ${
invalidArguments.join(", ").grey
}.`
);
continue;
}
if (invalidArguments.length) {
this.$logger.warn(
`${
hook.fullPath
} will NOT be executed because it has invalid arguments - ${
invalidArguments.join(", ").grey
}.`
);
return;
}

// HACK for backwards compatibility:
// In case $projectData wasn't resolved by the time we got here (most likely we got here without running a command but through a service directly)
// then it is probably passed as a hookArg
// if that is the case then pass it directly to the hook instead of trying to resolve $projectData via injector
// This helps make hooks stateless
const projectDataHookArg =
hookArguments["hookArgs"] && hookArguments["hookArgs"]["projectData"];
if (projectDataHookArg) {
hookArguments["projectData"] = hookArguments[
"$projectData"
] = projectDataHookArg;
}
// HACK for backwards compatibility:
// In case $projectData wasn't resolved by the time we got here (most likely we got here without running a command but through a service directly)
// then it is probably passed as a hookArg
// if that is the case then pass it directly to the hook instead of trying to resolve $projectData via injector
// This helps make hooks stateless
const projectDataHookArg =
hookArguments["hookArgs"] && hookArguments["hookArgs"]["projectData"];
if (projectDataHookArg) {
hookArguments["projectData"] = hookArguments[
"$projectData"
] = projectDataHookArg;
}

const maybePromise = this.$injector.resolve(
hookEntryPoint,
hookArguments
);
if (maybePromise) {
this.$logger.trace("Hook promises to signal completion");
try {
const result = await maybePromise;
results.push(result);
} catch (err) {
if (
err &&
_.isBoolean(err.stopExecution) &&
err.errorAsWarning === true
) {
this.$logger.warn(err.message || err);
} else {
// Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles.
this.$logger.error(err);
throw (
err || new Error(`Failed to execute hook: ${hook.fullPath}.`)
);
}
const maybePromise = this.$injector.resolve(
hookEntryPoint,
hookArguments
);
if (maybePromise) {
this.$logger.trace("Hook promises to signal completion");
try {
result = await maybePromise;
} catch (err) {
if (
err &&
_.isBoolean(err.stopExecution) &&
err.errorAsWarning === true
) {
this.$logger.warn(err.message || err);
} else {
// Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles.
this.$logger.error(err);
throw err || new Error(`Failed to execute hook: ${hook.fullPath}.`);
}

this.$logger.trace("Hook completed");
}
} else {
const environment = this.prepareEnvironment(hook.fullPath);
this.$logger.trace(
"Executing %s hook at location %s with environment ",
hookName,
hook.fullPath,
environment
);

const output = await this.$childProcess.spawnFromEvent(
command,
[hook.fullPath],
"close",
environment,
{ throwError: false }
);
results.push(output);
this.$logger.trace("Hook completed");
}
} else {
const environment = this.prepareEnvironment(hook.fullPath);
this.$logger.trace(
"Executing %s hook at location %s with environment ",
hookName,
hook.fullPath,
environment
);

if (output.exitCode !== 0) {
throw new Error(output.stdout + output.stderr);
}
const output = await this.$childProcess.spawnFromEvent(
command,
[hook.fullPath],
"close",
environment,
{ throwError: false }
);
result = output;

this.$logger.trace(
"Finished executing %s hook at location %s with environment ",
hookName,
hook.fullPath,
environment
);
if (output.exitCode !== 0) {
throw new Error(output.stdout + output.stderr);
}
const endTime = this.$performanceService.now();
this.$performanceService.processExecutionData(
trackId,
startTime,
endTime,
[hookArguments]

this.$logger.trace(
"Finished executing %s hook at location %s with environment ",
hookName,
hook.fullPath,
environment
);
}
const endTime = this.$performanceService.now();
this.$performanceService.processExecutionData(trackId, startTime, endTime, [
hookArguments,
]);

return result;
}

private async executeHooksInDirectory(
directoryPath: string,
hookName: string,
hookArguments?: IDictionary<any>
): Promise<any[]> {
hookArguments = hookArguments || {};
const results: any[] = [];
const hooks = this.getHooksByName(directoryPath, hookName);

for (let i = 0; i < hooks.length; ++i) {
const hook = hooks[i];
const result = await this.executeHook(
directoryPath,
hookName,
hook,
hookArguments
);

if (result) {
results.push(result);
}
}

return results;
}

private getCustomHooksByName(hookName: string): IHook[] {
const hooks: IHook[] = [];
const customHooks: INsConfigHooks[] =
this.$projectConfigService.getValue("hooks") || [];

for (const cHook of customHooks) {
if (cHook.type === hookName) {
const fullPath = path.join(
this.$projectHelper.projectDir,
cHook.script
);
const isFile = this.$fs.getFsStats(fullPath).isFile();

if (isFile) {
const fileNameParts = cHook.script.split("/");
hooks.push(
new Hook(
this.getBaseFilename(fileNameParts[fileNameParts.length - 1]),
fullPath
)
);
}
}
}

return hooks;
}

private getHooksByName(directoryPath: string, hookName: string): IHook[] {
const allBaseHooks = this.getHooksInDirectory(directoryPath);
const baseHooks = _.filter(
Expand Down
Loading