You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1979 lines
63 KiB
1979 lines
63 KiB
import { LRUCache } from 'lru-cache'; |
|
import { posix, win32 } from 'node:path'; |
|
import { fileURLToPath } from 'node:url'; |
|
import { lstatSync, readdir as readdirCB, readdirSync, readlinkSync, realpathSync as rps, } from 'fs'; |
|
import * as actualFS from 'node: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 'node: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 = 0b0000_0001_0000; |
|
// set after a successful lstat() |
|
const LSTAT_CALLED = 0b0000_0010_0000; |
|
// set if an entry (or one of its parents) is definitely not a dir |
|
const ENOTDIR = 0b0000_0100_0000; |
|
// 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 = 0b0000_1000_0000; |
|
// cannot have child entries -- also verify &IFMT is either IFDIR or IFLNK |
|
// set if we fail to readlink |
|
const ENOREADLINK = 0b0001_0000_0000; |
|
// set if we know realpath() will fail |
|
const ENOREALPATH = 0b0010_0000_0000; |
|
const ENOCHILD = ENOTDIR | ENOENT | ENOREALPATH; |
|
const TYPEMASK = 0b0011_1111_1111; |
|
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; |
|
/** |
|
* boolean indicating that this path is the current working directory |
|
* of the PathScurry collection that contains it. |
|
*/ |
|
isCWD = false; |
|
// 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['parentPath'] refers to the path of the |
|
* directory that was passed to readdir. For root entries, it's the path |
|
* to the entry itself. |
|
*/ |
|
get parentPath() { |
|
return (this.parent || this).fullpath(); |
|
} |
|
/** |
|
* Deprecated alias for Dirent['parentPath'] Somewhat counterintuitively, |
|
* this property refers to the *parent* path, not the path object itself. |
|
*/ |
|
get path() { |
|
return this.parentPath; |
|
} |
|
/** |
|
* 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.isCWD) |
|
return ''; |
|
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.isCWD) |
|
return ''; |
|
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 = (await this.parent.realpath())?.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.realpathSync()?.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++) { |
|
const c = children[p]; |
|
if (c) |
|
c.#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; |
|
oldCwd.isCWD = false; |
|
this.isCWD = true; |
|
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
|