index.js 6.9 KB

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