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;