372 lines
12 KiB
JavaScript
372 lines
12 KiB
JavaScript
|
/**
|
||
|
* 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;
|
||
|
constructor(patterns, path, opts) {
|
||
|
this.patterns = patterns;
|
||
|
this.path = path;
|
||
|
this.opts = opts;
|
||
|
this.#sep = !opts.posix && opts.platform === 'win32' ? '\\' : '/';
|
||
|
if (opts.ignore) {
|
||
|
this.#ignore = makeIgnore(opts.ignore, opts);
|
||
|
}
|
||
|
// 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;
|
||
|
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;
|
||
|
constructor(patterns, path, opts) {
|
||
|
super(patterns, path, opts);
|
||
|
this.matches = new Set();
|
||
|
}
|
||
|
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
|