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;