// synchronous utility for filtering entries and calculating subwalks import { GLOBSTAR } from 'minimatch'; /** * A cache of which patterns have been processed for a given Path */ export class HasWalkedCache { store; constructor(store = new Map()) { this.store = store; } copy() { return new HasWalkedCache(new Map(this.store)); } hasWalked(target, pattern) { return this.store.get(target.fullpath())?.has(pattern.globString()); } storeWalked(target, pattern) { const fullpath = target.fullpath(); const cached = this.store.get(fullpath); if (cached) cached.add(pattern.globString()); else this.store.set(fullpath, new Set([pattern.globString()])); } } /** * A record of which paths have been matched in a given walk step, * and whether they only are considered a match if they are a directory, * and whether their absolute or relative path should be returned. */ export class MatchRecord { store = new Map(); add(target, absolute, ifDir) { const n = (absolute ? 2 : 0) | (ifDir ? 1 : 0); const current = this.store.get(target); this.store.set(target, current === undefined ? n : n & current); } // match, absolute, ifdir entries() { return [...this.store.entries()].map(([path, n]) => [ path, !!(n & 2), !!(n & 1), ]); } } /** * A collection of patterns that must be processed in a subsequent step * for a given path. */ export class SubWalks { store = new Map(); add(target, pattern) { if (!target.canReaddir()) { return; } const subs = this.store.get(target); if (subs) { if (!subs.find(p => p.globString() === pattern.globString())) { subs.push(pattern); } } else this.store.set(target, [pattern]); } get(target) { const subs = this.store.get(target); /* c8 ignore start */ if (!subs) { throw new Error('attempting to walk unknown path'); } /* c8 ignore stop */ return subs; } entries() { return this.keys().map(k => [k, this.store.get(k)]); } keys() { return [...this.store.keys()].filter(t => t.canReaddir()); } } /** * The class that processes patterns for a given path. * * Handles child entry filtering, and determining whether a path's * directory contents must be read. */ export class Processor { hasWalkedCache; matches = new MatchRecord(); subwalks = new SubWalks(); patterns; follow; dot; opts; constructor(opts, hasWalkedCache) { this.opts = opts; this.follow = !!opts.follow; this.dot = !!opts.dot; this.hasWalkedCache = hasWalkedCache ? hasWalkedCache.copy() : new HasWalkedCache(); } processPatterns(target, patterns) { this.patterns = patterns; const processingSet = patterns.map(p => [target, p]); // map of paths to the magic-starting subwalks they need to walk // first item in patterns is the filter for (let [t, pattern] of processingSet) { this.hasWalkedCache.storeWalked(t, pattern); const root = pattern.root(); const absolute = pattern.isAbsolute() && this.opts.absolute !== false; // start absolute patterns at root if (root) { t = t.resolve(root === '/' && this.opts.root !== undefined ? this.opts.root : root); const rest = pattern.rest(); if (!rest) { this.matches.add(t, true, false); continue; } else { pattern = rest; } } if (t.isENOENT()) continue; let p; let rest; let changed = false; while (typeof (p = pattern.pattern()) === 'string' && (rest = pattern.rest())) { const c = t.resolve(p); t = c; pattern = rest; changed = true; } p = pattern.pattern(); rest = pattern.rest(); if (changed) { if (this.hasWalkedCache.hasWalked(t, pattern)) continue; this.hasWalkedCache.storeWalked(t, pattern); } // now we have either a final string for a known entry, // more strings for an unknown entry, // or a pattern starting with magic, mounted on t. if (typeof p === 'string') { // must not be final entry, otherwise we would have // concatenated it earlier. const ifDir = p === '..' || p === '' || p === '.'; this.matches.add(t.resolve(p), absolute, ifDir); continue; } else if (p === GLOBSTAR) { // if no rest, match and subwalk pattern // if rest, process rest and subwalk pattern // if it's a symlink, but we didn't get here by way of a // globstar match (meaning it's the first time THIS globstar // has traversed a symlink), then we follow it. Otherwise, stop. if (!t.isSymbolicLink() || this.follow || pattern.checkFollowGlobstar()) { this.subwalks.add(t, pattern); } const rp = rest?.pattern(); const rrest = rest?.rest(); if (!rest || ((rp === '' || rp === '.') && !rrest)) { // only HAS to be a dir if it ends in **/ or **/. // but ending in ** will match files as well. this.matches.add(t, absolute, rp === '' || rp === '.'); } else { if (rp === '..') { // this would mean you're matching **/.. at the fs root, // and no thanks, I'm not gonna test that specific case. /* c8 ignore start */ const tp = t.parent || t; /* c8 ignore stop */ if (!rrest) this.matches.add(tp, absolute, true); else if (!this.hasWalkedCache.hasWalked(tp, rrest)) { this.subwalks.add(tp, rrest); } } } } else if (p instanceof RegExp) { this.subwalks.add(t, pattern); } } return this; } subwalkTargets() { return this.subwalks.keys(); } child() { return new Processor(this.opts, this.hasWalkedCache); } // return a new Processor containing the subwalks for each // child entry, and a set of matches, and // a hasWalkedCache that's a copy of this one // then we're going to call filterEntries(parent, entries) { const patterns = this.subwalks.get(parent); // put matches and entry walks into the results processor const results = this.child(); for (const e of entries) { for (const pattern of patterns) { const absolute = pattern.isAbsolute(); const p = pattern.pattern(); const rest = pattern.rest(); if (p === GLOBSTAR) { results.testGlobstar(e, pattern, rest, absolute); } else if (p instanceof RegExp) { results.testRegExp(e, p, rest, absolute); } else { results.testString(e, p, rest, absolute); } } } return results; } testGlobstar(e, pattern, rest, absolute) { if (this.dot || !e.name.startsWith('.')) { if (!pattern.hasMore()) { this.matches.add(e, absolute, false); } if (e.canReaddir()) { // if we're in follow mode or it's not a symlink, just keep // testing the same pattern. If there's more after the globstar, // then this symlink consumes the globstar. If not, then we can // follow at most ONE symlink along the way, so we mark it, which // also checks to ensure that it wasn't already marked. if (this.follow || !e.isSymbolicLink()) { this.subwalks.add(e, pattern); } else if (e.isSymbolicLink()) { if (rest && pattern.checkFollowGlobstar()) { this.subwalks.add(e, rest); } else if (pattern.markFollowGlobstar()) { this.subwalks.add(e, pattern); } } } } // if the NEXT thing matches this entry, then also add // the rest. if (rest) { const rp = rest.pattern(); if (typeof rp === 'string' && // dots and empty were handled already rp !== '..' && rp !== '' && rp !== '.') { this.testString(e, rp, rest.rest(), absolute); } else if (rp === '..') { /* c8 ignore start */ const ep = e.parent || e; /* c8 ignore stop */ this.subwalks.add(ep, rest); } else if (rp instanceof RegExp) { this.testRegExp(e, rp, rest.rest(), absolute); } } } testRegExp(e, p, rest, absolute) { if (!p.test(e.name)) return; if (!rest) { this.matches.add(e, absolute, false); } else { this.subwalks.add(e, rest); } } testString(e, p, rest, absolute) { // should never happen? if (!e.isNamed(p)) return; if (!rest) { this.matches.add(e, absolute, false); } else { this.subwalks.add(e, rest); } } } //# sourceMappingURL=processor.js.map