@black-flag/core
Version:
A declarative framework for building fluent, deeply hierarchical command line interfaces with yargs
404 lines (403 loc) • 18.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.configureProgram = configureProgram;
exports.makeRunner = makeRunner;
exports.runProgram = runProgram;
require("core-js/modules/es.array.push.js");
require("core-js/modules/es.iterator.constructor.js");
require("core-js/modules/es.iterator.filter.js");
require("core-js/modules/es.iterator.for-each.js");
var _nodeAssert = _interopRequireDefault(require("node:assert"));
var _types = require("node:util/types");
var _js = require("@-xun/js");
var _rejoinder = require("rejoinder");
var _constant = require("./constant.js");
var _discover = require("./discover.js");
var _error = require("./error.js");
var _util = require("./util.js");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const coreDebug = (0, _rejoinder.createDebugLogger)({
namespace: _constant.globalDebuggerNamespace
});
async function configureProgram(commandModulesPath, configurationHooks) {
try {
const confDebug = coreDebug.extend('conf');
confDebug('configureProgram was invoked');
const {
hideBin
} = await import('yargs/helpers');
const {
default: makeVanillaYargs
} = await import('yargs/yargs');
const finalConfigurationHooks = {
configureArguments: rawArgv => rawArgv,
configureExecutionPrologue: noopConfigurationHook,
configureExecutionEpilogue: argv => argv,
configureExecutionContext: context => context,
configureErrorHandlingEpilogue: defaultErrorHandlingEpilogueConfigurationHook,
...Object.fromEntries(Object.entries((await configurationHooks) || {}).filter(([, v]) => !!v))
};
confDebug('command module auto-discovery path: %O', commandModulesPath);
confDebug('configuration hooks: %O', finalConfigurationHooks);
confDebug('entering configureExecutionContext');
const context = (0, _js.safeShallowClone)(await finalConfigurationHooks.configureExecutionContext({
commands: new Map(),
debug: coreDebug,
state: {
rawArgv: [],
initialTerminalWidth: makeVanillaYargs().terminalWidth(),
showHelpOnFail: {},
firstPassArgv: undefined,
deepestParseResult: undefined,
isGracefullyExiting: false,
didAlreadyHandleError: false,
isHandlingHelpOption: false,
isHandlingVersionOption: false,
didOutputHelpOrVersionText: false,
finalError: undefined,
globalHelpOption: {
name: _constant.defaultHelpOptionName,
description: _constant.defaultHelpTextDescription
},
globalVersionOption: {
name: _constant.defaultVersionOptionName,
description: _constant.defaultVersionTextDescription,
text: ''
}
}
}));
confDebug('exited configureExecutionContext');
confDebug('configured execution context: %O', context);
(0, _nodeAssert.default)(context, _error.BfErrorMessage.InvalidConfigureExecutionContextReturnType());
(0, _nodeAssert.default)(context.state.globalHelpOption === undefined || context.state.globalHelpOption.name?.length, _error.BfErrorMessage.InvalidExecutionContextBadField('state.globalHelpOption'));
(0, _nodeAssert.default)(context.state.globalVersionOption === undefined || context.state.globalVersionOption.name?.length, _error.BfErrorMessage.InvalidExecutionContextBadField('state.globalVersionOption'));
await (0, _discover.discoverCommands)(commandModulesPath, context);
const {
programs: rootPrograms
} = getRootCommand();
confDebug('entering configureExecutionPrologue');
await finalConfigurationHooks.configureExecutionPrologue(rootPrograms, context);
confDebug('exited configureExecutionPrologue');
confDebug('finalizing deferred command registrations');
context.commands.forEach((command, fullName) => {
confDebug('calling HelperProgram::command_finalize_deferred for command %O', fullName);
command.programs.helper.command_finalize_deferred();
confDebug('calling EffectorProgram::command_finalize_deferred for command %O', fullName);
command.programs.effector.command_finalize_deferred();
});
confDebug('configureProgram invocation succeeded');
let alreadyInvoked = false;
const parseAndExecuteWithErrorHandling = async argv_ => {
confDebug('execute was invoked');
if (alreadyInvoked) {
throw new _error.AssertionFailedError(_error.BfErrorMessage.CannotExecuteMultipleTimes());
}
alreadyInvoked = true;
try {
confDebug('raw argv: %O', argv_);
confDebug('entering configureArguments');
const argv = await finalConfigurationHooks.configureArguments(argv_?.length ? argv_ : hideBin(process.argv), context);
confDebug('exited configureArguments');
(0, _nodeAssert.default)(Array.isArray(argv), _error.BfErrorMessage.InvalidConfigureArgumentsReturnType());
const doubleDashIndex = argv.indexOf('--');
const hasDoubleDash = doubleDashIndex !== -1;
if (context.state.globalHelpOption) {
const helpOption = context.state.globalHelpOption.name;
const helpFlag = `${helpOption.length > 1 ? '--' : '-'}${helpOption}`;
const helpFlagIndex = argv.indexOf(helpFlag);
context.state.isHandlingHelpOption = helpFlagIndex !== -1 && (!hasDoubleDash || helpFlagIndex < doubleDashIndex);
} else {
confDebug.warn('disabled built-in help option since context.state.globalHelpOption was falsy');
}
confDebug('context.state.isHandlingHelpOption determination: %O', context.state.isHandlingHelpOption);
if (context.state.globalVersionOption) {
const versionOption = context.state.globalVersionOption.name;
const versionFlag = `${versionOption.length > 1 ? '--' : '-'}${versionOption}`;
const versionFlagIndex = argv.indexOf(versionFlag);
context.state.isHandlingVersionOption = versionFlagIndex !== -1 && (!hasDoubleDash || versionFlagIndex < doubleDashIndex);
} else {
confDebug.warn('disabled built-in version option since context.state.globalVersionOption was falsy');
}
confDebug('context.state.isHandlingVersionOption determination: %O', context.state.isHandlingVersionOption);
confDebug('configured argv (initialRawArgv): %O', argv);
context.state.rawArgv = argv;
confDebug('calling ::parseAsync on root program');
try {
await rootPrograms.router.parseAsync(argv, (0, _util.wrapExecutionContext)(context));
} catch (error_) {
const error = context.state.finalError ?? error_;
if (error !== error_) {
confDebug.warn('root router parse warning: context.state.finalError !== caught error (caught error was discarded)');
}
if ((0, _error.isGracefulEarlyExitError)(error)) {
confDebug.message('caught graceful early exit "error" in PreExecutionContext::execute');
context.state.isGracefullyExiting = true;
confDebug.warn('though runtime was gracefully interrupted, configureExecutionEpilogue will still be called and the program will exit normally');
} else {
throw error;
}
}
context.state.deepestParseResult ||= makeNullParseResult(context);
const finalArgv = context.state.deepestParseResult;
confDebug('final parsed argv: %O', finalArgv);
confDebug('context.state.isGracefullyExiting: %O', context.state.isGracefullyExiting);
confDebug('entering configureExecutionEpilogue');
const result = await finalConfigurationHooks.configureExecutionEpilogue(finalArgv, context);
confDebug('exited configureExecutionEpilogue');
confDebug('execution epilogue returned: %O', result);
(0, _nodeAssert.default)((0, _util.isArguments)(result), _error.BfErrorMessage.InvalidConfigureExecutionEpilogueReturnType());
confDebug('final execution context: %O', context);
confDebug('execution complete (no errors)');
confDebug.newline();
return result;
} catch (error) {
confDebug.error('caught fatal error (type %O): %O', typeof error, error);
context.state.deepestParseResult ||= makeNullParseResult(context);
const finalArgv = context.state.deepestParseResult;
confDebug('final parsed argv: %O', finalArgv);
if ((0, _error.isGracefulEarlyExitError)(error)) {
confDebug.message('caught (and released) graceful early exit "error" in catch block');
} else {
(0, _nodeAssert.default)(finalArgv[_constant.$executionContext], _error.BfErrorMessage.GuruMeditation());
let message = _error.BfErrorMessage.Generic();
let exitCode = _constant.FrameworkExitCode.DefaultError;
const {
isAssertionSystemError
} = await import("./util.js");
if (typeof error === 'string') {
message = error;
} else if ((0, _types.isNativeError)(error)) {
message = error.message;
exitCode = isAssertionSystemError(error) ? _constant.FrameworkExitCode.AssertionFailed : (0, _error.isCliError)(error) ? error.suggestedExitCode : _constant.FrameworkExitCode.DefaultError;
} else {
message = String(error);
}
confDebug('theoretical error message: %O', message);
confDebug('theoretical exit code: %O', exitCode);
confDebug('entering configureErrorHandlingEpilogue');
await finalConfigurationHooks.configureErrorHandlingEpilogue({
message,
error,
exitCode
}, finalArgv, context);
confDebug('exited configureErrorHandlingEpilogue');
confDebug('final execution context: %O', context);
if (!(0, _error.isCliError)(error)) {
confDebug('wrapping error with CliError');
error = new _error.CliError((0, _types.isNativeError)(error) ? error : message, {
suggestedExitCode: exitCode
});
}
}
context.state.didAlreadyHandleError = true;
confDebug.warn('forwarding error to top-level error handler');
throw error;
}
};
return {
rootPrograms,
execute: parseAndExecuteWithErrorHandling,
executionContext: context,
...context
};
function getRootCommand() {
const root = context.commands.get(context.commands.keys().next().value);
(0, _nodeAssert.default)(root, _error.BfErrorMessage.GuruMeditation());
return root;
}
} catch (error) {
if ((0, _util.isAssertionSystemError)(error)) {
throw new _error.AssertionFailedError(error);
}
throw error;
}
}
function makeRunner(options) {
const makerDebug = coreDebug.extend('makeRunner');
makerDebug('returning curried runProgram function');
return async function run(...args) {
const runDebug = coreDebug.extend('runProgram@');
runDebug('runProgram wrapper (curried) was invoked');
runDebug('options: %O', options);
const {
commandModulesPath,
configurationHooks: highConfigurationHooks,
preExecutionContext: highPreExecutionContext,
errorHandlingBehavior = 'default'
} = options;
(0, _nodeAssert.default)(highConfigurationHooks === undefined || highPreExecutionContext === undefined, _error.BfErrorMessage.BadParameterCombination());
const lowParameters = [commandModulesPath, ...args];
const derivedLowParameters = await deriveRunProgramParametersFromArgs(lowParameters, {
awaitPromisedParameters: false
});
const {
argv: lowArgv,
hooksOrContext: lowHooksOrContext
} = derivedLowParameters;
if (!lowHooksOrContext) {
lowParameters.push(highConfigurationHooks || highPreExecutionContext || {});
} else if (highConfigurationHooks || highPreExecutionContext) {
lowParameters[lowParameters.length - 1] = Promise.resolve(lowHooksOrContext).then(async lowLastArg => {
if (!(0, _util.isPreExecutionContext)(lowLastArg) && highConfigurationHooks) {
return {
...(await highConfigurationHooks),
...lowLastArg
};
}
return lowLastArg;
});
}
runDebug('invoking low-order runner with the following arguments: %O', lowParameters);
if (errorHandlingBehavior === 'throw') {
const lastLowParameter = lowParameters.at(-1);
(0, _nodeAssert.default)(lastLowParameter, _error.BfErrorMessage.GuruMeditation());
const hooksOrContext = await runBadProgramIfPromiseRejects(lastLowParameter, lowParameters);
const preExecutionContext = (0, _util.isPreExecutionContext)(hooksOrContext) ? hooksOrContext : await runBadProgramIfPromiseRejects(configureProgram(commandModulesPath, hooksOrContext), lowParameters);
const promisedResult = lowArgv ? runProgram(commandModulesPath, lowArgv, preExecutionContext) : runProgram(commandModulesPath, preExecutionContext);
const result = await promisedResult;
if (preExecutionContext.state.finalError !== undefined) {
throw preExecutionContext.state.finalError;
}
return result;
} else {
return runProgram(...lowParameters);
}
};
}
async function runProgram(...args) {
const runDebug = coreDebug.extend('runProgram');
runDebug('runProgram was invoked');
let parameters = undefined;
try {
parameters = await deriveRunProgramParametersFromArgs(args);
const {
argv,
commandModulesPath,
configurationHooks
} = parameters;
runDebug(parameters.preExecutionContext ? 'using provided preExecutionContext' : 'invoking configureProgram');
parameters.preExecutionContext ||= await configureProgram(commandModulesPath, configurationHooks);
const {
preExecutionContext
} = parameters;
runDebug('invoking preExecutionContext.execute');
const executeArguments = Array.isArray(argv) ? argv : typeof argv === 'string' ? argv.split(' ') : undefined;
const parsedArgv = await preExecutionContext.execute(executeArguments);
process.exitCode = _constant.FrameworkExitCode.Ok;
runDebug('exit code set to %O', process.exitCode);
runDebug('runProgram invocation succeeded');
return parsedArgv;
} catch (error) {
const {
preExecutionContext
} = parameters || {};
runDebug.error(`handling irrecoverable exception from ${preExecutionContext ? '::execute' : '::configureProgram'}: %O`, error);
process.exitCode = (0, _error.isCliError)(error) ? error.suggestedExitCode : (0, _util.isAssertionSystemError)(error) ? _constant.FrameworkExitCode.AssertionFailed : _constant.FrameworkExitCode.DefaultError;
runDebug('exit code (tentatively) set to %O', process.exitCode);
if (preExecutionContext) {
preExecutionContext.state.finalError = error;
runDebug('preExecutionContext.state.finalError set to %O', preExecutionContext.state.finalError);
}
if ((0, _error.isGracefulEarlyExitError)(error)) {
runDebug.message('the exception resulted in a graceful exit (maybe with parse result)');
return preExecutionContext?.state.deepestParseResult;
}
if (!preExecutionContext?.state.didAlreadyHandleError) {
process.exitCode = _constant.FrameworkExitCode.AssertionFailed;
runDebug.message('exit code set to %O', process.exitCode);
console.error(_error.BfErrorMessage.FrameworkError(error));
}
runDebug('runProgram invocation "succeeded" (via error handler)');
if ((0, _error.isCliError)(error) && error.dangerouslyFatal) {
runDebug.warn('error has dangerouslyFatal flag enabled; process.exit will be called');
process.exit();
}
return undefined;
}
}
function makeNullParseResult(context) {
coreDebug.warn('generated a NullArguments parse result');
return {
$0: _constant.nullArguments$0,
_: [],
[_constant.$executionContext]: context
};
}
async function defaultErrorHandlingEpilogueConfigurationHook(...[{
error
}, _, {
debug,
state: {
didOutputHelpOrVersionText
}
}]) {
const errorDebug = debug.extend('default-error-handler');
errorDebug('didOutputHelpOrVersionText: %O', didOutputHelpOrVersionText);
if (didOutputHelpOrVersionText) {
if (!(0, _error.isCommandNotImplementedError)(error)) {
console.error();
outputErrorMessage();
}
} else {
outputErrorMessage();
}
errorDebug('done');
function outputErrorMessage() {
const deepestError = (0, _util.getDeepestErrorCause)(error);
errorDebug('error: %O', error);
errorDebug('deepestError: %O', deepestError);
errorDebug('error === deepestError: %O', error === deepestError);
errorDebug('isCliError: %O', (0, _error.isCliError)(deepestError));
errorDebug('isYargsError: %O', (0, _error.isYargsError)(deepestError));
console.error((0, _error.isYargsError)(deepestError) || error === deepestError && (0, _error.isCliError)(deepestError) ? (0, _util.capitalize)(deepestError.message) : deepestError);
}
}
function noopConfigurationHook() {}
async function deriveRunProgramParametersFromArgs(args, {
awaitPromisedParameters = true
} = {}) {
const result = {
commandModulesPath: args[0],
argv: undefined,
configurationHooks: undefined,
preExecutionContext: undefined,
hooksOrContext: undefined
};
if (typeof args[1] === 'string' || Array.isArray(args[1])) {
result.argv = args[1];
if (args[2]) {
if (awaitPromisedParameters) {
const argument2 = await args[2];
if ((0, _util.isPreExecutionContext)(argument2)) {
result.preExecutionContext = argument2;
} else {
result.configurationHooks = argument2;
}
} else {
result.hooksOrContext = args[2];
}
}
} else if (args[1]) {
if (awaitPromisedParameters) {
const argument1 = await args[1];
if ((0, _util.isPreExecutionContext)(argument1)) {
result.preExecutionContext = argument1;
} else {
result.configurationHooks = argument1;
}
} else {
result.hooksOrContext = args[1];
}
}
return result;
}
async function runBadProgramIfPromiseRejects(maybePromise, lowParameters) {
try {
return await maybePromise;
} catch (error) {
await runProgram(...lowParameters);
throw error;
}
}