Skip to content

Commit

Permalink
feat: improve support for async/await (#1823)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: #1823 contains the following breaking API changes:
* now returns a promise if handler is async.
* onFinishCommand removed, in favor of being able to await promise.
* getCompletion now invokes callback with err and `completions, returns promise of completions.
  • Loading branch information
bcoe committed Jan 11, 2021
1 parent acff16d commit 169b815
Show file tree
Hide file tree
Showing 13 changed files with 857 additions and 191 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Expand Up @@ -62,7 +62,7 @@ jobs:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 13
node-version: 15
- run: npm install
- run: npm test
- run: npm run coverage
76 changes: 63 additions & 13 deletions docs/advanced.md
Expand Up @@ -484,19 +484,6 @@ yargs.parserConfiguration({
See the [yargs-parser](https://github.com/yargs/yargs-parser#configuration) module
for detailed documentation of this feature.

## Command finish hook
### Example
```js
yargs(process.argv.slice(2))
.command('cmd', 'a command', () => {}, async () => {
await this.model.find()
return Promise.resolve('result value')
})
.onFinishCommand(async (resultValue) => {
await this.db.disconnect()
}).argv
```

## Middleware

Sometimes you might want to transform arguments before they reach the command handler.
Expand Down Expand Up @@ -567,3 +554,66 @@ var argv = require('yargs/yargs')(process.argv.slice(2))
)
.argv;
```

## Using Yargs with Async/await

If you use async middleware or async handlers for commands, `yargs.parse` and
`yargs.argv` will return a `Promise`. When you `await` this promise the
parsed arguments object will be returned after the handler completes:

```js
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'

async function processValue(value) {
return new Promise((resolve) => {
// Perform some async operation on value.
setTimeout(() => {
return resolve(value)
}, 1000)
})
}

console.info('start')
await yargs(hideBin(process.argv))
.command('add <x> <y>', 'add two eventual values', () => {}, async (argv) => {
const sum = await processValue(argv.x) + await processValue(argv.y)
console.info(`x + y = ${sum}`)
}).parse()
console.info('finish')
```

### Handling async errors

By default, when an async error occurs within a command yargs will
exit with code `1` and print a help message. If you would rather
Use `try`/`catch` to perform error handling, you can do so by setting
`.fail(false)`:

```js
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'

async function processValue(value) {
return new Promise((resolve, reject) => {
// Perform some async operation on value.
setTimeout(() => {
return reject(Error('something went wrong'))
}, 1000)
})
}

console.info('start')
const parser = yargs(hideBin(process.argv))
.command('add <x> <y>', 'add two eventual values', () => {}, async (argv) => {
const sum = await processValue(argv.x) + await processValue(argv.y)
console.info(`x + y = ${sum}`)
})
.fail(false)
try {
const argv = await parser.parse();
} catch (err) {
console.info(`${err.message}\n ${await parser.getHelp()}`)
}
console.info('finish')
```
36 changes: 16 additions & 20 deletions docs/api.md
Expand Up @@ -809,11 +809,15 @@ error message when this promise rejects
Manually indicate that the program should exit, and provide context about why we
wanted to exit. Follows the behavior set by `.exitProcess()`.

<a name="fail"></a>.fail(fn)
<a name="fail"></a>.fail(fn | boolean)
---------

Method to execute when a failure occurs, rather than printing the failure message.

Providing `false` as a value for `fn` can be used to prevent yargs from
exiting and printing a failure message. This is useful if you wish to
handle failures yourself using `try`/`catch` and [`.getHelp()`](#get-help).

`fn` is called with the failure message that would have been printed, the
`Error` instance originally thrown and yargs state when the failure
occurred.
Expand All @@ -837,7 +841,10 @@ Allows to programmatically get completion choices for any line.

`args`: An array of the words in the command line to complete.

`done`: The callback to be called with the resulting completions.
`done`: Optional callback which will be invoked with `err`, or the resulting completions.

If no `done` callback is provided, `getCompletion` returns a promise that
resolves with the completions.

For example:

Expand All @@ -853,6 +860,12 @@ require('yargs/yargs')(process.argv.slice(2))

Outputs the same completion choices as `./test.js --foo`<kbd>TAB</kbd>: `--foobar` and `--foobaz`

<a name="get-help"></a>.getHelp()
---------------------------

Returns a promise that resolves with a `string` equivalent to what would
be output by [`.showHelp()`](#show-help), or by running yargs with `--help`.

<a name="global"></a>.global(globals, [global=true])
------------

Expand Down Expand Up @@ -1165,23 +1178,6 @@ var argv = require('yargs/yargs')(process.argv.slice(2))
.argv
```

.onFinishCommand([handler])
------------

Called after the completion of any command. `handler` is invoked with the
result returned by the command:

```js
yargs(process.argv.slice(2))
.command('cmd', 'a command', () => {}, async () => {
await this.model.find()
return Promise.resolve('result value')
})
.onFinishCommand(async (resultValue) => {
await this.db.disconnect()
}).argv
```

<a name="option"></a>.option(key, [opt])
-----------------
<a name="options"></a>.options(key, [opt])
Expand Down Expand Up @@ -1481,7 +1477,7 @@ Generate a bash completion script. Users of your application can install this
script in their `.bashrc`, and yargs will provide completion shortcuts for
commands and options.

.showHelp([consoleLevel | printCallback])
<a name="show-help">.showHelp([consoleLevel | printCallback])
---------------------------

Print the usage data.
Expand Down
81 changes: 48 additions & 33 deletions lib/command.ts
Expand Up @@ -211,7 +211,13 @@ export function command(

self.hasDefaultCommand = () => !!defaultCommand;

self.runCommand = function runCommand(command, yargs, parsed, commandIndex) {
self.runCommand = function runCommand(
command,
yargs,
parsed,
commandIndex = 0,
helpOnly = false
) {
let aliases = parsed.aliases;
const commandHandler =
handlers[command!] || handlers[aliasMap[command!]] || defaultCommand;
Expand Down Expand Up @@ -243,7 +249,13 @@ export function command(
commandHandler.description
);
}
innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
innerArgv = innerYargs._parseArgs(
null,
undefined,
true,
commandIndex,
helpOnly
);
aliases = (innerYargs.parsed as DetailedArguments).aliases;
} else if (isCommandBuilderOptionDefinitions(builder)) {
// as a short hand, an object can instead be provided, specifying
Expand All @@ -263,7 +275,13 @@ export function command(
Object.keys(commandHandler.builder).forEach(key => {
innerYargs.option(key, builder[key]);
});
innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
innerArgv = innerYargs._parseArgs(
null,
undefined,
true,
commandIndex,
helpOnly
);
aliases = (innerYargs.parsed as DetailedArguments).aliases;
}

Expand All @@ -275,6 +293,11 @@ export function command(
);
}

// If showHelp() or getHelp() is being run, we should not
// execute middleware or handlers (these may perform expensive operations
// like creating a DB connection).
if (helpOnly) return innerArgv;

const middlewares = globalMiddleware
.slice(0)
.concat(commandHandler.middlewares);
Expand Down Expand Up @@ -302,36 +325,31 @@ export function command(
yargs._postProcess(innerArgv, populateDoubleDash);

innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false);
let handlerResult;
if (isPromise(innerArgv)) {
handlerResult = innerArgv.then(argv => commandHandler.handler(argv));
const innerArgvRef = innerArgv;
innerArgv = innerArgv
.then(argv => commandHandler.handler(argv))
.then(() => innerArgvRef);
} else {
handlerResult = commandHandler.handler(innerArgv);
const handlerResult = commandHandler.handler(innerArgv);
if (isPromise(handlerResult)) {
const innerArgvRef = innerArgv;
innerArgv = handlerResult.then(() => innerArgvRef);
}
}

const handlerFinishCommand = yargs.getHandlerFinishCommand();
if (isPromise(handlerResult)) {
if (isPromise(innerArgv) && !yargs._hasParseCallback()) {
yargs.getUsageInstance().cacheHelpMessage();
innerArgv.catch(error => {
try {
yargs.getUsageInstance().fail(null, error);
} catch (_err) {
// If .fail(false) is not set, and no parse cb() has been
// registered, run usage's default fail method.
}
});
} else if (isPromise(innerArgv)) {
yargs.getUsageInstance().cacheHelpMessage();
handlerResult
.then(value => {
if (handlerFinishCommand) {
handlerFinishCommand(value);
}
})
.catch(error => {
try {
yargs.getUsageInstance().fail(null, error);
} catch (err) {
// fail's throwing would cause an unhandled rejection.
}
})
.then(() => {
yargs.getUsageInstance().clearCachedHelpMessage();
});
} else {
if (handlerFinishCommand) {
handlerFinishCommand(handlerResult);
}
}
}

Expand Down Expand Up @@ -582,7 +600,8 @@ export interface CommandInstance {
command: string | null,
yargs: YargsInstance,
parsed: DetailedArguments,
commandIndex?: number
commandIndex: number,
helpOnly: boolean
): Arguments | Promise<Arguments>;
runDefaultBuilderOn(yargs: YargsInstance): void;
unfreeze(): void;
Expand Down Expand Up @@ -680,7 +699,3 @@ type FrozenCommandInstance = {
aliasMap: Dictionary<string>;
defaultCommand: CommandHandler | undefined;
};

export interface FinishCommandHandler {
(handlerResult: any): any;
}

0 comments on commit 169b815

Please sign in to comment.