UNPKG

zod-opts

Version:

node.js CLI option parser / validator using Zod

291 lines (290 loc) 11.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandParser = void 0; const node_path_1 = __importDefault(require("node:path")); const zod_1 = require("zod"); const error_1 = require("./error"); const help_1 = require("./help"); const internal_parser_1 = require("./internal_parser"); const logger_1 = require("./logger"); const util = __importStar(require("./util")); const validator_1 = require("./validator"); class CommandParser { constructor({ name, version, description, handler, commands, } = {}) { this._commands = []; this._name = name; this._version = version; this._description = description; this._handler = handler; if (commands !== undefined) { this._commands = commands; } } name(name) { this._name = name; return this; } version(version) { this._version = version; return this; } description(description) { this._description = description; return this; } _internalHandler(handler) { this._handler = handler; return this; } subcommand(command) { if (this._commands.some((c) => c.toInternalCommand().name === command.toInternalCommand().name)) { throw new Error(`Duplicated command name: ${command.toInternalCommand().name}`); } this._commands = this._commands.concat([command]); return this; } showHelp(commandName) { const help = this.getHelp(commandName); console.log(help); // eslint-disable-line no-console } getHelp(commandName) { if (commandName === undefined) { const internalCommands = this._commands.map((command) => command.toInternalCommand()); return (0, help_1.generateGlobalCommandHelp)({ commands: internalCommands, name: this._scriptName(), description: this._description, version: this._version, }); } const foundCommand = this._commands.find((command) => command.toInternalCommand().name === commandName); if (foundCommand === undefined) { throw new Error(`Command not found: ${commandName !== null && commandName !== void 0 ? commandName : ""}`); } return (0, help_1.generateCommandHelp)({ command: foundCommand.toInternalCommand(), name: this._name, version: this._version, }); } parse(args) { var _a; const validArgs = args !== null && args !== void 0 ? args : process.argv.slice(2); const commands = this._commands.map((command) => { return command._toParseCommand(); }); const internalCommands = commands.map((command) => command.internalCommand); const internalResult = this._internalParseAndValidate({ args: validArgs, commands: internalCommands, scriptName: this._scriptName(), }); if (internalResult.type !== "match") { this._handleAndExit(internalResult); } const usedCommandIndex = internalCommands.findIndex((command) => internalResult.commandName === command.name); if (usedCommandIndex < 0) { throw new Error(`Command not found: ${(_a = internalResult.commandName) !== null && _a !== void 0 ? _a : ""}`); } const { shape, action, validation } = commands[usedCommandIndex]; const zodParseResult = this._zodParse(internalResult, shape); if (!zodParseResult.success) { this._handleAndExit(zodParseResult.error); } const validationError = this._validateByCustomValidation(validation, zodParseResult.value, internalResult.help); if (validationError !== undefined) { this._handleAndExit(validationError); } if (this._handler !== undefined) { this._handler({ ...internalResult, parsed: zodParseResult.value }); } action(zodParseResult.value); } _handleAndExit(handlerArg) { if (this._handler != null) { this._handler(handlerArg); } util.errorExit(handlerArg, this._version); } _scriptName() { if (this._name !== undefined) { return this._name; } const pathName = process.argv[1]; return node_path_1.default.basename(pathName); } _generateParseHelp(commands, selectedCommand, scriptName) { return selectedCommand === undefined ? (0, help_1.generateGlobalCommandHelp)({ commands, name: scriptName, description: this._description, version: this._version, }) : (0, help_1.generateCommandHelp)({ command: selectedCommand, name: scriptName, version: this._version, }); } _handleInternalParseHelpAndVersion(parsed, selectedCommand, commands, scriptName) { const help = this._generateParseHelp(commands, selectedCommand, scriptName); if (parsed.isHelp) { return { type: "help", help, exitCode: 0, commandName: parsed.commandName, }; } if (parsed.isVersion) { return { type: "version", help, exitCode: 0, }; } util.assertNever(parsed); } _handleInternalParseError(e, commands, scriptName) { const error = e; const selectedCommand = error.commandName !== undefined ? commands.find((command) => command.name === error.commandName) : undefined; return { type: "error", error: e, exitCode: 1, help: this._generateParseHelp(commands, selectedCommand, scriptName), commandName: error.commandName, }; } _handleInternalParseMatch(parsed, selectedCommand, commands, scriptName) { var _a; const commandName = (_a = parsed.commandName) !== null && _a !== void 0 ? _a : selectedCommand.name; const { options: validOptions, positionalArgs: validPositionalArguments } = (0, validator_1.validateMultipleCommands)(parsed, selectedCommand.options, selectedCommand.positionalArgs, commandName); (0, logger_1.debugLog)("createInternalParserAndParse", { validOptions: JSON.stringify(validOptions), validPositionalArguments: JSON.stringify(validPositionalArguments), }); const validOptionMap = Object.fromEntries(validOptions.map((option) => [option.name, option.value])); const validPositionalArgMap = Object.fromEntries(validPositionalArguments.map((option) => [option.name, option.value])); return { type: "match", parsed: { ...validOptionMap, ...validPositionalArgMap, }, commandName, help: this._generateParseHelp(commands, selectedCommand, scriptName), }; } _internalParseAndValidate({ args, commands, scriptName, }) { var _a; try { if (args.length === 0) { throw new error_1.ParseError("No command specified"); } const parsed = (0, internal_parser_1.parseMultipleCommands)({ args, commands, }); (0, logger_1.debugLog)("parseMultipleCommands", { parsed: JSON.stringify(parsed), }); const selectedCommand = commands.find((command) => command.name === parsed.commandName); if (parsed.isHelp || parsed.isVersion) { return this._handleInternalParseHelpAndVersion(parsed, selectedCommand, commands, scriptName); } if (selectedCommand === undefined) { throw new Error(`Command not found: ${(_a = parsed.commandName) !== null && _a !== void 0 ? _a : ""}`); } return this._handleInternalParseMatch(parsed, selectedCommand, commands, scriptName); } catch (e) { (0, logger_1.debugLog)("createInternalParserAndParse handle error", e); if (!(e instanceof error_1.ParseError)) { throw e; } return this._handleInternalParseError(e, commands, scriptName); } } _zodParse(prevResult, shape) { const result = zod_1.z.object(shape).safeParse(prevResult.parsed); if (!result.success) { const firstError = result.error.issues[0]; return { success: false, error: { type: "error", error: new error_1.ParseError(`${firstError.message}: ${firstError.path.join("")}`, result.error), help: prevResult.help, exitCode: 1, commandName: prevResult.commandName, }, }; } return { success: true, value: result.data }; } _validateByCustomValidation(validation, value, help) { if (validation === undefined) { return undefined; } const validateResult = (() => { try { return validation(value); } catch (e) { if (e instanceof Error) { return e.message; } throw e; } })(); if (validateResult !== true) { return { type: "error", error: new error_1.ParseError(validateResult), help, exitCode: 1, }; } } } exports.CommandParser = CommandParser;