2 var glob = require('glob')
3 var mm = require('minimatch')
6 var helper = require('./helper')
7 var log = require('./logger').create('watcher')
9 var createWinGlob = function (realGlob) {
10 return function (pattern, options, done) {
11 realGlob(pattern, options, function (err, results) {
12 done(err, results.map(helper.normalizeWinPath))
17 var findBucketByPath = function (buckets, path) {
18 for (var i = 0; i < buckets.length; i++) {
19 for (var j = 0; j < buckets[i].length; j++) {
20 if (buckets[i][j].originalPath === path) {
27 if (process.platform === 'win32') {
28 glob = createWinGlob(glob)
31 var File = function (path, mtime) {
32 // used for serving (processed path, eg some/file.coffee -> some/file.coffee.js)
35 // original absolute path, id of the file
36 this.originalPath = path
38 // where the content is stored (processed)
39 this.contentPath = path
45 var Url = function (path) {
50 Url.prototype.toString = File.prototype.toString = function () {
60 var byPath = function (a, b) {
61 if (a.path > b.path) {
64 if (a.path < b.path) {
70 // TODO(vojta): ignore changes (add/change/remove) when in the middle of refresh
71 // TODO(vojta): do not glob patterns that are watched (both on init and refresh)
72 var List = function (patterns, excludes, emitter, preprocess, batchInterval) {
79 var addError = function (path) {
80 if (errors.indexOf(path) === -1) {
85 var removeError = function (path) {
86 var idx = errors.indexOf(path)
93 var resolveFiles = function (buckets) {
100 buckets.forEach(function (bucket, idx) {
101 bucket.sort(byPath).forEach(function (file) {
102 if (!uniqueMap[file.path]) {
103 if (patterns[idx].served) {
104 files.served.push(file)
107 if (patterns[idx].included) {
108 files.included.push(file)
111 uniqueMap[file.path] = true
119 var resolveDeferred = function (files) {
120 clearPendingTimeout()
122 if (!errors.length) {
123 pendingDeferred.resolve(files || resolveFiles(self.buckets))
125 pendingDeferred.reject(errors.slice())
128 pendingDeferred = pendingTimeout = null
131 var fireEventAndDefer = function () {
132 clearPendingTimeout()
134 if (!pendingDeferred) {
135 pendingDeferred = q.defer()
136 emitter.emit('file_list_modified', pendingDeferred.promise)
139 pendingTimeout = setTimeout(resolveDeferred, batchInterval)
142 var clearPendingTimeout = function () {
143 if (pendingTimeout) {
144 clearTimeout(pendingTimeout)
148 // re-glob all the patterns
149 this.refresh = function () {
150 // TODO(vojta): cancel refresh if another refresh starts
151 var buckets = self.buckets = new Array(patterns.length)
153 var complete = function () {
154 if (buckets !== self.buckets) {
158 var files = resolveFiles(buckets)
160 resolveDeferred(files)
161 log.debug('Resolved files:\n\t' + files.served.join('\n\t'))
164 // TODO(vojta): use some async helper library for this
166 var finish = function () {
176 if (!pendingDeferred) {
177 pendingDeferred = q.defer()
178 emitter.emit('file_list_modified', pendingDeferred.promise)
181 clearPendingTimeout()
183 patterns.forEach(function (patternObject, i) {
184 var pattern = patternObject.pattern
186 if (helper.isUrlAbsolute(pattern)) {
187 buckets[i] = [new Url(pattern)]
192 glob(pattern, GLOB_OPTS, function (err, resolvedFiles) {
193 if (err) log.warn(err)
195 var matchedAndNotIgnored = 0
199 if (!resolvedFiles.length) {
200 log.warn('Pattern "%s" does not match any file.', pattern)
204 // stat each file to get mtime and isDirectory
205 resolvedFiles.forEach(function (path) {
206 var matchExclude = function (excludePattern) {
207 return mm(path, excludePattern)
210 if (excludes.some(matchExclude)) {
211 log.debug('Excluded file "%s"', path)
216 matchedAndNotIgnored++
217 fs.stat(path, function (error, stat) {
219 log.debug('An error occured while reading "%s"', path)
222 if (!stat.isDirectory()) {
223 // TODO(vojta): reuse file objects
224 var file = new File(path, stat.mtime)
226 preprocess(file, function (err) {
227 buckets[i].push(file)
236 log.debug('Ignored directory "%s"', path)
243 if (!matchedAndNotIgnored) {
244 log.warn('All files matched by "%s" were excluded.', pattern)
252 process.nextTick(complete)
255 return pendingDeferred.promise
258 // set new patterns and excludes
260 this.reload = function (newPatterns, newExcludes) {
261 patterns = newPatterns
262 excludes = newExcludes
264 return this.refresh()
268 * Adds a new file into the list (called by watcher)
269 * - ignore excluded files
270 * - ignore files that are already in the list
271 * - get mtime (by stat)
272 * - fires "file_list_modified"
274 this.addFile = function (path, done) {
275 var buckets = this.buckets
278 // sorry, this callback is just for easier testing
279 done = done || function () {}
282 for (i = 0; i < excludes.length; i++) {
283 if (mm(path, excludes[i])) {
284 log.debug('Add file "%s" ignored. Excluded by "%s".', path, excludes[i])
289 for (i = 0; i < patterns.length; i++) {
290 if (mm(path, patterns[i].pattern)) {
291 for (j = 0; j < buckets[i].length; j++) {
292 if (buckets[i][j].originalPath === path) {
293 log.debug('Add file "%s" ignored. Already in the list.', path)
302 if (i >= patterns.length) {
303 log.debug('Add file "%s" ignored. Does not match any pattern.', path)
307 var addedFile = new File(path)
308 buckets[i].push(addedFile)
310 clearPendingTimeout()
312 return fs.stat(path, function (err, stat) {
313 if (err) log.warn(err)
315 // in the case someone refresh() the list before stat callback
316 if (self.buckets === buckets) {
317 addedFile.mtime = stat.mtime
319 return preprocess(addedFile, function (err) {
320 // TODO(vojta): ignore if refresh/reload happens
321 log.info('Added file "%s".', path)
337 * Update mtime of a file (called by watcher)
338 * - ignore if file is not in the list
339 * - ignore if mtime has not changed
340 * - fire "file_list_modified"
342 this.changeFile = function (path, done) {
343 var buckets = this.buckets
345 // sorry, this callback is just for easier testing
346 done = done || function () {}
348 var indices = findBucketByPath(buckets, path) || []
353 if (i === undefined || !buckets[i]) {
354 log.debug('Changed file "%s" ignored. Does not match any file in the list.', path)
358 var changedFile = buckets[i][j]
359 return fs.stat(path, function (err, stat) {
360 // https://github.com/paulmillr/chokidar/issues/11
362 return self.removeFile(path, done)
365 if (self.buckets === buckets && stat.mtime > changedFile.mtime) {
366 log.info('Changed file "%s".', path)
367 changedFile.mtime = stat.mtime
368 // TODO(vojta): THIS CAN MAKE FILES INCONSISTENT
369 // if batched change is resolved before preprocessing is finished, the file can be in
370 // inconsistent state, when the promise is resolved.
372 // 1/ the preprocessor should not change the object in place, but create a copy that would
373 // be eventually merged into the original file, here in the callback, synchronously.
374 // 2/ delay the promise resolution - wait for any changeFile operations to finish
375 return preprocess(changedFile, function (err) {
382 // TODO(vojta): ignore if refresh/reload happens
393 * Remove a file from the list (called by watcher)
394 * - ignore if file is not in the list
395 * - fire "file_list_modified"
397 this.removeFile = function (path, done) {
398 var buckets = this.buckets
400 // sorry, this callback is just for easier testing
401 done = done || function () {}
403 for (var i = 0; i < buckets.length; i++) {
404 for (var j = 0; j < buckets[i].length; j++) {
405 if (buckets[i][j].originalPath === path) {
406 buckets[i].splice(j, 1)
407 log.info('Removed file "%s".', path)
415 log.debug('Removed file "%s" ignored. Does not match any file in the list.', path)
419 List.$inject = ['config.files', 'config.exclude', 'emitter', 'preprocess',
420 'config.autoWatchBatchDelay']