243 lines
6.7 KiB
JavaScript
243 lines
6.7 KiB
JavaScript
|
module.exports = readdirGlob;
|
|||
|
|
|||
|
const fs = require('fs');
|
|||
|
const { EventEmitter } = require('events');
|
|||
|
const { Minimatch } = require('minimatch');
|
|||
|
const { resolve } = require('path');
|
|||
|
|
|||
|
function readdir(dir, strict) {
|
|||
|
return new Promise((resolve, reject) => {
|
|||
|
fs.readdir(dir, {withFileTypes: true} ,(err, files) => {
|
|||
|
if(err) {
|
|||
|
switch (err.code) {
|
|||
|
case 'ENOTDIR': // Not a directory
|
|||
|
if(strict) {
|
|||
|
reject(err);
|
|||
|
} else {
|
|||
|
resolve([]);
|
|||
|
}
|
|||
|
break;
|
|||
|
case 'ENOTSUP': // Operation not supported
|
|||
|
case 'ENOENT': // No such file or directory
|
|||
|
case 'ENAMETOOLONG': // Filename too long
|
|||
|
case 'UNKNOWN':
|
|||
|
resolve([]);
|
|||
|
break;
|
|||
|
case 'ELOOP': // Too many levels of symbolic links
|
|||
|
default:
|
|||
|
reject(err);
|
|||
|
break;
|
|||
|
}
|
|||
|
} else {
|
|||
|
resolve(files);
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
function stat(file, followSymlinks) {
|
|||
|
return new Promise((resolve, reject) => {
|
|||
|
const statFunc = followSymlinks ? fs.stat : fs.lstat;
|
|||
|
statFunc(file, (err, stats) => {
|
|||
|
if(err) {
|
|||
|
switch (err.code) {
|
|||
|
case 'ENOENT':
|
|||
|
if(followSymlinks) {
|
|||
|
// Fallback to lstat to handle broken links as files
|
|||
|
resolve(stat(file, false));
|
|||
|
} else {
|
|||
|
resolve(null);
|
|||
|
}
|
|||
|
break;
|
|||
|
default:
|
|||
|
resolve(null);
|
|||
|
break;
|
|||
|
}
|
|||
|
} else {
|
|||
|
resolve(stats);
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
async function* exploreWalkAsync(dir, path, followSymlinks, useStat, shouldSkip, strict) {
|
|||
|
let files = await readdir(path + dir, strict);
|
|||
|
for(const file of files) {
|
|||
|
let name = file.name;
|
|||
|
if(name === undefined) {
|
|||
|
// undefined file.name means the `withFileTypes` options is not supported by node
|
|||
|
// we have to call the stat function to know if file is directory or not.
|
|||
|
name = file;
|
|||
|
useStat = true;
|
|||
|
}
|
|||
|
const filename = dir + '/' + name;
|
|||
|
const relative = filename.slice(1); // Remove the leading /
|
|||
|
const absolute = path + '/' + relative;
|
|||
|
let stats = null;
|
|||
|
if(useStat || followSymlinks) {
|
|||
|
stats = await stat(absolute, followSymlinks);
|
|||
|
}
|
|||
|
if(!stats && file.name !== undefined) {
|
|||
|
stats = file;
|
|||
|
}
|
|||
|
if(stats === null) {
|
|||
|
stats = { isDirectory: () => false };
|
|||
|
}
|
|||
|
|
|||
|
if(stats.isDirectory()) {
|
|||
|
if(!shouldSkip(relative)) {
|
|||
|
yield {relative, absolute, stats};
|
|||
|
yield* exploreWalkAsync(filename, path, followSymlinks, useStat, shouldSkip, false);
|
|||
|
}
|
|||
|
} else {
|
|||
|
yield {relative, absolute, stats};
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
async function* explore(path, followSymlinks, useStat, shouldSkip) {
|
|||
|
yield* exploreWalkAsync('', path, followSymlinks, useStat, shouldSkip, true);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
function readOptions(options) {
|
|||
|
return {
|
|||
|
pattern: options.pattern,
|
|||
|
dot: !!options.dot,
|
|||
|
noglobstar: !!options.noglobstar,
|
|||
|
matchBase: !!options.matchBase,
|
|||
|
nocase: !!options.nocase,
|
|||
|
ignore: options.ignore,
|
|||
|
skip: options.skip,
|
|||
|
|
|||
|
follow: !!options.follow,
|
|||
|
stat: !!options.stat,
|
|||
|
nodir: !!options.nodir,
|
|||
|
mark: !!options.mark,
|
|||
|
silent: !!options.silent,
|
|||
|
absolute: !!options.absolute
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
class ReaddirGlob extends EventEmitter {
|
|||
|
constructor(cwd, options, cb) {
|
|||
|
super();
|
|||
|
if(typeof options === 'function') {
|
|||
|
cb = options;
|
|||
|
options = null;
|
|||
|
}
|
|||
|
|
|||
|
this.options = readOptions(options || {});
|
|||
|
|
|||
|
this.matchers = [];
|
|||
|
if(this.options.pattern) {
|
|||
|
const matchers = Array.isArray(this.options.pattern) ? this.options.pattern : [this.options.pattern];
|
|||
|
this.matchers = matchers.map( m =>
|
|||
|
new Minimatch(m, {
|
|||
|
dot: this.options.dot,
|
|||
|
noglobstar:this.options.noglobstar,
|
|||
|
matchBase:this.options.matchBase,
|
|||
|
nocase:this.options.nocase
|
|||
|
})
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
this.ignoreMatchers = [];
|
|||
|
if(this.options.ignore) {
|
|||
|
const ignorePatterns = Array.isArray(this.options.ignore) ? this.options.ignore : [this.options.ignore];
|
|||
|
this.ignoreMatchers = ignorePatterns.map( ignore =>
|
|||
|
new Minimatch(ignore, {dot: true})
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
this.skipMatchers = [];
|
|||
|
if(this.options.skip) {
|
|||
|
const skipPatterns = Array.isArray(this.options.skip) ? this.options.skip : [this.options.skip];
|
|||
|
this.skipMatchers = skipPatterns.map( skip =>
|
|||
|
new Minimatch(skip, {dot: true})
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
this.iterator = explore(resolve(cwd || '.'), this.options.follow, this.options.stat, this._shouldSkipDirectory.bind(this));
|
|||
|
this.paused = false;
|
|||
|
this.inactive = false;
|
|||
|
this.aborted = false;
|
|||
|
|
|||
|
if(cb) {
|
|||
|
this._matches = [];
|
|||
|
this.on('match', match => this._matches.push(this.options.absolute ? match.absolute : match.relative));
|
|||
|
this.on('error', err => cb(err));
|
|||
|
this.on('end', () => cb(null, this._matches));
|
|||
|
}
|
|||
|
|
|||
|
setTimeout( () => this._next(), 0);
|
|||
|
}
|
|||
|
|
|||
|
_shouldSkipDirectory(relative) {
|
|||
|
//console.log(relative, this.skipMatchers.some(m => m.match(relative)));
|
|||
|
return this.skipMatchers.some(m => m.match(relative));
|
|||
|
}
|
|||
|
|
|||
|
_fileMatches(relative, isDirectory) {
|
|||
|
const file = relative + (isDirectory ? '/' : '');
|
|||
|
return (this.matchers.length === 0 || this.matchers.some(m => m.match(file)))
|
|||
|
&& !this.ignoreMatchers.some(m => m.match(file))
|
|||
|
&& (!this.options.nodir || !isDirectory);
|
|||
|
}
|
|||
|
|
|||
|
_next() {
|
|||
|
if(!this.paused && !this.aborted) {
|
|||
|
this.iterator.next()
|
|||
|
.then((obj)=> {
|
|||
|
if(!obj.done) {
|
|||
|
const isDirectory = obj.value.stats.isDirectory();
|
|||
|
if(this._fileMatches(obj.value.relative, isDirectory )) {
|
|||
|
let relative = obj.value.relative;
|
|||
|
let absolute = obj.value.absolute;
|
|||
|
if(this.options.mark && isDirectory) {
|
|||
|
relative += '/';
|
|||
|
absolute += '/';
|
|||
|
}
|
|||
|
if(this.options.stat) {
|
|||
|
this.emit('match', {relative, absolute, stat:obj.value.stats});
|
|||
|
} else {
|
|||
|
this.emit('match', {relative, absolute});
|
|||
|
}
|
|||
|
}
|
|||
|
this._next(this.iterator);
|
|||
|
} else {
|
|||
|
this.emit('end');
|
|||
|
}
|
|||
|
})
|
|||
|
.catch((err) => {
|
|||
|
this.abort();
|
|||
|
this.emit('error', err);
|
|||
|
if(!err.code && !this.options.silent) {
|
|||
|
console.error(err);
|
|||
|
}
|
|||
|
});
|
|||
|
} else {
|
|||
|
this.inactive = true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
abort() {
|
|||
|
this.aborted = true;
|
|||
|
}
|
|||
|
|
|||
|
pause() {
|
|||
|
this.paused = true;
|
|||
|
}
|
|||
|
|
|||
|
resume() {
|
|||
|
this.paused = false;
|
|||
|
if(this.inactive) {
|
|||
|
this.inactive = false;
|
|||
|
this._next();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
function readdirGlob(pattern, options, cb) {
|
|||
|
return new ReaddirGlob(pattern, options, cb);
|
|||
|
}
|
|||
|
readdirGlob.ReaddirGlob = ReaddirGlob;
|