Skip to content

Commit 97bbe33

Browse files
Archezrigor789
andauthored
feat(hooks): project persistent hooks in config (#5597)
* feat(hooks): execute project persistent hooks * chore(hooks): add hook service tests * Apply suggestions from code review Co-authored-by: Igor Randjelovic <[email protected]>
1 parent daa6028 commit 97bbe33

File tree

3 files changed

+362
-113
lines changed

3 files changed

+362
-113
lines changed

lib/common/services/hooks-service.ts

+181-113
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import {
1414
IProjectHelper,
1515
IStringDictionary,
1616
} from "../declarations";
17+
import {
18+
INsConfigHooks,
19+
IProjectConfigService,
20+
} from "../../definitions/project";
1721
import { IInjector } from "../definitions/yok";
1822
import { injector } from "../yok";
1923

@@ -38,7 +42,8 @@ export class HooksService implements IHooksService {
3842
private $injector: IInjector,
3943
private $projectHelper: IProjectHelper,
4044
private $options: IOptions,
41-
private $performanceService: IPerformanceService
45+
private $performanceService: IPerformanceService,
46+
private $projectConfigService: IProjectConfigService
4247
) {}
4348

4449
public get hookArgsName(): string {
@@ -61,6 +66,12 @@ export class HooksService implements IHooksService {
6166
this.$logger.trace(
6267
"Hooks directories: " + util.inspect(this.hooksDirectories)
6368
);
69+
70+
const customHooks = this.$projectConfigService.getValue("hooks", []);
71+
72+
if (customHooks.length) {
73+
this.$logger.trace("Custom hooks: " + util.inspect(customHooks));
74+
}
6475
}
6576

6677
private static formatHookName(commandName: string): string {
@@ -118,6 +129,19 @@ export class HooksService implements IHooksService {
118129
)
119130
);
120131
}
132+
133+
const customHooks = this.getCustomHooksByName(hookName);
134+
135+
for (const hook of customHooks) {
136+
results.push(
137+
await this.executeHook(
138+
this.$projectHelper.projectDir,
139+
hookName,
140+
hook,
141+
hookArguments
142+
)
143+
);
144+
}
121145
} catch (err) {
122146
this.$logger.trace(`Failed during hook execution ${hookName}.`);
123147
this.$errors.fail(err.message || err);
@@ -126,142 +150,186 @@ export class HooksService implements IHooksService {
126150
return _.flatten(results);
127151
}
128152

129-
private async executeHooksInDirectory(
153+
private async executeHook(
130154
directoryPath: string,
131155
hookName: string,
156+
hook: IHook,
132157
hookArguments?: IDictionary<any>
133-
): Promise<any[]> {
158+
): Promise<any> {
134159
hookArguments = hookArguments || {};
135-
const results: any[] = [];
136-
const hooks = this.getHooksByName(directoryPath, hookName);
137160

138-
for (let i = 0; i < hooks.length; ++i) {
139-
const hook = hooks[i];
140-
const relativePath = path.relative(directoryPath, hook.fullPath);
141-
const trackId = relativePath.replace(
142-
new RegExp("\\" + path.sep, "g"),
143-
AnalyticsEventLabelDelimiter
144-
);
145-
let command = this.getSheBangInterpreter(hook);
146-
let inProc = false;
147-
if (!command) {
148-
command = hook.fullPath;
149-
if (path.extname(hook.fullPath).toLowerCase() === ".js") {
150-
command = process.argv[0];
151-
inProc = this.shouldExecuteInProcess(
152-
this.$fs.readText(hook.fullPath)
153-
);
154-
}
161+
let result;
162+
163+
const relativePath = path.relative(directoryPath, hook.fullPath);
164+
const trackId = relativePath.replace(
165+
new RegExp("\\" + path.sep, "g"),
166+
AnalyticsEventLabelDelimiter
167+
);
168+
let command = this.getSheBangInterpreter(hook);
169+
let inProc = false;
170+
if (!command) {
171+
command = hook.fullPath;
172+
if (path.extname(hook.fullPath).toLowerCase() === ".js") {
173+
command = process.argv[0];
174+
inProc = this.shouldExecuteInProcess(this.$fs.readText(hook.fullPath));
155175
}
176+
}
156177

157-
const startTime = this.$performanceService.now();
158-
if (inProc) {
159-
this.$logger.trace(
160-
"Executing %s hook at location %s in-process",
161-
hookName,
162-
hook.fullPath
163-
);
164-
const hookEntryPoint = require(hook.fullPath);
178+
const startTime = this.$performanceService.now();
179+
if (inProc) {
180+
this.$logger.trace(
181+
"Executing %s hook at location %s in-process",
182+
hookName,
183+
hook.fullPath
184+
);
185+
const hookEntryPoint = require(hook.fullPath);
165186

166-
this.$logger.trace(`Validating ${hookName} arguments.`);
187+
this.$logger.trace(`Validating ${hookName} arguments.`);
167188

168-
const invalidArguments = this.validateHookArguments(
169-
hookEntryPoint,
170-
hook.fullPath
171-
);
189+
const invalidArguments = this.validateHookArguments(
190+
hookEntryPoint,
191+
hook.fullPath
192+
);
172193

173-
if (invalidArguments.length) {
174-
this.$logger.warn(
175-
`${
176-
hook.fullPath
177-
} will NOT be executed because it has invalid arguments - ${
178-
invalidArguments.join(", ").grey
179-
}.`
180-
);
181-
continue;
182-
}
194+
if (invalidArguments.length) {
195+
this.$logger.warn(
196+
`${
197+
hook.fullPath
198+
} will NOT be executed because it has invalid arguments - ${
199+
invalidArguments.join(", ").grey
200+
}.`
201+
);
202+
return;
203+
}
183204

184-
// HACK for backwards compatibility:
185-
// 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)
186-
// then it is probably passed as a hookArg
187-
// if that is the case then pass it directly to the hook instead of trying to resolve $projectData via injector
188-
// This helps make hooks stateless
189-
const projectDataHookArg =
190-
hookArguments["hookArgs"] && hookArguments["hookArgs"]["projectData"];
191-
if (projectDataHookArg) {
192-
hookArguments["projectData"] = hookArguments[
193-
"$projectData"
194-
] = projectDataHookArg;
195-
}
205+
// HACK for backwards compatibility:
206+
// 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)
207+
// then it is probably passed as a hookArg
208+
// if that is the case then pass it directly to the hook instead of trying to resolve $projectData via injector
209+
// This helps make hooks stateless
210+
const projectDataHookArg =
211+
hookArguments["hookArgs"] && hookArguments["hookArgs"]["projectData"];
212+
if (projectDataHookArg) {
213+
hookArguments["projectData"] = hookArguments[
214+
"$projectData"
215+
] = projectDataHookArg;
216+
}
196217

197-
const maybePromise = this.$injector.resolve(
198-
hookEntryPoint,
199-
hookArguments
200-
);
201-
if (maybePromise) {
202-
this.$logger.trace("Hook promises to signal completion");
203-
try {
204-
const result = await maybePromise;
205-
results.push(result);
206-
} catch (err) {
207-
if (
208-
err &&
209-
_.isBoolean(err.stopExecution) &&
210-
err.errorAsWarning === true
211-
) {
212-
this.$logger.warn(err.message || err);
213-
} else {
214-
// Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles.
215-
this.$logger.error(err);
216-
throw (
217-
err || new Error(`Failed to execute hook: ${hook.fullPath}.`)
218-
);
219-
}
218+
const maybePromise = this.$injector.resolve(
219+
hookEntryPoint,
220+
hookArguments
221+
);
222+
if (maybePromise) {
223+
this.$logger.trace("Hook promises to signal completion");
224+
try {
225+
result = await maybePromise;
226+
} catch (err) {
227+
if (
228+
err &&
229+
_.isBoolean(err.stopExecution) &&
230+
err.errorAsWarning === true
231+
) {
232+
this.$logger.warn(err.message || err);
233+
} else {
234+
// Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles.
235+
this.$logger.error(err);
236+
throw err || new Error(`Failed to execute hook: ${hook.fullPath}.`);
220237
}
221-
222-
this.$logger.trace("Hook completed");
223238
}
224-
} else {
225-
const environment = this.prepareEnvironment(hook.fullPath);
226-
this.$logger.trace(
227-
"Executing %s hook at location %s with environment ",
228-
hookName,
229-
hook.fullPath,
230-
environment
231-
);
232239

233-
const output = await this.$childProcess.spawnFromEvent(
234-
command,
235-
[hook.fullPath],
236-
"close",
237-
environment,
238-
{ throwError: false }
239-
);
240-
results.push(output);
240+
this.$logger.trace("Hook completed");
241+
}
242+
} else {
243+
const environment = this.prepareEnvironment(hook.fullPath);
244+
this.$logger.trace(
245+
"Executing %s hook at location %s with environment ",
246+
hookName,
247+
hook.fullPath,
248+
environment
249+
);
241250

242-
if (output.exitCode !== 0) {
243-
throw new Error(output.stdout + output.stderr);
244-
}
251+
const output = await this.$childProcess.spawnFromEvent(
252+
command,
253+
[hook.fullPath],
254+
"close",
255+
environment,
256+
{ throwError: false }
257+
);
258+
result = output;
245259

246-
this.$logger.trace(
247-
"Finished executing %s hook at location %s with environment ",
248-
hookName,
249-
hook.fullPath,
250-
environment
251-
);
260+
if (output.exitCode !== 0) {
261+
throw new Error(output.stdout + output.stderr);
252262
}
253-
const endTime = this.$performanceService.now();
254-
this.$performanceService.processExecutionData(
255-
trackId,
256-
startTime,
257-
endTime,
258-
[hookArguments]
263+
264+
this.$logger.trace(
265+
"Finished executing %s hook at location %s with environment ",
266+
hookName,
267+
hook.fullPath,
268+
environment
269+
);
270+
}
271+
const endTime = this.$performanceService.now();
272+
this.$performanceService.processExecutionData(trackId, startTime, endTime, [
273+
hookArguments,
274+
]);
275+
276+
return result;
277+
}
278+
279+
private async executeHooksInDirectory(
280+
directoryPath: string,
281+
hookName: string,
282+
hookArguments?: IDictionary<any>
283+
): Promise<any[]> {
284+
hookArguments = hookArguments || {};
285+
const results: any[] = [];
286+
const hooks = this.getHooksByName(directoryPath, hookName);
287+
288+
for (let i = 0; i < hooks.length; ++i) {
289+
const hook = hooks[i];
290+
const result = await this.executeHook(
291+
directoryPath,
292+
hookName,
293+
hook,
294+
hookArguments
259295
);
296+
297+
if (result) {
298+
results.push(result);
299+
}
260300
}
261301

262302
return results;
263303
}
264304

305+
private getCustomHooksByName(hookName: string): IHook[] {
306+
const hooks: IHook[] = [];
307+
const customHooks: INsConfigHooks[] =
308+
this.$projectConfigService.getValue("hooks", []);
309+
310+
for (const cHook of customHooks) {
311+
if (cHook.type === hookName) {
312+
const fullPath = path.join(
313+
this.$projectHelper.projectDir,
314+
cHook.script
315+
);
316+
const isFile = this.$fs.getFsStats(fullPath).isFile();
317+
318+
if (isFile) {
319+
const fileNameParts = cHook.script.split("/");
320+
hooks.push(
321+
new Hook(
322+
this.getBaseFilename(fileNameParts[fileNameParts.length - 1]),
323+
fullPath
324+
)
325+
);
326+
}
327+
}
328+
}
329+
330+
return hooks;
331+
}
332+
265333
private getHooksByName(directoryPath: string, hookName: string): IHook[] {
266334
const allBaseHooks = this.getHooksInDirectory(directoryPath);
267335
const baseHooks = _.filter(

0 commit comments

Comments
 (0)