Skip to content

Commit 2c7f687

Browse files
authored
feat: add preSubcommand hook (#1763)
1 parent 3ae30a2 commit 2c7f687

File tree

7 files changed

+111
-34
lines changed

7 files changed

+111
-34
lines changed

Readme.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -719,10 +719,10 @@ The callback hook can be `async`, in which case you call `.parseAsync` rather th
719719

720720
The supported events are:
721721

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

727727
## Automated help
728728

Readme_zh-CN.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -696,10 +696,10 @@ program
696696

697697
支持的事件有:
698698

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

704704
## 自动化帮助信息
705705

examples/hook.js

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#!/usr/bin/env node
22

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

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

99
const timeLabel = 'command duration';
1010
program
11-
.option('-p, --profile', 'show how long command takes')
11+
.option('--profile', 'show how long command takes')
1212
.hook('preAction', (thisCommand) => {
1313
if (thisCommand.opts().profile) {
1414
console.time(timeLabel);
@@ -21,7 +21,7 @@ program
2121
});
2222

2323
program
24-
.option('-t, --trace', 'display trace statements for commands')
24+
.option('--trace', 'display trace statements for commands')
2525
.hook('preAction', (thisCommand, actionCommand) => {
2626
if (thisCommand.opts().trace) {
2727
console.log('>>>>');
@@ -32,25 +32,34 @@ program
3232
}
3333
});
3434

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

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

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

5061
// Try the following:
51-
// node hook.js hello
52-
// node hook.js --profile hello
53-
// node hook.js --trace hello --example
54-
// node hook.js delay
55-
// node hook.js --trace delay 5 --message bye
56-
// node hook.js --profile delay
62+
// node hook.js start
63+
// node hook.js --trace start --port 9000 test.js
64+
// node hook.js --profile start --delay 5
65+
// node hook.js --env=production.env start

lib/command.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ class Command extends EventEmitter {
399399
*/
400400

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

1057-
if (subCommand._executableHandler) {
1058-
this._executeSubCommand(subCommand, operands.concat(unknown));
1059-
} else {
1060-
return subCommand._parseCommand(operands, unknown);
1061-
}
1057+
let hookResult;
1058+
hookResult = this._chainOrCallSubCommandHook(hookResult, subCommand, 'preSubcommand');
1059+
hookResult = this._chainOrCall(hookResult, () => {
1060+
if (subCommand._executableHandler) {
1061+
this._executeSubCommand(subCommand, operands.concat(unknown));
1062+
} else {
1063+
return subCommand._parseCommand(operands, unknown);
1064+
}
1065+
});
1066+
return hookResult;
10621067
}
10631068

10641069
/**
@@ -1185,6 +1190,27 @@ Expecting one of '${allowedValues.join("', '")}'`);
11851190
return result;
11861191
}
11871192

1193+
/**
1194+
*
1195+
* @param {Promise|undefined} promise
1196+
* @param {Command} subCommand
1197+
* @param {string} event
1198+
* @return {Promise|undefined}
1199+
* @api private
1200+
*/
1201+
1202+
_chainOrCallSubCommandHook(promise, subCommand, event) {
1203+
let result = promise;
1204+
if (this._lifeCycleHooks[event] !== undefined) {
1205+
this._lifeCycleHooks[event].forEach((hook) => {
1206+
result = this._chainOrCall(result, () => {
1207+
return hook(this, subCommand);
1208+
});
1209+
});
1210+
}
1211+
return result;
1212+
}
1213+
11881214
/**
11891215
* Process arguments in context of this command.
11901216
* Returns action result, in case it is a promise.

tests/command.hook.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,40 @@ describe('action hooks async', () => {
320320
await result;
321321
expect(calls).toEqual(['pb1', 'pb2', 'sb', 'action', 'sa', 'pa2', 'pa1']);
322322
});
323+
324+
test('preSubcommand hook should work', async() => {
325+
const calls = [];
326+
const program = new commander.Command();
327+
program
328+
.hook('preSubcommand', async() => { await 0; calls.push(0); });
329+
program.command('sub')
330+
.action(async() => { await 1; calls.push(1); });
331+
program.action(async() => { await 2; calls.push(2); });
332+
const result = program.parseAsync(['sub'], { from: 'user' });
333+
expect(calls).toEqual([]);
334+
await result;
335+
expect(calls).toEqual([0, 1]);
336+
});
337+
test('preSubcommand hook should effective for direct subcommands', async() => {
338+
const calls = [];
339+
const program = new commander.Command();
340+
program
341+
.hook('preSubcommand', async(thisCommand, subCommand) => {
342+
await 'preSubcommand';
343+
calls.push('preSubcommand');
344+
calls.push(subCommand.name());
345+
});
346+
program
347+
.command('first')
348+
.action(async() => { await 'first'; calls.push('first'); })
349+
.command('second')
350+
.action(async() => { await 'second'; calls.push('second'); })
351+
.command('third')
352+
.action(async() => { await 'third'; calls.push('third'); });
353+
program.action(async() => { await 2; calls.push(2); });
354+
const result = program.parseAsync(['first', 'second', 'third'], { from: 'user' });
355+
expect(calls).toEqual([]);
356+
await result;
357+
expect(calls).toEqual(['preSubcommand', 'first', 'third']);
358+
});
323359
});

typings/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ export interface OutputConfiguration {
265265
}
266266

267267
export type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll';
268-
export type HookEvent = 'preAction' | 'postAction';
268+
export type HookEvent = 'preSubcommand' | 'preAction' | 'postAction';
269269
export type OptionValueSource = 'default' | 'env' | 'config' | 'cli';
270270

271271
export interface OptionValues {

typings/index.test-d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ expectType<commander.Command>(program.hook('preAction', (thisCommand, actionComm
9090
expectType<commander.Command>(thisCommand);
9191
expectType<commander.Command>(actionCommand);
9292
}));
93+
expectType<commander.Command>(program.hook('preSubcommand', () => {}));
94+
expectType<commander.Command>(program.hook('preSubcommand', (thisCommand, subcommand) => {
95+
// implicit parameter types
96+
expectType<commander.Command>(thisCommand);
97+
expectType<commander.Command>(subcommand);
98+
}));
9399

94100
// action
95101
expectType<commander.Command>(program.action(() => {}));

0 commit comments

Comments
 (0)