From 32d0d62ed30c65cdbca7c6da630b5542b38ab3b1 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 30 Aug 2021 08:46:04 -0700 Subject: [PATCH] fix: improve Hooks interface (#234) --- src/config/config.ts | 23 +++++++++---- src/interfaces/config.ts | 4 +-- src/interfaces/hooks.ts | 73 +++++++++++++++++++++++++--------------- src/interfaces/index.ts | 2 +- 4 files changed, 66 insertions(+), 36 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index c8df7d40b..28a7f05bb 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -7,7 +7,7 @@ import {format} from 'util' import {Options, Plugin as IPlugin} from '../interfaces/plugin' import {Config as IConfig, ArchTypes, PlatformTypes, LoadOptions} from '../interfaces/config' -import {Command, Hook, PJSON, Topic} from '../interfaces' +import {Command, Hook, Hooks, PJSON, Topic} from '../interfaces' import {Debug} from './util' import * as Plugin from './plugin' import {compact, flatMap, loadJSON, uniq} from './util' @@ -203,14 +203,22 @@ export class Config implements IConfig { } } - async runHook(event: string, opts: T): Promise { + async runHook(event: T, opts: Hooks[T]['options'], timeout?: number): Promise> { debug('start %s hook', event) const search = (m: any): Hook => { if (typeof m === 'function') return m if (m.default && typeof m.default === 'function') return m.default return Object.values(m).find((m: any) => typeof m === 'function') as Hook } - const results = [] + + const withTimeout = async (ms: number, promise: any) => { + const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${ms} ms.`)), ms)) + return Promise.race([promise, timeout]) + } + + const successes = [] + const failures = [] + for (const p of this.plugins) { const debug = require('debug')([this.bin, p.name, 'hooks', event].join(':')) const context: Hook.Context = { @@ -239,11 +247,14 @@ export class Config implements IConfig { debug('start', isESM ? '(import)' : '(require)', filePath) - const result = await search(module).call(context, {...opts as any, config: this}) - results.push(result) + const result = timeout ? + await withTimeout(timeout, search(module).call(context, {...opts as any, config: this})) : + await search(module).call(context, {...opts as any, config: this}) + successes.push({plugin: p, result}) debug('done') } catch (error) { + failures.push({plugin: p, error}) if (error && error.oclif && error.oclif.exit !== undefined) throw error this.warn(error, `runHook ${event}`) } @@ -251,7 +262,7 @@ export class Config implements IConfig { } debug('%s hook done', event) - return results + return {successes, failures} } async runCommand(id: string, argv: string[] = [], cachedCommand?: Command.Plugin): Promise { diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 6b357784b..c6378f5e8 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -1,5 +1,5 @@ import {PJSON} from './pjson' -import {Hooks} from './hooks' +import {Hooks, Hook} from './hooks' import {Command} from './command' import {Plugin, Options} from './plugin' import {Topic} from './topic' @@ -95,7 +95,7 @@ export interface Config { runCommand(id: string, argv?: string[]): Promise; runCommand(id: string, argv?: string[], cachedCommand?: Command.Plugin): Promise; - runHook>(event: K, opts: T[K]): Promise; + runHook(event: T, opts: Hooks[T]['options'], timeout?: number): Promise>; findCommand(id: string, opts: { must: true }): Command.Plugin; findCommand(id: string, opts?: { must: boolean }): Command.Plugin | undefined; findTopic(id: string, opts: { must: true }): Topic; diff --git a/src/interfaces/hooks.ts b/src/interfaces/hooks.ts index dfeeebc4e..5fddf8bdc 100644 --- a/src/interfaces/hooks.ts +++ b/src/interfaces/hooks.ts @@ -1,47 +1,60 @@ import {Command} from './command' import {Config} from './config' +import {Plugin} from './plugin' + +interface HookMeta { + options: Record; + return: any; +} export interface Hooks { - [event: string]: object; + [event: string]: HookMeta; init: { - id: string | undefined; - argv: string[]; + options: { id: string | undefined; argv: string[] }; + return: void; }; prerun: { - Command: Command.Class; - argv: string[]; + options: { Command: Command.Class; argv: string[] }; + return: void; }; postrun: { - Command: Command.Class; - result?: any; - argv: string[]; + options: { + Command: Command.Class; + result?: any; + argv: string[]; + }; + return: void; + }; + preupdate: { + options: {channel: string}; + return: void; + }; + update: { + options: {channel: string}; + return: void; + }; + 'command_not_found': { + options: {id: string; argv?: string[]}; + return: void; }; - preupdate: {channel: string}; - update: {channel: string}; - 'command_not_found': {id: string; argv?: string[]}; 'plugins:preinstall': { - plugin: { - name: string; - tag: string; - type: 'npm'; - } | { - url: string; - type: 'repo'; + options: { + plugin: { name: string; tag: string; type: 'npm' } | { url: string; type: 'repo' }; }; + return: void; }; } -export type HookKeyOrOptions = K extends (keyof Hooks) ? Hooks[K] : K -export type Hook = (this: Hook.Context, options: HookKeyOrOptions & {config: Config}) => any +export type Hook = (this: Hook.Context, options: P[T]['options'] & {config: Config}) => Promise export namespace Hook { - export type Init = Hook - export type PluginsPreinstall = Hook - export type Prerun = Hook - export type Postrun = Hook - export type Preupdate = Hook - export type Update = Hook - export type CommandNotFound = Hook + export type Init = Hook<'init'> + export type PluginsPreinstall = Hook<'plugins:preinstall'> + export type Prerun = Hook<'prerun'> + export type Postrun = Hook<'postrun'> + export type Preupdate = Hook<'preupdate'> + export type Update = Hook<'update'> + export type CommandNotFound = Hook<'command_not_found'> export interface Context { config: Config; @@ -51,4 +64,10 @@ export namespace Hook { log(message?: any, ...args: any[]): void; debug(...args: any[]): void; } + + export interface Result { + successes: Array<{ result: T; plugin: Plugin }>; + failures: Array<{ error: typeof Error; plugin: Plugin }>; + } } + diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 0090f093b..134792722 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -3,7 +3,7 @@ export {Config, ArchTypes, PlatformTypes, LoadOptions} from './config' export {Command, Example} from './command' export {OclifError, PrettyPrintableError} from './errors' export {HelpOptions} from './help' -export {Hook, HookKeyOrOptions, Hooks} from './hooks' +export {Hook, Hooks} from './hooks' export {Manifest} from './manifest' export { ParserArg, Arg, ParseFn, ParserOutput, ParserInput, ArgToken,