file-list.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. 'use strict'
  2. const { promisify } = require('util')
  3. const mm = require('minimatch')
  4. const Glob = require('glob').Glob
  5. const fs = require('graceful-fs')
  6. const statAsync = promisify(fs.stat.bind(fs))
  7. const pathLib = require('path')
  8. const _ = require('lodash')
  9. const File = require('./file')
  10. const Url = require('./url')
  11. const helper = require('./helper')
  12. const log = require('./logger').create('filelist')
  13. const createPatternObject = require('./config').createPatternObject
  14. class FileList {
  15. constructor (patterns, excludes, emitter, preprocess, autoWatchBatchDelay) {
  16. this._patterns = patterns || []
  17. this._excludes = excludes || []
  18. this._emitter = emitter
  19. this._preprocess = preprocess
  20. this.buckets = new Map()
  21. // A promise that is pending if and only if we are active in this.refresh_()
  22. this._refreshing = null
  23. const emit = () => {
  24. this._emitter.emit('file_list_modified', this.files)
  25. }
  26. const debouncedEmit = _.debounce(emit, autoWatchBatchDelay)
  27. this._emitModified = (immediate) => {
  28. immediate ? emit() : debouncedEmit()
  29. }
  30. }
  31. _findExcluded (path) {
  32. return this._excludes.find((pattern) => mm(path, pattern))
  33. }
  34. _findIncluded (path) {
  35. return this._patterns.find((pattern) => mm(path, pattern.pattern))
  36. }
  37. _findFile (path, pattern) {
  38. if (!path || !pattern) return
  39. return this._getFilesByPattern(pattern.pattern).find((file) => file.originalPath === path)
  40. }
  41. _exists (path) {
  42. return !!this._patterns.find((pattern) => mm(path, pattern.pattern) && this._findFile(path, pattern))
  43. }
  44. _getFilesByPattern (pattern) {
  45. return this.buckets.get(pattern) || []
  46. }
  47. _refresh () {
  48. const matchedFiles = new Set()
  49. let lastCompletedRefresh = this._refreshing
  50. lastCompletedRefresh = Promise.all(
  51. this._patterns.map(async ({ pattern, type, nocache, isBinary }) => {
  52. if (helper.isUrlAbsolute(pattern)) {
  53. this.buckets.set(pattern, [new Url(pattern, type)])
  54. return
  55. }
  56. const mg = new Glob(pathLib.normalize(pattern), { cwd: '/', follow: true, nodir: true, sync: true })
  57. const files = mg.found
  58. .filter((path) => {
  59. if (this._findExcluded(path)) {
  60. log.debug(`Excluded file "${path}"`)
  61. return false
  62. } else if (matchedFiles.has(path)) {
  63. return false
  64. } else {
  65. matchedFiles.add(path)
  66. return true
  67. }
  68. })
  69. .map((path) => new File(path, mg.statCache[path].mtime, nocache, type, isBinary))
  70. if (nocache) {
  71. log.debug(`Not preprocessing "${pattern}" due to nocache`)
  72. } else {
  73. await Promise.all(files.map((file) => this._preprocess(file)))
  74. }
  75. this.buckets.set(pattern, files)
  76. if (_.isEmpty(mg.found)) {
  77. log.warn(`Pattern "${pattern}" does not match any file.`)
  78. } else if (_.isEmpty(files)) {
  79. log.warn(`All files matched by "${pattern}" were excluded or matched by prior matchers.`)
  80. }
  81. })
  82. )
  83. .then(() => {
  84. // When we return from this function the file processing chain will be
  85. // complete. In the case of two fast refresh() calls, the second call
  86. // will overwrite this._refreshing, and we want the status to reflect
  87. // the second call and skip the modification event from the first call.
  88. if (this._refreshing !== lastCompletedRefresh) {
  89. return this._refreshing
  90. }
  91. this._emitModified(true)
  92. return this.files
  93. })
  94. return lastCompletedRefresh
  95. }
  96. get files () {
  97. const served = []
  98. const included = {}
  99. const lookup = {}
  100. this._patterns.forEach((p) => {
  101. // This needs to be here sadly, as plugins are modifiying
  102. // the _patterns directly resulting in elements not being
  103. // instantiated properly
  104. if (p.constructor.name !== 'Pattern') {
  105. p = createPatternObject(p)
  106. }
  107. const files = this._getFilesByPattern(p.pattern)
  108. files.sort((a, b) => {
  109. if (a.path > b.path) return 1
  110. if (a.path < b.path) return -1
  111. return 0
  112. })
  113. if (p.served) {
  114. served.push(...files)
  115. }
  116. files.forEach((file) => {
  117. if (lookup[file.path] && lookup[file.path].compare(p) < 0) return
  118. lookup[file.path] = p
  119. if (p.included) {
  120. included[file.path] = file
  121. } else {
  122. delete included[file.path]
  123. }
  124. })
  125. })
  126. return {
  127. served: _.uniq(served, 'path'),
  128. included: _.values(included)
  129. }
  130. }
  131. refresh () {
  132. this._refreshing = this._refresh()
  133. return this._refreshing
  134. }
  135. reload (patterns, excludes) {
  136. this._patterns = patterns || []
  137. this._excludes = excludes || []
  138. return this.refresh()
  139. }
  140. async addFile (path) {
  141. const excluded = this._findExcluded(path)
  142. if (excluded) {
  143. log.debug(`Add file "${path}" ignored. Excluded by "${excluded}".`)
  144. return this.files
  145. }
  146. const pattern = this._findIncluded(path)
  147. if (!pattern) {
  148. log.debug(`Add file "${path}" ignored. Does not match any pattern.`)
  149. return this.files
  150. }
  151. if (this._exists(path)) {
  152. log.debug(`Add file "${path}" ignored. Already in the list.`)
  153. return this.files
  154. }
  155. const file = new File(path)
  156. this._getFilesByPattern(pattern.pattern).push(file)
  157. const [stat] = await Promise.all([statAsync(path), this._refreshing])
  158. file.mtime = stat.mtime
  159. await this._preprocess(file)
  160. log.info(`Added file "${path}".`)
  161. this._emitModified()
  162. return this.files
  163. }
  164. async changeFile (path, force) {
  165. const pattern = this._findIncluded(path)
  166. const file = this._findFile(path, pattern)
  167. if (!file) {
  168. log.debug(`Changed file "${path}" ignored. Does not match any file in the list.`)
  169. return this.files
  170. }
  171. const [stat] = await Promise.all([statAsync(path), this._refreshing])
  172. if (force || stat.mtime > file.mtime) {
  173. file.mtime = stat.mtime
  174. await this._preprocess(file)
  175. log.info(`Changed file "${path}".`)
  176. this._emitModified(force)
  177. }
  178. return this.files
  179. }
  180. async removeFile (path) {
  181. const pattern = this._findIncluded(path)
  182. const file = this._findFile(path, pattern)
  183. if (file) {
  184. helper.arrayRemove(this._getFilesByPattern(pattern.pattern), file)
  185. log.info(`Removed file "${path}".`)
  186. this._emitModified()
  187. } else {
  188. log.debug(`Removed file "${path}" ignored. Does not match any file in the list.`)
  189. }
  190. return this.files
  191. }
  192. }
  193. FileList.factory = function (config, emitter, preprocess) {
  194. return new FileList(config.files, config.exclude, emitter, preprocess, config.autoWatchBatchDelay)
  195. }
  196. FileList.factory.$inject = ['config', 'emitter', 'preprocess']
  197. module.exports = FileList