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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: improve support for async/await #1823

Merged
merged 17 commits into from Jan 11, 2021
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
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;
}