From 2c7f687eaea6e7b29b917db8d71a06fe6fbfb53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B8=BF=E5=88=99?= Date: Tue, 12 Jul 2022 14:41:50 +0800 Subject: [PATCH] feat: add preSubcommand hook (#1763) --- Readme.md | 8 +++---- Readme_zh-CN.md | 8 +++---- examples/hook.js | 47 +++++++++++++++++++++++--------------- lib/command.js | 38 +++++++++++++++++++++++++----- tests/command.hook.test.js | 36 +++++++++++++++++++++++++++++ typings/index.d.ts | 2 +- typings/index.test-d.ts | 6 +++++ 7 files changed, 111 insertions(+), 34 deletions(-) diff --git a/Readme.md b/Readme.md index 6391ee7d2..cdbd97df2 100644 --- a/Readme.md +++ b/Readme.md @@ -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 diff --git a/Readme_zh-CN.md b/Readme_zh-CN.md index e6a5a6a5e..c50626f55 100644 --- a/Readme_zh-CN.md +++ b/Readme_zh-CN.md @@ -696,10 +696,10 @@ program 支持的事件有: -- `preAction`:在本命令或其子命令的处理函数执行前 -- `postAction`:在本命令或其子命令的处理函数执行后 - -钩子函数的参数为添加上钩子的命令,及实际执行的命令。 +| 事件名称 | 触发时机 | 参数列表 | +| :-- | :-- | :-- | +| `preAction`, `postAction` | 本命令或其子命令的处理函数执行前/后 | `(thisCommand, actionCommand)` | +| `preSubcommand` | 在其直接子命令解析之前调用 | `(thisCommand, subcommand)` | ## 自动化帮助信息 diff --git a/examples/hook.js b/examples/hook.js index ed8ef9d4d..3518ee075 100644 --- a/examples/hook.js +++ b/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); @@ -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('>>>>'); @@ -32,25 +32,34 @@ program } }); -program.command('delay') - .option('--message ', '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 ', '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 ', 'how long to delay before starting') + .addOption(new Option('-p, --port ', '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 diff --git a/lib/command.js b/lib/command.js index d1d0c4f95..3cfc3ba32 100644 --- a/lib/command.js +++ b/lib/command.js @@ -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("', '")}'`); @@ -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; } /** @@ -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. diff --git a/tests/command.hook.test.js b/tests/command.hook.test.js index 93033107c..15f153b93 100644 --- a/tests/command.hook.test.js +++ b/tests/command.hook.test.js @@ -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']); + }); }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 6aa791bf1..dbe7c57c3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -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 { diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index a908a1a16..94c0b53d1 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -90,6 +90,12 @@ expectType(program.hook('preAction', (thisCommand, actionComm expectType(thisCommand); expectType(actionCommand); })); +expectType(program.hook('preSubcommand', () => {})); +expectType(program.hook('preSubcommand', (thisCommand, subcommand) => { + // implicit parameter types + expectType(thisCommand); + expectType(subcommand); +})); // action expectType(program.action(() => {}));