@appium/support
Version:
Support libs used across Appium packages
507 lines • 19.9 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.GiB = exports.MiB = exports.KiB = exports.W3C_WEB_ELEMENT_IDENTIFIER = exports.uuidV5 = exports.uuidV4 = exports.uuidV3 = exports.uuidV1 = exports.shellParse = void 0;
exports.hasContent = hasContent;
exports.hasValue = hasValue;
exports.escapeSpace = escapeSpace;
exports.escapeSpecialChars = escapeSpecialChars;
exports.localIp = localIp;
exports.cancellableDelay = cancellableDelay;
exports.multiResolve = multiResolve;
exports.safeJsonParse = safeJsonParse;
exports.jsonStringify = jsonStringify;
exports.unwrapElement = unwrapElement;
exports.wrapElement = wrapElement;
exports.filterObject = filterObject;
exports.toReadableSizeString = toReadableSizeString;
exports.isSubPath = isSubPath;
exports.isSameDestination = isSameDestination;
exports.coerceVersion = coerceVersion;
exports.compareVersions = compareVersions;
exports.quote = quote;
exports.pluralize = pluralize;
exports.toInMemoryBase64 = toInMemoryBase64;
exports.getLockFileGuard = getLockFileGuard;
const bluebird_1 = __importDefault(require("bluebird"));
const lodash_1 = __importDefault(require("lodash"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_stream_1 = __importDefault(require("node:stream"));
const node_util_1 = require("node:util");
const asyncbox_1 = require("asyncbox");
const fs_1 = require("./fs");
const semver = __importStar(require("semver"));
const shell_quote_1 = require("shell-quote");
Object.defineProperty(exports, "shellParse", { enumerable: true, get: function () { return shell_quote_1.parse; } });
const pluralize_1 = __importDefault(require("pluralize"));
const base64_stream_1 = require("base64-stream");
var uuid_1 = require("uuid");
Object.defineProperty(exports, "uuidV1", { enumerable: true, get: function () { return uuid_1.v1; } });
Object.defineProperty(exports, "uuidV3", { enumerable: true, get: function () { return uuid_1.v3; } });
Object.defineProperty(exports, "uuidV4", { enumerable: true, get: function () { return uuid_1.v4; } });
Object.defineProperty(exports, "uuidV5", { enumerable: true, get: function () { return uuid_1.v5; } });
const _lockfile = __importStar(require("lockfile"));
/** W3C WebDriver element identifier key used in element objects. */
exports.W3C_WEB_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
/** Size of one kibibyte in bytes (1024). */
exports.KiB = 1024;
/** Size of one mebibyte in bytes (1024 * 1024). */
exports.MiB = exports.KiB * 1024;
/** Size of one gibibyte in bytes (1024 * 1024 * 1024). */
exports.GiB = exports.MiB * 1024;
/**
* Type guard: returns true if the value is a non-empty string.
*
* @param val - Value to check
* @returns `true` if `val` is a string with at least one character
*/
function hasContent(val) {
return lodash_1.default.isString(val) && val !== '';
}
/**
* Type guard: returns true if the value is not `undefined`, `null`, or `NaN`.
*
* @param val - Value to check
* @returns `true` if `val` is non-null and non-undefined (and not NaN for numbers)
*/
function hasValue(val) {
if (lodash_1.default.isNumber(val)) {
return !lodash_1.default.isNaN(val);
}
return !lodash_1.default.isUndefined(val) && !lodash_1.default.isNull(val);
}
/**
* Escapes spaces in a string for use in command-line arguments (e.g. ` ` → `\ `).
*
* @param str - String that may contain spaces
* @returns String with spaces escaped by a backslash
*/
function escapeSpace(str) {
return str.split(/ /).join('\\ ');
}
/**
* Escapes special characters in a string (backslash, slash, quotes, control chars).
* If `quoteEscape` is provided, that character is also escaped.
*
* @param str - String to escape, or non-string value (returned unchanged)
* @param quoteEscape - Optional character to escape, or `false` to skip
* @returns Escaped string, or original value if `str` is not a string
*/
function escapeSpecialChars(str, quoteEscape) {
if (typeof str !== 'string') {
return str;
}
const result = str
.replace(/[\\]/g, '\\\\')
.replace(/[/]/g, '\\/')
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t')
.replace(/["]/g, '\\"')
.replace(/\\'/g, "\\'");
if (!quoteEscape) {
return result;
}
const re = new RegExp(quoteEscape, 'g');
return result.replace(re, `\\${quoteEscape}`);
}
/**
* Returns the first non-internal IPv4 address of the machine, if any.
*
* @returns The local IPv4 address, or `undefined` if none found
*/
function localIp() {
const ifaces = node_os_1.default.networkInterfaces();
for (const addrs of Object.values(ifaces)) {
if (!addrs) {
continue;
}
for (const iface of addrs) {
if (iface.family === 'IPv4' && !iface.internal) {
return iface.address;
}
}
}
return undefined;
}
/**
* Creates a promise that resolves after a delay and can be cancelled via `.cancel()`.
*
* @param ms - Delay in milliseconds before the promise resolves
* @returns A Bluebird promise with a `cancel()` method; cancel rejects with CancellationError
*/
// TODO: replace with a native implementation in Appium 4
function cancellableDelay(ms) {
let timer;
let resolve;
let reject;
const delay = new bluebird_1.default((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
timer = setTimeout(() => resolve(), ms);
});
delay.cancel = function () {
clearTimeout(timer);
reject(new bluebird_1.default.CancellationError());
};
return delay;
}
/**
* Resolves each root path with the given path segments, returning an array of absolute paths.
*
* @param roots - Base directory paths to resolve against
* @param args - Path segments to join with each root (e.g. 'foo', 'bar' → root/foo/bar)
* @returns Array of absolute paths, one per root
*/
function multiResolve(roots, ...args) {
return roots.map((root) => node_path_1.default.resolve(root, ...args));
}
/**
* Parses a value as JSON if it is a string; otherwise returns the value as-is.
*
* @param obj - String (to parse) or other value (returned unchanged)
* @returns Parsed object or original value
*/
function safeJsonParse(obj) {
try {
return JSON.parse(obj);
}
catch {
return obj;
}
}
/**
* Stringifies an object to JSON, converting Buffers to strings for readable output.
*
* @param obj - Object to serialize
* @param replacer - Optional replacer function (same as JSON.stringify)
* @param space - Indentation for pretty-printing. Defaults to 2
* @returns JSON string
*/
function jsonStringify(obj, replacer = null, space = 2) {
const replacerFunc = lodash_1.default.isFunction(replacer) ? replacer : (_k, v) => v;
const bufferToJSON = Buffer.prototype.toJSON;
delete Buffer.prototype.toJSON;
try {
return JSON.stringify(obj, (key, value) => {
const updatedValue = Buffer.isBuffer(value) ? value.toString('utf8') : value;
return replacerFunc(key, updatedValue);
}, space);
}
finally {
Buffer.prototype.toJSON = bufferToJSON;
}
}
/**
* Extracts the element ID from a W3C or JSONWP element object, or returns the string if already an ID.
*
* @param el - Element object (with ELEMENT or W3C identifier) or raw element ID string
* @returns The element ID string
*/
function unwrapElement(el) {
const elObj = el;
for (const propName of [exports.W3C_WEB_ELEMENT_IDENTIFIER, 'ELEMENT']) {
if (lodash_1.default.has(elObj, propName)) {
return elObj[propName];
}
}
return el;
}
/**
* Wraps an element ID string in an element object compatible with both W3C and JSONWP.
*
* @param elementId - The element ID to wrap
* @returns Element object with both ELEMENT and W3C identifier keys
*/
function wrapElement(elementId) {
return {
ELEMENT: elementId,
[exports.W3C_WEB_ELEMENT_IDENTIFIER]: elementId,
};
}
/**
* Returns a copy of the object containing only properties that pass the predicate.
* If the predicate is missing, removes properties whose values are undefined.
* If the predicate is a scalar, keeps only properties whose value equals that scalar.
* If the predicate is a function, calls it for each (value, obj) and keeps properties where it returns true.
*
* @param obj - Source object to filter
* @param predicate - Optional filter: undefined (drop undefined values), scalar (value match), or function
* @returns New object with only the properties that pass the predicate
*/
function filterObject(obj, predicate) {
const newObj = lodash_1.default.clone(obj);
let pred;
if (lodash_1.default.isUndefined(predicate)) {
pred = (v) => !lodash_1.default.isUndefined(v);
}
else if (!lodash_1.default.isFunction(predicate)) {
const valuePredicate = predicate;
pred = (v) => v === valuePredicate;
}
else {
pred = predicate;
}
for (const key of Object.keys(obj)) {
if (!pred(obj[key], obj)) {
delete newObj[key];
}
}
return newObj;
}
/**
* Converts a byte count to a human-readable size string (e.g. "1.50 MB").
*
* @param bytes - Number of bytes (or string coercible to a number)
* @returns Formatted string like "123 B", "1.50 KB", "2.00 MB", "3.00 GB"
* @throws {Error} If bytes cannot be converted to a non-negative integer
*/
function toReadableSizeString(bytes) {
const intBytes = parseInt(String(bytes), 10);
if (isNaN(intBytes) || intBytes < 0) {
throw new Error(`Cannot convert '${bytes}' to a readable size format`);
}
if (intBytes >= exports.GiB) {
return `${(intBytes / (exports.GiB * 1.0)).toFixed(2)} GB`;
}
else if (intBytes >= exports.MiB) {
return `${(intBytes / (exports.MiB * 1.0)).toFixed(2)} MB`;
}
else if (intBytes >= exports.KiB) {
return `${(intBytes / (exports.KiB * 1.0)).toFixed(2)} KB`;
}
return `${intBytes} B`;
}
/**
* Checks whether the given path is a subpath of the given root folder.
*
* @param originalPath - The absolute file or folder path to test
* @param root - The absolute root folder path
* @param forcePosix - If true, interpret paths in POSIX format (e.g. on Windows)
* @returns `true` if `originalPath` is under `root`
* @throws {Error} If either path is not absolute
*/
function isSubPath(originalPath, root, forcePosix = null) {
const pathObj = forcePosix ? node_path_1.default.posix : node_path_1.default;
for (const p of [originalPath, root]) {
if (!pathObj.isAbsolute(p)) {
throw new Error(`'${p}' is expected to be an absolute path`);
}
}
const normalizedRoot = pathObj.normalize(root);
const normalizedPath = pathObj.normalize(originalPath);
return normalizedPath.startsWith(normalizedRoot);
}
/**
* Checks whether the given paths refer to the same file system entity (same inode).
* All paths must exist.
*
* @param path1 - First path
* @param path2 - Second path
* @param pathN - Additional paths to compare
* @returns `true` if all paths resolve to the same file/directory
*/
async function isSameDestination(path1, path2, ...pathN) {
const allPaths = [path1, path2, ...pathN];
if (!(await (0, asyncbox_1.asyncmap)(allPaths, async (p) => fs_1.fs.exists(p))).every(Boolean)) {
return false;
}
const areAllItemsEqual = (arr) => !!arr.reduce((a, b) => (a === b ? a : NaN));
if (areAllItemsEqual(allPaths)) {
return true;
}
const mapCb = async (x) => (await fs_1.fs.stat(x, { bigint: true })).ino;
return areAllItemsEqual(await (0, asyncbox_1.asyncmap)(allPaths, mapCb));
}
function coerceVersion(ver, strict = true) {
let result = semver.valid(`${ver}`);
if (!result) {
result = semver.valid(semver.coerce(`${ver}`));
}
if (strict && !result) {
throw new Error(`'${ver}' cannot be coerced to a valid version number`);
}
return result;
}
const SUPPORTED_OPERATORS = ['==', '!=', '>', '<', '>=', '<=', '='];
/**
* Compares two version strings using the given operator.
*
* @param ver1 - First version string
* @param operator - One of: ==, !=, >, <, >=, <=, =
* @param ver2 - Second version string
* @returns `true` if ver1 operator ver2 holds (e.g. "2.0.0" >= "1.0.0")
* @throws {Error} If operator is unsupported or either version cannot be coerced
*/
function compareVersions(ver1, operator, ver2) {
if (!SUPPORTED_OPERATORS.includes(operator)) {
throw new Error(`The '${operator}' comparison operator is not supported. ` +
`Only '${JSON.stringify(SUPPORTED_OPERATORS)}' operators are supported`);
}
const semverOperator = ['==', '!='].includes(operator) ? '=' : operator;
const v1 = coerceVersion(ver1, true);
const v2 = coerceVersion(ver2, true);
const result = semver.satisfies(v1, `${semverOperator}${v2}`);
return operator === '!=' ? !result : result;
}
/**
* Quotes and escapes command-line arguments so they can be safely passed to a shell.
*
* @param args - Single argument or array of arguments to quote
* @returns Quoted string suitable for shell parsing
*/
function quote(args) {
return (0, shell_quote_1.quote)(lodash_1.default.castArray(args));
}
/**
* Returns the plural or singular form of a word appropriate to the count (e.g. "duck" + 1 → "duck", + 2 → "ducks").
*
* @param word - The word to pluralize (or singularize when count is 1)
* @param count - The count used to choose singular vs plural
* @param options - Options object or boolean: use `inclusive: true` (or `true`) to prefix with the number (e.g. "3 ducks")
* @returns The correctly inflected word, optionally prefixed with the count
*/
function pluralize(word, count, options = {}) {
let inclusive = false;
if (lodash_1.default.isBoolean(options)) {
inclusive = options;
}
else if (lodash_1.default.isBoolean(options?.inclusive)) {
inclusive = options.inclusive;
}
return (0, pluralize_1.default)(word, count, inclusive);
}
/**
* Reads a file and returns its contents as a base64-encoded buffer.
*
* @param srcPath - Full path to the file to encode
* @param opts - Encoding options (e.g. maxSize to cap buffer size)
* @returns Buffer containing the base64-encoded file content
* @throws {Error} If the file does not exist, is a directory, cannot be read, or exceeds maxSize
*/
async function toInMemoryBase64(srcPath, opts = {}) {
if (!(await fs_1.fs.exists(srcPath)) || (await fs_1.fs.stat(srcPath)).isDirectory()) {
throw new Error(`No such file: ${srcPath}`);
}
const { maxSize = 1 * exports.GiB } = opts;
const resultBuffers = [];
let resultBuffersSize = 0;
const resultWriteStream = new node_stream_1.default.Writable({
write(buffer, _encoding, next) {
resultBuffers.push(buffer);
resultBuffersSize += buffer.length;
if (maxSize > 0 && resultBuffersSize > maxSize) {
resultWriteStream.emit('error', new Error(`The size of the resulting buffer must not be greater than ${toReadableSizeString(maxSize)}`));
}
next();
},
});
const readerStream = fs_1.fs.createReadStream(srcPath);
const base64EncoderStream = new base64_stream_1.Base64Encode();
const encoderWritable = base64EncoderStream;
const encoderReadable = base64EncoderStream;
const resultWriteStreamPromise = new Promise((resolve, reject) => {
resultWriteStream.once('error', (e) => {
readerStream.unpipe(encoderWritable);
encoderReadable.unpipe(resultWriteStream);
readerStream.destroy();
reject(e);
});
resultWriteStream.once('finish', () => resolve());
});
const readStreamPromise = new Promise((resolve, reject) => {
readerStream.once('close', () => resolve());
readerStream.once('error', (e) => reject(new Error(`Failed to read '${srcPath}': ${e.message}`)));
});
readerStream.pipe(encoderWritable);
encoderReadable.pipe(resultWriteStream);
await Promise.all([readStreamPromise, resultWriteStreamPromise]);
return Buffer.concat(resultBuffers);
}
/**
* Creates a guard that serializes access to a critical section using a lock file.
* The returned function acquires the lock, runs the given behavior, then releases the lock.
* Also exposes `.check()` to test whether the lock is currently held.
*
* @param lockFile - Full path to the lock file
* @param opts - Options (see {@link LockFileOptions})
* @returns Async function that accepts a callback to run under the lock, plus a `.check()` method
*/
function getLockFileGuard(lockFile, opts = {}) {
const { timeout = 120, tryRecovery = false } = opts;
const lock = (0, node_util_1.promisify)(_lockfile.lock);
const checkLock = (0, node_util_1.promisify)(_lockfile.check);
const unlock = (0, node_util_1.promisify)(_lockfile.unlock);
const guard = Object.assign(async (behavior) => {
let triedRecovery = false;
let acquired = false;
while (!acquired) {
try {
if (_lockfile.checkSync(lockFile)) {
await lock(lockFile, { wait: timeout * 1000 });
}
else {
_lockfile.lockSync(lockFile);
}
acquired = true;
}
catch (e) {
const err = e;
if (lodash_1.default.includes(err.message, 'EEXIST') && tryRecovery && !triedRecovery) {
_lockfile.unlockSync(lockFile);
triedRecovery = true;
}
else {
throw new Error(`Could not acquire lock on '${lockFile}' after ${timeout}s. ` +
`Original error: ${err.message}`);
}
}
}
try {
return await behavior();
}
finally {
await unlock(lockFile);
}
}, { check: () => checkLock(lockFile) });
return guard;
}
//# sourceMappingURL=util.js.map