Skip to content

Commit

Permalink
fix: improve Hooks interface (#234)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Aug 30, 2021
1 parent 310bf21 commit 32d0d62
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 36 deletions.
23 changes: 17 additions & 6 deletions src/config/config.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -203,14 +203,22 @@ export class Config implements IConfig {
}
}

async runHook<T>(event: string, opts: T): Promise<any> {
async runHook<T extends keyof Hooks>(event: T, opts: Hooks[T]['options'], timeout?: number): Promise<Hook.Result<Hooks[T]['return']>> {
debug('start %s hook', event)
const search = (m: any): Hook<T> => {
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<T>
}
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 = {
Expand Down Expand Up @@ -239,19 +247,22 @@ 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}`)
}
}
}

debug('%s hook done', event)
return results
return {successes, failures}
}

async runCommand<T = unknown>(id: string, argv: string[] = [], cachedCommand?: Command.Plugin): Promise<T> {
Expand Down
4 changes: 2 additions & 2 deletions 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'
Expand Down Expand Up @@ -95,7 +95,7 @@ export interface Config {

runCommand<T = unknown>(id: string, argv?: string[]): Promise<T>;
runCommand<T = unknown>(id: string, argv?: string[], cachedCommand?: Command.Plugin): Promise<T>;
runHook<T extends Hooks, K extends Extract<keyof T, string>>(event: K, opts: T[K]): Promise<any>;
runHook<T extends keyof Hooks>(event: T, opts: Hooks[T]['options'], timeout?: number): Promise<Hook.Result<Hooks[T]['return']>>;
findCommand(id: string, opts: { must: true }): Command.Plugin;
findCommand(id: string, opts?: { must: boolean }): Command.Plugin | undefined;
findTopic(id: string, opts: { must: true }): Topic;
Expand Down
73 changes: 46 additions & 27 deletions src/interfaces/hooks.ts
@@ -1,47 +1,60 @@
import {Command} from './command'
import {Config} from './config'
import {Plugin} from './plugin'

interface HookMeta {
options: Record<string, unknown>;
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> = K extends (keyof Hooks) ? Hooks[K] : K
export type Hook<T> = (this: Hook.Context, options: HookKeyOrOptions<T> & {config: Config}) => any
export type Hook<T extends keyof P, P extends Hooks = Hooks> = (this: Hook.Context, options: P[T]['options'] & {config: Config}) => Promise<P[T]['return']>

export namespace Hook {
export type Init = Hook<Hooks['init']>
export type PluginsPreinstall = Hook<Hooks['plugins:preinstall']>
export type Prerun = Hook<Hooks['prerun']>
export type Postrun = Hook<Hooks['postrun']>
export type Preupdate = Hook<Hooks['preupdate']>
export type Update = Hook<Hooks['update']>
export type CommandNotFound = Hook<Hooks['command_not_found']>
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;
Expand All @@ -51,4 +64,10 @@ export namespace Hook {
log(message?: any, ...args: any[]): void;
debug(...args: any[]): void;
}

export interface Result<T> {
successes: Array<{ result: T; plugin: Plugin }>;
failures: Array<{ error: typeof Error; plugin: Plugin }>;
}
}

2 changes: 1 addition & 1 deletion src/interfaces/index.ts
Expand Up @@ -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,
Expand Down

0 comments on commit 32d0d62

Please sign in to comment.