You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1983 lines
63 KiB
JavaScript
1983 lines
63 KiB
JavaScript
import { LRUCache } from 'lru-cache';
|
|
import { posix, win32 } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import * as actualFS from 'fs';
|
|
import { lstatSync, readdir as readdirCB, readdirSync, readlinkSync, realpathSync as rps, } from 'fs';
|
|
const realpathSync = rps.native;
|
|
// TODO: test perf of fs/promises realpath vs realpathCB,
|
|
// since the promises one uses realpath.native
|
|
import { lstat, readdir, readlink, realpath } from 'fs/promises';
|
|
import { Minipass } from 'minipass';
|
|
const defaultFS = {
|
|
lstatSync,
|
|
readdir: readdirCB,
|
|
readdirSync,
|
|
readlinkSync,
|
|
realpathSync,
|
|
promises: {
|
|
lstat,
|
|
readdir,
|
|
readlink,
|
|
realpath,
|
|
},
|
|
};
|
|
// if they just gave us require('fs') then use our default
|
|
const fsFromOption = (fsOption) => !fsOption || fsOption === defaultFS || fsOption === actualFS
|
|
? defaultFS
|
|
: {
|
|
...defaultFS,
|
|
...fsOption,
|
|
promises: {
|
|
...defaultFS.promises,
|
|
...(fsOption.promises || {}),
|
|
},
|
|
};
|
|
// turn something like //?/c:/ into c:\
|
|
const uncDriveRegexp = /^\\\\\?\\([a-z]:)\\?$/i;
|
|
const uncToDrive = (rootPath) => rootPath.replace(/\//g, '\\').replace(uncDriveRegexp, '$1\\');
|
|
// windows paths are separated by either / or \
|
|
const eitherSep = /[\\\/]/;
|
|
const UNKNOWN = 0; // may not even exist, for all we know
|
|
const IFIFO = 0b0001;
|
|
const IFCHR = 0b0010;
|
|
const IFDIR = 0b0100;
|
|
const IFBLK = 0b0110;
|
|
const IFREG = 0b1000;
|
|
const IFLNK = 0b1010;
|
|
const IFSOCK = 0b1100;
|
|
const IFMT = 0b1111;
|
|
// mask to unset low 4 bits
|
|
const IFMT_UNKNOWN = ~IFMT;
|
|
// set after successfully calling readdir() and getting entries.
|
|
const READDIR_CALLED = 16;
|
|
// set after a successful lstat()
|
|
const LSTAT_CALLED = 32;
|
|
// set if an entry (or one of its parents) is definitely not a dir
|
|
const ENOTDIR = 64;
|
|
// set if an entry (or one of its parents) does not exist
|
|
// (can also be set on lstat errors like EACCES or ENAMETOOLONG)
|
|
const ENOENT = 128;
|
|
// cannot have child entries -- also verify &IFMT is either IFDIR or IFLNK
|
|
// set if we fail to readlink
|
|
const ENOREADLINK = 256;
|
|
// set if we know realpath() will fail
|
|
const ENOREALPATH = 512;
|
|
const ENOCHILD = ENOTDIR | ENOENT | ENOREALPATH;
|
|
const TYPEMASK = 1023;
|
|
const entToType = (s) => s.isFile()
|
|
? IFREG
|
|
: s.isDirectory()
|
|
? IFDIR
|
|
: s.isSymbolicLink()
|
|
? IFLNK
|
|
: s.isCharacterDevice()
|
|
? IFCHR
|
|
: s.isBlockDevice()
|
|
? IFBLK
|
|
: s.isSocket()
|
|
? IFSOCK
|
|
: s.isFIFO()
|
|
? IFIFO
|
|
: UNKNOWN;
|
|
// normalize unicode path names
|
|
const normalizeCache = new Map();
|
|
const normalize = (s) => {
|
|
const c = normalizeCache.get(s);
|
|
if (c)
|
|
return c;
|
|
const n = s.normalize('NFKD');
|
|
normalizeCache.set(s, n);
|
|
return n;
|
|
};
|
|
const normalizeNocaseCache = new Map();
|
|
const normalizeNocase = (s) => {
|
|
const c = normalizeNocaseCache.get(s);
|
|
if (c)
|
|
return c;
|
|
const n = normalize(s.toLowerCase());
|
|
normalizeNocaseCache.set(s, n);
|
|
return n;
|
|
};
|
|
/**
|
|
* An LRUCache for storing resolved path strings or Path objects.
|
|
* @internal
|
|
*/
|
|
export class ResolveCache extends LRUCache {
|
|
constructor() {
|
|
super({ max: 256 });
|
|
}
|
|
}
|
|
// In order to prevent blowing out the js heap by allocating hundreds of
|
|
// thousands of Path entries when walking extremely large trees, the "children"
|
|
// in this tree are represented by storing an array of Path entries in an
|
|
// LRUCache, indexed by the parent. At any time, Path.children() may return an
|
|
// empty array, indicating that it doesn't know about any of its children, and
|
|
// thus has to rebuild that cache. This is fine, it just means that we don't
|
|
// benefit as much from having the cached entries, but huge directory walks
|
|
// don't blow out the stack, and smaller ones are still as fast as possible.
|
|
//
|
|
//It does impose some complexity when building up the readdir data, because we
|
|
//need to pass a reference to the children array that we started with.
|
|
/**
|
|
* an LRUCache for storing child entries.
|
|
* @internal
|
|
*/
|
|
export class ChildrenCache extends LRUCache {
|
|
constructor(maxSize = 16 * 1024) {
|
|
super({
|
|
maxSize,
|
|
// parent + children
|
|
sizeCalculation: a => a.length + 1,
|
|
});
|
|
}
|
|
}
|
|
const setAsCwd = Symbol('PathScurry setAsCwd');
|
|
/**
|
|
* Path objects are sort of like a super-powered
|
|
* {@link https://nodejs.org/docs/latest/api/fs.html#class-fsdirent fs.Dirent}
|
|
*
|
|
* Each one represents a single filesystem entry on disk, which may or may not
|
|
* exist. It includes methods for reading various types of information via
|
|
* lstat, readlink, and readdir, and caches all information to the greatest
|
|
* degree possible.
|
|
*
|
|
* Note that fs operations that would normally throw will instead return an
|
|
* "empty" value. This is in order to prevent excessive overhead from error
|
|
* stack traces.
|
|
*/
|
|
export class PathBase {
|
|
/**
|
|
* the basename of this path
|
|
*
|
|
* **Important**: *always* test the path name against any test string
|
|
* usingthe {@link isNamed} method, and not by directly comparing this
|
|
* string. Otherwise, unicode path strings that the system sees as identical
|
|
* will not be properly treated as the same path, leading to incorrect
|
|
* behavior and possible security issues.
|
|
*/
|
|
name;
|
|
/**
|
|
* the Path entry corresponding to the path root.
|
|
*
|
|
* @internal
|
|
*/
|
|
root;
|
|
/**
|
|
* All roots found within the current PathScurry family
|
|
*
|
|
* @internal
|
|
*/
|
|
roots;
|
|
/**
|
|
* a reference to the parent path, or undefined in the case of root entries
|
|
*
|
|
* @internal
|
|
*/
|
|
parent;
|
|
/**
|
|
* boolean indicating whether paths are compared case-insensitively
|
|
* @internal
|
|
*/
|
|
nocase;
|
|
// potential default fs override
|
|
#fs;
|
|
// Stats fields
|
|
#dev;
|
|
get dev() {
|
|
return this.#dev;
|
|
}
|
|
#mode;
|
|
get mode() {
|
|
return this.#mode;
|
|
}
|
|
#nlink;
|
|
get nlink() {
|
|
return this.#nlink;
|
|
}
|
|
#uid;
|
|
get uid() {
|
|
return this.#uid;
|
|
}
|
|
#gid;
|
|
get gid() {
|
|
return this.#gid;
|
|
}
|
|
#rdev;
|
|
get rdev() {
|
|
return this.#rdev;
|
|
}
|
|
#blksize;
|
|
get blksize() {
|
|
return this.#blksize;
|
|
}
|
|
#ino;
|
|
get ino() {
|
|
return this.#ino;
|
|
}
|
|
#size;
|
|
get size() {
|
|
return this.#size;
|
|
}
|
|
#blocks;
|
|
get blocks() {
|
|
return this.#blocks;
|
|
}
|
|
#atimeMs;
|
|
get atimeMs() {
|
|
return this.#atimeMs;
|
|
}
|
|
#mtimeMs;
|
|
get mtimeMs() {
|
|
return this.#mtimeMs;
|
|
}
|
|
#ctimeMs;
|
|
get ctimeMs() {
|
|
return this.#ctimeMs;
|
|
}
|
|
#birthtimeMs;
|
|
get birthtimeMs() {
|
|
return this.#birthtimeMs;
|
|
}
|
|
#atime;
|
|
get atime() {
|
|
return this.#atime;
|
|
}
|
|
#mtime;
|
|
get mtime() {
|
|
return this.#mtime;
|
|
}
|
|
#ctime;
|
|
get ctime() {
|
|
return this.#ctime;
|
|
}
|
|
#birthtime;
|
|
get birthtime() {
|
|
return this.#birthtime;
|
|
}
|
|
#matchName;
|
|
#depth;
|
|
#fullpath;
|
|
#fullpathPosix;
|
|
#relative;
|
|
#relativePosix;
|
|
#type;
|
|
#children;
|
|
#linkTarget;
|
|
#realpath;
|
|
/**
|
|
* This property is for compatibility with the Dirent class as of
|
|
* Node v20, where Dirent['path'] refers to the path of the directory
|
|
* that was passed to readdir. So, somewhat counterintuitively, this
|
|
* property refers to the *parent* path, not the path object itself.
|
|
* For root entries, it's the path to the entry itself.
|
|
*/
|
|
get path() {
|
|
return (this.parent || this).fullpath();
|
|
}
|
|
/**
|
|
* Do not create new Path objects directly. They should always be accessed
|
|
* via the PathScurry class or other methods on the Path class.
|
|
*
|
|
* @internal
|
|
*/
|
|
constructor(name, type = UNKNOWN, root, roots, nocase, children, opts) {
|
|
this.name = name;
|
|
this.#matchName = nocase ? normalizeNocase(name) : normalize(name);
|
|
this.#type = type & TYPEMASK;
|
|
this.nocase = nocase;
|
|
this.roots = roots;
|
|
this.root = root || this;
|
|
this.#children = children;
|
|
this.#fullpath = opts.fullpath;
|
|
this.#relative = opts.relative;
|
|
this.#relativePosix = opts.relativePosix;
|
|
this.parent = opts.parent;
|
|
if (this.parent) {
|
|
this.#fs = this.parent.#fs;
|
|
}
|
|
else {
|
|
this.#fs = fsFromOption(opts.fs);
|
|
}
|
|
}
|
|
/**
|
|
* Returns the depth of the Path object from its root.
|
|
*
|
|
* For example, a path at `/foo/bar` would have a depth of 2.
|
|
*/
|
|
depth() {
|
|
if (this.#depth !== undefined)
|
|
return this.#depth;
|
|
if (!this.parent)
|
|
return (this.#depth = 0);
|
|
return (this.#depth = this.parent.depth() + 1);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
childrenCache() {
|
|
return this.#children;
|
|
}
|
|
/**
|
|
* Get the Path object referenced by the string path, resolved from this Path
|
|
*/
|
|
resolve(path) {
|
|
if (!path) {
|
|
return this;
|
|
}
|
|
const rootPath = this.getRootString(path);
|
|
const dir = path.substring(rootPath.length);
|
|
const dirParts = dir.split(this.splitSep);
|
|
const result = rootPath
|
|
? this.getRoot(rootPath).#resolveParts(dirParts)
|
|
: this.#resolveParts(dirParts);
|
|
return result;
|
|
}
|
|
#resolveParts(dirParts) {
|
|
let p = this;
|
|
for (const part of dirParts) {
|
|
p = p.child(part);
|
|
}
|
|
return p;
|
|
}
|
|
/**
|
|
* Returns the cached children Path objects, if still available. If they
|
|
* have fallen out of the cache, then returns an empty array, and resets the
|
|
* READDIR_CALLED bit, so that future calls to readdir() will require an fs
|
|
* lookup.
|
|
*
|
|
* @internal
|
|
*/
|
|
children() {
|
|
const cached = this.#children.get(this);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const children = Object.assign([], { provisional: 0 });
|
|
this.#children.set(this, children);
|
|
this.#type &= ~READDIR_CALLED;
|
|
return children;
|
|
}
|
|
/**
|
|
* Resolves a path portion and returns or creates the child Path.
|
|
*
|
|
* Returns `this` if pathPart is `''` or `'.'`, or `parent` if pathPart is
|
|
* `'..'`.
|
|
*
|
|
* This should not be called directly. If `pathPart` contains any path
|
|
* separators, it will lead to unsafe undefined behavior.
|
|
*
|
|
* Use `Path.resolve()` instead.
|
|
*
|
|
* @internal
|
|
*/
|
|
child(pathPart, opts) {
|
|
if (pathPart === '' || pathPart === '.') {
|
|
return this;
|
|
}
|
|
if (pathPart === '..') {
|
|
return this.parent || this;
|
|
}
|
|
// find the child
|
|
const children = this.children();
|
|
const name = this.nocase
|
|
? normalizeNocase(pathPart)
|
|
: normalize(pathPart);
|
|
for (const p of children) {
|
|
if (p.#matchName === name) {
|
|
return p;
|
|
}
|
|
}
|
|
// didn't find it, create provisional child, since it might not
|
|
// actually exist. If we know the parent isn't a dir, then
|
|
// in fact it CAN'T exist.
|
|
const s = this.parent ? this.sep : '';
|
|
const fullpath = this.#fullpath
|
|
? this.#fullpath + s + pathPart
|
|
: undefined;
|
|
const pchild = this.newChild(pathPart, UNKNOWN, {
|
|
...opts,
|
|
parent: this,
|
|
fullpath,
|
|
});
|
|
if (!this.canReaddir()) {
|
|
pchild.#type |= ENOENT;
|
|
}
|
|
// don't have to update provisional, because if we have real children,
|
|
// then provisional is set to children.length, otherwise a lower number
|
|
children.push(pchild);
|
|
return pchild;
|
|
}
|
|
/**
|
|
* The relative path from the cwd. If it does not share an ancestor with
|
|
* the cwd, then this ends up being equivalent to the fullpath()
|
|
*/
|
|
relative() {
|
|
if (this.#relative !== undefined) {
|
|
return this.#relative;
|
|
}
|
|
const name = this.name;
|
|
const p = this.parent;
|
|
if (!p) {
|
|
return (this.#relative = this.name);
|
|
}
|
|
const pv = p.relative();
|
|
return pv + (!pv || !p.parent ? '' : this.sep) + name;
|
|
}
|
|
/**
|
|
* The relative path from the cwd, using / as the path separator.
|
|
* If it does not share an ancestor with
|
|
* the cwd, then this ends up being equivalent to the fullpathPosix()
|
|
* On posix systems, this is identical to relative().
|
|
*/
|
|
relativePosix() {
|
|
if (this.sep === '/')
|
|
return this.relative();
|
|
if (this.#relativePosix !== undefined)
|
|
return this.#relativePosix;
|
|
const name = this.name;
|
|
const p = this.parent;
|
|
if (!p) {
|
|
return (this.#relativePosix = this.fullpathPosix());
|
|
}
|
|
const pv = p.relativePosix();
|
|
return pv + (!pv || !p.parent ? '' : '/') + name;
|
|
}
|
|
/**
|
|
* The fully resolved path string for this Path entry
|
|
*/
|
|
fullpath() {
|
|
if (this.#fullpath !== undefined) {
|
|
return this.#fullpath;
|
|
}
|
|
const name = this.name;
|
|
const p = this.parent;
|
|
if (!p) {
|
|
return (this.#fullpath = this.name);
|
|
}
|
|
const pv = p.fullpath();
|
|
const fp = pv + (!p.parent ? '' : this.sep) + name;
|
|
return (this.#fullpath = fp);
|
|
}
|
|
/**
|
|
* On platforms other than windows, this is identical to fullpath.
|
|
*
|
|
* On windows, this is overridden to return the forward-slash form of the
|
|
* full UNC path.
|
|
*/
|
|
fullpathPosix() {
|
|
if (this.#fullpathPosix !== undefined)
|
|
return this.#fullpathPosix;
|
|
if (this.sep === '/')
|
|
return (this.#fullpathPosix = this.fullpath());
|
|
if (!this.parent) {
|
|
const p = this.fullpath().replace(/\\/g, '/');
|
|
if (/^[a-z]:\//i.test(p)) {
|
|
return (this.#fullpathPosix = `//?/${p}`);
|
|
}
|
|
else {
|
|
return (this.#fullpathPosix = p);
|
|
}
|
|
}
|
|
const p = this.parent;
|
|
const pfpp = p.fullpathPosix();
|
|
const fpp = pfpp + (!pfpp || !p.parent ? '' : '/') + this.name;
|
|
return (this.#fullpathPosix = fpp);
|
|
}
|
|
/**
|
|
* Is the Path of an unknown type?
|
|
*
|
|
* Note that we might know *something* about it if there has been a previous
|
|
* filesystem operation, for example that it does not exist, or is not a
|
|
* link, or whether it has child entries.
|
|
*/
|
|
isUnknown() {
|
|
return (this.#type & IFMT) === UNKNOWN;
|
|
}
|
|
isType(type) {
|
|
return this[`is${type}`]();
|
|
}
|
|
getType() {
|
|
return this.isUnknown()
|
|
? 'Unknown'
|
|
: this.isDirectory()
|
|
? 'Directory'
|
|
: this.isFile()
|
|
? 'File'
|
|
: this.isSymbolicLink()
|
|
? 'SymbolicLink'
|
|
: this.isFIFO()
|
|
? 'FIFO'
|
|
: this.isCharacterDevice()
|
|
? 'CharacterDevice'
|
|
: this.isBlockDevice()
|
|
? 'BlockDevice'
|
|
: /* c8 ignore start */ this.isSocket()
|
|
? 'Socket'
|
|
: 'Unknown';
|
|
/* c8 ignore stop */
|
|
}
|
|
/**
|
|
* Is the Path a regular file?
|
|
*/
|
|
isFile() {
|
|
return (this.#type & IFMT) === IFREG;
|
|
}
|
|
/**
|
|
* Is the Path a directory?
|
|
*/
|
|
isDirectory() {
|
|
return (this.#type & IFMT) === IFDIR;
|
|
}
|
|
/**
|
|
* Is the path a character device?
|
|
*/
|
|
isCharacterDevice() {
|
|
return (this.#type & IFMT) === IFCHR;
|
|
}
|
|
/**
|
|
* Is the path a block device?
|
|
*/
|
|
isBlockDevice() {
|
|
return (this.#type & IFMT) === IFBLK;
|
|
}
|
|
/**
|
|
* Is the path a FIFO pipe?
|
|
*/
|
|
isFIFO() {
|
|
return (this.#type & IFMT) === IFIFO;
|
|
}
|
|
/**
|
|
* Is the path a socket?
|
|
*/
|
|
isSocket() {
|
|
return (this.#type & IFMT) === IFSOCK;
|
|
}
|
|
/**
|
|
* Is the path a symbolic link?
|
|
*/
|
|
isSymbolicLink() {
|
|
return (this.#type & IFLNK) === IFLNK;
|
|
}
|
|
/**
|
|
* Return the entry if it has been subject of a successful lstat, or
|
|
* undefined otherwise.
|
|
*
|
|
* Does not read the filesystem, so an undefined result *could* simply
|
|
* mean that we haven't called lstat on it.
|
|
*/
|
|
lstatCached() {
|
|
return this.#type & LSTAT_CALLED ? this : undefined;
|
|
}
|
|
/**
|
|
* Return the cached link target if the entry has been the subject of a
|
|
* successful readlink, or undefined otherwise.
|
|
*
|
|
* Does not read the filesystem, so an undefined result *could* just mean we
|
|
* don't have any cached data. Only use it if you are very sure that a
|
|
* readlink() has been called at some point.
|
|
*/
|
|
readlinkCached() {
|
|
return this.#linkTarget;
|
|
}
|
|
/**
|
|
* Returns the cached realpath target if the entry has been the subject
|
|
* of a successful realpath, or undefined otherwise.
|
|
*
|
|
* Does not read the filesystem, so an undefined result *could* just mean we
|
|
* don't have any cached data. Only use it if you are very sure that a
|
|
* realpath() has been called at some point.
|
|
*/
|
|
realpathCached() {
|
|
return this.#realpath;
|
|
}
|
|
/**
|
|
* Returns the cached child Path entries array if the entry has been the
|
|
* subject of a successful readdir(), or [] otherwise.
|
|
*
|
|
* Does not read the filesystem, so an empty array *could* just mean we
|
|
* don't have any cached data. Only use it if you are very sure that a
|
|
* readdir() has been called recently enough to still be valid.
|
|
*/
|
|
readdirCached() {
|
|
const children = this.children();
|
|
return children.slice(0, children.provisional);
|
|
}
|
|
/**
|
|
* Return true if it's worth trying to readlink. Ie, we don't (yet) have
|
|
* any indication that readlink will definitely fail.
|
|
*
|
|
* Returns false if the path is known to not be a symlink, if a previous
|
|
* readlink failed, or if the entry does not exist.
|
|
*/
|
|
canReadlink() {
|
|
if (this.#linkTarget)
|
|
return true;
|
|
if (!this.parent)
|
|
return false;
|
|
// cases where it cannot possibly succeed
|
|
const ifmt = this.#type & IFMT;
|
|
return !((ifmt !== UNKNOWN && ifmt !== IFLNK) ||
|
|
this.#type & ENOREADLINK ||
|
|
this.#type & ENOENT);
|
|
}
|
|
/**
|
|
* Return true if readdir has previously been successfully called on this
|
|
* path, indicating that cachedReaddir() is likely valid.
|
|
*/
|
|
calledReaddir() {
|
|
return !!(this.#type & READDIR_CALLED);
|
|
}
|
|
/**
|
|
* Returns true if the path is known to not exist. That is, a previous lstat
|
|
* or readdir failed to verify its existence when that would have been
|
|
* expected, or a parent entry was marked either enoent or enotdir.
|
|
*/
|
|
isENOENT() {
|
|
return !!(this.#type & ENOENT);
|
|
}
|
|
/**
|
|
* Return true if the path is a match for the given path name. This handles
|
|
* case sensitivity and unicode normalization.
|
|
*
|
|
* Note: even on case-sensitive systems, it is **not** safe to test the
|
|
* equality of the `.name` property to determine whether a given pathname
|
|
* matches, due to unicode normalization mismatches.
|
|
*
|
|
* Always use this method instead of testing the `path.name` property
|
|
* directly.
|
|
*/
|
|
isNamed(n) {
|
|
return !this.nocase
|
|
? this.#matchName === normalize(n)
|
|
: this.#matchName === normalizeNocase(n);
|
|
}
|
|
/**
|
|
* Return the Path object corresponding to the target of a symbolic link.
|
|
*
|
|
* If the Path is not a symbolic link, or if the readlink call fails for any
|
|
* reason, `undefined` is returned.
|
|
*
|
|
* Result is cached, and thus may be outdated if the filesystem is mutated.
|
|
*/
|
|
async readlink() {
|
|
const target = this.#linkTarget;
|
|
if (target) {
|
|
return target;
|
|
}
|
|
if (!this.canReadlink()) {
|
|
return undefined;
|
|
}
|
|
/* c8 ignore start */
|
|
// already covered by the canReadlink test, here for ts grumples
|
|
if (!this.parent) {
|
|
return undefined;
|
|
}
|
|
/* c8 ignore stop */
|
|
try {
|
|
const read = await this.#fs.promises.readlink(this.fullpath());
|
|
const linkTarget = this.parent.resolve(read);
|
|
if (linkTarget) {
|
|
return (this.#linkTarget = linkTarget);
|
|
}
|
|
}
|
|
catch (er) {
|
|
this.#readlinkFail(er.code);
|
|
return undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Synchronous {@link PathBase.readlink}
|
|
*/
|
|
readlinkSync() {
|
|
const target = this.#linkTarget;
|
|
if (target) {
|
|
return target;
|
|
}
|
|
if (!this.canReadlink()) {
|
|
return undefined;
|
|
}
|
|
/* c8 ignore start */
|
|
// already covered by the canReadlink test, here for ts grumples
|
|
if (!this.parent) {
|
|
return undefined;
|
|
}
|
|
/* c8 ignore stop */
|
|
try {
|
|
const read = this.#fs.readlinkSync(this.fullpath());
|
|
const linkTarget = this.parent.resolve(read);
|
|
if (linkTarget) {
|
|
return (this.#linkTarget = linkTarget);
|
|
}
|
|
}
|
|
catch (er) {
|
|
this.#readlinkFail(er.code);
|
|
return undefined;
|
|
}
|
|
}
|
|
#readdirSuccess(children) {
|
|
// succeeded, mark readdir called bit
|
|
this.#type |= READDIR_CALLED;
|
|
// mark all remaining provisional children as ENOENT
|
|
for (let p = children.provisional; p < children.length; p++) {
|
|
children[p].#markENOENT();
|
|
}
|
|
}
|
|
#markENOENT() {
|
|
// mark as UNKNOWN and ENOENT
|
|
if (this.#type & ENOENT)
|
|
return;
|
|
this.#type = (this.#type | ENOENT) & IFMT_UNKNOWN;
|
|
this.#markChildrenENOENT();
|
|
}
|
|
#markChildrenENOENT() {
|
|
// all children are provisional and do not exist
|
|
const children = this.children();
|
|
children.provisional = 0;
|
|
for (const p of children) {
|
|
p.#markENOENT();
|
|
}
|
|
}
|
|
#markENOREALPATH() {
|
|
this.#type |= ENOREALPATH;
|
|
this.#markENOTDIR();
|
|
}
|
|
// save the information when we know the entry is not a dir
|
|
#markENOTDIR() {
|
|
// entry is not a directory, so any children can't exist.
|
|
// this *should* be impossible, since any children created
|
|
// after it's been marked ENOTDIR should be marked ENOENT,
|
|
// so it won't even get to this point.
|
|
/* c8 ignore start */
|
|
if (this.#type & ENOTDIR)
|
|
return;
|
|
/* c8 ignore stop */
|
|
let t = this.#type;
|
|
// this could happen if we stat a dir, then delete it,
|
|
// then try to read it or one of its children.
|
|
if ((t & IFMT) === IFDIR)
|
|
t &= IFMT_UNKNOWN;
|
|
this.#type = t | ENOTDIR;
|
|
this.#markChildrenENOENT();
|
|
}
|
|
#readdirFail(code = '') {
|
|
// markENOTDIR and markENOENT also set provisional=0
|
|
if (code === 'ENOTDIR' || code === 'EPERM') {
|
|
this.#markENOTDIR();
|
|
}
|
|
else if (code === 'ENOENT') {
|
|
this.#markENOENT();
|
|
}
|
|
else {
|
|
this.children().provisional = 0;
|
|
}
|
|
}
|
|
#lstatFail(code = '') {
|
|
// Windows just raises ENOENT in this case, disable for win CI
|
|
/* c8 ignore start */
|
|
if (code === 'ENOTDIR') {
|
|
// already know it has a parent by this point
|
|
const p = this.parent;
|
|
p.#markENOTDIR();
|
|
}
|
|
else if (code === 'ENOENT') {
|
|
/* c8 ignore stop */
|
|
this.#markENOENT();
|
|
}
|
|
}
|
|
#readlinkFail(code = '') {
|
|
let ter = this.#type;
|
|
ter |= ENOREADLINK;
|
|
if (code === 'ENOENT')
|
|
ter |= ENOENT;
|
|
// windows gets a weird error when you try to readlink a file
|
|
if (code === 'EINVAL' || code === 'UNKNOWN') {
|
|
// exists, but not a symlink, we don't know WHAT it is, so remove
|
|
// all IFMT bits.
|
|
ter &= IFMT_UNKNOWN;
|
|
}
|
|
this.#type = ter;
|
|
// windows just gets ENOENT in this case. We do cover the case,
|
|
// just disabled because it's impossible on Windows CI
|
|
/* c8 ignore start */
|
|
if (code === 'ENOTDIR' && this.parent) {
|
|
this.parent.#markENOTDIR();
|
|
}
|
|
/* c8 ignore stop */
|
|
}
|
|
#readdirAddChild(e, c) {
|
|
return (this.#readdirMaybePromoteChild(e, c) ||
|
|
this.#readdirAddNewChild(e, c));
|
|
}
|
|
#readdirAddNewChild(e, c) {
|
|
// alloc new entry at head, so it's never provisional
|
|
const type = entToType(e);
|
|
const child = this.newChild(e.name, type, { parent: this });
|
|
const ifmt = child.#type & IFMT;
|
|
if (ifmt !== IFDIR && ifmt !== IFLNK && ifmt !== UNKNOWN) {
|
|
child.#type |= ENOTDIR;
|
|
}
|
|
c.unshift(child);
|
|
c.provisional++;
|
|
return child;
|
|
}
|
|
#readdirMaybePromoteChild(e, c) {
|
|
for (let p = c.provisional; p < c.length; p++) {
|
|
const pchild = c[p];
|
|
const name = this.nocase
|
|
? normalizeNocase(e.name)
|
|
: normalize(e.name);
|
|
if (name !== pchild.#matchName) {
|
|
continue;
|
|
}
|
|
return this.#readdirPromoteChild(e, pchild, p, c);
|
|
}
|
|
}
|
|
#readdirPromoteChild(e, p, index, c) {
|
|
const v = p.name;
|
|
// retain any other flags, but set ifmt from dirent
|
|
p.#type = (p.#type & IFMT_UNKNOWN) | entToType(e);
|
|
// case sensitivity fixing when we learn the true name.
|
|
if (v !== e.name)
|
|
p.name = e.name;
|
|
// just advance provisional index (potentially off the list),
|
|
// otherwise we have to splice/pop it out and re-insert at head
|
|
if (index !== c.provisional) {
|
|
if (index === c.length - 1)
|
|
c.pop();
|
|
else
|
|
c.splice(index, 1);
|
|
c.unshift(p);
|
|
}
|
|
c.provisional++;
|
|
return p;
|
|
}
|
|
/**
|
|
* Call lstat() on this Path, and update all known information that can be
|
|
* determined.
|
|
*
|
|
* Note that unlike `fs.lstat()`, the returned value does not contain some
|
|
* information, such as `mode`, `dev`, `nlink`, and `ino`. If that
|
|
* information is required, you will need to call `fs.lstat` yourself.
|
|
*
|
|
* If the Path refers to a nonexistent file, or if the lstat call fails for
|
|
* any reason, `undefined` is returned. Otherwise the updated Path object is
|
|
* returned.
|
|
*
|
|
* Results are cached, and thus may be out of date if the filesystem is
|
|
* mutated.
|
|
*/
|
|
async lstat() {
|
|
if ((this.#type & ENOENT) === 0) {
|
|
try {
|
|
this.#applyStat(await this.#fs.promises.lstat(this.fullpath()));
|
|
return this;
|
|
}
|
|
catch (er) {
|
|
this.#lstatFail(er.code);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* synchronous {@link PathBase.lstat}
|
|
*/
|
|
lstatSync() {
|
|
if ((this.#type & ENOENT) === 0) {
|
|
try {
|
|
this.#applyStat(this.#fs.lstatSync(this.fullpath()));
|
|
return this;
|
|
}
|
|
catch (er) {
|
|
this.#lstatFail(er.code);
|
|
}
|
|
}
|
|
}
|
|
#applyStat(st) {
|
|
const { atime, atimeMs, birthtime, birthtimeMs, blksize, blocks, ctime, ctimeMs, dev, gid, ino, mode, mtime, mtimeMs, nlink, rdev, size, uid, } = st;
|
|
this.#atime = atime;
|
|
this.#atimeMs = atimeMs;
|
|
this.#birthtime = birthtime;
|
|
this.#birthtimeMs = birthtimeMs;
|
|
this.#blksize = blksize;
|
|
this.#blocks = blocks;
|
|
this.#ctime = ctime;
|
|
this.#ctimeMs = ctimeMs;
|
|
this.#dev = dev;
|
|
this.#gid = gid;
|
|
this.#ino = ino;
|
|
this.#mode = mode;
|
|
this.#mtime = mtime;
|
|
this.#mtimeMs = mtimeMs;
|
|
this.#nlink = nlink;
|
|
this.#rdev = rdev;
|
|
this.#size = size;
|
|
this.#uid = uid;
|
|
const ifmt = entToType(st);
|
|
// retain any other flags, but set the ifmt
|
|
this.#type = (this.#type & IFMT_UNKNOWN) | ifmt | LSTAT_CALLED;
|
|
if (ifmt !== UNKNOWN && ifmt !== IFDIR && ifmt !== IFLNK) {
|
|
this.#type |= ENOTDIR;
|
|
}
|
|
}
|
|
#onReaddirCB = [];
|
|
#readdirCBInFlight = false;
|
|
#callOnReaddirCB(children) {
|
|
this.#readdirCBInFlight = false;
|
|
const cbs = this.#onReaddirCB.slice();
|
|
this.#onReaddirCB.length = 0;
|
|
cbs.forEach(cb => cb(null, children));
|
|
}
|
|
/**
|
|
* Standard node-style callback interface to get list of directory entries.
|
|
*
|
|
* If the Path cannot or does not contain any children, then an empty array
|
|
* is returned.
|
|
*
|
|
* Results are cached, and thus may be out of date if the filesystem is
|
|
* mutated.
|
|
*
|
|
* @param cb The callback called with (er, entries). Note that the `er`
|
|
* param is somewhat extraneous, as all readdir() errors are handled and
|
|
* simply result in an empty set of entries being returned.
|
|
* @param allowZalgo Boolean indicating that immediately known results should
|
|
* *not* be deferred with `queueMicrotask`. Defaults to `false`. Release
|
|
* zalgo at your peril, the dark pony lord is devious and unforgiving.
|
|
*/
|
|
readdirCB(cb, allowZalgo = false) {
|
|
if (!this.canReaddir()) {
|
|
if (allowZalgo)
|
|
cb(null, []);
|
|
else
|
|
queueMicrotask(() => cb(null, []));
|
|
return;
|
|
}
|
|
const children = this.children();
|
|
if (this.calledReaddir()) {
|
|
const c = children.slice(0, children.provisional);
|
|
if (allowZalgo)
|
|
cb(null, c);
|
|
else
|
|
queueMicrotask(() => cb(null, c));
|
|
return;
|
|
}
|
|
// don't have to worry about zalgo at this point.
|
|
this.#onReaddirCB.push(cb);
|
|
if (this.#readdirCBInFlight) {
|
|
return;
|
|
}
|
|
this.#readdirCBInFlight = true;
|
|
// else read the directory, fill up children
|
|
// de-provisionalize any provisional children.
|
|
const fullpath = this.fullpath();
|
|
this.#fs.readdir(fullpath, { withFileTypes: true }, (er, entries) => {
|
|
if (er) {
|
|
this.#readdirFail(er.code);
|
|
children.provisional = 0;
|
|
}
|
|
else {
|
|
// if we didn't get an error, we always get entries.
|
|
//@ts-ignore
|
|
for (const e of entries) {
|
|
this.#readdirAddChild(e, children);
|
|
}
|
|
this.#readdirSuccess(children);
|
|
}
|
|
this.#callOnReaddirCB(children.slice(0, children.provisional));
|
|
return;
|
|
});
|
|
}
|
|
#asyncReaddirInFlight;
|
|
/**
|
|
* Return an array of known child entries.
|
|
*
|
|
* If the Path cannot or does not contain any children, then an empty array
|
|
* is returned.
|
|
*
|
|
* Results are cached, and thus may be out of date if the filesystem is
|
|
* mutated.
|
|
*/
|
|
async readdir() {
|
|
if (!this.canReaddir()) {
|
|
return [];
|
|
}
|
|
const children = this.children();
|
|
if (this.calledReaddir()) {
|
|
return children.slice(0, children.provisional);
|
|
}
|
|
// else read the directory, fill up children
|
|
// de-provisionalize any provisional children.
|
|
const fullpath = this.fullpath();
|
|
if (this.#asyncReaddirInFlight) {
|
|
await this.#asyncReaddirInFlight;
|
|
}
|
|
else {
|
|
/* c8 ignore start */
|
|
let resolve = () => { };
|
|
/* c8 ignore stop */
|
|
this.#asyncReaddirInFlight = new Promise(res => (resolve = res));
|
|
try {
|
|
for (const e of await this.#fs.promises.readdir(fullpath, {
|
|
withFileTypes: true,
|
|
})) {
|
|
this.#readdirAddChild(e, children);
|
|
}
|
|
this.#readdirSuccess(children);
|
|
}
|
|
catch (er) {
|
|
this.#readdirFail(er.code);
|
|
children.provisional = 0;
|
|
}
|
|
this.#asyncReaddirInFlight = undefined;
|
|
resolve();
|
|
}
|
|
return children.slice(0, children.provisional);
|
|
}
|
|
/**
|
|
* synchronous {@link PathBase.readdir}
|
|
*/
|
|
readdirSync() {
|
|
if (!this.canReaddir()) {
|
|
return [];
|
|
}
|
|
const children = this.children();
|
|
if (this.calledReaddir()) {
|
|
return children.slice(0, children.provisional);
|
|
}
|
|
// else read the directory, fill up children
|
|
// de-provisionalize any provisional children.
|
|
const fullpath = this.fullpath();
|
|
try {
|
|
for (const e of this.#fs.readdirSync(fullpath, {
|
|
withFileTypes: true,
|
|
})) {
|
|
this.#readdirAddChild(e, children);
|
|
}
|
|
this.#readdirSuccess(children);
|
|
}
|
|
catch (er) {
|
|
this.#readdirFail(er.code);
|
|
children.provisional = 0;
|
|
}
|
|
return children.slice(0, children.provisional);
|
|
}
|
|
canReaddir() {
|
|
if (this.#type & ENOCHILD)
|
|
return false;
|
|
const ifmt = IFMT & this.#type;
|
|
// we always set ENOTDIR when setting IFMT, so should be impossible
|
|
/* c8 ignore start */
|
|
if (!(ifmt === UNKNOWN || ifmt === IFDIR || ifmt === IFLNK)) {
|
|
return false;
|
|
}
|
|
/* c8 ignore stop */
|
|
return true;
|
|
}
|
|
shouldWalk(dirs, walkFilter) {
|
|
return ((this.#type & IFDIR) === IFDIR &&
|
|
!(this.#type & ENOCHILD) &&
|
|
!dirs.has(this) &&
|
|
(!walkFilter || walkFilter(this)));
|
|
}
|
|
/**
|
|
* Return the Path object corresponding to path as resolved
|
|
* by realpath(3).
|
|
*
|
|
* If the realpath call fails for any reason, `undefined` is returned.
|
|
*
|
|
* Result is cached, and thus may be outdated if the filesystem is mutated.
|
|
* On success, returns a Path object.
|
|
*/
|
|
async realpath() {
|
|
if (this.#realpath)
|
|
return this.#realpath;
|
|
if ((ENOREALPATH | ENOREADLINK | ENOENT) & this.#type)
|
|
return undefined;
|
|
try {
|
|
const rp = await this.#fs.promises.realpath(this.fullpath());
|
|
return (this.#realpath = this.resolve(rp));
|
|
}
|
|
catch (_) {
|
|
this.#markENOREALPATH();
|
|
}
|
|
}
|
|
/**
|
|
* Synchronous {@link realpath}
|
|
*/
|
|
realpathSync() {
|
|
if (this.#realpath)
|
|
return this.#realpath;
|
|
if ((ENOREALPATH | ENOREADLINK | ENOENT) & this.#type)
|
|
return undefined;
|
|
try {
|
|
const rp = this.#fs.realpathSync(this.fullpath());
|
|
return (this.#realpath = this.resolve(rp));
|
|
}
|
|
catch (_) {
|
|
this.#markENOREALPATH();
|
|
}
|
|
}
|
|
/**
|
|
* Internal method to mark this Path object as the scurry cwd,
|
|
* called by {@link PathScurry#chdir}
|
|
*
|
|
* @internal
|
|
*/
|
|
[setAsCwd](oldCwd) {
|
|
if (oldCwd === this)
|
|
return;
|
|
const changed = new Set([]);
|
|
let rp = [];
|
|
let p = this;
|
|
while (p && p.parent) {
|
|
changed.add(p);
|
|
p.#relative = rp.join(this.sep);
|
|
p.#relativePosix = rp.join('/');
|
|
p = p.parent;
|
|
rp.push('..');
|
|
}
|
|
// now un-memoize parents of old cwd
|
|
p = oldCwd;
|
|
while (p && p.parent && !changed.has(p)) {
|
|
p.#relative = undefined;
|
|
p.#relativePosix = undefined;
|
|
p = p.parent;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Path class used on win32 systems
|
|
*
|
|
* Uses `'\\'` as the path separator for returned paths, either `'\\'` or `'/'`
|
|
* as the path separator for parsing paths.
|
|
*/
|
|
export class PathWin32 extends PathBase {
|
|
/**
|
|
* Separator for generating path strings.
|
|
*/
|
|
sep = '\\';
|
|
/**
|
|
* Separator for parsing path strings.
|
|
*/
|
|
splitSep = eitherSep;
|
|
/**
|
|
* Do not create new Path objects directly. They should always be accessed
|
|
* via the PathScurry class or other methods on the Path class.
|
|
*
|
|
* @internal
|
|
*/
|
|
constructor(name, type = UNKNOWN, root, roots, nocase, children, opts) {
|
|
super(name, type, root, roots, nocase, children, opts);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
newChild(name, type = UNKNOWN, opts = {}) {
|
|
return new PathWin32(name, type, this.root, this.roots, this.nocase, this.childrenCache(), opts);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getRootString(path) {
|
|
return win32.parse(path).root;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getRoot(rootPath) {
|
|
rootPath = uncToDrive(rootPath.toUpperCase());
|
|
if (rootPath === this.root.name) {
|
|
return this.root;
|
|
}
|
|
// ok, not that one, check if it matches another we know about
|
|
for (const [compare, root] of Object.entries(this.roots)) {
|
|
if (this.sameRoot(rootPath, compare)) {
|
|
return (this.roots[rootPath] = root);
|
|
}
|
|
}
|
|
// otherwise, have to create a new one.
|
|
return (this.roots[rootPath] = new PathScurryWin32(rootPath, this).root);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
sameRoot(rootPath, compare = this.root.name) {
|
|
// windows can (rarely) have case-sensitive filesystem, but
|
|
// UNC and drive letters are always case-insensitive, and canonically
|
|
// represented uppercase.
|
|
rootPath = rootPath
|
|
.toUpperCase()
|
|
.replace(/\//g, '\\')
|
|
.replace(uncDriveRegexp, '$1\\');
|
|
return rootPath === compare;
|
|
}
|
|
}
|
|
/**
|
|
* Path class used on all posix systems.
|
|
*
|
|
* Uses `'/'` as the path separator.
|
|
*/
|
|
export class PathPosix extends PathBase {
|
|
/**
|
|
* separator for parsing path strings
|
|
*/
|
|
splitSep = '/';
|
|
/**
|
|
* separator for generating path strings
|
|
*/
|
|
sep = '/';
|
|
/**
|
|
* Do not create new Path objects directly. They should always be accessed
|
|
* via the PathScurry class or other methods on the Path class.
|
|
*
|
|
* @internal
|
|
*/
|
|
constructor(name, type = UNKNOWN, root, roots, nocase, children, opts) {
|
|
super(name, type, root, roots, nocase, children, opts);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getRootString(path) {
|
|
return path.startsWith('/') ? '/' : '';
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getRoot(_rootPath) {
|
|
return this.root;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
newChild(name, type = UNKNOWN, opts = {}) {
|
|
return new PathPosix(name, type, this.root, this.roots, this.nocase, this.childrenCache(), opts);
|
|
}
|
|
}
|
|
/**
|
|
* The base class for all PathScurry classes, providing the interface for path
|
|
* resolution and filesystem operations.
|
|
*
|
|
* Typically, you should *not* instantiate this class directly, but rather one
|
|
* of the platform-specific classes, or the exported {@link PathScurry} which
|
|
* defaults to the current platform.
|
|
*/
|
|
export class PathScurryBase {
|
|
/**
|
|
* The root Path entry for the current working directory of this Scurry
|
|
*/
|
|
root;
|
|
/**
|
|
* The string path for the root of this Scurry's current working directory
|
|
*/
|
|
rootPath;
|
|
/**
|
|
* A collection of all roots encountered, referenced by rootPath
|
|
*/
|
|
roots;
|
|
/**
|
|
* The Path entry corresponding to this PathScurry's current working directory.
|
|
*/
|
|
cwd;
|
|
#resolveCache;
|
|
#resolvePosixCache;
|
|
#children;
|
|
/**
|
|
* Perform path comparisons case-insensitively.
|
|
*
|
|
* Defaults true on Darwin and Windows systems, false elsewhere.
|
|
*/
|
|
nocase;
|
|
#fs;
|
|
/**
|
|
* This class should not be instantiated directly.
|
|
*
|
|
* Use PathScurryWin32, PathScurryDarwin, PathScurryPosix, or PathScurry
|
|
*
|
|
* @internal
|
|
*/
|
|
constructor(cwd = process.cwd(), pathImpl, sep, { nocase, childrenCacheSize = 16 * 1024, fs = defaultFS, } = {}) {
|
|
this.#fs = fsFromOption(fs);
|
|
if (cwd instanceof URL || cwd.startsWith('file://')) {
|
|
cwd = fileURLToPath(cwd);
|
|
}
|
|
// resolve and split root, and then add to the store.
|
|
// this is the only time we call path.resolve()
|
|
const cwdPath = pathImpl.resolve(cwd);
|
|
this.roots = Object.create(null);
|
|
this.rootPath = this.parseRootPath(cwdPath);
|
|
this.#resolveCache = new ResolveCache();
|
|
this.#resolvePosixCache = new ResolveCache();
|
|
this.#children = new ChildrenCache(childrenCacheSize);
|
|
const split = cwdPath.substring(this.rootPath.length).split(sep);
|
|
// resolve('/') leaves '', splits to [''], we don't want that.
|
|
if (split.length === 1 && !split[0]) {
|
|
split.pop();
|
|
}
|
|
/* c8 ignore start */
|
|
if (nocase === undefined) {
|
|
throw new TypeError('must provide nocase setting to PathScurryBase ctor');
|
|
}
|
|
/* c8 ignore stop */
|
|
this.nocase = nocase;
|
|
this.root = this.newRoot(this.#fs);
|
|
this.roots[this.rootPath] = this.root;
|
|
let prev = this.root;
|
|
let len = split.length - 1;
|
|
const joinSep = pathImpl.sep;
|
|
let abs = this.rootPath;
|
|
let sawFirst = false;
|
|
for (const part of split) {
|
|
const l = len--;
|
|
prev = prev.child(part, {
|
|
relative: new Array(l).fill('..').join(joinSep),
|
|
relativePosix: new Array(l).fill('..').join('/'),
|
|
fullpath: (abs += (sawFirst ? '' : joinSep) + part),
|
|
});
|
|
sawFirst = true;
|
|
}
|
|
this.cwd = prev;
|
|
}
|
|
/**
|
|
* Get the depth of a provided path, string, or the cwd
|
|
*/
|
|
depth(path = this.cwd) {
|
|
if (typeof path === 'string') {
|
|
path = this.cwd.resolve(path);
|
|
}
|
|
return path.depth();
|
|
}
|
|
/**
|
|
* Return the cache of child entries. Exposed so subclasses can create
|
|
* child Path objects in a platform-specific way.
|
|
*
|
|
* @internal
|
|
*/
|
|
childrenCache() {
|
|
return this.#children;
|
|
}
|
|
/**
|
|
* Resolve one or more path strings to a resolved string
|
|
*
|
|
* Same interface as require('path').resolve.
|
|
*
|
|
* Much faster than path.resolve() when called multiple times for the same
|
|
* path, because the resolved Path objects are cached. Much slower
|
|
* otherwise.
|
|
*/
|
|
resolve(...paths) {
|
|
// first figure out the minimum number of paths we have to test
|
|
// we always start at cwd, but any absolutes will bump the start
|
|
let r = '';
|
|
for (let i = paths.length - 1; i >= 0; i--) {
|
|
const p = paths[i];
|
|
if (!p || p === '.')
|
|
continue;
|
|
r = r ? `${p}/${r}` : p;
|
|
if (this.isAbsolute(p)) {
|
|
break;
|
|
}
|
|
}
|
|
const cached = this.#resolveCache.get(r);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
const result = this.cwd.resolve(r).fullpath();
|
|
this.#resolveCache.set(r, result);
|
|
return result;
|
|
}
|
|
/**
|
|
* Resolve one or more path strings to a resolved string, returning
|
|
* the posix path. Identical to .resolve() on posix systems, but on
|
|
* windows will return a forward-slash separated UNC path.
|
|
*
|
|
* Same interface as require('path').resolve.
|
|
*
|
|
* Much faster than path.resolve() when called multiple times for the same
|
|
* path, because the resolved Path objects are cached. Much slower
|
|
* otherwise.
|
|
*/
|
|
resolvePosix(...paths) {
|
|
// first figure out the minimum number of paths we have to test
|
|
// we always start at cwd, but any absolutes will bump the start
|
|
let r = '';
|
|
for (let i = paths.length - 1; i >= 0; i--) {
|
|
const p = paths[i];
|
|
if (!p || p === '.')
|
|
continue;
|
|
r = r ? `${p}/${r}` : p;
|
|
if (this.isAbsolute(p)) {
|
|
break;
|
|
}
|
|
}
|
|
const cached = this.#resolvePosixCache.get(r);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
const result = this.cwd.resolve(r).fullpathPosix();
|
|
this.#resolvePosixCache.set(r, result);
|
|
return result;
|
|
}
|
|
/**
|
|
* find the relative path from the cwd to the supplied path string or entry
|
|
*/
|
|
relative(entry = this.cwd) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
return entry.relative();
|
|
}
|
|
/**
|
|
* find the relative path from the cwd to the supplied path string or
|
|
* entry, using / as the path delimiter, even on Windows.
|
|
*/
|
|
relativePosix(entry = this.cwd) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
return entry.relativePosix();
|
|
}
|
|
/**
|
|
* Return the basename for the provided string or Path object
|
|
*/
|
|
basename(entry = this.cwd) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
return entry.name;
|
|
}
|
|
/**
|
|
* Return the dirname for the provided string or Path object
|
|
*/
|
|
dirname(entry = this.cwd) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
return (entry.parent || entry).fullpath();
|
|
}
|
|
async readdir(entry = this.cwd, opts = {
|
|
withFileTypes: true,
|
|
}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
opts = entry;
|
|
entry = this.cwd;
|
|
}
|
|
const { withFileTypes } = opts;
|
|
if (!entry.canReaddir()) {
|
|
return [];
|
|
}
|
|
else {
|
|
const p = await entry.readdir();
|
|
return withFileTypes ? p : p.map(e => e.name);
|
|
}
|
|
}
|
|
readdirSync(entry = this.cwd, opts = {
|
|
withFileTypes: true,
|
|
}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
opts = entry;
|
|
entry = this.cwd;
|
|
}
|
|
const { withFileTypes = true } = opts;
|
|
if (!entry.canReaddir()) {
|
|
return [];
|
|
}
|
|
else if (withFileTypes) {
|
|
return entry.readdirSync();
|
|
}
|
|
else {
|
|
return entry.readdirSync().map(e => e.name);
|
|
}
|
|
}
|
|
/**
|
|
* Call lstat() on the string or Path object, and update all known
|
|
* information that can be determined.
|
|
*
|
|
* Note that unlike `fs.lstat()`, the returned value does not contain some
|
|
* information, such as `mode`, `dev`, `nlink`, and `ino`. If that
|
|
* information is required, you will need to call `fs.lstat` yourself.
|
|
*
|
|
* If the Path refers to a nonexistent file, or if the lstat call fails for
|
|
* any reason, `undefined` is returned. Otherwise the updated Path object is
|
|
* returned.
|
|
*
|
|
* Results are cached, and thus may be out of date if the filesystem is
|
|
* mutated.
|
|
*/
|
|
async lstat(entry = this.cwd) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
return entry.lstat();
|
|
}
|
|
/**
|
|
* synchronous {@link PathScurryBase.lstat}
|
|
*/
|
|
lstatSync(entry = this.cwd) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
return entry.lstatSync();
|
|
}
|
|
async readlink(entry = this.cwd, { withFileTypes } = {
|
|
withFileTypes: false,
|
|
}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
withFileTypes = entry.withFileTypes;
|
|
entry = this.cwd;
|
|
}
|
|
const e = await entry.readlink();
|
|
return withFileTypes ? e : e?.fullpath();
|
|
}
|
|
readlinkSync(entry = this.cwd, { withFileTypes } = {
|
|
withFileTypes: false,
|
|
}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
withFileTypes = entry.withFileTypes;
|
|
entry = this.cwd;
|
|
}
|
|
const e = entry.readlinkSync();
|
|
return withFileTypes ? e : e?.fullpath();
|
|
}
|
|
async realpath(entry = this.cwd, { withFileTypes } = {
|
|
withFileTypes: false,
|
|
}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
withFileTypes = entry.withFileTypes;
|
|
entry = this.cwd;
|
|
}
|
|
const e = await entry.realpath();
|
|
return withFileTypes ? e : e?.fullpath();
|
|
}
|
|
realpathSync(entry = this.cwd, { withFileTypes } = {
|
|
withFileTypes: false,
|
|
}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
withFileTypes = entry.withFileTypes;
|
|
entry = this.cwd;
|
|
}
|
|
const e = entry.realpathSync();
|
|
return withFileTypes ? e : e?.fullpath();
|
|
}
|
|
async walk(entry = this.cwd, opts = {}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
opts = entry;
|
|
entry = this.cwd;
|
|
}
|
|
const { withFileTypes = true, follow = false, filter, walkFilter, } = opts;
|
|
const results = [];
|
|
if (!filter || filter(entry)) {
|
|
results.push(withFileTypes ? entry : entry.fullpath());
|
|
}
|
|
const dirs = new Set();
|
|
const walk = (dir, cb) => {
|
|
dirs.add(dir);
|
|
dir.readdirCB((er, entries) => {
|
|
/* c8 ignore start */
|
|
if (er) {
|
|
return cb(er);
|
|
}
|
|
/* c8 ignore stop */
|
|
let len = entries.length;
|
|
if (!len)
|
|
return cb();
|
|
const next = () => {
|
|
if (--len === 0) {
|
|
cb();
|
|
}
|
|
};
|
|
for (const e of entries) {
|
|
if (!filter || filter(e)) {
|
|
results.push(withFileTypes ? e : e.fullpath());
|
|
}
|
|
if (follow && e.isSymbolicLink()) {
|
|
e.realpath()
|
|
.then(r => (r?.isUnknown() ? r.lstat() : r))
|
|
.then(r => r?.shouldWalk(dirs, walkFilter) ? walk(r, next) : next());
|
|
}
|
|
else {
|
|
if (e.shouldWalk(dirs, walkFilter)) {
|
|
walk(e, next);
|
|
}
|
|
else {
|
|
next();
|
|
}
|
|
}
|
|
}
|
|
}, true); // zalgooooooo
|
|
};
|
|
const start = entry;
|
|
return new Promise((res, rej) => {
|
|
walk(start, er => {
|
|
/* c8 ignore start */
|
|
if (er)
|
|
return rej(er);
|
|
/* c8 ignore stop */
|
|
res(results);
|
|
});
|
|
});
|
|
}
|
|
walkSync(entry = this.cwd, opts = {}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
opts = entry;
|
|
entry = this.cwd;
|
|
}
|
|
const { withFileTypes = true, follow = false, filter, walkFilter, } = opts;
|
|
const results = [];
|
|
if (!filter || filter(entry)) {
|
|
results.push(withFileTypes ? entry : entry.fullpath());
|
|
}
|
|
const dirs = new Set([entry]);
|
|
for (const dir of dirs) {
|
|
const entries = dir.readdirSync();
|
|
for (const e of entries) {
|
|
if (!filter || filter(e)) {
|
|
results.push(withFileTypes ? e : e.fullpath());
|
|
}
|
|
let r = e;
|
|
if (e.isSymbolicLink()) {
|
|
if (!(follow && (r = e.realpathSync())))
|
|
continue;
|
|
if (r.isUnknown())
|
|
r.lstatSync();
|
|
}
|
|
if (r.shouldWalk(dirs, walkFilter)) {
|
|
dirs.add(r);
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
/**
|
|
* Support for `for await`
|
|
*
|
|
* Alias for {@link PathScurryBase.iterate}
|
|
*
|
|
* Note: As of Node 19, this is very slow, compared to other methods of
|
|
* walking. Consider using {@link PathScurryBase.stream} if memory overhead
|
|
* and backpressure are concerns, or {@link PathScurryBase.walk} if not.
|
|
*/
|
|
[Symbol.asyncIterator]() {
|
|
return this.iterate();
|
|
}
|
|
iterate(entry = this.cwd, options = {}) {
|
|
// iterating async over the stream is significantly more performant,
|
|
// especially in the warm-cache scenario, because it buffers up directory
|
|
// entries in the background instead of waiting for a yield for each one.
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
options = entry;
|
|
entry = this.cwd;
|
|
}
|
|
return this.stream(entry, options)[Symbol.asyncIterator]();
|
|
}
|
|
/**
|
|
* Iterating over a PathScurry performs a synchronous walk.
|
|
*
|
|
* Alias for {@link PathScurryBase.iterateSync}
|
|
*/
|
|
[Symbol.iterator]() {
|
|
return this.iterateSync();
|
|
}
|
|
*iterateSync(entry = this.cwd, opts = {}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
opts = entry;
|
|
entry = this.cwd;
|
|
}
|
|
const { withFileTypes = true, follow = false, filter, walkFilter, } = opts;
|
|
if (!filter || filter(entry)) {
|
|
yield withFileTypes ? entry : entry.fullpath();
|
|
}
|
|
const dirs = new Set([entry]);
|
|
for (const dir of dirs) {
|
|
const entries = dir.readdirSync();
|
|
for (const e of entries) {
|
|
if (!filter || filter(e)) {
|
|
yield withFileTypes ? e : e.fullpath();
|
|
}
|
|
let r = e;
|
|
if (e.isSymbolicLink()) {
|
|
if (!(follow && (r = e.realpathSync())))
|
|
continue;
|
|
if (r.isUnknown())
|
|
r.lstatSync();
|
|
}
|
|
if (r.shouldWalk(dirs, walkFilter)) {
|
|
dirs.add(r);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stream(entry = this.cwd, opts = {}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
opts = entry;
|
|
entry = this.cwd;
|
|
}
|
|
const { withFileTypes = true, follow = false, filter, walkFilter, } = opts;
|
|
const results = new Minipass({ objectMode: true });
|
|
if (!filter || filter(entry)) {
|
|
results.write(withFileTypes ? entry : entry.fullpath());
|
|
}
|
|
const dirs = new Set();
|
|
const queue = [entry];
|
|
let processing = 0;
|
|
const process = () => {
|
|
let paused = false;
|
|
while (!paused) {
|
|
const dir = queue.shift();
|
|
if (!dir) {
|
|
if (processing === 0)
|
|
results.end();
|
|
return;
|
|
}
|
|
processing++;
|
|
dirs.add(dir);
|
|
const onReaddir = (er, entries, didRealpaths = false) => {
|
|
/* c8 ignore start */
|
|
if (er)
|
|
return results.emit('error', er);
|
|
/* c8 ignore stop */
|
|
if (follow && !didRealpaths) {
|
|
const promises = [];
|
|
for (const e of entries) {
|
|
if (e.isSymbolicLink()) {
|
|
promises.push(e
|
|
.realpath()
|
|
.then((r) => r?.isUnknown() ? r.lstat() : r));
|
|
}
|
|
}
|
|
if (promises.length) {
|
|
Promise.all(promises).then(() => onReaddir(null, entries, true));
|
|
return;
|
|
}
|
|
}
|
|
for (const e of entries) {
|
|
if (e && (!filter || filter(e))) {
|
|
if (!results.write(withFileTypes ? e : e.fullpath())) {
|
|
paused = true;
|
|
}
|
|
}
|
|
}
|
|
processing--;
|
|
for (const e of entries) {
|
|
const r = e.realpathCached() || e;
|
|
if (r.shouldWalk(dirs, walkFilter)) {
|
|
queue.push(r);
|
|
}
|
|
}
|
|
if (paused && !results.flowing) {
|
|
results.once('drain', process);
|
|
}
|
|
else if (!sync) {
|
|
process();
|
|
}
|
|
};
|
|
// zalgo containment
|
|
let sync = true;
|
|
dir.readdirCB(onReaddir, true);
|
|
sync = false;
|
|
}
|
|
};
|
|
process();
|
|
return results;
|
|
}
|
|
streamSync(entry = this.cwd, opts = {}) {
|
|
if (typeof entry === 'string') {
|
|
entry = this.cwd.resolve(entry);
|
|
}
|
|
else if (!(entry instanceof PathBase)) {
|
|
opts = entry;
|
|
entry = this.cwd;
|
|
}
|
|
const { withFileTypes = true, follow = false, filter, walkFilter, } = opts;
|
|
const results = new Minipass({ objectMode: true });
|
|
const dirs = new Set();
|
|
if (!filter || filter(entry)) {
|
|
results.write(withFileTypes ? entry : entry.fullpath());
|
|
}
|
|
const queue = [entry];
|
|
let processing = 0;
|
|
const process = () => {
|
|
let paused = false;
|
|
while (!paused) {
|
|
const dir = queue.shift();
|
|
if (!dir) {
|
|
if (processing === 0)
|
|
results.end();
|
|
return;
|
|
}
|
|
processing++;
|
|
dirs.add(dir);
|
|
const entries = dir.readdirSync();
|
|
for (const e of entries) {
|
|
if (!filter || filter(e)) {
|
|
if (!results.write(withFileTypes ? e : e.fullpath())) {
|
|
paused = true;
|
|
}
|
|
}
|
|
}
|
|
processing--;
|
|
for (const e of entries) {
|
|
let r = e;
|
|
if (e.isSymbolicLink()) {
|
|
if (!(follow && (r = e.realpathSync())))
|
|
continue;
|
|
if (r.isUnknown())
|
|
r.lstatSync();
|
|
}
|
|
if (r.shouldWalk(dirs, walkFilter)) {
|
|
queue.push(r);
|
|
}
|
|
}
|
|
}
|
|
if (paused && !results.flowing)
|
|
results.once('drain', process);
|
|
};
|
|
process();
|
|
return results;
|
|
}
|
|
chdir(path = this.cwd) {
|
|
const oldCwd = this.cwd;
|
|
this.cwd = typeof path === 'string' ? this.cwd.resolve(path) : path;
|
|
this.cwd[setAsCwd](oldCwd);
|
|
}
|
|
}
|
|
/**
|
|
* Windows implementation of {@link PathScurryBase}
|
|
*
|
|
* Defaults to case insensitve, uses `'\\'` to generate path strings. Uses
|
|
* {@link PathWin32} for Path objects.
|
|
*/
|
|
export class PathScurryWin32 extends PathScurryBase {
|
|
/**
|
|
* separator for generating path strings
|
|
*/
|
|
sep = '\\';
|
|
constructor(cwd = process.cwd(), opts = {}) {
|
|
const { nocase = true } = opts;
|
|
super(cwd, win32, '\\', { ...opts, nocase });
|
|
this.nocase = nocase;
|
|
for (let p = this.cwd; p; p = p.parent) {
|
|
p.nocase = this.nocase;
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
parseRootPath(dir) {
|
|
// if the path starts with a single separator, it's not a UNC, and we'll
|
|
// just get separator as the root, and driveFromUNC will return \
|
|
// In that case, mount \ on the root from the cwd.
|
|
return win32.parse(dir).root.toUpperCase();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
newRoot(fs) {
|
|
return new PathWin32(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs });
|
|
}
|
|
/**
|
|
* Return true if the provided path string is an absolute path
|
|
*/
|
|
isAbsolute(p) {
|
|
return (p.startsWith('/') || p.startsWith('\\') || /^[a-z]:(\/|\\)/i.test(p));
|
|
}
|
|
}
|
|
/**
|
|
* {@link PathScurryBase} implementation for all posix systems other than Darwin.
|
|
*
|
|
* Defaults to case-sensitive matching, uses `'/'` to generate path strings.
|
|
*
|
|
* Uses {@link PathPosix} for Path objects.
|
|
*/
|
|
export class PathScurryPosix extends PathScurryBase {
|
|
/**
|
|
* separator for generating path strings
|
|
*/
|
|
sep = '/';
|
|
constructor(cwd = process.cwd(), opts = {}) {
|
|
const { nocase = false } = opts;
|
|
super(cwd, posix, '/', { ...opts, nocase });
|
|
this.nocase = nocase;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
parseRootPath(_dir) {
|
|
return '/';
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
newRoot(fs) {
|
|
return new PathPosix(this.rootPath, IFDIR, undefined, this.roots, this.nocase, this.childrenCache(), { fs });
|
|
}
|
|
/**
|
|
* Return true if the provided path string is an absolute path
|
|
*/
|
|
isAbsolute(p) {
|
|
return p.startsWith('/');
|
|
}
|
|
}
|
|
/**
|
|
* {@link PathScurryBase} implementation for Darwin (macOS) systems.
|
|
*
|
|
* Defaults to case-insensitive matching, uses `'/'` for generating path
|
|
* strings.
|
|
*
|
|
* Uses {@link PathPosix} for Path objects.
|
|
*/
|
|
export class PathScurryDarwin extends PathScurryPosix {
|
|
constructor(cwd = process.cwd(), opts = {}) {
|
|
const { nocase = true } = opts;
|
|
super(cwd, { ...opts, nocase });
|
|
}
|
|
}
|
|
/**
|
|
* Default {@link PathBase} implementation for the current platform.
|
|
*
|
|
* {@link PathWin32} on Windows systems, {@link PathPosix} on all others.
|
|
*/
|
|
export const Path = process.platform === 'win32' ? PathWin32 : PathPosix;
|
|
/**
|
|
* Default {@link PathScurryBase} implementation for the current platform.
|
|
*
|
|
* {@link PathScurryWin32} on Windows systems, {@link PathScurryDarwin} on
|
|
* Darwin (macOS) systems, {@link PathScurryPosix} on all others.
|
|
*/
|
|
export const PathScurry = process.platform === 'win32'
|
|
? PathScurryWin32
|
|
: process.platform === 'darwin'
|
|
? PathScurryDarwin
|
|
: PathScurryPosix;
|
|
//# sourceMappingURL=index.js.map
|