UNPKG

yaro

Version:

Fast, minimalist, pluggable and functional command line parsing. Create CLIs easily. A wrapper package exporting everything from several Yaro packages.

294 lines (239 loc) 8.06 kB
// SPDX-License-Identifier: MPL-2.0 /* eslint-disable no-nested-ternary */ /* eslint-disable max-statements */ import { buildOutput } from './utils.js'; const UNNAMED_COMMAND_PREFIX = '___UNNAMED_COMMAND-'; /** * Utility function to be used when you want a cli app * that includes both `config.rootCommand` and and have multiple in `config.commands` */ export function rootWithMultipleCommands(globalOptions) { // Note: 1) // this root command can stay empty; // or to merge global and command options, cuz they are not propagated by default; // or if you want to know what command is matched and called; // console.log('hela root command:', globalOptions); // Note: 2) // the reason we should do this, in case we have both `rootCommand` and `commands` defined, // is because we do not have access to the correct parsed `argv`, it's not full, // and we cannot do it internally either. return async ({ matchedCommand, argv }) => // console.log({ argv, rest }); matchedCommand({ ...argv, __GLOBAL_OPTIONS__: globalOptions }); } export { UNNAMED_COMMAND_PREFIX, yaroCreateCli }; export default yaroCreateCli; async function yaroCreateCli(argv, config) { const cfg = Array.isArray(argv) ? { argv, ...config } : { argv: [], ...argv }; if (cfg.buildOutput && typeof cfg.buildOutput !== 'function') { throw new TypeError('option `buildOutput` should be function when given'); } if (cfg.exit && typeof cfg.exit !== 'function') { throw new TypeError('option `exit` should function when given'); } cfg.exit = cfg.exit ?? (() => {}); cfg.buildOutput = cfg.buildOutput ?? buildOutput; cfg.yaroParser = cfg.yaroParse || cfg.yaroParser || (cfg.yaro && cfg.yaro.parse) || (cfg.yaro && cfg.yaro.parser); cfg.yaroCommand = cfg.yaroCommand || (cfg.yaro && cfg.yaro.command); if (typeof cfg.yaroParser !== 'function') { throw new TypeError('requires parser: `cfg.yaroParser` or `cfg.yaro.parser`'); } const parsedInfo = cfg.yaroParser(cfg.argv); const { rootCommand, entries } = await getCommands(cfg); const cliInfo = getCliInfo(rootCommand, entries, cfg); if (parsedInfo.version) { console.log(cliInfo.version); cfg.exit(0); return; } const meta = { config: cfg, cliInfo: { ...cliInfo }, argv: { ...parsedInfo }, rootCommand, commands: Object.fromEntries(entries), entries, }; if (rootCommand && meta.entries.length === 0) { meta.matchedCommand = rootCommand; meta.singleMode = true; await tryCatch('ERR_ROOT_COMMAND_FAILED', meta, rootCommand); return; } const matchedCommand = await findMatchCommand(meta.entries, meta); if (!matchedCommand) { if (meta.argv.help || cfg.showHelpOnEmpty) { await cfg.buildOutput(meta.argv, meta, { isHelp: true }); return; } const noCommandSpecified = meta.argv._.length === 0; const commandNotFound = noCommandSpecified === false; await cfg.buildOutput(meta.argv, meta, { noCommandSpecified, commandNotFound, exitCode: 1, }); return; } if (rootCommand) { const rootReturn = await tryCatch('ERR_ROOT_FAILURE', meta, rootCommand); if (typeof rootReturn === 'function') { if (meta.argv.help) { await cfg.buildOutput(meta.argv, meta, { isHelp: true, matchedCommand }); return; } const handler = () => rootReturn({ ...meta, matchedCommand }); meta.matchedCommand = matchedCommand; await tryCatch('ERR_MATCHED_COMMAND_FAILURE', meta, handler); return; } const code = 'ERR_UNKNOWN_STATE'; console.error( '%s: expects `rootCommand` to return a function which calls `{ matchedCommand }` which is passed as argument', code, ); await cfg.buildOutput(meta.argv, meta, { isHelp: true, exitCode: 1 }); return; } await tryCatch('ERR_COMMAND_FAILED', meta, matchedCommand); } async function getCommands(cfg) { const fns = typeof cfg.commands === 'object' && cfg.commands.then && cfg.commands.catch ? await cfg.commands : cfg.commands; const cmds = { ...fns }; let commands = Object.entries(cmds); let rootCmd = null; if (typeof cfg.rootCommand === 'function') { rootCmd = cfg.rootCommand; } if (!rootCmd) { const allDef = Object.keys(cmds).length; rootCmd = allDef === 1 ? (commands[0] ? commands[0][1] : null) : null; commands = allDef === 1 ? [] : commands; } const rootCommand = typeof rootCmd === 'function' ? rootCmd : null; if (rootCommand && rootCommand.cli) { const { name: n } = rootCommand.cli; rootCommand.cli.name = n.startsWith(UNNAMED_COMMAND_PREFIX) ? '_' : n; } const entries = commands .filter(([_x, cmd]) => cmd.isYaroCommand || typeof cmd === 'function') .map(([key, _cmd]) => { const cmd = _cmd; let kk = key; if (cmd.isYaroCommand) { if (cmd.cli.name.startsWith(UNNAMED_COMMAND_PREFIX)) { cmd.cli.name = kk; cmd.cli.parts = ['']; } else { kk = cmd.cli.name; } } if ( !cmd.isYaroCommand && typeof cmd === 'function' && typeof cfg.yaroCommand === 'function' ) { // in case we get just a bare function, treat it like command const yaroAction = cfg.yaroCommand(cmd); return [kk, yaroAction]; } return [kk, cmd]; }); return { rootCommand, entries }; } function getCliInfo(rootCommand, commands, cfg) { const version = cfg.version || '0.0.0'; if (rootCommand && rootCommand.cli) { const { name, usage } = rootCommand.cli; const n = name === '_' ? (cfg.name ?? 'cli') : name; return { name: n, usage, helpLine: `${n} ${usage}`.trim(), version, }; } const name = cfg.name || 'cli'; const usage = commands.length > 0 ? '<command>' : ''; return { name, usage, helpLine: `${name} ${usage}`.trim(), version, }; } async function tryCatch(code, meta, handler) { let result = null; try { result = await handler(meta.argv); } catch (err) { // @ts-ignore const exitCode = err.code && typeof err.code === 'number' ? err.code : 1; // @ts-ignore err.code = code; // @ts-ignore err.meta = meta; // @ts-ignore err.exitCode = exitCode; if (meta.entries.length === 0) { meta.config.buildOutput(meta.argv, meta, { error: err, exitCode, code }); return null; } if (handler.isYaroCommand) { const nnn = handler.key || handler.cli.name; const uuu = handler.cli.usage; meta.cliInfo.name = nnn; meta.cliInfo.usage = uuu; meta.cliInfo.helpLine = `${meta.config.name || 'cli'} ${nnn} ${uuu}`.trim(); meta.consolefig.buildOutput(meta.argv, meta, { error: err, exitCode, code }); return null; } meta.config.buildOutput(meta.argv, meta, { error: err, exitCode, code }); return null; } return result; } function findMatchCommand(entries, meta) { const match = entries.find(([name, cmd]) => { if (cmd.isYaroCommand) { let matched = false; for (const [index, arg] of meta.argv._.entries()) { if (cmd.cli.aliases.includes(arg)) { matched = arg; break; } const temp = meta.argv._.slice(0, index + 1).join(' '); matched = cmd.cli.aliases.find((x) => x === temp) || ''; if (matched) { break; } } if (matched) { // @ts-ignore meta.argv._ = meta.argv._.slice(matched.split(' ').length); return true; } const cmdName = meta.argv._.slice(0, cmd.cli.parts.length).join(' '); if (name === cmdName) { meta.argv._ = meta.argv._.slice(cmd.cli.parts.length); return true; } return false; } return false; }); if (match && match[1]) { const handler = match[1]; handler.key = handler.key || handler.cli?.name || match[0]; handler.cli.name = handler.key; return handler; } return null; }