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(middleware)!: middleware improvements #1852

Merged
merged 4 commits into from Feb 16, 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
17 changes: 13 additions & 4 deletions lib/command.ts
Expand Up @@ -301,18 +301,27 @@ export function command(
const middlewares = globalMiddleware
.slice(0)
.concat(commandHandler.middlewares);
applyMiddleware(innerArgv, yargs, middlewares, true);
innerArgv = applyMiddleware(innerArgv, yargs, middlewares, true);

// we apply validation post-hoc, so that custom
// checks get passed populated positional arguments.
if (!yargs._hasOutput()) {
yargs._runValidation(
innerArgv as Arguments,
const validation = yargs._runValidation(
aliases,
positionalMap,
(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);
}
}

if (commandHandler.handler && !yargs._hasOutput()) {
Expand All @@ -322,7 +331,7 @@ export function command(
const populateDoubleDash = !!yargs.getOptions().configuration[
'populate--'
];
yargs._postProcess(innerArgv, populateDoubleDash);
yargs._postProcess(innerArgv, populateDoubleDash, false, false);

innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false);
if (isPromise(innerArgv)) {
Expand Down
5 changes: 0 additions & 5 deletions lib/middleware.ts
Expand Up @@ -49,9 +49,6 @@ export function applyMiddleware(
middlewares: Middleware[],
beforeValidation: boolean
) {
const beforeValidationError = new Error(
'middleware cannot return a promise when applyBeforeValidation is true'
);
return middlewares.reduce<Arguments | Promise<Arguments>>(
(acc, middleware) => {
if (middleware.applyBeforeValidation !== beforeValidation) {
Expand All @@ -71,8 +68,6 @@ export function applyMiddleware(
);
} else {
const result = middleware(acc, yargs);
if (beforeValidation && isPromise(result)) throw beforeValidationError;

return isPromise(result)
? result.then(middlewareObj => Object.assign(acc, middlewareObj))
: Object.assign(acc, result);
Expand Down
3 changes: 2 additions & 1 deletion lib/usage.ts
Expand Up @@ -48,7 +48,8 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) {
for (let i = fails.length - 1; i >= 0; --i) {
const fail = fails[i];
if (isBoolean(fail)) {
throw err;
if (err) throw err;
else if (msg) throw Error(msg);
} else {
fail(msg, err, self);
}
Expand Down
12 changes: 8 additions & 4 deletions lib/validation.ts
Expand Up @@ -101,8 +101,10 @@ export function validation(
};

// make sure all the required arguments are present.
self.requiredArguments = function requiredArguments(argv) {
const demandedOptions = yargs.getDemandedOptions();
self.requiredArguments = function requiredArguments(
argv,
demandedOptions: Dictionary<string | undefined>
) {
let missing: Dictionary<string | undefined> | null = null;

for (const key of Object.keys(demandedOptions)) {
Expand All @@ -125,7 +127,6 @@ export function validation(
}

const customMsg = customMsgs.length ? `\n${customMsgs.join('\n')}` : '';

usage.fail(
__n(
'Missing required argument: %s',
Expand Down Expand Up @@ -486,7 +487,10 @@ export interface ValidationInstance {
nonOptionCount(argv: Arguments): void;
positionalCount(required: number, observed: number): void;
recommendCommands(cmd: string, potentialCommands: string[]): void;
requiredArguments(argv: Arguments): void;
requiredArguments(
argv: Arguments,
demandedOptions: Dictionary<string | undefined>
): void;
reset(localLookup: Dictionary): ValidationInstance;
unfreeze(): void;
unknownArguments(
Expand Down
147 changes: 105 additions & 42 deletions lib/yargs-factory.ts
Expand Up @@ -52,6 +52,7 @@ import {
import {objFilter} from './utils/obj-filter.js';
import {applyExtends} from './utils/apply-extends.js';
import {
applyMiddleware,
globalMiddlewareFactory,
MiddlewareCallback,
Middleware,
Expand Down Expand Up @@ -1478,11 +1479,11 @@ function Yargs(
self._parseArgs = function parseArgs(
args: string | string[] | null,
shortCircuit?: boolean | null,
_calledFromCommand?: boolean,
calledFromCommand?: boolean,
commandIndex = 0,
helpOnly = false
) {
let skipValidation = !!_calledFromCommand;
let skipValidation = !!calledFromCommand;
args = args || processArgs;

options.__ = y18n.__;
Expand All @@ -1499,7 +1500,9 @@ function Yargs(
})
) as DetailedArguments;

let argv = parsed.argv as Arguments;
let argv: Arguments = parsed.argv as Arguments;
let argvPromise: Arguments | Promise<Arguments> | undefined = undefined;
// Used rather than argv if middleware introduces an async step:
if (parseContext) argv = Object.assign({}, argv, parseContext);
const aliases = parsed.aliases;

Expand All @@ -1510,7 +1513,7 @@ function Yargs(
// const y = yargs(); y.parse('foo --bar'); yargs.parse('bar --foo').
// When a prior parse has completed and a new parse is beginning, we
// need to clear the cached help message from the previous parse:
if (!_calledFromCommand) {
if (commandIndex === 0) {
usage.clearCachedHelpMessage();
}

Expand All @@ -1521,7 +1524,12 @@ function Yargs(
// are two passes through the parser. If completion
// is being performed short-circuit on the first pass.
if (shortCircuit) {
return self._postProcess(argv, populateDoubleDash, _calledFromCommand);
return self._postProcess(
argv,
populateDoubleDash,
!!calledFromCommand,
false // Don't run middleware when figuring out completion.
);
}

// if there's a handler associated with a
Expand Down Expand Up @@ -1563,7 +1571,12 @@ function Yargs(
i + 1,
helpOnly // Don't run a handler, just figure out the help string.
);
return self._postProcess(innerArgv, populateDoubleDash);
return self._postProcess(
innerArgv,
populateDoubleDash,
!!calledFromCommand,
false
);
} else if (!firstUnknownCommand && cmd !== completionCommand) {
firstUnknownCommand = cmd;
break;
Expand All @@ -1579,7 +1592,12 @@ function Yargs(
0,
helpOnly
);
return self._postProcess(innerArgv, populateDoubleDash);
return self._postProcess(
innerArgv,
populateDoubleDash,
!!calledFromCommand,
false
);
}

// recommend a command if recommendCommands() has
Expand All @@ -1601,7 +1619,12 @@ function Yargs(
}
} else if (command.hasDefaultCommand() && !skipDefaultCommand) {
const innerArgv = command.runCommand(null, self, parsed, 0, helpOnly);
return self._postProcess(innerArgv, populateDoubleDash);
return self._postProcess(
innerArgv,
populateDoubleDash,
!!calledFromCommand,
false
);
}

// we must run completions first, a user might
Expand All @@ -1622,7 +1645,12 @@ function Yargs(
});
self.exit(0);
});
return self._postProcess(argv, !populateDoubleDash, _calledFromCommand);
return self._postProcess(
argv,
!populateDoubleDash,
!!calledFromCommand,
false // Don't run middleware when figuring out completion.
);
}

// Handle 'help' and 'version' options
Expand Down Expand Up @@ -1660,26 +1688,54 @@ function Yargs(
// if we're executed via bash completion, don't
// bother with validation.
if (!requestCompletions) {
self._runValidation(argv, aliases, {}, parsed.error);
const validation = self._runValidation(aliases, {}, parsed.error);
if (!calledFromCommand) {
argvPromise = applyMiddleware(argv, self, globalMiddleware, true);
}
argvPromise = validateAsync(validation, argvPromise ?? argv);
}
}
} catch (err) {
if (err instanceof YError) usage.fail(err.message, err);
else throw err;
}

return self._postProcess(argv, populateDoubleDash, _calledFromCommand);
return self._postProcess(
argvPromise ?? argv,
populateDoubleDash,
!!calledFromCommand,
true
);
};

// If argv is a promise (which is possible if async middleware is used)
// delay applying validation until the promise has resolved:
function validateAsync(
validation: (argv: Arguments) => void,
argv: Arguments | Promise<Arguments>
): Arguments | Promise<Arguments> {
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;
}

// Applies a couple post processing steps that are easier to perform
// as a final step.
self._postProcess = function (
argv: Arguments | Promise<Arguments>,
populateDoubleDash: boolean,
calledFromCommand = false
calledFromCommand: boolean,
runGlobalMiddleware: boolean
): any {
if (isPromise(argv)) return argv;
if (calledFromCommand) return argv;
if (isPromise(argv)) return argv;
if (!populateDoubleDash) {
argv = self._copyDoubleDash(argv);
}
Expand All @@ -1689,6 +1745,9 @@ function Yargs(
if (parsePositionalNumbers) {
argv = self._parsePositionalNumbers(argv);
}
if (runGlobalMiddleware) {
argv = applyMiddleware(argv, self, globalMiddleware, false);
}
return argv;
};

Expand Down Expand Up @@ -1728,33 +1787,37 @@ function Yargs(
};

self._runValidation = function runValidation(
argv,
aliases,
positionalMap,
parseErrors,
isDefaultCommand = false
) {
if (parseErrors) throw new YError(parseErrors.message);
validation.nonOptionCount(argv);
validation.requiredArguments(argv);
let failedStrictCommands = false;
if (strictCommands) {
failedStrictCommands = validation.unknownCommands(argv);
}
if (strict && !failedStrictCommands) {
validation.unknownArguments(
argv,
aliases,
positionalMap,
isDefaultCommand
);
} else if (strictOptions) {
validation.unknownArguments(argv, aliases, {}, false, false);
}
validation.customChecks(argv, aliases);
validation.limitedChoices(argv);
validation.implications(argv);
validation.conflicting(argv);
): (argv: Arguments) => void {
aliases = {...aliases};
positionalMap = {...positionalMap};
const demandedOptions = {...self.getDemandedOptions()};
return (argv: Arguments) => {
if (parseErrors) throw new YError(parseErrors.message);
validation.nonOptionCount(argv);
validation.requiredArguments(argv, demandedOptions);
let failedStrictCommands = false;
if (strictCommands) {
failedStrictCommands = validation.unknownCommands(argv);
}
if (strict && !failedStrictCommands) {
validation.unknownArguments(
argv,
aliases,
positionalMap,
isDefaultCommand
);
} else if (strictOptions) {
validation.unknownArguments(argv, aliases, {}, false, false);
}
validation.customChecks(argv, aliases);
validation.limitedChoices(argv);
validation.implications(argv);
validation.conflicting(argv);
};
};

function guessLocale() {
Expand All @@ -1775,6 +1838,7 @@ function Yargs(

return self;
}

// rebase an absolute path to a relative one with respect to a base directory
// exported for tests
export interface RebaseFunction {
Expand All @@ -1795,11 +1859,11 @@ export interface YargsInstance {
_postProcess<T extends Arguments | Promise<Arguments>>(
argv: T,
populateDoubleDash: boolean,
calledFromCommand?: boolean
calledFromCommand: boolean,
runGlobalMiddleware: boolean
): T;
_copyDoubleDash<T extends Arguments>(argv: T): T;
_parsePositionalNumbers<T extends Arguments>(argv: T): T;

_getLoggerInstance(): LoggerInstance;
_getParseContext(): Object;
_hasOutput(): boolean;
Expand All @@ -1808,7 +1872,7 @@ export interface YargsInstance {
(
args: string | string[] | null,
shortCircuit?: boolean,
_calledFromCommand?: boolean,
calledFromCommand?: boolean,
commandIndex?: number,
helpOnly?: boolean
): Arguments | Promise<Arguments>;
Expand All @@ -1817,12 +1881,11 @@ export interface YargsInstance {
| Promise<Arguments>;
};
_runValidation(
argv: Arguments,
aliases: Dictionary<string[]>,
positionalMap: Dictionary<string[]>,
parseErrors: Error | null,
isDefaultCommand?: boolean
): void;
): (argv: Arguments) => void;
_setHasOutput(): void;
addHelpOpt: {
(opt?: string | false): YargsInstance;
Expand Down