UNPKG

appium

Version:

Automation for Apps.

522 lines 20 kB
"use strict"; /** * Module containing {@link Manifest} which handles reading & writing of extension config files. */ 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.Manifest = void 0; const bluebird_1 = __importDefault(require("bluebird")); const support_1 = require("@appium/support"); const lodash_1 = __importDefault(require("lodash")); const node_path_1 = __importDefault(require("node:path")); const YAML = __importStar(require("yaml")); const constants_1 = require("../constants"); const extension_config_1 = require("./extension-config"); const package_changed_1 = require("./package-changed"); const manifest_migrations_1 = require("./manifest-migrations"); /** * The name of the prop (`drivers`) used in `extensions.yaml` for drivers. * @type {`${typeof DRIVER_TYPE}s`} */ const CONFIG_DATA_DRIVER_KEY = `${constants_1.DRIVER_TYPE}s`; /** * The name of the prop (`plugins`) used in `extensions.yaml` for plugins. * @type {`${typeof PLUGIN_TYPE}s`} */ const CONFIG_DATA_PLUGIN_KEY = `${constants_1.PLUGIN_TYPE}s`; /** * @type {Readonly<ManifestData>} */ const INITIAL_MANIFEST_DATA = Object.freeze({ [CONFIG_DATA_DRIVER_KEY]: Object.freeze({}), [CONFIG_DATA_PLUGIN_KEY]: Object.freeze({}), schemaRev: constants_1.CURRENT_SCHEMA_REV, }); /** * Given a `package.json` return `true` if it represents an Appium Extension (either a driver or plugin). * * _This is a type guard; not a validator._ * * The `package.json` must have an `appium` property which is an object. * @param {any} value * @returns {value is ExtPackageJson<ExtensionType>} */ function isExtension(value) { return (lodash_1.default.isPlainObject(value) && lodash_1.default.isPlainObject(value.appium) && lodash_1.default.isString(value.name) && lodash_1.default.isString(value.version)); } /** * Given a `package.json`, return `true` if it represents an Appium Driver. * * _This is a type guard; not a validator._ * * To be considered a driver, a `package.json` must have an `appium.driverName` field. * * Further validation of the `appium` property happens elsewhere. * @param {any} value - Value to test * @returns {value is ExtPackageJson<DriverType>} */ function isDriver(value) { return isExtension(value) && 'driverName' in value.appium && lodash_1.default.isString(value.appium.driverName); } /** * Given a `package.json`, return `true` if it represents an Appium Plugin. * * _This is a type guard; not a validator._ * * To be considered a plugin, a `package.json` must have an `appium.pluginName` field. * * Further validation of the `appium` property happens elsewhere. * @param {any} value - Value to test * @returns {value is ExtPackageJson<PluginType>} */ function isPlugin(value) { return isExtension(value) && 'pluginName' in value.appium && lodash_1.default.isString(value.appium.pluginName); } /** * Handles reading & writing of extension config files. * * Only one instance of this class exists per value of `APPIUM_HOME`. */ class Manifest { /** * The entire contents of a parsed YAML extension config file. * * Contains proxies for automatic persistence on disk * @type {ManifestData} */ #data; /** * Path to `APPIUM_HOME`. * @type {Readonly<string>} */ #appiumHome; /** * Path to `extensions.yaml` * @type {string} * Not set until {@link Manifest.read} is called. */ #manifestPath; /** * Helps avoid writing multiple times. * * If this is `undefined`, calling {@link Manifest.write} will cause it to be * set to a `Promise`. When the call to `write()` is complete, the `Promise` * will resolve and then this value will be set to `undefined`. Concurrent calls * made while this value is a `Promise` will return the `Promise` itself. * @type {Promise<boolean>|undefined} */ #writing; /** * Helps avoid reading multiple times. * * If this is `undefined`, calling {@link Manifest.read} will cause it to be * set to a `Promise`. When the call to `read()` is complete, the `Promise` * will resolve and then this value will be set to `undefined`. Concurrent calls * made while this value is a `Promise` will return the `Promise` itself. * @type {Promise<void>|undefined} */ #reading; /** * Sets internal data to a fresh clone of {@link INITIAL_MANIFEST_DATA} * * Use {@link Manifest.getInstance} instead. * @param {string} appiumHome * @private */ constructor(appiumHome) { this.#appiumHome = appiumHome; this.#data = lodash_1.default.cloneDeep(INITIAL_MANIFEST_DATA); } /** * Returns a new or existing {@link Manifest} instance, based on the value of `appiumHome`. * * Maintains one instance per value of `appiumHome`. */ static getInstance = lodash_1.default.memoize( /** * @param {string} appiumHome - Path to `APPIUM_HOME` * @returns {Manifest} */ function _getInstance(appiumHome) { return new Manifest(appiumHome); }); /** * Searches `APPIUM_HOME` for installed extensions and adds them to the manifest. * @param {boolean} hasAppiumDependency - This affects whether or not the "dev" `InstallType` is used * @returns {Promise<boolean>} `true` if any extensions were added, `false` otherwise. */ async syncWithInstalledExtensions(hasAppiumDependency = false) { // this could be parallelized, but we can't use fs.walk as an async iterator let didChange = false; /** * Listener for the `match` event of a `glob` instance * @param {string} filepath - Path to a `package.json` * @param {boolean} [devType] - If `true`, this is an extension in "dev mode" * @returns {Promise<void>} */ const onMatch = async (filepath, devType = false) => { try { const pkg = JSON.parse(await support_1.fs.readFile(filepath, 'utf8')); if (isExtension(pkg)) { const installType = devType && hasAppiumDependency ? extension_config_1.INSTALL_TYPE_DEV : extension_config_1.INSTALL_TYPE_NPM; const changed = this.addExtensionFromPackage(pkg, filepath, installType); didChange = didChange || changed; } } catch { } }; /** * A list of `Promise`s which read `package.json` files looking for Appium extensions. * @type {Promise<void>[]} */ const queue = [ // look at `package.json` in `APPIUM_HOME` only. // this causes extensions in "dev mode" to be automatically found onMatch(node_path_1.default.join(this.#appiumHome, 'package.json'), true), ]; // add dependencies to the queue const filepaths = await support_1.fs.glob('node_modules/{*,@*/*}/package.json', { cwd: this.#appiumHome, absolute: true, }); for (const filepath of filepaths) { queue.push(onMatch(filepath)); } // wait for everything to finish await bluebird_1.default.all(queue); return didChange; } /** * Returns `true` if driver with name `name` is registered. * @param {string} name - Driver name * @returns {boolean} */ hasDriver(name) { return Boolean(this.#data.drivers[name]); } /** * Returns `true` if plugin with name `name` is registered. * @param {string} name - Plugin name * @returns {boolean} */ hasPlugin(name) { return Boolean(this.#data.plugins[name]); } /** * Given a path to a `package.json`, add it as either a driver or plugin to the manifest. * * @template {ExtensionType} ExtType * @param {ExtPackageJson<ExtType>} pkgJson * @param {string} pkgPath * @param {typeof INSTALL_TYPE_NPM | typeof INSTALL_TYPE_DEV} [installType] * @returns {boolean} - `true` if this method did anything. */ addExtensionFromPackage(pkgJson, pkgPath, installType = extension_config_1.INSTALL_TYPE_NPM) { const extensionPath = node_path_1.default.dirname(pkgPath); /** * @type {InternalMetadata} */ const internal = { pkgName: /** @type {string} */ (pkgJson.name), version: /** @type {string} */ (pkgJson.version), appiumVersion: pkgJson.peerDependencies?.appium, installType, installSpec: `${pkgJson.name}@${pkgJson.version}`, installPath: extensionPath, }; if (isDriver(pkgJson)) { const value = { ...lodash_1.default.omit(pkgJson.appium, 'driverName'), ...internal, }; if (!lodash_1.default.isEqual(value, this.#data.drivers[pkgJson.appium.driverName])) { this.setExtension( /** @type {ExtType} */ (constants_1.DRIVER_TYPE), pkgJson.appium.driverName, /** @type {ExtManifest<ExtType>} */ (value)); return true; } return false; } else if (isPlugin(pkgJson)) { const value = { ...lodash_1.default.omit(pkgJson.appium, 'pluginName'), ...internal, }; if (!lodash_1.default.isEqual(value, this.#data.plugins[pkgJson.appium.pluginName])) { this.setExtension( /** @type {ExtType} */ (constants_1.PLUGIN_TYPE), pkgJson.appium.pluginName, /** @type {ExtManifest<ExtType>} */ (value)); return true; } return false; } else { throw new TypeError(`The extension in ${extensionPath} is neither a valid ${constants_1.DRIVER_TYPE} nor a valid ${constants_1.PLUGIN_TYPE}.`); } } /** * Adds an extension to the manifest as was installed by the `appium` CLI. The * `extData`, `extType`, and `extName` have already been determined. * * See {@link Manifest.addExtensionFromPackage} for adding an extension from an on-disk package. * @template {ExtensionType} ExtType * @param {ExtType} extType - `driver` or `plugin` * @param {string} extName - Name of extension * @param {ExtManifest<ExtType>} extData - Extension metadata * @returns {ExtManifest<ExtType>} A clone of `extData`, potentially with a mutated `appiumVersion` field */ setExtension(extType, extName, extData) { const data = lodash_1.default.cloneDeep(extData); this.#data[`${extType}s`][extName] = data; return data; } /** * Sets the schema revision * @param {keyof import('./manifest-migrations').ManifestDataVersions} rev */ setSchemaRev(rev) { this.#data.schemaRev = rev; } /** * Remove an extension from the manifest. * @param {ExtensionType} extType * @param {string} extName */ deleteExtension(extType, extName) { delete this.#data[`${extType}s`][extName]; } /** * Returns the `APPIUM_HOME` path */ get appiumHome() { return this.#appiumHome; } /** * Returns the path to the manifest file (`extensions.yaml`) */ get manifestPath() { return this.#manifestPath; } /** * Returns the schema rev of this manifest */ get schemaRev() { return this.#data.schemaRev; } /** * Returns extension data for a particular type. * * @template {ExtensionType} ExtType * @param {ExtType} extType * @returns {Readonly<ExtRecord<ExtType>>} */ getExtensionData(extType) { return this.#data[ /** @type {string} */(`${extType}s`)]; } /** * Reads manifest from disk and _overwrites_ the internal data. * * If the manifest does not exist on disk, an * {@link INITIAL_MANIFEST_DATA "empty"} manifest file will be created, as * well as its directory if needed. * * This will also, if necessary: * 1. perform a migration of the manifest data * 2. sync the manifest with extensions on-disk (kind of like "auto * discovery") * 3. write the manifest to disk. * * Only one read operation can happen at a time. * * @returns {Promise<ManifestData>} The data */ async read() { if (this.#reading) { await this.#reading; return this.#data; } this.#reading = (async () => { /** @type {ManifestData} */ let data; /** * This will be `true` if, after reading, we need to update the manifest data * and write it again to disk. */ let shouldWrite = false; await this.#setManifestPath(); try { const yaml = await support_1.fs.readFile(this.#manifestPath, 'utf8'); data = YAML.parse(yaml); } catch (err) { if (err.code === 'ENOENT') { data = lodash_1.default.cloneDeep(INITIAL_MANIFEST_DATA); shouldWrite = true; } else if (this.#manifestPath) { throw new Error(`Appium had trouble loading the extension installation ` + `cache file (${this.#manifestPath}). It may be invalid YAML. Specific error: ${err.message}`); } else { throw new Error(`Appium encountered an unknown problem. Specific error: ${err.message}`); } } this.#data = data; /** * the only way `shouldWrite` is `true` is if we have a new file. a new * file will get the latest schema revision, so we can skip the migration. */ if (!shouldWrite && (data.schemaRev ?? 0) < constants_1.CURRENT_SCHEMA_REV) { shouldWrite = await (0, manifest_migrations_1.migrate)(this); } const hasAppiumDependency = await support_1.env.hasAppiumDependency(this.appiumHome); /** * we still may want to sync with installed extensions even if we have a * new file. right now this is limited to the following cases: * 1. we have a brand new manifest file * 2. we have performed a migration on a manifest file * 3. `appium` is a dependency within `package.json`, and `package.json` * has changed since last time we checked. * * It may also make sense to sync with the extensions in an arbitrary * `APPIUM_HOME`, but we don't do that here. */ if (shouldWrite || (hasAppiumDependency && (await (0, package_changed_1.packageDidChange)(this.appiumHome)))) { shouldWrite = (await this.syncWithInstalledExtensions(hasAppiumDependency)) || shouldWrite; } if (shouldWrite) { await this.write(); } })(); try { await this.#reading; return this.#data; } finally { this.#reading = undefined; } } /** * Ensures the internal manifest path is set. * * Creates the directory if necessary. * @returns {Promise<string>} */ async #setManifestPath() { if (!this.#manifestPath) { this.#manifestPath = await support_1.env.resolveManifestPath(this.#appiumHome); /* istanbul ignore if */ if (node_path_1.default.relative(this.#appiumHome, this.#manifestPath).startsWith('.')) { throw new Error(`Mismatch between location of APPIUM_HOME and manifest file. APPIUM_HOME: ${this.appiumHome}, manifest file: ${this.#manifestPath}`); } } return this.#manifestPath; } /** * Writes the data if it need s writing. * * If the `schemaRev` prop needs updating, the file will be written. * * @todo If this becomes too much of a bottleneck, throttle it. * @returns {Promise<boolean>} Whether the data was written */ async write() { if (this.#writing) { return this.#writing; } this.#writing = (async () => { await this.#setManifestPath(); try { await support_1.fs.mkdirp(node_path_1.default.dirname(this.#manifestPath)); } catch (err) { throw new Error(`Appium could not create the directory for the manifest file: ${node_path_1.default.dirname(this.#manifestPath)}. Original error: ${err.message}`); } try { await support_1.fs.writeFile(this.#manifestPath, YAML.stringify(this.#data), 'utf8'); return true; } catch (err) { throw new Error(`Appium could not write to manifest at ${this.#manifestPath} using APPIUM_HOME ${this.#appiumHome}. Please ensure it is writable. Original error: ${err.message}`); } })(); try { return await this.#writing; } finally { this.#writing = undefined; } } } exports.Manifest = Manifest; /** * Type of the string referring to a driver (typically as a key or type string) * @typedef {import('@appium/types').DriverType} DriverType */ /** * Type of the string referring to a plugin (typically as a key or type string) * @typedef {import('@appium/types').PluginType} PluginType */ /** * @typedef SyncWithInstalledExtensionsOpts * @property {number} [depthLimit] - Maximum depth to recurse into subdirectories */ /** * @typedef {import('appium/types').ManifestData} ManifestData * @typedef {import('appium/types').InternalMetadata} InternalMetadata */ /** * @template {ExtensionType} ExtType * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson */ /** * @template {ExtensionType} ExtType * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest */ /** * @template {ExtensionType} ExtType * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord */ /** * Either `driver` or `plugin` rn * @typedef {import('@appium/types').ExtensionType} ExtensionType */ //# sourceMappingURL=manifest.js.map