diff --git a/docs/api.md b/docs/api.md
index 83300445f..2440dd318 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -115,7 +115,7 @@ If `key` is an array, interpret all the elements as booleans.
Check that certain conditions are met in the provided arguments.
-`fn` is called with two arguments, the parsed `argv` hash and an array of options and their aliases.
+`fn` is called with the parsed `argv` hash.
If `fn` throws or returns a non-truthy value, Yargs will show the thrown error
and usage information. Yargs will then exit, unless
@@ -175,7 +175,7 @@ var argv = require('yargs/yargs')(process.argv.slice(2))
.coerce(key, fn)
----------------
-Provide a synchronous function to coerce or transform the value(s) given on the
+Provide a function to coerce or transform the value(s) given on the
command line for `key`.
The coercion function should accept one argument, representing the parsed value from
@@ -184,8 +184,7 @@ return a new value or throw an error. The returned value will be used as the val
`key` (or one of its aliases) in `argv`.
If the function throws, the error will be treated as a validation
-failure, delegating to either a custom [`.fail()`](#fail) handler or printing
-the error message in the console.
+failure, delegating to either a custom [`.fail()`](#fail) handler or printing the error message in the console.
Coercion will be applied to a value after
all other modifications, such as [`.normalize()`](#normalize).
@@ -195,7 +194,7 @@ _Examples:_
```js
var argv = require('yargs/yargs')(process.argv.slice(2))
.coerce('file', function (arg) {
- return require('fs').readFileSync(arg, 'utf8')
+ return await require('fs').promises.readFile(arg, 'utf8')
})
.argv
```
@@ -212,8 +211,7 @@ var argv = require('yargs/yargs')(process.argv.slice(2))
.argv
```
-You can also map the same function to several keys at one time. Just pass an
-array of keys as the first argument to `.coerce()`:
+You can also map the same function to several keys at one time. Just pass an array of keys as the first argument to `.coerce()`:
```js
var path = require('path')
@@ -222,8 +220,7 @@ var argv = require('yargs/yargs')(process.argv.slice(2))
.argv
```
-If you are using dot-notion or arrays, .e.g., `user.email` and `user.password`,
-coercion will be applied to the final object that has been parsed:
+If you are using dot-notion or arrays, .e.g., `user.email` and `user.password`, coercion will be applied to the final object that has been parsed:
```js
// --user.name Batman --user.password 123
diff --git a/lib/cjs.ts b/lib/cjs.ts
index 3940c29e7..64b57f8ba 100644
--- a/lib/cjs.ts
+++ b/lib/cjs.ts
@@ -5,7 +5,6 @@ import {applyExtends} from './utils/apply-extends';
import {argsert} from './argsert.js';
import {isPromise} from './utils/is-promise.js';
import {objFilter} from './utils/obj-filter.js';
-import {globalMiddlewareFactory} from './middleware.js';
import {parseCommand} from './parse-command.js';
import * as processArgv from './utils/process-argv.js';
import {YargsWithShim, rebase} from './yargs-factory.js';
@@ -35,7 +34,6 @@ export default {
cjsPlatformShim,
Yargs,
argsert,
- globalMiddlewareFactory,
isPromise,
objFilter,
parseCommand,
diff --git a/lib/command.ts b/lib/command.ts
index 8cf3c2db0..5434c1149 100644
--- a/lib/command.ts
+++ b/lib/command.ts
@@ -8,6 +8,7 @@ import {isPromise} from './utils/is-promise.js';
import {
applyMiddleware,
commandMiddlewareFactory,
+ GlobalMiddleware,
Middleware,
} from './middleware.js';
import {parseCommand, Positional} from './parse-command.js';
@@ -23,6 +24,7 @@ import {
Arguments,
DetailedArguments,
} from './yargs-factory.js';
+import {maybeAsyncResult} from './utils/maybe-async-result.js';
import whichModule from './utils/which-module.js';
const DEFAULT_MARKER = /(^\*)|(^\$0)/;
@@ -35,7 +37,7 @@ export function command(
yargs: YargsInstance,
usage: UsageInstance,
validation: ValidationInstance,
- globalMiddleware: Middleware[] = [],
+ globalMiddleware: GlobalMiddleware,
shim: PlatformShim
) {
const self: CommandInstance = {} as CommandInstance;
@@ -307,6 +309,7 @@ export function command(
if (helpOnly) return innerArgv;
const middlewares = globalMiddleware
+ .getMiddleware()
.slice(0)
.concat(commandHandler.middlewares);
innerArgv = applyMiddleware(innerArgv, yargs, middlewares, true);
@@ -320,16 +323,10 @@ export function command(
(yargs.parsed as DetailedArguments).error,
!command
);
- if (isPromise(innerArgv)) {
- // If the middlware returned a promise, resolve the middleware
- // before applying the validation:
- innerArgv = innerArgv.then(argv => {
- validation(argv);
- return argv;
- });
- } else {
- validation(innerArgv);
- }
+ innerArgv = maybeAsyncResult(innerArgv, result => {
+ validation(result);
+ return result;
+ });
}
if (commandHandler.handler && !yargs._hasOutput()) {
@@ -342,18 +339,14 @@ export function command(
yargs._postProcess(innerArgv, populateDoubleDash, false, false);
innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false);
- if (isPromise(innerArgv)) {
- const innerArgvRef = innerArgv;
- innerArgv = innerArgv
- .then(argv => commandHandler.handler(argv))
- .then(() => innerArgvRef);
- } else {
- const handlerResult = commandHandler.handler(innerArgv);
+ innerArgv = maybeAsyncResult(innerArgv, result => {
+ const handlerResult = commandHandler.handler(result as Arguments);
if (isPromise(handlerResult)) {
- const innerArgvRef = innerArgv;
- innerArgv = handlerResult.then(() => innerArgvRef);
+ return handlerResult.then(() => result);
+ } else {
+ return result;
}
- }
+ });
yargs.getUsageInstance().cacheHelpMessage();
if (isPromise(innerArgv) && !yargs._hasParseCallback()) {
@@ -499,8 +492,9 @@ export function command(
if (!unparsed.length) return;
const config: Configuration = Object.assign({}, options.configuration, {
- 'populate--': true,
+ 'populate--': false,
});
+
const parsed = shim.Parser.detailed(
unparsed,
Object.assign({}, options, {
diff --git a/lib/middleware.ts b/lib/middleware.ts
index 7028d055b..35d35463d 100644
--- a/lib/middleware.ts
+++ b/lib/middleware.ts
@@ -2,17 +2,21 @@ import {argsert} from './argsert.js';
import {isPromise} from './utils/is-promise.js';
import {YargsInstance, Arguments} from './yargs-factory.js';
-export function globalMiddlewareFactory(
- globalMiddleware: Middleware[],
- context: T
-) {
- return function (
+export class GlobalMiddleware {
+ globalMiddleware: Middleware[] = [];
+ yargs: YargsInstance;
+ frozens: Array = [];
+ constructor(yargs: YargsInstance) {
+ this.yargs = yargs;
+ }
+ addMiddleware(
callback: MiddlewareCallback | MiddlewareCallback[],
- applyBeforeValidation = false
- ) {
+ applyBeforeValidation: boolean,
+ global = true
+ ): YargsInstance {
argsert(
- ' [boolean]',
- [callback, applyBeforeValidation],
+ ' [boolean] [boolean]',
+ [callback, applyBeforeValidation, global],
arguments.length
);
if (Array.isArray(callback)) {
@@ -20,17 +24,50 @@ export function globalMiddlewareFactory(
if (typeof callback[i] !== 'function') {
throw Error('middleware must be a function');
}
- (callback[
- i
- ] as Middleware).applyBeforeValidation = applyBeforeValidation;
+ const m = callback[i] as Middleware;
+ m.applyBeforeValidation = applyBeforeValidation;
+ m.global = global;
}
- Array.prototype.push.apply(globalMiddleware, callback as Middleware[]);
+ Array.prototype.push.apply(
+ this.globalMiddleware,
+ callback as Middleware[]
+ );
} else if (typeof callback === 'function') {
- (callback as Middleware).applyBeforeValidation = applyBeforeValidation;
- globalMiddleware.push(callback as Middleware);
+ const m = callback as Middleware;
+ m.applyBeforeValidation = applyBeforeValidation;
+ m.global = global;
+ this.globalMiddleware.push(callback as Middleware);
}
- return context;
- };
+ return this.yargs;
+ }
+ // For "coerce" middleware, only one middleware instance can be registered
+ // per option:
+ addCoerceMiddleware(
+ callback: MiddlewareCallback,
+ option: string
+ ): YargsInstance {
+ const aliases = this.yargs.getAliases();
+ this.globalMiddleware = this.globalMiddleware.filter(m => {
+ const toCheck = [...(aliases[option] ? aliases[option] : []), option];
+ if (!m.option) return true;
+ else return !toCheck.includes(m.option);
+ });
+ (callback as Middleware).option = option;
+ return this.addMiddleware(callback, true, true);
+ }
+ getMiddleware() {
+ return this.globalMiddleware;
+ }
+ freeze() {
+ this.frozens.push([...this.globalMiddleware]);
+ }
+ unfreeze() {
+ const frozen = this.frozens.pop();
+ if (frozen !== undefined) this.globalMiddleware = frozen;
+ }
+ reset() {
+ this.globalMiddleware = this.globalMiddleware.filter(m => m.global);
+ }
}
export function commandMiddlewareFactory(
@@ -85,4 +122,6 @@ export interface MiddlewareCallback {
export interface Middleware extends MiddlewareCallback {
applyBeforeValidation: boolean;
+ global: boolean;
+ option?: string;
}
diff --git a/lib/utils/maybe-async-result.ts b/lib/utils/maybe-async-result.ts
new file mode 100644
index 000000000..1e288cb11
--- /dev/null
+++ b/lib/utils/maybe-async-result.ts
@@ -0,0 +1,32 @@
+// maybeAsyncResult() allows the same error/completion handler to be
+// applied to a value regardless of whether it is a concrete value or an
+// eventual value.
+//
+// As of yargs@v17, if no asynchronous steps are run, .e.g, a
+// check() script that resolves a promise, yargs will return a concrete
+// value. If any asynchronous steps are introduced, yargs resolves a promise.
+import {isPromise} from './is-promise.js';
+export function maybeAsyncResult(
+ getResult: (() => T | Promise) | T | Promise,
+ resultHandler: (result: T) => T | Promise,
+ errorHandler: (err: Error) => T = (err: Error) => {
+ throw err;
+ }
+): T | Promise {
+ try {
+ const result = isFunction(getResult) ? getResult() : getResult;
+ if (isPromise(result)) {
+ return result.then((result: T) => {
+ return resultHandler(result);
+ });
+ } else {
+ return resultHandler(result);
+ }
+ } catch (err) {
+ return errorHandler(err);
+ }
+}
+
+function isFunction(arg: (() => any) | any): arg is () => any {
+ return typeof arg === 'function';
+}
diff --git a/lib/validation.ts b/lib/validation.ts
index 371374d46..871d4784a 100644
--- a/lib/validation.ts
+++ b/lib/validation.ts
@@ -14,7 +14,7 @@ import {DetailedArguments} from './typings/yargs-parser-types.js';
const specialKeys = ['$0', '--', '_'];
// validation-type-stuff, missing params,
-// bad implications, custom checks.
+// bad implications:
export function validation(
yargs: YargsInstance,
usage: UsageInstance,
@@ -275,34 +275,6 @@ export function validation(
usage.fail(msg);
};
- // custom checks, added using the `check` option on yargs.
- let checks: CustomCheck[] = [];
- self.check = function check(f, global) {
- checks.push({
- func: f,
- global,
- });
- };
-
- self.customChecks = function customChecks(argv, aliases) {
- for (let i = 0, f; (f = checks[i]) !== undefined; i++) {
- const func = f.func;
- let result = null;
- try {
- result = func(argv, aliases);
- } catch (err) {
- usage.fail(err.message ? err.message : err, err);
- continue;
- }
-
- if (!result) {
- usage.fail(__('Argument check failed: %s', func.toString()));
- } else if (typeof result === 'string' || result instanceof Error) {
- usage.fail(result.toString(), result);
- }
- }
- };
-
// check implications, argument foo implies => argument bar.
let implied: Dictionary = {};
self.implies = function implies(key, value) {
@@ -441,7 +413,6 @@ export function validation(
self.reset = function reset(localLookup) {
implied = objFilter(implied, k => !localLookup[k]);
conflicting = objFilter(conflicting, k => !localLookup[k]);
- checks = checks.filter(c => c.global);
return self;
};
@@ -449,14 +420,13 @@ export function validation(
self.freeze = function freeze() {
frozens.push({
implied,
- checks,
conflicting,
});
};
self.unfreeze = function unfreeze() {
const frozen = frozens.pop();
assertNotStrictEqual(frozen, undefined, shim);
- ({implied, checks, conflicting} = frozen);
+ ({implied, conflicting} = frozen);
};
return self;
@@ -464,13 +434,11 @@ export function validation(
/** Instance of the validation module. */
export interface ValidationInstance {
- check(f: CustomCheck['func'], global: boolean): void;
conflicting(argv: Arguments): void;
conflicts(
key: string | Dictionary,
value?: string | string[]
): void;
- customChecks(argv: Arguments, aliases: DetailedArguments['aliases']): void;
freeze(): void;
getConflicting(): Dictionary<(string | undefined)[]>;
getImplied(): Dictionary;
@@ -503,14 +471,8 @@ export interface ValidationInstance {
unknownCommands(argv: Arguments): boolean;
}
-interface CustomCheck {
- func: (argv: Arguments, aliases: DetailedArguments['aliases']) => any;
- global: boolean;
-}
-
interface FrozenValidationInstance {
implied: Dictionary;
- checks: CustomCheck[];
conflicting: Dictionary<(string | undefined)[]>;
}
diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts
index f41c9c8b3..7a9185928 100644
--- a/lib/yargs-factory.ts
+++ b/lib/yargs-factory.ts
@@ -53,11 +53,12 @@ import {objFilter} from './utils/obj-filter.js';
import {applyExtends} from './utils/apply-extends.js';
import {
applyMiddleware,
- globalMiddlewareFactory,
+ GlobalMiddleware,
MiddlewareCallback,
Middleware,
} from './middleware.js';
import {isPromise} from './utils/is-promise.js';
+import {maybeAsyncResult} from './utils/maybe-async-result.js';
import setBlocking from './utils/set-blocking.js';
let shim: PlatformShim;
@@ -75,16 +76,14 @@ function Yargs(
let command: CommandInstance;
let completion: CompletionInstance | null = null;
let groups: Dictionary = {};
- const globalMiddleware: Middleware[] = [];
let output = '';
const preservedGroups: Dictionary = {};
+ const globalMiddleware = new GlobalMiddleware(self);
let usage: UsageInstance;
let validation: ValidationInstance;
const y18n = shim.y18n;
- self.middleware = globalMiddlewareFactory(globalMiddleware, self);
-
self.scriptName = function (scriptName) {
self.customScriptName = true;
self.$0 = scriptName;
@@ -223,7 +222,6 @@ function Yargs(
'choices',
'demandedOptions',
'demandedCommands',
- 'coerce',
'deprecatedOptions',
];
@@ -248,6 +246,7 @@ function Yargs(
? command.reset()
: Command(self, usage, validation, globalMiddleware, shim);
if (!completion) completion = Completion(self, usage, command, shim);
+ globalMiddleware.reset();
completionCommand = null;
output = '';
@@ -281,6 +280,7 @@ function Yargs(
usage.freeze();
validation.freeze();
command.freeze();
+ globalMiddleware.freeze();
}
function unfreeze() {
const frozen = frozens.pop();
@@ -306,6 +306,7 @@ function Yargs(
usage.unfreeze();
validation.unfreeze();
command.unfreeze();
+ globalMiddleware.unfreeze();
}
self.boolean = function (keys) {
@@ -491,7 +492,56 @@ function Yargs(
[keys, value],
arguments.length
);
- populateParserHintSingleValueDictionary(self.coerce, 'coerce', keys, value);
+ if (Array.isArray(keys)) {
+ if (!value) {
+ throw new YError('coerce callback must be provided');
+ }
+ for (const key of keys) {
+ self.coerce(key, value);
+ }
+ return self;
+ } else if (typeof keys === 'object') {
+ for (const key of Object.keys(keys)) {
+ self.coerce(key, keys[key]);
+ }
+ return self;
+ }
+ if (!value) {
+ throw new YError('coerce callback must be provided');
+ }
+ // This noop tells yargs-parser about the existence of the option
+ // represented by "keys", so that it can apply camel case expansion
+ // if needed:
+ self.alias(keys, keys);
+ globalMiddleware.addCoerceMiddleware(
+ (
+ argv: Arguments,
+ yargs: YargsInstance
+ ): Partial | Promise> => {
+ let aliases: Dictionary;
+ return maybeAsyncResult<
+ Partial | Promise> | any
+ >(
+ () => {
+ aliases = yargs.getAliases();
+ return value(argv[keys]);
+ },
+ (result: any): Partial => {
+ argv[keys] = result;
+ if (aliases[keys]) {
+ for (const alias of aliases[keys]) {
+ argv[alias] = result;
+ }
+ }
+ return argv;
+ },
+ (err: Error): Partial | Promise> => {
+ throw new YError(err.message);
+ }
+ );
+ },
+ keys
+ );
return self;
};
@@ -758,6 +808,10 @@ function Yargs(
return self;
};
+ self.getAliases = () => {
+ return self.parsed ? self.parsed.aliases : {};
+ };
+
self.getDemandedOptions = () => {
argsert([], 0);
return options.demandedOptions;
@@ -847,12 +901,51 @@ function Yargs(
return self;
};
- self.check = function (f, _global) {
- argsert(' [boolean]', [f, _global], arguments.length);
- validation.check(f, _global !== false);
+ self.check = function (f, global) {
+ argsert(' [boolean]', [f, global], arguments.length);
+ self.middleware(
+ (
+ argv: Arguments,
+ _yargs: YargsInstance
+ ): Partial | Promise> => {
+ return maybeAsyncResult<
+ Partial | Promise> | any
+ >(
+ () => {
+ return f(argv);
+ },
+ (result: any): Partial | Promise> => {
+ if (!result) {
+ usage.fail(y18n.__('Argument check failed: %s', f.toString()));
+ } else if (typeof result === 'string' || result instanceof Error) {
+ usage.fail(result.toString(), result);
+ }
+ return argv;
+ },
+ (err: Error): Partial | Promise> => {
+ usage.fail(err.message ? err.message : err.toString(), err);
+ return argv;
+ }
+ );
+ },
+ false,
+ global
+ );
return self;
};
+ self.middleware = (
+ callback: MiddlewareCallback | MiddlewareCallback[],
+ applyBeforeValidation?: boolean,
+ global = true
+ ) => {
+ return globalMiddleware.addMiddleware(
+ callback,
+ !!applyBeforeValidation,
+ global
+ );
+ };
+
self.global = function global(globals, global) {
argsert(' [boolean]', [globals, global], arguments.length);
globals = ([] as string[]).concat(globals);
@@ -1689,9 +1782,24 @@ function Yargs(
if (!requestCompletions) {
const validation = self._runValidation(aliases, {}, parsed.error);
if (!calledFromCommand) {
- argvPromise = applyMiddleware(argv, self, globalMiddleware, true);
+ argvPromise = applyMiddleware(
+ argv,
+ self,
+ globalMiddleware.getMiddleware(),
+ true
+ );
}
argvPromise = validateAsync(validation, argvPromise ?? argv);
+ if (isPromise(argvPromise) && !calledFromCommand) {
+ argvPromise = argvPromise.then(() => {
+ return applyMiddleware(
+ argv,
+ self,
+ globalMiddleware.getMiddleware(),
+ false
+ );
+ });
+ }
}
}
} catch (err) {
@@ -1712,17 +1820,10 @@ function Yargs(
validation: (argv: Arguments) => void,
argv: Arguments | Promise
): Arguments | Promise {
- if (isPromise(argv)) {
- // If the middlware returned a promise, resolve the middleware
- // before applying the validation:
- argv = argv.then(argv => {
- validation(argv);
- return argv;
- });
- } else {
- validation(argv);
- }
- return argv;
+ return maybeAsyncResult(argv, result => {
+ validation(result);
+ return result;
+ });
}
// Applies a couple post processing steps that are easier to perform
@@ -1745,7 +1846,12 @@ function Yargs(
argv = self._parsePositionalNumbers(argv);
}
if (runGlobalMiddleware) {
- argv = applyMiddleware(argv, self, globalMiddleware, false);
+ argv = applyMiddleware(
+ argv,
+ self,
+ globalMiddleware.getMiddleware(),
+ false
+ );
}
return argv;
};
@@ -1812,7 +1918,6 @@ function Yargs(
} else if (strictOptions) {
validation.unknownArguments(argv, aliases, {}, false, false);
}
- validation.customChecks(argv, aliases);
validation.limitedChoices(argv);
validation.implications(argv);
validation.conflicting(argv);
@@ -1900,10 +2005,7 @@ export interface YargsInstance {
};
array(keys: string | string[]): YargsInstance;
boolean(keys: string | string[]): YargsInstance;
- check(
- f: (argv: Arguments, aliases: Dictionary) => any,
- _global?: boolean
- ): YargsInstance;
+ check(f: (argv: Arguments) => any, global?: boolean): YargsInstance;
choices: {
(keys: string | string[], choices: string | string[]): YargsInstance;
(keyChoices: Dictionary): YargsInstance;
@@ -2003,6 +2105,7 @@ export interface YargsInstance {
): Promise | any;
getHelp(): Promise;
getContext(): Context;
+ getAliases(): Dictionary;
getDemandedCommands(): Options['demandedCommands'];
getDemandedOptions(): Options['demandedOptions'];
getDeprecatedOptions(): Options['deprecatedOptions'];
@@ -2030,7 +2133,8 @@ export interface YargsInstance {
};
middleware(
callback: MiddlewareCallback | MiddlewareCallback[],
- applyBeforeValidation?: boolean
+ applyBeforeValidation?: boolean,
+ global?: boolean
): YargsInstance;
nargs: {
(keys: string | string[], nargs: number): YargsInstance;
diff --git a/package.json b/package.json
index ceeaca2d2..60c83ecea 100644
--- a/package.json
+++ b/package.json
@@ -79,8 +79,8 @@
"fix": "gts fix && npm run fix:js",
"fix:js": "eslint . --ext cjs --ext mjs --ext js --fix",
"posttest": "npm run check",
- "test": "c8 mocha ./test/*.cjs --require ./test/before.cjs --timeout=12000 --check-leaks",
- "test:esm": "c8 mocha ./test/esm/*.mjs --check-leaks",
+ "test": "c8 mocha --enable-source-maps ./test/*.cjs --require ./test/before.cjs --timeout=12000 --check-leaks",
+ "test:esm": "c8 mocha --enable-source-maps ./test/esm/*.mjs --check-leaks",
"coverage": "c8 report --check-coverage",
"prepare": "npm run compile",
"pretest": "npm run compile -- -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs",
diff --git a/test/command.cjs b/test/command.cjs
index 5a3c01269..59f05ada4 100644
--- a/test/command.cjs
+++ b/test/command.cjs
@@ -1,6 +1,7 @@
/* global describe, it, beforeEach */
/* eslint-disable no-unused-vars */
'use strict';
+const assert = require('assert');
const yargs = require('../index.cjs');
const expect = require('chai').expect;
const checkOutput = require('./helpers/utils.cjs').checkOutput;
@@ -1250,6 +1251,29 @@ describe('Command', () => {
checkCalled.should.equal(false);
});
+ it('allows each builder to specify own middleware', () => {
+ let checkCalled1 = 0;
+ let checkCalled2 = 0;
+ const y = yargs()
+ .command('command ', 'a command', () => {
+ yargs.check(argv => {
+ checkCalled1++;
+ return true;
+ });
+ })
+ .command('command2 ', 'a second command', yargs => {
+ yargs.check(argv => {
+ checkCalled2++;
+ return true;
+ });
+ });
+ y.parse('command blerg --foo');
+ y.parse('command2 blerg --foo');
+ y.parse('command blerg --foo');
+ checkCalled1.should.equal(2);
+ checkCalled2.should.equal(1);
+ });
+
it('applies demandOption globally', done => {
yargs('command blerg --foo')
.command('command ', 'a command')
@@ -1885,4 +1909,37 @@ describe('Command', () => {
yargs().command(cmds).parse('a c 10 5', context);
context.output.value.should.equal(15);
});
+
+ it('allows async exception in handler to be caught', async () => {
+ await assert.rejects(
+ yargs(['mw'])
+ .fail(false)
+ .command(
+ 'mw',
+ 'adds func to middleware',
+ () => {},
+ async () => {
+ throw Error('not cool');
+ }
+ )
+ .parse(),
+ /not cool/
+ );
+ });
+
+ it('reports error if error occurs parsing positional', () => {
+ try {
+ yargs(['cmd', 'neat'])
+ .fail(false)
+ .command('cmd ', 'run a foo command', yargs => {
+ yargs.option('foo', {
+ nargs: 3,
+ });
+ })
+ .parse();
+ throw Error('unreachable');
+ } catch (err) {
+ err.message.should.match(/Not enough arguments/);
+ }
+ });
});
diff --git a/test/middleware.cjs b/test/middleware.cjs
index ed1074c81..625be7d3d 100644
--- a/test/middleware.cjs
+++ b/test/middleware.cjs
@@ -2,8 +2,8 @@
/* global describe, it, beforeEach, afterEach */
/* eslint-disable no-unused-vars */
+const assert = require('assert');
const {expect} = require('chai');
-const {globalMiddlewareFactory} = require('../build/index.cjs');
let yargs;
require('chai').should();
@@ -12,6 +12,12 @@ function clearRequireCache() {
delete require.cache[require.resolve('../build/index.cjs')];
}
+async function wait() {
+ return Promise(resolve => {
+ setTimeout(resolve, 10);
+ });
+}
+
describe('middleware', () => {
beforeEach(() => {
yargs = require('../index.cjs');
@@ -20,30 +26,6 @@ describe('middleware', () => {
clearRequireCache();
});
- it('should add a list of callbacks to global middleware', () => {
- const globalMiddleware = [];
-
- globalMiddlewareFactory(globalMiddleware)([() => {}, () => {}]);
-
- globalMiddleware.should.have.lengthOf(2);
- });
-
- it('should throw exception if middleware is not a function', () => {
- const globalMiddleware = [];
-
- expect(() => {
- globalMiddlewareFactory(globalMiddleware)(['callback1', 'callback2']);
- }).to.throw('middleware must be a function');
- });
-
- it('should add a single callback to global middleware', () => {
- const globalMiddleware = [];
-
- globalMiddlewareFactory(globalMiddleware)(() => {});
-
- globalMiddleware.should.have.lengthOf(1);
- });
-
it('runs the middleware before reaching the handler', done => {
yargs(['mw'])
.middleware(argv => {
@@ -623,4 +605,264 @@ describe('middleware', () => {
.parse();
argv.foo.should.equal(198);
});
+
+ it('throws error if middleware not function', () => {
+ let err;
+ assert.throws(() => {
+ yargs('snuh --foo 99').middleware(['hello']).parse();
+ }, /middleware must be a function/);
+ });
+
+ describe('async check', () => {
+ describe('success', () => {
+ it('returns promise if check is async', async () => {
+ const argvPromise = yargs('--foo 100')
+ .middleware(argv => {
+ argv.foo *= 2;
+ }, true)
+ .check(async argv => {
+ wait();
+ if (argv.foo < 200) return false;
+ else return true;
+ })
+ .parse();
+ (!!argvPromise.then).should.equal(true);
+ const argv = await argvPromise;
+ argv.foo.should.equal(200);
+ });
+ it('returns promise if check and middleware is async', async () => {
+ const argvPromise = yargs('--foo 100')
+ .middleware(async argv => {
+ wait();
+ argv.foo *= 2;
+ }, true)
+ .check(async argv => {
+ wait();
+ if (argv.foo < 200) return false;
+ else return true;
+ })
+ .parse();
+ (!!argvPromise.then).should.equal(true);
+ const argv = await argvPromise;
+ argv.foo.should.equal(200);
+ });
+ it('allows async check to be used with command', async () => {
+ let output = '';
+ const argv = await yargs('cmd --foo 300')
+ .command(
+ 'cmd',
+ 'a command',
+ yargs => {
+ yargs.check(async argv => {
+ wait();
+ output += 'first';
+ if (argv.foo < 200) return false;
+ else return true;
+ });
+ },
+ async argv => {
+ wait();
+ output += 'second';
+ }
+ )
+ .parse();
+ argv._.should.include('cmd');
+ argv.foo.should.equal(300);
+ output.should.equal('firstsecond');
+ });
+ it('allows async check to be used with command and middleware', async () => {
+ let output = '';
+ const argv = await yargs('cmd --foo 100')
+ .command(
+ 'cmd',
+ 'a command',
+ yargs => {
+ yargs.check(async argv => {
+ wait();
+ output += 'second';
+ if (argv.foo < 200) return false;
+ else return true;
+ });
+ },
+ async argv => {
+ wait();
+ output += 'fourth';
+ },
+ [
+ async argv => {
+ wait();
+ output += 'third';
+ argv.foo *= 2;
+ },
+ ]
+ )
+ .middleware(async argv => {
+ wait();
+ output += 'first';
+ argv.foo *= 2;
+ }, true)
+ .parse();
+ argv._.should.include('cmd');
+ argv.foo.should.equal(400);
+ output.should.equal('firstsecondthirdfourth');
+ });
+ });
+ describe('failure', () => {
+ it('allows failed check to be caught', async () => {
+ await assert.rejects(
+ yargs('--f 33')
+ .alias('foo', 'f')
+ .fail(false)
+ .check(async argv => {
+ wait();
+ return argv.foo > 50;
+ })
+ .parse(),
+ /Argument check failed/
+ );
+ });
+ it('allows error to be caught before calling command', async () => {
+ let output = '';
+ await assert.rejects(
+ yargs('cmd --foo 100')
+ .fail(false)
+ .command(
+ 'cmd',
+ 'a command',
+ yargs => {
+ yargs.check(async argv => {
+ wait();
+ output += 'first';
+ if (argv.foo < 200) return false;
+ else return true;
+ });
+ },
+ async argv => {
+ wait();
+ output += 'second';
+ }
+ )
+ .parse(),
+ /Argument check failed/
+ );
+ output.should.equal('first');
+ });
+ it('allows error to be caught before calling command and middleware', async () => {
+ let output = '';
+ await assert.rejects(
+ yargs('cmd --foo 10')
+ .fail(false)
+ .command(
+ 'cmd',
+ 'a command',
+ yargs => {
+ yargs.check(async argv => {
+ wait();
+ output += 'second';
+ if (argv.foo < 200) return false;
+ else return true;
+ });
+ },
+ async argv => {
+ wait();
+ output += 'fourth';
+ },
+ [
+ async argv => {
+ wait();
+ output += 'third';
+ argv.foo *= 2;
+ },
+ ]
+ )
+ .middleware(async argv => {
+ wait();
+ output += 'first';
+ argv.foo *= 2;
+ }, true)
+ .parse(),
+ /Argument check failed/
+ );
+ output.should.equal('firstsecond');
+ });
+ });
+ it('applies alliases prior to calling check', async () => {
+ const argv = await yargs('--f 99')
+ .alias('foo', 'f')
+ .check(async argv => {
+ wait();
+ return argv.foo > 50;
+ })
+ .parse();
+ argv.foo.should.equal(99);
+ });
+ });
+
+ describe('async coerce', () => {
+ it('allows two commands to register different coerce methods', async () => {
+ const y = yargs()
+ .command('command1', 'first command', yargs => {
+ yargs.coerce('foo', async arg => {
+ wait();
+ return new Date(arg);
+ });
+ })
+ .command('command2', 'second command', yargs => {
+ yargs.coerce('foo', async arg => {
+ wait();
+ return new Number(arg);
+ });
+ })
+ .coerce('foo', async _arg => {
+ return 'hello';
+ });
+ const r1 = await y.parse('command1 --foo 2020-10-10');
+ expect(r1.foo).to.be.an.instanceof(Date);
+ const r2 = await y.parse('command2 --foo 302');
+ r2.foo.should.equal(302);
+ });
+ it('coerce is applied to argument and aliases', async () => {
+ let callCount = 0;
+ const argvPromise = yargs()
+ .alias('f', 'foo')
+ .coerce('foo', async arg => {
+ wait();
+ callCount++;
+ return new Date(arg.toString());
+ })
+ .parse('-f 2014');
+ (!!argvPromise.then).should.equal(true);
+ const argv = await argvPromise;
+ callCount.should.equal(1);
+ expect(argv.foo).to.be.an.instanceof(Date);
+ expect(argv.f).to.be.an.instanceof(Date);
+ });
+ it('applies coerce before validation', async () => {
+ const argv = await yargs()
+ .option('foo', {
+ choices: [10, 20, 30],
+ })
+ .coerce('foo', async arg => {
+ wait();
+ return (arg *= 2);
+ })
+ .parse('--foo 5');
+ argv.foo.should.equal(10);
+ });
+ it('allows error to be caught', async () => {
+ await assert.rejects(
+ yargs()
+ .fail(false)
+ .option('foo', {
+ choices: [10, 20, 30],
+ })
+ .coerce('foo', async arg => {
+ wait();
+ return (arg *= 2);
+ })
+ .parse('--foo 2'),
+ /Choices: 10, 20, 30/
+ );
+ });
+ });
});
diff --git a/test/usage.cjs b/test/usage.cjs
index f09b88c41..d914bb7c1 100644
--- a/test/usage.cjs
+++ b/test/usage.cjs
@@ -4175,6 +4175,7 @@ describe('usage tests', () => {
describe('help message caching', () => {
it('should display proper usage when an async handler fails', done => {
const y = yargs()
+ .scriptName('mocha')
.command('cmd', 'test command', {}, () => {
return new Promise((resolve, reject) => setTimeout(reject, 10));
})
@@ -4207,8 +4208,9 @@ describe('usage tests', () => {
it('should not display a cached help message for the next parsing', done => {
const y = yargs()
+ .scriptName('mocha')
.command('cmd', 'test command', {}, () => {
- return new Promise((resolve, reject) => setTimeout(resolve, 10));
+ return new Promise((resolve, _reject) => setTimeout(resolve, 10));
})
.demandCommand(1, 'You need at least one command before moving on')
.exitProcess(false);
diff --git a/test/yargs.cjs b/test/yargs.cjs
index e279ec425..d8e016280 100644
--- a/test/yargs.cjs
+++ b/test/yargs.cjs
@@ -2,6 +2,7 @@
/* global context, describe, it, beforeEach, afterEach */
/* eslint-disable no-unused-vars */
+const assert = require('assert');
const expect = require('chai').expect;
const fs = require('fs');
const path = require('path');
@@ -303,7 +304,6 @@ describe('yargs dsl tests', () => {
narg: {},
defaultDescription: {},
choices: {},
- coerce: {},
skipValidation: [],
count: [],
normalize: [],
@@ -1985,6 +1985,7 @@ describe('yargs dsl tests', () => {
'--file',
path.join(__dirname, 'fixtures', 'package.json'),
])
+ .alias('file', 'f')
.coerce('file', arg => JSON.parse(fs.readFileSync(arg, 'utf8')))
.parse();
expect(argv.file).to.have.property('version').and.equal('9.9.9');
@@ -2138,6 +2139,15 @@ describe('yargs dsl tests', () => {
expect(msg).to.equal('ball');
expect(err).to.not.equal(undefined);
});
+
+ it('throws error if coerce callback is missing', () => {
+ assert.throws(() => {
+ yargs().coerce(['a', 'b']);
+ }, /coerce callback must be provided/);
+ assert.throws(() => {
+ yargs().coerce('c');
+ }, /coerce callback must be provided/);
+ });
});
describe('stop parsing', () => {