Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add preSubcommand hook #1763

Merged
merged 4 commits into from Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions Readme.md
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
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
@@ -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
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
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
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
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