appium
Version:
Automation for Apps.
522 lines • 20 kB
JavaScript
"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