UNPKG

@black-flag/core

Version:

A declarative framework for building fluent, deeply hierarchical command line interfaces with yargs

404 lines (403 loc) 18.4 kB
"use strict"; 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; } }