index.js
  1  module.exports = readdirGlob;
  2  
  3  const fs = require('fs');
  4  const { EventEmitter } = require('events');
  5  const { Minimatch } = require('minimatch');
  6  const { resolve } = require('path');
  7  
  8  function readdir(dir, strict) {
  9    return new Promise((resolve, reject) => {
 10      fs.readdir(dir, {withFileTypes: true} ,(err, files) => {
 11        if(err) {
 12          switch (err.code) {
 13            case 'ENOTDIR':      // Not a directory
 14              if(strict) {
 15                reject(err);
 16              } else {
 17                resolve([]);
 18              }
 19              break;
 20            case 'ENOTSUP':      // Operation not supported
 21            case 'ENOENT':       // No such file or directory
 22            case 'ENAMETOOLONG': // Filename too long
 23            case 'UNKNOWN':
 24              resolve([]);
 25              break;
 26            case 'ELOOP':        // Too many levels of symbolic links
 27            default:
 28              reject(err);
 29              break;
 30          }
 31        } else {
 32          resolve(files);
 33        }
 34      });
 35    });
 36  }
 37  function stat(file, followSyslinks) {
 38    return new Promise((resolve, reject) => {
 39      const statFunc = followSyslinks ? fs.stat : fs.lstat;
 40      statFunc(file, (err, stats) => {
 41        if(err) {
 42          switch (err.code) {
 43            case 'ENOENT':
 44              if(followSyslinks) {
 45                // Fallback to lstat to handle broken links as files
 46                resolve(stat(file, false)); 
 47              } else {
 48                resolve(null);
 49              }
 50              break;
 51            default:
 52              resolve(null);
 53              break;
 54          }
 55        } else {
 56          resolve(stats);
 57        }
 58      });
 59    });
 60  }
 61  
 62  async function* exploreWalkAsync(dir, path, followSyslinks, useStat, shouldSkip, strict) {
 63    let files = await readdir(path + dir, strict);
 64    for(const file of files) {
 65      let name = file.name;
 66      if(name === undefined) {
 67        // undefined file.name means the `withFileTypes` options is not supported by node
 68        // we have to call the stat function to know if file is directory or not.
 69        name = file;
 70        useStat = true;
 71      }
 72      const filename = dir + '/' + name;
 73      const relative = filename.slice(1); // Remove the leading /
 74      const absolute = path + '/' + relative;
 75      let stats = null;
 76      if(useStat || followSyslinks) {
 77        stats = await stat(absolute, followSyslinks);
 78      }
 79      if(!stats && file.name !== undefined) {
 80        stats = file;
 81      }
 82      if(stats === null) {
 83        stats = { isDirectory: () => false };
 84      }
 85  
 86      if(stats.isDirectory()) {
 87        if(!shouldSkip(relative)) {
 88          yield {relative, absolute, stats};
 89          yield* exploreWalkAsync(filename, path, followSyslinks, useStat, shouldSkip, false);
 90        }
 91      } else {
 92        yield {relative, absolute, stats};
 93      }
 94    }
 95  }
 96  async function* explore(path, followSyslinks, useStat, shouldSkip) {
 97    yield* exploreWalkAsync('', path, followSyslinks, useStat, shouldSkip, true);
 98  }
 99  
100  
101  function readOptions(options) {
102    return {
103      pattern: options.pattern,
104      dot: !!options.dot,
105      noglobstar: !!options.noglobstar,
106      matchBase: !!options.matchBase,
107      nocase: !!options.nocase,
108      ignore: options.ignore,
109      skip: options.skip,
110  
111      follow: !!options.follow,
112      stat: !!options.stat,
113      nodir: !!options.nodir,
114      mark: !!options.mark,
115      silent: !!options.silent,
116      absolute: !!options.absolute
117    };
118  }
119  
120  class ReaddirGlob extends EventEmitter {
121    constructor(cwd, options, cb) {
122      super();
123      if(typeof options === 'function') {
124        cb = options;
125        options = null;
126      }
127  
128      this.options = readOptions(options || {});
129    
130      this.matchers = [];
131      if(this.options.pattern) {
132        const matchers = Array.isArray(this.options.pattern) ? this.options.pattern : [this.options.pattern];
133        this.matchers = matchers.map( m =>
134          new Minimatch(m, {
135            dot: this.options.dot,
136            noglobstar:this.options.noglobstar,
137            matchBase:this.options.matchBase,
138            nocase:this.options.nocase
139          })
140        );
141      }
142    
143      this.ignoreMatchers = [];
144      if(this.options.ignore) {
145        const ignorePatterns = Array.isArray(this.options.ignore) ? this.options.ignore : [this.options.ignore];
146        this.ignoreMatchers = ignorePatterns.map( ignore =>
147          new Minimatch(ignore, {dot: true})
148        );
149      }
150    
151      this.skipMatchers = [];
152      if(this.options.skip) {
153        const skipPatterns = Array.isArray(this.options.skip) ? this.options.skip : [this.options.skip];
154        this.skipMatchers = skipPatterns.map( skip =>
155          new Minimatch(skip, {dot: true})
156        );
157      }
158  
159      this.iterator = explore(resolve(cwd || '.'), this.options.follow, this.options.stat, this._shouldSkipDirectory.bind(this));
160      this.paused = false;
161      this.inactive = false;
162      this.aborted = false;
163    
164      if(cb) {
165        this._matches = []; 
166        this.on('match', match => this._matches.push(this.options.absolute ? match.absolute : match.relative));
167        this.on('error', err => cb(err));
168        this.on('end', () => cb(null, this._matches));
169      }
170  
171      setTimeout( () => this._next(), 0);
172    }
173  
174    _shouldSkipDirectory(relative) {
175      //console.log(relative, this.skipMatchers.some(m => m.match(relative)));
176      return this.skipMatchers.some(m => m.match(relative));
177    }
178  
179    _fileMatches(relative, isDirectory) {
180      const file = relative + (isDirectory ? '/' : '');
181      return (this.matchers.length === 0 || this.matchers.some(m => m.match(file)))
182        && !this.ignoreMatchers.some(m => m.match(file))
183        && (!this.options.nodir || !isDirectory);
184    }
185  
186    _next() {
187      if(!this.paused && !this.aborted) {
188        this.iterator.next()
189        .then((obj)=> {
190          if(!obj.done) {
191            const isDirectory = obj.value.stats.isDirectory();
192            if(this._fileMatches(obj.value.relative, isDirectory )) {
193              let relative = obj.value.relative;
194              let absolute = obj.value.absolute;
195              if(this.options.mark && isDirectory) {
196                relative += '/';
197                absolute += '/';
198              }
199              if(this.options.stat) {
200                this.emit('match', {relative, absolute, stat:obj.value.stats});
201              } else {
202                this.emit('match', {relative, absolute});
203              }
204            }
205            this._next(this.iterator);
206          } else {
207            this.emit('end');
208          }
209        })
210        .catch((err) => {
211          this.abort();
212          this.emit('error', err);
213          if(!err.code && !this.options.silent) {
214            console.error(err);
215          }
216        });
217      } else {
218        this.inactive = true;
219      }
220    }
221  
222    abort() {
223      this.aborted = true;
224    }
225  
226    pause() {
227      this.paused = true;
228    }
229  
230    resume() {
231      this.paused = false;
232      if(this.inactive) {
233        this.inactive = false;
234        this._next();
235      }
236    }
237  }
238  
239  
240  function readdirGlob(pattern, options, cb) {
241    return new ReaddirGlob(pattern, options, cb);
242  }
243  readdirGlob.ReaddirGlob = ReaddirGlob;