index.js 7.8 KB

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