@appium/support
Version:
Support libs used across Appium packages
457 lines • 17.5 kB
JavaScript
;
// @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