codekingpro/portable-devtools
114k
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 