Skip to content

Commit

Permalink
feat: add preSubcommand hook (#1763)
Browse files Browse the repository at this point in the history
  • Loading branch information
hungtcs authored Jul 12, 2022
1 parent 3ae30a2 commit 2c7f687
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 34 deletions.
8 changes: 4 additions & 4 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -719,10 +719,10 @@ The callback hook can be `async`, in which case you call `.parseAsync` rather th

The supported events are:

- `preAction`: called before action handler for this command and its subcommands
- `postAction`: called after action handler for this command and its subcommands

The hook is passed the command it was added to, and the command running the action handler.
| event name | when hook called | callback parameters |
| :-- | :-- | :-- |
| `preAction`, `postAction` | before/after action handler for this command and its nested subcommands | `(thisCommand, actionCommand)` |
| `preSubcommand` | before parsing direct subcommand | `(thisCommand, subcommand)` |

## Automated help

Expand Down
8 changes: 4 additions & 4 deletions Readme_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -696,10 +696,10 @@ program

支持的事件有:

- `preAction`:在本命令或其子命令的处理函数执行前
- `postAction`:在本命令或其子命令的处理函数执行后

钩子函数的参数为添加上钩子的命令,及实际执行的命令。
| 事件名称 | 触发时机 | 参数列表 |
| :-- | :-- | :-- |
| `preAction`, `postAction` | 本命令或其子命令的处理函数执行前/后 | `(thisCommand, actionCommand)` |
| `preSubcommand` | 在其直接子命令解析之前调用 | `(thisCommand, subcommand)` |

## 自动化帮助信息

Expand Down
47 changes: 28 additions & 19 deletions examples/hook.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env node

// const commander = require('commander'); // (normal include)
const commander = require('../'); // include commander in git clone of commander repo
const program = new commander.Command();
const { Command, Option } = require('../'); // include commander in git clone of commander repo
const program = new Command();

// This example shows using some hooks for life cycle events.

const timeLabel = 'command duration';
program
.option('-p, --profile', 'show how long command takes')
.option('--profile', 'show how long command takes')
.hook('preAction', (thisCommand) => {
if (thisCommand.opts().profile) {
console.time(timeLabel);
Expand All @@ -21,7 +21,7 @@ program
});

program
.option('-t, --trace', 'display trace statements for commands')
.option('--trace', 'display trace statements for commands')
.hook('preAction', (thisCommand, actionCommand) => {
if (thisCommand.opts().trace) {
console.log('>>>>');
Expand All @@ -32,25 +32,34 @@ program
}
});

program.command('delay')
.option('--message <value>', 'custom message to display', 'Thanks for waiting')
.argument('[seconds]', 'how long to delay', '1')
.action(async(waitSeconds, options) => {
await new Promise(resolve => setTimeout(resolve, parseInt(waitSeconds) * 1000));
console.log(options.message);
program
.option('--env <filename>', 'specify environment file')
.hook('preSubcommand', (thisCommand, subcommand) => {
if (thisCommand.opts().env) {
// One use case for this hook is modifying environment variables before
// parsing the subcommand, say by reading .env file.
console.log(`Reading ${thisCommand.opts().env}...`);
process.env.PORT = 80;
console.log(`About to call subcommand: ${subcommand.name()}`);
}
});

program.command('hello')
.option('-e, --example')
.action(() => console.log('Hello, world'));
program.command('start')
.argument('[script]', 'script name', 'server.js')
.option('-d, --delay <seconds>', 'how long to delay before starting')
.addOption(new Option('-p, --port <number>', 'port number').default(8080).env('PORT'))
.action(async(script, options) => {
if (options.delay) {
await new Promise(resolve => setTimeout(resolve, parseInt(options.delay) * 1000));
}
console.log(`Starting ${script} on port ${options.port}`);
});

// Some of the hooks or actions are async, so call parseAsync rather than parse.
program.parseAsync().then(() => {});

// Try the following:
// node hook.js hello
// node hook.js --profile hello
// node hook.js --trace hello --example
// node hook.js delay
// node hook.js --trace delay 5 --message bye
// node hook.js --profile delay
// node hook.js start
// node hook.js --trace start --port 9000 test.js
// node hook.js --profile start --delay 5
// node hook.js --env=production.env start
38 changes: 32 additions & 6 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ class Command extends EventEmitter {
*/

hook(event, listener) {
const allowedValues = ['preAction', 'postAction'];
const allowedValues = ['preSubcommand', 'preAction', 'postAction'];
if (!allowedValues.includes(event)) {
throw new Error(`Unexpected value for event passed to hook : '${event}'.
Expecting one of '${allowedValues.join("', '")}'`);
Expand Down Expand Up @@ -1054,11 +1054,16 @@ Expecting one of '${allowedValues.join("', '")}'`);
const subCommand = this._findCommand(commandName);
if (!subCommand) this.help({ error: true });

if (subCommand._executableHandler) {
this._executeSubCommand(subCommand, operands.concat(unknown));
} else {
return subCommand._parseCommand(operands, unknown);
}
let hookResult;
hookResult = this._chainOrCallSubCommandHook(hookResult, subCommand, 'preSubcommand');
hookResult = this._chainOrCall(hookResult, () => {
if (subCommand._executableHandler) {
this._executeSubCommand(subCommand, operands.concat(unknown));
} else {
return subCommand._parseCommand(operands, unknown);
}
});
return hookResult;
}

/**
Expand Down Expand Up @@ -1185,6 +1190,27 @@ Expecting one of '${allowedValues.join("', '")}'`);
return result;
}

/**
*
* @param {Promise|undefined} promise
* @param {Command} subCommand
* @param {string} event
* @return {Promise|undefined}
* @api private
*/

_chainOrCallSubCommandHook(promise, subCommand, event) {
let result = promise;
if (this._lifeCycleHooks[event] !== undefined) {
this._lifeCycleHooks[event].forEach((hook) => {
result = this._chainOrCall(result, () => {
return hook(this, subCommand);
});
});
}
return result;
}

/**
* Process arguments in context of this command.
* Returns action result, in case it is a promise.
Expand Down
36 changes: 36 additions & 0 deletions tests/command.hook.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,40 @@ describe('action hooks async', () => {
await result;
expect(calls).toEqual(['pb1', 'pb2', 'sb', 'action', 'sa', 'pa2', 'pa1']);
});

test('preSubcommand hook should work', async() => {
const calls = [];
const program = new commander.Command();
program
.hook('preSubcommand', async() => { await 0; calls.push(0); });
program.command('sub')
.action(async() => { await 1; calls.push(1); });
program.action(async() => { await 2; calls.push(2); });
const result = program.parseAsync(['sub'], { from: 'user' });
expect(calls).toEqual([]);
await result;
expect(calls).toEqual([0, 1]);
});
test('preSubcommand hook should effective for direct subcommands', async() => {
const calls = [];
const program = new commander.Command();
program
.hook('preSubcommand', async(thisCommand, subCommand) => {
await 'preSubcommand';
calls.push('preSubcommand');
calls.push(subCommand.name());
});
program
.command('first')
.action(async() => { await 'first'; calls.push('first'); })
.command('second')
.action(async() => { await 'second'; calls.push('second'); })
.command('third')
.action(async() => { await 'third'; calls.push('third'); });
program.action(async() => { await 2; calls.push(2); });
const result = program.parseAsync(['first', 'second', 'third'], { from: 'user' });
expect(calls).toEqual([]);
await result;
expect(calls).toEqual(['preSubcommand', 'first', 'third']);
});
});
2 changes: 1 addition & 1 deletion typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export interface OutputConfiguration {
}

export type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll';
export type HookEvent = 'preAction' | 'postAction';
export type HookEvent = 'preSubcommand' | 'preAction' | 'postAction';
export type OptionValueSource = 'default' | 'env' | 'config' | 'cli';

export interface OptionValues {
Expand Down
6 changes: 6 additions & 0 deletions typings/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ expectType<commander.Command>(program.hook('preAction', (thisCommand, actionComm
expectType<commander.Command>(thisCommand);
expectType<commander.Command>(actionCommand);
}));
expectType<commander.Command>(program.hook('preSubcommand', () => {}));
expectType<commander.Command>(program.hook('preSubcommand', (thisCommand, subcommand) => {
// implicit parameter types
expectType<commander.Command>(thisCommand);
expectType<commander.Command>(subcommand);
}));

// action
expectType<commander.Command>(program.action(() => {}));
Expand Down

0 comments on commit 2c7f687

Please sign in to comment.