file-list.js 6.7 KB

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