appium
Version:
Automation for Apps.
311 lines • 13.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ArgParser = exports.EXTRA_ARGS = void 0;
exports.getParser = getParser;
const support_1 = require("@appium/support");
const argparse_1 = require("argparse");
const lodash_1 = __importDefault(require("lodash"));
const node_path_1 = __importDefault(require("node:path"));
const constants_1 = require("../constants");
const schema_1 = require("../schema");
const config_1 = require("../config");
const args_1 = require("./args");
const setup_command_1 = require("./setup-command");
exports.EXTRA_ARGS = 'extraArgs';
/**
* If the parsed args do not contain any of these values, then we
* will automatically inject the `server` subcommand.
*/
const NON_SERVER_ARGS = Object.freeze(new Set([constants_1.SETUP_SUBCOMMAND, constants_1.DRIVER_TYPE, constants_1.PLUGIN_TYPE, constants_1.SERVER_SUBCOMMAND, '-h', '--help', '-v', '--version']));
const version = support_1.fs.readPackageJsonFrom(config_1.rootDir).version;
/**
* A wrapper around `argparse`
*
* - Handles instantiation, configuration, and monkeypatching of an
* `ArgumentParser` instance for Appium server and its extensions
* - Handles error conditions, messages, and exit behavior
*/
class ArgParser {
/**
* @param {boolean} [debug] - If true, throw instead of exit on error.
*/
constructor(debug = false) {
const prog = process.argv[1] ? node_path_1.default.basename(process.argv[1]) : 'appium';
const parser = new argparse_1.ArgumentParser({
add_help: true,
description: 'A webdriver-compatible server that facilitates automation of web, mobile, and other ' +
'types of apps across various platforms.',
prog,
});
ArgParser._patchExit(parser);
/**
* Program name (typically `appium`)
* @type {string}
*/
this.prog = prog;
/**
* If `true`, throw an error on parse failure instead of printing help
* @type {boolean}
*/
this.debug = debug;
/**
* Wrapped `ArgumentParser` instance
* @type {ArgumentParser}
*/
this.parser = parser;
parser.add_argument('-v', '--version', {
action: 'version',
version,
});
const subParsers = parser.add_subparsers({ dest: 'subcommand' });
// add the 'server' subcommand, and store the raw arguments on the parser
// object as a way for other parts of the code to work with the arguments
// conceptually rather than just through argparse
const serverArgs = ArgParser._addServerToParser(subParsers);
this.rawArgs = serverArgs;
// add the 'driver' and 'plugin' subcommands
ArgParser._addExtensionCommandsToParser(subParsers);
// add the 'setup' command
ArgParser._addSetupToParser(subParsers);
// backwards compatibility / drop-in wrapper
/**
* @type {ArgParser['parseArgs']}
*/
this.parse_args = this.parseArgs;
}
/**
* Parse arguments from the command line.
*
* If no subcommand is passed in, this method will inject the `server` subcommand.
*
* `ArgParser.prototype.parse_args` is an alias of this method.
* @template {import('appium/types').CliCommand} [Cmd=import('appium/types').CliCommandServer]
* @param {string[]} [args] - Array of arguments, ostensibly from `process.argv`. Gathers args from `process.argv` if not provided.
* @returns {import('appium/types').Args<Cmd>} - The parsed arguments
*/
parseArgs(args = process.argv.slice(2)) {
if (!NON_SERVER_ARGS.has(args[0])) {
args.unshift(constants_1.SERVER_SUBCOMMAND);
}
try {
const parsed = this.parser.parse_known_args(args);
const [knownArgs, unknownArgs] = parsed;
// XXX: you'd think that argparse, when given an alias for a subcommand,
// would set this value to the original subcommand name, but it doesn't.
if (knownArgs?.driverCommand === 'ls') {
knownArgs.driverCommand = 'list';
}
else if (knownArgs?.pluginCommand === 'ls') {
knownArgs.pluginCommand = 'list';
}
if (unknownArgs?.length &&
(knownArgs.driverCommand === 'run' || knownArgs.pluginCommand === 'run')) {
return ArgParser._transformParsedArgs(knownArgs, unknownArgs);
}
else if (unknownArgs?.length) {
throw new Error(`[ERROR] Unrecognized arguments: ${unknownArgs.join(' ')}`);
}
return ArgParser._transformParsedArgs(knownArgs);
}
catch (err) {
if (this.debug) {
throw err;
}
// this isn't tested via unit tests (we use `debug: true`) so may escape coverage.
/* istanbul ignore next */
{
// eslint-disable-next-line no-console
console.error(); // need an extra space since argparse prints usage.
// eslint-disable-next-line no-console
console.error(err.message);
process.exit(1);
}
}
}
/**
* Given an object full of arguments as returned by `argparser.parse_args`,
* expand the ones for extensions into a nested object structure and rename
* keys to match the intended destination.
*
* E.g., `{'driver-foo-bar': baz}` becomes `{driver: {foo: {bar: 'baz'}}}`
* @param {object} args
* @param {string[]} [unknownArgs]
* @returns {object}
*/
static _transformParsedArgs(args, unknownArgs = []) {
const result = lodash_1.default.reduce(args, (unpacked, value, key) => {
if (!lodash_1.default.isUndefined(value) && (0, schema_1.hasArgSpec)(key)) {
const { dest } = /** @type {import('../schema/arg-spec').ArgSpec} */ ((0, schema_1.getArgSpec)(key));
lodash_1.default.set(unpacked, dest, value);
}
else {
// this could be anything that _isn't_ a server arg
unpacked[key] = value;
}
return unpacked;
}, {});
result[exports.EXTRA_ARGS] = unknownArgs;
return result;
}
/**
* Patches the `exit()` method of the parser to throw an error, so we can handle it manually.
* @param {ArgumentParser} parser
*/
static _patchExit(parser) {
parser.exit = (code, msg) => {
if (code) {
throw new Error(msg);
}
process.exit();
};
}
/**
*
* @param {import('argparse').SubParser} subParser
* @returns {import('./args').ArgumentDefinitions}
*/
static _addServerToParser(subParser) {
const serverParser = subParser.add_parser('server', {
add_help: true,
help: 'Start an Appium server',
description: 'Start an Appium server (the "server" subcommand is optional)',
});
ArgParser._patchExit(serverParser);
const serverArgs = (0, args_1.getServerArgs)();
for (const [flagsOrNames, opts] of serverArgs) {
// @ts-ignore TS doesn't like the spread operator here.
serverParser.add_argument(...flagsOrNames, { ...opts });
}
return serverArgs;
}
/**
* Adds extension sub-sub-commands to `driver`/`plugin` subcommands
* @param {import('argparse').SubParser} subParsers
*/
static _addExtensionCommandsToParser(subParsers) {
for (const type of /** @type {[DriverType, PluginType]} */ ([constants_1.DRIVER_TYPE, constants_1.PLUGIN_TYPE])) {
const extParser = subParsers.add_parser(type, {
add_help: true,
help: `Manage Appium ${type}s`,
description: `Manage Appium ${type}s using various subcommands`,
});
ArgParser._patchExit(extParser);
const extSubParsers = extParser.add_subparsers({
dest: `${type}Command`,
});
const extensionArgs = (0, args_1.getExtensionArgs)();
/**
* @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string, aliases?: import('argparse').SubArgumentParserOptions['aliases']}[] }
*/
const parserSpecs = [
{
command: constants_1.EXT_SUBCOMMAND_LIST,
args: extensionArgs[type].list,
help: `List available and installed ${type}s`,
aliases: ['ls'],
},
{
command: constants_1.EXT_SUBCOMMAND_INSTALL,
args: extensionArgs[type].install,
help: `Install a ${type}`,
},
{
command: constants_1.EXT_SUBCOMMAND_UNINSTALL,
args: extensionArgs[type].uninstall,
help: `Uninstall a ${type}`,
},
{
command: constants_1.EXT_SUBCOMMAND_UPDATE,
args: extensionArgs[type].update,
help: `Update one or more installed ${type}s to the latest version`,
},
{
command: constants_1.EXT_SUBCOMMAND_RUN,
args: extensionArgs[type].run,
help: `Run a script (if available) from the given ${type}`,
},
{
command: constants_1.EXT_SUBCOMMAND_DOCTOR,
args: extensionArgs[type].doctor,
help: `Run doctor checks (if available) for the given ${type}`,
},
];
for (const { command, args, help, aliases } of parserSpecs) {
const parser = extSubParsers.add_parser(command, { help, aliases: aliases ?? [] });
ArgParser._patchExit(parser);
for (const [flagsOrNames, opts] of args) {
// add_argument mutates params so make sure to send in copies instead
if (flagsOrNames.length === 2) {
parser.add_argument(flagsOrNames[0], flagsOrNames[1], { ...opts });
}
else {
parser.add_argument(flagsOrNames[0], { ...opts });
}
}
}
}
}
/**
* Add subcommand and sub-sub commands for 'setup' subcommand.
* @param {import('argparse').SubParser} subParser
*/
static _addSetupToParser(subParser) {
const setupParser = subParser.add_parser('setup', {
add_help: true,
help: 'Batch install or uninstall Appium drivers and plugins',
description: `Install a preset of official drivers/plugins compatible with the current host platform ` +
`(${(0, setup_command_1.determinePlatformName)()}). Existing drivers/plugins will remain. The default preset ` +
`is "mobile". Providing the special "reset" subcommand will instead uninstall all ` +
`drivers and plugins, and remove their related manifest files.`,
});
ArgParser._patchExit(setupParser);
const extSubParsers = setupParser.add_subparsers({
dest: `setupCommand`,
});
const parserSpecs = [
{
command: setup_command_1.SUBCOMMAND_MOBILE,
help: `The preset for mobile devices ` +
`(drivers: ${lodash_1.default.join((0, setup_command_1.getPresetDrivers)(setup_command_1.SUBCOMMAND_MOBILE), ',')}; plugins: ${setup_command_1.DEFAULT_PLUGINS})`
},
{
command: setup_command_1.SUBCOMMAND_BROWSER,
help: `The preset for desktop browsers ` +
`(drivers: ${lodash_1.default.join((0, setup_command_1.getPresetDrivers)(setup_command_1.SUBCOMMAND_BROWSER), ',')}; plugins: ${setup_command_1.DEFAULT_PLUGINS})`
},
{
command: setup_command_1.SUBCOMMAND_DESKTOP,
help: `The preset for desktop applications ` +
`(drivers: ${lodash_1.default.join((0, setup_command_1.getPresetDrivers)(setup_command_1.SUBCOMMAND_DESKTOP), ',')}; plugins: ${setup_command_1.DEFAULT_PLUGINS})`
},
{
command: setup_command_1.SUBCOMMAND_RESET,
help: 'Remove all installed drivers and plugins'
},
];
for (const { command, help } of parserSpecs) {
const parser = extSubParsers.add_parser(command, { help });
ArgParser._patchExit(parser);
}
}
}
exports.ArgParser = ArgParser;
/**
* Creates a {@link ArgParser} instance; finalizes the config schema.
*
* @constructs ArgParser
* @param {boolean} [debug] - If `true`, throw instead of exit upon parsing error
* @returns {ArgParser}
*/
function getParser(debug) {
(0, schema_1.finalizeSchema)();
return new ArgParser(debug);
}
/**
* @typedef {import('@appium/types').DriverType} DriverType
* @typedef {import('@appium/types').PluginType} PluginType
*/
//# sourceMappingURL=parser.js.map