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