1'use strict' 2 3const fs = require('fs') 4const path = require('path') 5const EE = require('events').EventEmitter 6const Minimatch = require('minimatch').Minimatch 7 8class Walker extends EE { 9 constructor (opts) { 10 opts = opts || {} 11 super(opts) 12 // set to true if this.path is a symlink, whether follow is true or not 13 this.isSymbolicLink = opts.isSymbolicLink 14 this.path = opts.path || process.cwd() 15 this.basename = path.basename(this.path) 16 this.ignoreFiles = opts.ignoreFiles || ['.ignore'] 17 this.ignoreRules = {} 18 this.parent = opts.parent || null 19 this.includeEmpty = !!opts.includeEmpty 20 this.root = this.parent ? this.parent.root : this.path 21 this.follow = !!opts.follow 22 this.result = this.parent ? this.parent.result : new Set() 23 this.entries = null 24 this.sawError = false 25 this.exact = opts.exact 26 } 27 28 sort (a, b) { 29 return a.localeCompare(b, 'en') 30 } 31 32 emit (ev, data) { 33 let ret = false 34 if (!(this.sawError && ev === 'error')) { 35 if (ev === 'error') { 36 this.sawError = true 37 } else if (ev === 'done' && !this.parent) { 38 data = Array.from(data) 39 .map(e => /^@/.test(e) ? `./${e}` : e).sort(this.sort) 40 this.result = data 41 } 42 43 if (ev === 'error' && this.parent) { 44 ret = this.parent.emit('error', data) 45 } else { 46 ret = super.emit(ev, data) 47 } 48 } 49 return ret 50 } 51 52 start () { 53 fs.readdir(this.path, (er, entries) => 54 er ? this.emit('error', er) : this.onReaddir(entries)) 55 return this 56 } 57 58 isIgnoreFile (e) { 59 return e !== '.' && 60 e !== '..' && 61 this.ignoreFiles.indexOf(e) !== -1 62 } 63 64 onReaddir (entries) { 65 this.entries = entries 66 if (entries.length === 0) { 67 if (this.includeEmpty) { 68 this.result.add(this.path.slice(this.root.length + 1)) 69 } 70 this.emit('done', this.result) 71 } else { 72 const hasIg = this.entries.some(e => 73 this.isIgnoreFile(e)) 74 75 if (hasIg) { 76 this.addIgnoreFiles() 77 } else { 78 this.filterEntries() 79 } 80 } 81 } 82 83 addIgnoreFiles () { 84 const newIg = this.entries 85 .filter(e => this.isIgnoreFile(e)) 86 87 let igCount = newIg.length 88 const then = _ => { 89 if (--igCount === 0) { 90 this.filterEntries() 91 } 92 } 93 94 newIg.forEach(e => this.addIgnoreFile(e, then)) 95 } 96 97 addIgnoreFile (file, then) { 98 const ig = path.resolve(this.path, file) 99 fs.readFile(ig, 'utf8', (er, data) => 100 er ? this.emit('error', er) : this.onReadIgnoreFile(file, data, then)) 101 } 102 103 onReadIgnoreFile (file, data, then) { 104 const mmopt = { 105 matchBase: true, 106 dot: true, 107 flipNegate: true, 108 nocase: true, 109 } 110 const rules = data.split(/\r?\n/) 111 .filter(line => !/^#|^$/.test(line.trim())) 112 .map(rule => { 113 return new Minimatch(rule.trim(), mmopt) 114 }) 115 116 this.ignoreRules[file] = rules 117 118 then() 119 } 120 121 filterEntries () { 122 // at this point we either have ignore rules, or just inheriting 123 // this exclusion is at the point where we know the list of 124 // entries in the dir, but don't know what they are. since 125 // some of them *might* be directories, we have to run the 126 // match in dir-mode as well, so that we'll pick up partials 127 // of files that will be included later. Anything included 128 // at this point will be checked again later once we know 129 // what it is. 130 const filtered = this.entries.map(entry => { 131 // at this point, we don't know if it's a dir or not. 132 const passFile = this.filterEntry(entry) 133 const passDir = this.filterEntry(entry, true) 134 return (passFile || passDir) ? [entry, passFile, passDir] : false 135 }).filter(e => e) 136 137 // now we stat them all 138 // if it's a dir, and passes as a dir, then recurse 139 // if it's not a dir, but passes as a file, add to set 140 let entryCount = filtered.length 141 if (entryCount === 0) { 142 this.emit('done', this.result) 143 } else { 144 const then = _ => { 145 if (--entryCount === 0) { 146 this.emit('done', this.result) 147 } 148 } 149 filtered.forEach(filt => { 150 const entry = filt[0] 151 const file = filt[1] 152 const dir = filt[2] 153 this.stat({ entry, file, dir }, then) 154 }) 155 } 156 } 157 158 onstat ({ st, entry, file, dir, isSymbolicLink }, then) { 159 const abs = this.path + '/' + entry 160 if (!st.isDirectory()) { 161 if (file) { 162 this.result.add(abs.slice(this.root.length + 1)) 163 } 164 then() 165 } else { 166 // is a directory 167 if (dir) { 168 this.walker(entry, { isSymbolicLink, exact: file || this.filterEntry(entry + '/') }, then) 169 } else { 170 then() 171 } 172 } 173 } 174 175 stat ({ entry, file, dir }, then) { 176 const abs = this.path + '/' + entry 177 fs.lstat(abs, (lstatErr, lstatResult) => { 178 if (lstatErr) { 179 this.emit('error', lstatErr) 180 } else { 181 const isSymbolicLink = lstatResult.isSymbolicLink() 182 if (this.follow && isSymbolicLink) { 183 fs.stat(abs, (statErr, statResult) => { 184 if (statErr) { 185 this.emit('error', statErr) 186 } else { 187 this.onstat({ st: statResult, entry, file, dir, isSymbolicLink }, then) 188 } 189 }) 190 } else { 191 this.onstat({ st: lstatResult, entry, file, dir, isSymbolicLink }, then) 192 } 193 } 194 }) 195 } 196 197 walkerOpt (entry, opts) { 198 return { 199 path: this.path + '/' + entry, 200 parent: this, 201 ignoreFiles: this.ignoreFiles, 202 follow: this.follow, 203 includeEmpty: this.includeEmpty, 204 ...opts, 205 } 206 } 207 208 walker (entry, opts, then) { 209 new Walker(this.walkerOpt(entry, opts)).on('done', then).start() 210 } 211 212 filterEntry (entry, partial, entryBasename) { 213 let included = true 214 215 // this = /a/b/c 216 // entry = d 217 // parent /a/b sees c/d 218 if (this.parent && this.parent.filterEntry) { 219 const parentEntry = this.basename + '/' + entry 220 const parentBasename = entryBasename || entry 221 included = this.parent.filterEntry(parentEntry, partial, parentBasename) 222 if (!included && !this.exact) { 223 return false 224 } 225 } 226 227 this.ignoreFiles.forEach(f => { 228 if (this.ignoreRules[f]) { 229 this.ignoreRules[f].forEach(rule => { 230 // negation means inclusion 231 // so if it's negated, and already included, no need to check 232 // likewise if it's neither negated nor included 233 if (rule.negate !== included) { 234 const isRelativeRule = entryBasename && rule.globParts.some(part => 235 part.length <= (part.slice(-1)[0] ? 1 : 2) 236 ) 237 238 // first, match against /foo/bar 239 // then, against foo/bar 240 // then, in the case of partials, match with a / 241 // then, if also the rule is relative, match against basename 242 const match = rule.match('/' + entry) || 243 rule.match(entry) || 244 !!partial && ( 245 rule.match('/' + entry + '/') || 246 rule.match(entry + '/') || 247 rule.negate && ( 248 rule.match('/' + entry, true) || 249 rule.match(entry, true)) || 250 isRelativeRule && ( 251 rule.match('/' + entryBasename + '/') || 252 rule.match(entryBasename + '/') || 253 rule.negate && ( 254 rule.match('/' + entryBasename, true) || 255 rule.match(entryBasename, true)))) 256 257 if (match) { 258 included = rule.negate 259 } 260 } 261 }) 262 } 263 }) 264 265 return included 266 } 267} 268 269class WalkerSync extends Walker { 270 start () { 271 this.onReaddir(fs.readdirSync(this.path)) 272 return this 273 } 274 275 addIgnoreFile (file, then) { 276 const ig = path.resolve(this.path, file) 277 this.onReadIgnoreFile(file, fs.readFileSync(ig, 'utf8'), then) 278 } 279 280 stat ({ entry, file, dir }, then) { 281 const abs = this.path + '/' + entry 282 let st = fs.lstatSync(abs) 283 const isSymbolicLink = st.isSymbolicLink() 284 if (this.follow && isSymbolicLink) { 285 st = fs.statSync(abs) 286 } 287 288 // console.error('STAT SYNC', {st, entry, file, dir, isSymbolicLink, then}) 289 this.onstat({ st, entry, file, dir, isSymbolicLink }, then) 290 } 291 292 walker (entry, opts, then) { 293 new WalkerSync(this.walkerOpt(entry, opts)).start() 294 then() 295 } 296} 297 298const walk = (opts, callback) => { 299 const p = new Promise((resolve, reject) => { 300 new Walker(opts).on('done', resolve).on('error', reject).start() 301 }) 302 return callback ? p.then(res => callback(null, res), callback) : p 303} 304 305const walkSync = opts => new WalkerSync(opts).start().result 306 307module.exports = walk 308walk.sync = walkSync 309walk.Walker = Walker 310walk.WalkerSync = WalkerSync 311