/** * Single-use utility classes to provide functionality to the {@link Glob} * methods. * * @module */ import { Minipass } from 'minipass'; import { Ignore } from './ignore.js'; import { Processor } from './processor.js'; const makeIgnore = (ignore, opts) => typeof ignore === 'string' ? new Ignore([ignore], opts) : Array.isArray(ignore) ? new Ignore(ignore, opts) : ignore; /** * basic walking utilities that all the glob walker types use */ export class GlobUtil { path; patterns; opts; seen = new Set(); paused = false; aborted = false; #onResume = []; #ignore; #sep; signal; maxDepth; includeChildMatches; constructor(patterns, path, opts) { this.patterns = patterns; this.path = path; this.opts = opts; this.#sep = !opts.posix && opts.platform === 'win32' ? '\\' : '/'; this.includeChildMatches = opts.includeChildMatches !== false; if (opts.ignore || !this.includeChildMatches) { this.#ignore = makeIgnore(opts.ignore ?? [], opts); if (!this.includeChildMatches && typeof this.#ignore.add !== 'function') { const m = 'cannot ignore child matches, ignore lacks add() method.'; throw new Error(m); } } // ignore, always set with maxDepth, but it's optional on the // GlobOptions type /* c8 ignore start */ this.maxDepth = opts.maxDepth || Infinity; /* c8 ignore stop */ if (opts.signal) { this.signal = opts.signal; this.signal.addEventListener('abort', () => { this.#onResume.length = 0; }); } } #ignored(path) { return this.seen.has(path) || !!this.#ignore?.ignored?.(path); } #childrenIgnored(path) { return !!this.#ignore?.childrenIgnored?.(path); } // backpressure mechanism pause() { this.paused = true; } resume() { /* c8 ignore start */ if (this.signal?.aborted) return; /* c8 ignore stop */ this.paused = false; let fn = undefined; while (!this.paused && (fn = this.#onResume.shift())) { fn(); } } onResume(fn) { if (this.signal?.aborted) return; /* c8 ignore start */ if (!this.paused) { fn(); } else { /* c8 ignore stop */ this.#onResume.push(fn); } } // do the requisite realpath/stat checking, and return the path // to add or undefined to filter it out. async matchCheck(e, ifDir) { if (ifDir && this.opts.nodir) return undefined; let rpc; if (this.opts.realpath) { rpc = e.realpathCached() || (await e.realpath()); if (!rpc) return undefined; e = rpc; } const needStat = e.isUnknown() || this.opts.stat; const s = needStat ? await e.lstat() : e; if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) { const target = await s.realpath(); /* c8 ignore start */ if (target && (target.isUnknown() || this.opts.stat)) { await target.lstat(); } /* c8 ignore stop */ } return this.matchCheckTest(s, ifDir); } matchCheckTest(e, ifDir) { return (e && (this.maxDepth === Infinity || e.depth() <= this.maxDepth) && (!ifDir || e.canReaddir()) && (!this.opts.nodir || !e.isDirectory()) && (!this.opts.nodir || !this.opts.follow || !e.isSymbolicLink() || !e.realpathCached()?.isDirectory()) && !this.#ignored(e)) ? e : undefined; } matchCheckSync(e, ifDir) { if (ifDir && this.opts.nodir) return undefined; let rpc; if (this.opts.realpath) { rpc = e.realpathCached() || e.realpathSync(); if (!rpc) return undefined; e = rpc; } const needStat = e.isUnknown() || this.opts.stat; const s = needStat ? e.lstatSync() : e; if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) { const target = s.realpathSync(); if (target && (target?.isUnknown() || this.opts.stat)) { target.lstatSync(); } } return this.matchCheckTest(s, ifDir); } matchFinish(e, absolute) { if (this.#ignored(e)) return; // we know we have an ignore if this is false, but TS doesn't if (!this.includeChildMatches && this.#ignore?.add) { const ign = `${e.relativePosix()}/**`; this.#ignore.add(ign); } const abs = this.opts.absolute === undefined ? absolute : this.opts.absolute; this.seen.add(e); const mark = this.opts.mark && e.isDirectory() ? this.#sep : ''; // ok, we have what we need! if (this.opts.withFileTypes) { this.matchEmit(e); } else if (abs) { const abs = this.opts.posix ? e.fullpathPosix() : e.fullpath(); this.matchEmit(abs + mark); } else { const rel = this.opts.posix ? e.relativePosix() : e.relative(); const pre = this.opts.dotRelative && !rel.startsWith('..' + this.#sep) ? '.' + this.#sep : ''; this.matchEmit(!rel ? '.' + mark : pre + rel + mark); } } async match(e, absolute, ifDir) { const p = await this.matchCheck(e, ifDir); if (p) this.matchFinish(p, absolute); } matchSync(e, absolute, ifDir) { const p = this.matchCheckSync(e, ifDir); if (p) this.matchFinish(p, absolute); } walkCB(target, patterns, cb) { /* c8 ignore start */ if (this.signal?.aborted) cb(); /* c8 ignore stop */ this.walkCB2(target, patterns, new Processor(this.opts), cb); } walkCB2(target, patterns, processor, cb) { if (this.#childrenIgnored(target)) return cb(); if (this.signal?.aborted) cb(); if (this.paused) { this.onResume(() => this.walkCB2(target, patterns, processor, cb)); return; } processor.processPatterns(target, patterns); // done processing. all of the above is sync, can be abstracted out. // subwalks is a map of paths to the entry filters they need // matches is a map of paths to [absolute, ifDir] tuples. let tasks = 1; const next = () => { if (--tasks === 0) cb(); }; for (const [m, absolute, ifDir] of processor.matches.entries()) { if (this.#ignored(m)) continue; tasks++; this.match(m, absolute, ifDir).then(() => next()); } for (const t of processor.subwalkTargets()) { if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) { continue; } tasks++; const childrenCached = t.readdirCached(); if (t.calledReaddir()) this.walkCB3(t, childrenCached, processor, next); else { t.readdirCB((_, entries) => this.walkCB3(t, entries, processor, next), true); } } next(); } walkCB3(target, entries, processor, cb) { processor = processor.filterEntries(target, entries); let tasks = 1; const next = () => { if (--tasks === 0) cb(); }; for (const [m, absolute, ifDir] of processor.matches.entries()) { if (this.#ignored(m)) continue; tasks++; this.match(m, absolute, ifDir).then(() => next()); } for (const [target, patterns] of processor.subwalks.entries()) { tasks++; this.walkCB2(target, patterns, processor.child(), next); } next(); } walkCBSync(target, patterns, cb) { /* c8 ignore start */ if (this.signal?.aborted) cb(); /* c8 ignore stop */ this.walkCB2Sync(target, patterns, new Processor(this.opts), cb); } walkCB2Sync(target, patterns, processor, cb) { if (this.#childrenIgnored(target)) return cb(); if (this.signal?.aborted) cb(); if (this.paused) { this.onResume(() => this.walkCB2Sync(target, patterns, processor, cb)); return; } processor.processPatterns(target, patterns); // done processing. all of the above is sync, can be abstracted out. // subwalks is a map of paths to the entry filters they need // matches is a map of paths to [absolute, ifDir] tuples. let tasks = 1; const next = () => { if (--tasks === 0) cb(); }; for (const [m, absolute, ifDir] of processor.matches.entries()) { if (this.#ignored(m)) continue; this.matchSync(m, absolute, ifDir); } for (const t of processor.subwalkTargets()) { if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) { continue; } tasks++; const children = t.readdirSync(); this.walkCB3Sync(t, children, processor, next); } next(); } walkCB3Sync(target, entries, processor, cb) { processor = processor.filterEntries(target, entries); let tasks = 1; const next = () => { if (--tasks === 0) cb(); }; for (const [m, absolute, ifDir] of processor.matches.entries()) { if (this.#ignored(m)) continue; this.matchSync(m, absolute, ifDir); } for (const [target, patterns] of processor.subwalks.entries()) { tasks++; this.walkCB2Sync(target, patterns, processor.child(), next); } next(); } } export class GlobWalker extends GlobUtil { matches = new Set(); constructor(patterns, path, opts) { super(patterns, path, opts); } matchEmit(e) { this.matches.add(e); } async walk() { if (this.signal?.aborted) throw this.signal.reason; if (this.path.isUnknown()) { await this.path.lstat(); } await new Promise((res, rej) => { this.walkCB(this.path, this.patterns, () => { if (this.signal?.aborted) { rej(this.signal.reason); } else { res(this.matches); } }); }); return this.matches; } walkSync() { if (this.signal?.aborted) throw this.signal.reason; if (this.path.isUnknown()) { this.path.lstatSync(); } // nothing for the callback to do, because this never pauses this.walkCBSync(this.path, this.patterns, () => { if (this.signal?.aborted) throw this.signal.reason; }); return this.matches; } } export class GlobStream extends GlobUtil { results; constructor(patterns, path, opts) { super(patterns, path, opts); this.results = new Minipass({ signal: this.signal, objectMode: true, }); this.results.on('drain', () => this.resume()); this.results.on('resume', () => this.resume()); } matchEmit(e) { this.results.write(e); if (!this.results.flowing) this.pause(); } stream() { const target = this.path; if (target.isUnknown()) { target.lstat().then(() => { this.walkCB(target, this.patterns, () => this.results.end()); }); } else { this.walkCB(target, this.patterns, () => this.results.end()); } return this.results; } streamSync() { if (this.path.isUnknown()) { this.path.lstatSync(); } this.walkCBSync(this.path, this.patterns, () => this.results.end()); return this.results; } } //# sourceMappingURL=walker.js.map