UNPKG

@appium/support

Version:

Support libs used across Appium packages

457 lines 17.5 kB
"use strict"; // @ts-check var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.fs = void 0; const bluebird_1 = __importDefault(require("bluebird")); const crypto_1 = __importDefault(require("crypto")); const fs_1 = require("fs"); const glob_1 = require("glob"); const klaw_1 = __importDefault(require("klaw")); const lodash_1 = __importDefault(require("lodash")); const ncp_1 = __importDefault(require("ncp")); const path_1 = __importDefault(require("path")); const pkg_dir_1 = __importDefault(require("pkg-dir")); const read_pkg_1 = __importDefault(require("read-pkg")); const sanitize_filename_1 = __importDefault(require("sanitize-filename")); const which_1 = __importDefault(require("which")); const logger_1 = __importDefault(require("./logger")); const timing_1 = require("./timing"); const system_1 = require("./system"); const util_1 = require("./util"); const ncpAsync = /** @type {(source: string, dest: string, opts: ncp.Options|undefined) => B<void>} */ (bluebird_1.default.promisify(ncp_1.default)); const findRootCached = lodash_1.default.memoize(pkg_dir_1.default.sync); const fs = { /** * Resolves `true` if `path` is _readable_, which differs from Node.js' default behavior of "can we see it?" * * On Windows, ACLs are not supported, so this becomes a simple check for existence. * * This function will never reject. * @param {PathLike} path * @returns {Promise<boolean>} */ async hasAccess(path) { try { await fs_1.promises.access(path, fs_1.constants.R_OK); } catch { return false; } return true; }, /** * Resolves `true` if `path` is executable; `false` otherwise. * * On Windows, this function delegates to {@linkcode fs.hasAccess}. * * This function will never reject. * @param {PathLike} path * @returns {Promise<boolean>} */ async isExecutable(path) { try { if ((0, system_1.isWindows)()) { return await fs.hasAccess(path); } await fs_1.promises.access(path, fs_1.constants.R_OK | fs_1.constants.X_OK); } catch { return false; } return true; }, /** * Alias for {@linkcode fs.hasAccess} * @param {PathLike} path */ async exists(path) { return await fs.hasAccess(path); }, /** * Remove a directory and all its contents, recursively * @param {PathLike} filepath * @returns Promise<void> * @see https://nodejs.org/api/fs.html#fspromisesrmpath-options */ async rimraf(filepath) { return await fs_1.promises.rm(filepath, { recursive: true, force: true }); }, /** * Remove a directory and all its contents, recursively in sync * @param {PathLike} filepath * @returns undefined * @see https://nodejs.org/api/fs.html#fsrmsyncpath-options */ rimrafSync(filepath) { return (0, fs_1.rmSync)(filepath, { recursive: true, force: true }); }, /** * Like Node.js' `fsPromises.mkdir()`, but will _not_ reject if the directory already exists. * * @param {string|Buffer|URL} filepath * @param {import('fs').MakeDirectoryOptions} [opts] * @returns {Promise<string|undefined>} * @see https://nodejs.org/api/fs.html#fspromisesmkdirpath-options */ async mkdir(filepath, opts = {}) { try { return await fs_1.promises.mkdir(filepath, opts); } catch (err) { if (err?.code !== 'EEXIST') { throw err; } } }, /** * Copies files _and entire directories_ * @param {string} source - Source to copy * @param {string} destination - Destination to copy to * @param {ncp.Options} [opts] - Additional arguments to pass to `ncp` * @see https://npm.im/ncp * @returns {Promise<void>} */ async copyFile(source, destination, opts = {}) { if (!(await fs.hasAccess(source))) { throw new Error(`The file at '${source}' does not exist or is not accessible`); } return await ncpAsync(source, destination, opts); }, /** * Create an MD5 hash of a file. * @param {PathLike} filePath * @returns {Promise<string>} */ async md5(filePath) { return await fs.hash(filePath, 'md5'); }, /** * Move a file or a folder * * @param {string} from Source file/folder * @param {string} to Destination file/folder * @param {mv} [opts] Move options * @returns {Promise<void>} */ async mv(from, to, opts) { const ensureDestination = async (/** @type {import('fs').PathLike} */ p) => { if (opts?.mkdirp && !(await this.exists(p))) { await fs_1.promises.mkdir(p, { recursive: true }); return true; } return false; }; const renameFile = async ( /** @type {import('fs').PathLike} */ src, /** @type {import('fs').PathLike} */ dst, /** @type {boolean} */ skipExistenceCheck) => { if (!skipExistenceCheck && await this.exists(dst)) { if (opts?.clobber === false) { const err = new Error(`The destination path '${dst}' already exists`); // @ts-ignore Legacy compat err.code = 'EEXIST'; throw err; } await this.rimraf(dst); } try { await fs_1.promises.rename(src, dst); } catch (err) { // Handle cross-device link error by falling back to copy-and-delete if (err.code === 'EXDEV') { await this.copyFile(String(src), String(dst)); await this.rimraf(src); } else { throw err; } } }; /** @type {import('fs').Stats} */ let fromStat; try { fromStat = await fs_1.promises.stat(from); } catch (err) { if (err.code === 'ENOENT') { throw new Error(`The source path '${from}' does not exist or is not accessible`); } throw err; } if (fromStat.isFile()) { const dstRootWasCreated = await ensureDestination(path_1.default.dirname(to)); await renameFile(from, to, dstRootWasCreated); } else if (fromStat.isDirectory()) { const dstRootWasCreated = await ensureDestination(to); const items = await fs_1.promises.readdir(from, { withFileTypes: true }); for (const item of items) { const srcPath = path_1.default.join(from, item.name); const destPath = path_1.default.join(to, item.name); if (item.isDirectory()) { await this.mv(srcPath, destPath, opts); } else if (item.isFile()) { await renameFile(srcPath, destPath, dstRootWasCreated); } } } else { return; } await this.rimraf(from); }, /** * Find path to an executable in system `PATH` * @see https://github.com/npm/node-which */ which: which_1.default, /** * Given a glob pattern, resolve with list of files matching that pattern * @see https://github.com/isaacs/node-glob */ glob: /** @type {(pattern: string, opts?: import('glob').GlobOptions) => B<string[]>} */ ((pattern, options) => bluebird_1.default.resolve(options ? (0, glob_1.glob)(pattern, options) : (0, glob_1.glob)(pattern))), /** * Sanitize a filename * @see https://github.com/parshap/node-sanitize-filename */ sanitizeName: sanitize_filename_1.default, /** * Create a hex digest of some file at `filePath` * @param {PathLike} filePath * @param {string} [algorithm] * @returns {Promise<string>} */ async hash(filePath, algorithm = 'sha1') { return await new bluebird_1.default((resolve, reject) => { const fileHash = crypto_1.default.createHash(algorithm); const readStream = (0, fs_1.createReadStream)(filePath); readStream.on('error', (e) => reject(new Error(`Cannot calculate ${algorithm} hash for '${filePath}'. Original error: ${e.message}`))); readStream.on('data', (chunk) => fileHash.update(chunk)); readStream.on('end', () => resolve(fileHash.digest('hex'))); }); }, /** * Returns an `Walker` instance, which is a readable stream (and thusly an async iterator). * * @param {string} dir - Dir to start walking at * @param {import('klaw').Options} [opts] * @returns {import('klaw').Walker} * @see https://www.npmjs.com/package/klaw */ walk(dir, opts) { return (0, klaw_1.default)(dir, opts); }, /** * Recursively create a directory. * @param {PathLike} dir * @returns {Promise<string|undefined>} */ async mkdirp(dir) { return await fs.mkdir(dir, { recursive: true }); }, /** * Walks a directory given according to the parameters given. The callback will be invoked with a path joined with the dir parameter * @param {string} dir Directory path where we will start walking * @param {boolean} recursive Set it to true if you want to continue walking sub directories * @param {WalkDirCallback} callback The callback to be called when a new path is found * @throws {Error} If the `dir` parameter contains a path to an invalid folder * @returns {Promise<string?>} returns the found path or null if the item was not found */ // eslint-disable-next-line promise/prefer-await-to-callbacks async walkDir(dir, recursive, callback) { let isValidRoot = false; let errMsg = null; try { isValidRoot = (await fs.stat(dir)).isDirectory(); } catch (e) { errMsg = e.message; } if (!isValidRoot) { throw Error(`'${dir}' is not a valid root directory` + (errMsg ? `. Original error: ${errMsg}` : '')); } let walker; let fileCount = 0; let directoryCount = 0; const timer = new timing_1.Timer().start(); return await new bluebird_1.default(function (resolve, reject) { /** @type {Promise} */ let lastFileProcessed = bluebird_1.default.resolve(); walker = (0, klaw_1.default)(dir, { depthLimit: recursive ? -1 : 0, }); walker .on('data', function (item) { walker.pause(); if (!item.stats.isDirectory()) { fileCount++; } else { directoryCount++; } lastFileProcessed = (async () => { try { // eslint-disable-next-line promise/prefer-await-to-callbacks const done = await callback(item.path, item.stats.isDirectory()); if (done) { return resolve(item.path); } walker.resume(); } catch (err) { return reject(err); } })(); }) .on('error', function (err, item) { logger_1.default.warn(`Got an error while walking '${item.path}': ${err.message}`); // klaw cannot get back from an ENOENT error if (err.code === 'ENOENT') { logger_1.default.warn('All files may not have been accessed'); reject(err); } }) .on('end', function () { (async () => { try { const file = await lastFileProcessed; return resolve(/** @type {string|undefined} */ (file) ?? null); } catch (err) { logger_1.default.warn(`Unexpected error: ${err.message}`); return reject(err); } })(); }); }).finally(function () { logger_1.default.debug(`Traversed ${(0, util_1.pluralize)('directory', directoryCount, true)} ` + `and ${(0, util_1.pluralize)('file', fileCount, true)} ` + `in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); if (walker) { walker.destroy(); } }); }, /** * Reads the closest `package.json` file from absolute path `dir`. * @param {string} dir - Directory to search from * @param {import('read-pkg').Options} [opts] - Additional options for `read-pkg` * @throws {Error} If there were problems finding or reading a `package.json` file * @returns {import('read-pkg').NormalizedPackageJson} A parsed `package.json` */ readPackageJsonFrom(dir, opts = {}) { const cwd = fs.findRoot(dir); try { return read_pkg_1.default.sync( /** @type {import('read-pkg').NormalizeOptions} */ ({ normalize: true, ...opts, cwd })); } catch (err) { err.message = `Failed to read a \`package.json\` from dir \`${dir}\`:\n\n${err.message}`; throw err; } }, /** * Finds the project root directory from `dir`. * @param {string} dir - Directory to search from * @throws {TypeError} If `dir` is not a nonempty string or relative path * @throws {Error} If there were problems finding the project root * @returns {string} The closeset parent dir containing `package.json` */ findRoot(dir) { if (!dir || !path_1.default.isAbsolute(dir)) { throw new TypeError('`findRoot()` must be provided a non-empty, absolute path'); } const result = findRootCached(dir); if (!result) { throw new Error(`\`findRoot()\` could not find \`package.json\` from ${dir}`); } return result; }, // add the supported `fs` functions access: fs_1.promises.access, appendFile: fs_1.promises.appendFile, chmod: fs_1.promises.chmod, close: bluebird_1.default.promisify(fs_1.close), constants: fs_1.constants, createWriteStream: fs_1.createWriteStream, createReadStream: fs_1.createReadStream, lstat: fs_1.promises.lstat, /** * Warning: this is a promisified {@linkcode open fs.open}. * It resolves w/a file descriptor instead of a {@linkcode fsPromises.FileHandle FileHandle} object, as {@linkcode fsPromises.open} does. Use {@linkcode fs.openFile} if you want a `FileHandle`. * @type {(path: PathLike, flags: import('fs').OpenMode, mode?: import('fs').Mode) => Promise<number>} */ open: bluebird_1.default.promisify(fs_1.open), openFile: fs_1.promises.open, readdir: fs_1.promises.readdir, read: /** * @type {ReadFn<NodeJS.ArrayBufferView>} */ ( /** @type {unknown} */(bluebird_1.default.promisify(fs_1.read))), readFile: fs_1.promises.readFile, readlink: fs_1.promises.readlink, realpath: fs_1.promises.realpath, rename: fs_1.promises.rename, stat: fs_1.promises.stat, symlink: fs_1.promises.symlink, unlink: fs_1.promises.unlink, write: bluebird_1.default.promisify(fs_1.write), writeFile: fs_1.promises.writeFile, // deprecated props /** * Use `constants.F_OK` instead. * @deprecated */ F_OK: fs_1.constants.F_OK, /** * Use `constants.R_OK` instead. * @deprecated */ R_OK: fs_1.constants.R_OK, /** * Use `constants.W_OK` instead. * @deprecated */ W_OK: fs_1.constants.W_OK, /** * Use `constants.X_OK` instead. * @deprecated */ X_OK: fs_1.constants.X_OK, }; exports.fs = fs; exports.default = fs; /** * The callback function which will be called during the directory walking * @callback WalkDirCallback * @param {string} itemPath The path of the file or folder * @param {boolean} isDirectory Shows if it is a directory or a file * @return {boolean|void} return true if you want to stop walking */ /** * @typedef {import('glob')} glob * @typedef {import('fs').PathLike} PathLike */ /** * @typedef {Object} mv * @property {boolean} [mkdirp=false] Whether to automatically create the destination folder structure * @property {boolean} [clobber=true] Set it to false if you want an exception to be thrown * if the destination file already exists * @property {number} [limit=16] Legacy deprecated property, not used anymore */ /** * @template {NodeJS.ArrayBufferView} TBuffer * @callback ReadFn * @param {number} fd * @param {TBuffer|import('node:fs').ReadAsyncOptions<TBuffer>} buffer * @param {number} [offset] * @param {number} [length] * @param {number?} [position] * @returns {B<{bytesRead: number, buffer: TBuffer}>} */ //# sourceMappingURL=fs.js.map