appium
Version: 
Automation for Apps.
390 lines • 15.1 kB
JavaScript
;
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.rootDir = exports.APPIUM_VER = void 0;
exports.updateBuildInfo = updateBuildInfo;
exports.showDebugInfo = showDebugInfo;
exports.getGitRev = getGitRev;
exports.getBuildInfo = getBuildInfo;
exports.checkNodeOk = checkNodeOk;
exports.showBuildInfo = showBuildInfo;
exports.getNonDefaultServerArgs = getNonDefaultServerArgs;
exports.showConfig = showConfig;
exports.requireDir = requireDir;
/* eslint-disable no-console */
const lodash_1 = __importDefault(require("lodash"));
const support_1 = require("@appium/support");
const axios_1 = __importDefault(require("axios"));
const teen_process_1 = require("teen_process");
const semver = __importStar(require("semver"));
const node_os_1 = __importDefault(require("node:os"));
const utils_1 = require("./utils");
const bluebird_1 = __importDefault(require("bluebird"));
const schema_1 = require("./schema/schema");
exports.APPIUM_VER = utils_1.npmPackage.version;
const ENGINES = /** @type {Record<string,string>} */ (utils_1.npmPackage.engines);
const MIN_NODE_VERSION = ENGINES.node;
exports.rootDir = support_1.fs.findRoot(__dirname);
const GIT_BINARY = `git${support_1.system.isWindows() ? '.exe' : ''}`;
const GITHUB_API = 'https://api.github.com/repos/appium/appium';
/**
 * @type {import('appium/types').BuildInfo}
 */
const BUILD_INFO = {
    version: exports.APPIUM_VER,
};
function getNodeVersion() {
    return /** @type {import('semver').SemVer} */ (semver.coerce(process.version));
}
/**
 * @param {boolean} [useGithubApiFallback]
 * @returns {Promise<void>}
 */
async function updateBuildInfo(useGithubApiFallback = false) {
    const sha = await getGitRev(useGithubApiFallback);
    if (!sha) {
        return;
    }
    BUILD_INFO['git-sha'] = sha;
    const buildTimestamp = await getGitTimestamp(sha, useGithubApiFallback);
    if (buildTimestamp) {
        BUILD_INFO.built = buildTimestamp;
    }
}
/** @type {() => Promise<string?>} */
const getFullGitPath = lodash_1.default.memoize(async function getFullGitPath() {
    try {
        return await support_1.fs.which(GIT_BINARY);
    }
    catch {
        return null;
    }
});
/**
 * Prints server debug into into the console.
 *
 * @param {{
 * driverConfig: import('./extension/driver-config').DriverConfig,
 * pluginConfig: import('./extension/plugin-config').PluginConfig,
 * appiumHome: string
 * }} info
 * @returns {Promise<void>}
 */
async function showDebugInfo({ driverConfig, pluginConfig, appiumHome }) {
    const getNpmVersion = async () => {
        const { stdout } = await support_1.npm.exec('--version', [], { cwd: process.cwd() });
        return lodash_1.default.trim(stdout);
    };
    const findNpmLocation = async () => await support_1.fs.which(support_1.system.isWindows() ? 'npm.cmd' : 'npm');
    const [npmVersion, npmLocation] = await bluebird_1.default.all([
        ...([getNpmVersion, findNpmLocation].map((f) => getSafeResult(f, 'unknown'))),
        /** @type {any} */ (updateBuildInfo()),
    ]);
    const debugInfo = {
        os: {
            platform: node_os_1.default.platform(),
            release: node_os_1.default.release(),
            arch: node_os_1.default.arch(),
            homedir: node_os_1.default.homedir(),
            username: node_os_1.default.userInfo().username,
        },
        node: {
            version: process.version,
            arch: process.arch,
            cwd: process.cwd(),
            argv: process.argv,
            env: process.env,
            npm: {
                location: npmLocation,
                version: npmVersion,
            },
        },
        appium: {
            location: exports.rootDir,
            homedir: appiumHome,
            build: getBuildInfo(),
            drivers: driverConfig.installedExtensions,
            plugins: pluginConfig.installedExtensions,
        },
    };
    console.log(JSON.stringify(debugInfo, null, 2));
}
/**
 * @param {boolean} [useGithubApiFallback]
 * @returns {Promise<string?>}
 */
async function getGitRev(useGithubApiFallback = false) {
    const fullGitPath = await getFullGitPath();
    if (fullGitPath) {
        try {
            const { stdout } = await (0, teen_process_1.exec)(fullGitPath, ['rev-parse', 'HEAD'], {
                cwd: __dirname,
            });
            return stdout.trim();
        }
        catch { }
    }
    if (!useGithubApiFallback) {
        return null;
    }
    // If the package folder is not a valid git repository
    // then fetch the corresponding tag info from GitHub
    try {
        return (await axios_1.default.get(`${GITHUB_API}/git/refs/tags/appium@${exports.APPIUM_VER}`, {
            headers: {
                'User-Agent': `Appium ${exports.APPIUM_VER}`,
            },
        })).data?.object?.sha;
    }
    catch { }
    return null;
}
/**
 * @param {string} commitSha
 * @param {boolean} [useGithubApiFallback]
 * @returns {Promise<string?>}
 */
async function getGitTimestamp(commitSha, useGithubApiFallback = false) {
    const fullGitPath = await getFullGitPath();
    if (fullGitPath) {
        try {
            const { stdout } = await (0, teen_process_1.exec)(fullGitPath, ['show', '-s', '--format=%ci', commitSha], {
                cwd: __dirname,
            });
            return stdout.trim();
        }
        catch { }
    }
    if (!useGithubApiFallback) {
        return null;
    }
    try {
        return (await axios_1.default.get(`${GITHUB_API}/git/tags/${commitSha}`, {
            headers: {
                'User-Agent': `Appium ${exports.APPIUM_VER}`,
            },
        })).data?.tagger?.date;
    }
    catch { }
    return null;
}
/**
 * Mutable object containing Appium build information. By default it
 * only contains the Appium version, but is updated with the build timestamp
 * and git commit hash asynchronously as soon as `updateBuildInfo` is called
 * and succeeds.
 * @returns {import('appium/types').BuildInfo}
 */
function getBuildInfo() {
    return BUILD_INFO;
}
/**
 * @returns {void}
 * @throws {Error} If Node version is outside of the supported range
 */
function checkNodeOk() {
    const version = getNodeVersion();
    if (!semver.satisfies(version, MIN_NODE_VERSION)) {
        throw new Error(`Node version must be at least ${MIN_NODE_VERSION}; current is ${version.version}`);
    }
}
async function showBuildInfo() {
    await updateBuildInfo(true);
    console.log(JSON.stringify(getBuildInfo()));
}
/**
 * Returns k/v pairs of server arguments which are _not_ the defaults.
 * @param {Args} parsedArgs
 * @returns {Args}
 */
function getNonDefaultServerArgs(parsedArgs) {
    /**
     * Flattens parsed args into a single level object for comparison with
     * flattened defaults across server args and extension args.
     * @param {Args} args
     * @returns {Record<string, { value: any, argSpec: ArgSpec }>}
     */
    const flatten = (args) => {
        const argSpecs = (0, schema_1.getAllArgSpecs)();
        const flattened = lodash_1.default.reduce([...argSpecs.values()], (acc, argSpec) => {
            if (lodash_1.default.has(args, argSpec.dest)) {
                acc[argSpec.dest] = { value: lodash_1.default.get(args, argSpec.dest), argSpec };
            }
            return acc;
        }, 
        /** @type {Record<string, { value: any, argSpec: ArgSpec }>} */ ({}));
        return flattened;
    };
    const args = flatten(parsedArgs);
    // hopefully these function names are descriptive enough
    const typesDiffer = /** @param {string} dest */ (dest) => typeof args[dest].value !== typeof defaultsFromSchema[dest];
    const defaultValueIsArray = /** @param {string} dest */ (dest) => lodash_1.default.isArray(defaultsFromSchema[dest]);
    const argsValueIsArray = /** @param {string} dest */ (dest) => lodash_1.default.isArray(args[dest].value);
    const arraysDiffer = /** @param {string} dest */ (dest) => lodash_1.default.gt(lodash_1.default.size(lodash_1.default.difference(args[dest].value, defaultsFromSchema[dest])), 0);
    const valuesDiffer = /** @param {string} dest */ (dest) => args[dest].value !== defaultsFromSchema[dest];
    const defaultIsDefined = /** @param {string} dest */ (dest) => !lodash_1.default.isUndefined(defaultsFromSchema[dest]);
    // note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR"
    const argValueNotArrayOrArraysDiffer = lodash_1.default.overSome([lodash_1.default.negate(argsValueIsArray), arraysDiffer]);
    const defaultValueNotArrayAndValuesDiffer = lodash_1.default.overEvery([
        lodash_1.default.negate(defaultValueIsArray),
        valuesDiffer,
    ]);
    /**
     * This used to be a hideous conditional, but it's broken up into a hideous function instead.
     * hopefully this makes things a little more understandable.
     * - checks if the default value is defined
     * - if so, and the default is not an array:
     *   - ensures the types are the same
     *   - ensures the values are equal
     * - if so, and the default is an array:
     *   - ensures the args value is an array
     *   - ensures the args values do not differ from the default values
     * @type {(dest: string) => boolean}
     */
    const isNotDefault = lodash_1.default.overEvery([
        defaultIsDefined,
        lodash_1.default.overSome([
            typesDiffer,
            lodash_1.default.overEvery([defaultValueIsArray, argValueNotArrayOrArraysDiffer]),
            defaultValueNotArrayAndValuesDiffer,
        ]),
    ]);
    const defaultsFromSchema = (0, schema_1.getDefaultsForSchema)(true);
    return lodash_1.default.reduce(lodash_1.default.pickBy(args, (__, key) => isNotDefault(key)), 
    // explodes the flattened object back into nested one
    (acc, { value, argSpec }) => lodash_1.default.set(acc, argSpec.dest, value), 
    /** @type {Args} */ ({}));
}
/**
 * Compacts an object for {@link showConfig}:
 * 1. Removes `subcommand` key/value
 * 2. Removes `undefined` values
 * 3. Removes empty objects (but not `false` values)
 * Does not operate recursively.
 */
const compactConfig = lodash_1.default.partial(lodash_1.default.omitBy, lodash_1.default, (value, key) => key === 'subcommand' || lodash_1.default.isUndefined(value) || (lodash_1.default.isObject(value) && lodash_1.default.isEmpty(value)));
/**
 * Shows a breakdown of the current config after CLI params, config file loaded & defaults applied.
 *
 * The actual shape of `preConfigParsedArgs` and `defaults` does not matter for the purposes of this function,
 * but it's intended to be called with values of type {@link ParsedArgs} and `DefaultValues<true>`, respectively.
 *
 * @param {Partial<ParsedArgs>} nonDefaultPreConfigParsedArgs - Parsed CLI args (or param to `init()`) before config & defaults applied
 * @param {import('./config-file').ReadConfigFileResult} configResult - Result of attempting to load a config file.  _Must_ be normalized
 * @param {Partial<ParsedArgs>} defaults - Configuration defaults from schemas
 * @param {ParsedArgs} parsedArgs - Entire parsed args object
 */
function showConfig(nonDefaultPreConfigParsedArgs, configResult, defaults, parsedArgs) {
    console.log('Appium Configuration\n');
    console.log('from defaults:\n');
    console.dir(compactConfig(defaults));
    if (configResult.config) {
        console.log(`\nfrom config file at ${configResult.filepath}:\n`);
        console.dir(compactConfig(configResult.config));
    }
    else {
        console.log(`\n(no configuration file loaded)`);
    }
    const compactedNonDefaultPreConfigArgs = compactConfig(nonDefaultPreConfigParsedArgs);
    if (lodash_1.default.isEmpty(compactedNonDefaultPreConfigArgs)) {
        console.log(`\n(no CLI parameters provided)`);
    }
    else {
        console.log('\nvia CLI or function call:\n');
        console.dir(compactedNonDefaultPreConfigArgs);
    }
    console.log('\nfinal configuration:\n');
    console.dir(compactConfig(parsedArgs));
}
/**
 * @param {string} root
 * @param {boolean} [requireWriteable=true]
 * @param {string} [displayName='folder path']
 * @throws {Error}
 */
async function requireDir(root, requireWriteable = true, displayName = 'folder path') {
    let stat;
    try {
        stat = await support_1.fs.stat(root);
    }
    catch (e) {
        if (e.code === 'ENOENT') {
            try {
                await support_1.fs.mkdir(root, { recursive: true });
                return;
            }
            catch { }
        }
        throw new Error(`The ${displayName} '${root}' must exist and be a valid directory`);
    }
    if (stat && !stat.isDirectory()) {
        throw new Error(`The ${displayName} '${root}' must be a valid directory`);
    }
    if (requireWriteable) {
        try {
            await support_1.fs.access(root, support_1.fs.constants.W_OK);
        }
        catch {
            throw new Error(`The ${displayName} '${root}' must be ` +
                `writeable for the current user account '${node_os_1.default.userInfo().username}'`);
        }
    }
}
/**
 * Calculates the result of the given function and return its value
 * or the default one if there was an exception.
 *
 * @template T
 * @param {() => Promise<T>} f
 * @param {T} defaultValue
 * @returns {Promise<T>}
 */
async function getSafeResult(f, defaultValue) {
    try {
        return await f();
    }
    catch {
        return defaultValue;
    }
}
/**
 * @typedef {import('appium/types').ParsedArgs} ParsedArgs
 * @typedef {import('appium/types').Args} Args
 * @typedef {import('./schema/arg-spec').ArgSpec} ArgSpec
 */
//# sourceMappingURL=config.js.map