UNPKG

9.58 kBJavaScriptView Raw
1// File List
2// =========
3//
4// The List is an object for tracking all files that karma knows about
5// currently.
6
7// Dependencies
8// ------------
9
10require('core-js')
11var from = require('core-js/library/fn/array/from')
12var Promise = require('bluebird')
13var mm = require('minimatch')
14var Glob = require('glob').Glob
15var fs = Promise.promisifyAll(require('graceful-fs'))
16var pathLib = require('path')
17
18var File = require('./file')
19var Url = require('./url')
20var helper = require('./helper')
21var _ = helper._
22var log = require('./logger').create('watcher')
23
24// Constants
25// ---------
26
27var GLOB_OPTS = {
28 cwd: '/',
29 follow: true,
30 nodir: true,
31 sync: true
32}
33
34// Helper Functions
35// ----------------
36
37function byPath (a, b) {
38 if (a.path > b.path) return 1
39 if (a.path < b.path) return -1
40
41 return 0
42}
43
44// Constructor
45//
46// patterns - Array
47// excludes - Array
48// emitter - EventEmitter
49// preprocess - Function
50// batchInterval - Number
51var List = function (patterns, excludes, emitter, preprocess, batchInterval) {
52 // Store options
53 this._patterns = patterns
54 this._excludes = excludes
55 this._emitter = emitter
56 this._preprocess = Promise.promisify(preprocess)
57 this._batchInterval = batchInterval
58
59 // The actual list of files
60 this.buckets = new Map()
61
62 // Internal tracker if we are refreshing.
63 // When a refresh is triggered this gets set
64 // to the promise that `this._refresh` returns.
65 // So we know we are refreshing when this promise
66 // is still pending, and we are done when it's either
67 // resolved or rejected.
68 this._refreshing = Promise.resolve()
69
70 var self = this
71
72 // Emit the `file_list_modified` event.
73 // This function is throttled to the value of `batchInterval`
74 // to avoid spamming the listener.
75 function emit () {
76 self._emitter.emit('file_list_modified', self.files)
77 }
78 var throttledEmit = _.throttle(emit, self._batchInterval, {leading: false})
79 self._emitModified = function (immediate) {
80 immediate ? emit() : throttledEmit()
81 }
82}
83
84// Private Interface
85// -----------------
86
87// Is the given path matched by any exclusion filter
88//
89// path - String
90//
91// Returns `undefined` if no match, otherwise the matching
92// pattern.
93List.prototype._isExcluded = function (path) {
94 return _.find(this._excludes, function (pattern) {
95 return mm(path, pattern)
96 })
97}
98
99// Find the matching include pattern for the given path.
100//
101// path - String
102//
103// Returns the match or `undefined` if none found.
104List.prototype._isIncluded = function (path) {
105 return _.find(this._patterns, function (pattern) {
106 return mm(path, pattern.pattern)
107 })
108}
109
110// Find the given path in the bucket corresponding
111// to the given pattern.
112//
113// path - String
114// pattern - Object
115//
116// Returns a File or undefined
117List.prototype._findFile = function (path, pattern) {
118 if (!path || !pattern) return
119 if (!this.buckets.has(pattern.pattern)) return
120
121 return _.find(from(this.buckets.get(pattern.pattern)), function (file) {
122 return file.originalPath === path
123 })
124}
125
126// Is the given path already in the files list.
127//
128// path - String
129//
130// Returns a boolean.
131List.prototype._exists = function (path) {
132 var self = this
133
134 var patterns = this._patterns.filter(function (pattern) {
135 return mm(path, pattern.pattern)
136 })
137
138 return !!_.find(patterns, function (pattern) {
139 return self._findFile(path, pattern)
140 })
141}
142
143// Check if we are currently refreshing
144List.prototype._isRefreshing = function () {
145 return this._refreshing.isPending()
146}
147
148// Do the actual work of refreshing
149List.prototype._refresh = function () {
150 var self = this
151 var buckets = this.buckets
152
153 return Promise.all(this._patterns.map(function (patternObject) {
154 var pattern = patternObject.pattern
155
156 if (helper.isUrlAbsolute(pattern)) {
157 buckets.set(pattern, new Set([new Url(pattern)]))
158 return Promise.resolve()
159 }
160
161 var mg = new Glob(pathLib.normalize(pattern), GLOB_OPTS)
162 var files = mg.found
163 buckets.set(pattern, new Set())
164
165 if (_.isEmpty(files)) {
166 log.warn('Pattern "%s" does not match any file.', pattern)
167 return
168 }
169
170 return Promise.all(files.map(function (path) {
171 if (self._isExcluded(path)) {
172 log.debug('Excluded file "%s"', path)
173 return Promise.resolve()
174 }
175
176 var mtime = mg.statCache[path].mtime
177 var doNotCache = patternObject.nocache
178 var file = new File(path, mtime, doNotCache)
179
180 if (file.doNotCache) {
181 log.debug('Not preprocessing "%s" due to nocache')
182 return Promise.resolve(file)
183 }
184
185 return self._preprocess(file).then(function () {
186 return file
187 })
188 }))
189 .then(function (files) {
190 files = _.compact(files)
191
192 if (_.isEmpty(files)) {
193 log.warn('All files matched by "%s" were excluded.', pattern)
194 } else {
195 buckets.set(pattern, new Set(files))
196 }
197 })
198 }))
199 .cancellable()
200 .then(function () {
201 self.buckets = buckets
202 self._emitModified(true)
203 return self.files
204 })
205 .catch(Promise.CancellationError, function () {
206 // We were canceled so return the resolution of the new run
207 return self._refreshing
208 })
209}
210
211// Public Interface
212// ----------------
213
214Object.defineProperty(List.prototype, 'files', {
215 get: function () {
216 var self = this
217
218 var served = this._patterns.filter(function (pattern) {
219 return pattern.served
220 })
221 .map(function (p) {
222 return from(self.buckets.get(p.pattern) || []).sort(byPath)
223 })
224
225 var included = this._patterns.filter(function (pattern) {
226 return pattern.included
227 })
228 .map(function (p) {
229 return from(self.buckets.get(p.pattern) || []).sort(byPath)
230 })
231
232 var uniqFlat = function (list) {
233 return _.uniq(_.flatten(list), 'path')
234 }
235
236 return {
237 served: uniqFlat(served),
238 included: uniqFlat(included)
239 }
240 }
241})
242
243// Reglob all patterns to update the list.
244//
245// Returns a promise that is resolved when the refresh
246// is completed.
247List.prototype.refresh = function () {
248 if (this._isRefreshing()) {
249 this._refreshing.cancel()
250 }
251
252 this._refreshing = this._refresh()
253 return this._refreshing
254}
255
256// Set new patterns and excludes and update
257// the list accordingly
258//
259// patterns - Array, the new patterns.
260// excludes - Array, the new exclude patterns.
261//
262// Returns a promise that is resolved when the refresh
263// is completed.
264List.prototype.reload = function (patterns, excludes) {
265 this._patterns = patterns
266 this._excludes = excludes
267
268 // Wait until the current refresh is done and then do a
269 // refresh to ensure a refresh actually happens
270 return this.refresh()
271}
272
273// Add a new file from the list.
274// This is called by the watcher
275//
276// path - String, the path of the file to update.
277//
278// Returns a promise that is resolved when the update
279// is completed.
280List.prototype.addFile = function (path) {
281 var self = this
282
283 // Ensure we are not adding a file that should be excluded
284 var excluded = this._isExcluded(path)
285 if (excluded) {
286 log.debug('Add file "%s" ignored. Excluded by "%s".', path, excluded)
287
288 return Promise.resolve(this.files)
289 }
290
291 var pattern = this._isIncluded(path)
292
293 if (!pattern) {
294 log.debug('Add file "%s" ignored. Does not match any pattern.', path)
295 return Promise.resolve(this.files)
296 }
297
298 if (this._exists(path)) {
299 log.debug('Add file "%s" ignored. Already in the list.', path)
300 return Promise.resolve(this.files)
301 }
302
303 var file = new File(path)
304 this.buckets.get(pattern.pattern).add(file)
305
306 return Promise.all([
307 fs.statAsync(path),
308 this._refreshing
309 ]).spread(function (stat) {
310 file.mtime = stat.mtime
311 return self._preprocess(file)
312 })
313 .then(function () {
314 log.info('Added file "%s".', path)
315 self._emitModified()
316 return self.files
317 })
318}
319
320// Update the `mtime` of a file.
321// This is called by the watcher
322//
323// path - String, the path of the file to update.
324//
325// Returns a promise that is resolved when the update
326// is completed.
327List.prototype.changeFile = function (path) {
328 var self = this
329
330 var pattern = this._isIncluded(path)
331 var file = this._findFile(path, pattern)
332
333 if (!pattern || !file) {
334 log.debug('Changed file "%s" ignored. Does not match any file in the list.', path)
335 return Promise.resolve(this.files)
336 }
337
338 return Promise.all([
339 fs.statAsync(path),
340 this._refreshing
341 ]).spread(function (stat) {
342 if (stat.mtime <= file.mtime) throw new Promise.CancellationError()
343
344 file.mtime = stat.mtime
345 return self._preprocess(file)
346 })
347 .then(function () {
348 log.info('Changed file "%s".', path)
349 self._emitModified()
350 return self.files
351 })
352 .catch(Promise.CancellationError, function () {
353 return self.files
354 })
355}
356
357// Remove a file from the list.
358// This is called by the watcher
359//
360// path - String, the path of the file to update.
361//
362// Returns a promise that is resolved when the update
363// is completed.
364List.prototype.removeFile = function (path) {
365 var self = this
366
367 return Promise.try(function () {
368 var pattern = self._isIncluded(path)
369 var file = self._findFile(path, pattern)
370
371 if (!pattern || !file) {
372 log.debug('Removed file "%s" ignored. Does not match any file in the list.', path)
373 return self.files
374 }
375
376 self.buckets.get(pattern.pattern).delete(file)
377
378 log.info('Removed file "%s".', path)
379 self._emitModified()
380 return self.files
381 })
382}
383
384// Inject dependencies
385List.$inject = ['config.files', 'config.exclude', 'emitter', 'preprocess',
386 'config.autoWatchBatchDelay']
387
388// PUBLIC
389module.exports = List