Skip to content

Commit

Permalink
meta: middleware improvements (#1852)
Browse files Browse the repository at this point in the history
feat(middleware)!: global middleware now applied when no command is configured.
feat(middleware): async middleware can now be used before validation.
  • Loading branch information
bcoe committed Feb 16, 2021
1 parent d57ca77 commit e0f9363
Show file tree
Hide file tree
Showing 6 changed files with 431 additions and 209 deletions.
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

0 comments on commit e0f9363

Please sign in to comment.