2 var EventEmitter = require('events').EventEmitter;
3 var fs = require('fs');
4 var sysPath = require('path');
5 var asyncEach = require('async-each');
6 var anymatch = require('anymatch');
7 var globParent = require('glob-parent');
8 var isGlob = require('is-glob');
9 var isAbsolute = require('path-is-absolute');
10 var inherits = require('inherits');
12 var NodeFsHandler = require('./lib/nodefs-handler');
13 var FsEventsHandler = require('./lib/fsevents-handler');
15 var arrify = function(value) {
16 if (value == null) return [];
17 return Array.isArray(value) ? value : [value];
20 var flatten = function(list, result) {
21 if (result == null) result = [];
22 list.forEach(function(item) {
23 if (Array.isArray(item)) {
24 flatten(item, result);
32 // Little isString util for use in Array#every.
33 var isString = function(thing) {
34 return typeof thing === 'string';
37 // Public: Main class.
38 // Watches files & directories for changes.
40 // * _opts - object, chokidar options hash
43 // `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error`
47 // var watcher = new FSWatcher()
49 // .on('add', path => console.log('File', path, 'was added'))
50 // .on('change', path => console.log('File', path, 'was changed'))
51 // .on('unlink', path => console.log('File', path, 'was removed'))
52 // .on('all', (event, path) => console.log(path, ' emitted ', event))
54 function FSWatcher(_opts) {
55 EventEmitter.call(this);
57 // in case _opts that is passed in is a frozen object
58 if (_opts) for (var opt in _opts) opts[opt] = _opts[opt];
59 this._watched = Object.create(null);
60 this._closers = Object.create(null);
61 this._ignoredPaths = Object.create(null);
62 Object.defineProperty(this, '_globIgnored', {
63 get: function() { return Object.keys(this._ignoredPaths); }
66 this._throttled = Object.create(null);
67 this._symlinkPaths = Object.create(null);
70 return opts[key] === undefined;
73 // Set up default options.
74 if (undef('persistent')) opts.persistent = true;
75 if (undef('ignoreInitial')) opts.ignoreInitial = false;
76 if (undef('ignorePermissionErrors')) opts.ignorePermissionErrors = false;
77 if (undef('interval')) opts.interval = 100;
78 if (undef('binaryInterval')) opts.binaryInterval = 300;
79 this.enableBinaryInterval = opts.binaryInterval !== opts.interval;
81 // Enable fsevents on OS X when polling isn't explicitly enabled.
82 if (undef('useFsEvents')) opts.useFsEvents = !opts.usePolling;
84 // If we can't use fsevents, ensure the options reflect it's disabled.
85 if (!FsEventsHandler.canUse()) opts.useFsEvents = false;
87 // Use polling on Mac if not using fsevents.
88 // Other platforms use non-polling fs.watch.
89 if (undef('usePolling') && !opts.useFsEvents) {
90 opts.usePolling = process.platform === 'darwin';
93 // Global override (useful for end-developers that need to force polling for all
94 // instances of chokidar, regardless of usage/dependency depth)
95 var envPoll = process.env.CHOKIDAR_USEPOLLING;
96 if (envPoll !== undefined) {
97 var envLower = envPoll.toLowerCase();
99 if (envLower === 'false' || envLower === '0') {
100 opts.usePolling = false;
101 } else if (envLower === 'true' || envLower === '1') {
102 opts.usePolling = true;
104 opts.usePolling = !!envLower
108 // Editor atomic write normalization enabled by default with fs.watch
109 if (undef('atomic')) opts.atomic = !opts.usePolling && !opts.useFsEvents;
110 if (opts.atomic) this._pendingUnlinks = Object.create(null);
112 if (undef('followSymlinks')) opts.followSymlinks = true;
114 if (undef('awaitWriteFinish')) opts.awaitWriteFinish = false;
115 if (opts.awaitWriteFinish === true) opts.awaitWriteFinish = {};
116 var awf = opts.awaitWriteFinish;
118 if (!awf.stabilityThreshold) awf.stabilityThreshold = 2000;
119 if (!awf.pollInterval) awf.pollInterval = 100;
121 this._pendingWrites = Object.create(null);
123 if (opts.ignored) opts.ignored = arrify(opts.ignored);
125 this._isntIgnored = function(path, stat) {
126 return !this._isIgnored(path, stat);
130 this._emitReady = function() {
131 if (++readyCalls >= this._readyCount) {
132 this._emitReady = Function.prototype;
133 this._readyEmitted = true;
134 // use process.nextTick to allow time for listener to be bound
135 process.nextTick(this.emit.bind(this, 'ready'));
141 // You’re frozen when your heart’s not open.
145 inherits(FSWatcher, EventEmitter);
150 // Private method: Normalize and emit events
152 // * event - string, type of event
153 // * path - string, file or directory path
154 // * val[1..3] - arguments to be passed with event
156 // Returns the error if defined, otherwise the value of the
157 // FSWatcher instance's `closed` flag
158 FSWatcher.prototype._emit = function(event, path, val1, val2, val3) {
159 if (this.options.cwd) path = sysPath.relative(this.options.cwd, path);
160 var args = [event, path];
161 if (val3 !== undefined) args.push(val1, val2, val3);
162 else if (val2 !== undefined) args.push(val1, val2);
163 else if (val1 !== undefined) args.push(val1);
165 var awf = this.options.awaitWriteFinish;
166 if (awf && this._pendingWrites[path]) {
167 this._pendingWrites[path].lastChange = new Date();
171 if (this.options.atomic) {
172 if (event === 'unlink') {
173 this._pendingUnlinks[path] = args;
174 setTimeout(function() {
175 Object.keys(this._pendingUnlinks).forEach(function(path) {
176 this.emit.apply(this, this._pendingUnlinks[path]);
177 this.emit.apply(this, ['all'].concat(this._pendingUnlinks[path]));
178 delete this._pendingUnlinks[path];
180 }.bind(this), typeof this.options.atomic === "number"
181 ? this.options.atomic
184 } else if (event === 'add' && this._pendingUnlinks[path]) {
185 event = args[0] = 'change';
186 delete this._pendingUnlinks[path];
190 var emitEvent = function() {
191 this.emit.apply(this, args);
192 if (event !== 'error') this.emit.apply(this, ['all'].concat(args));
195 if (awf && (event === 'add' || event === 'change') && this._readyEmitted) {
196 var awfEmit = function(err, stats) {
198 event = args[0] = 'error';
202 // if stats doesn't exist the file must have been deleted
203 if (args.length > 2) {
212 this._awaitWriteFinish(path, awf.stabilityThreshold, event, awfEmit);
216 if (event === 'change') {
217 if (!this._throttle('change', path, 50)) return this;
221 this.options.alwaysStat && val1 === undefined &&
222 (event === 'add' || event === 'addDir' || event === 'change')
224 var fullPath = this.options.cwd ? sysPath.join(this.options.cwd, path) : path;
225 fs.stat(fullPath, function(error, stats) {
226 // Suppress event when fs.stat fails, to avoid sending undefined 'stat'
227 if (error || !stats) return;
239 // Private method: Common handler for errors
241 // * error - object, Error instance
243 // Returns the error if defined, otherwise the value of the
244 // FSWatcher instance's `closed` flag
245 FSWatcher.prototype._handleError = function(error) {
246 var code = error && error.code;
247 var ipe = this.options.ignorePermissionErrors;
250 code !== 'ENOTDIR' &&
251 (!ipe || (code !== 'EPERM' && code !== 'EACCES'))
252 ) this.emit('error', error);
253 return error || this.closed;
256 // Private method: Helper utility for throttling
258 // * action - string, type of action being throttled
259 // * path - string, path being acted upon
260 // * timeout - int, duration of time to suppress duplicate actions
262 // Returns throttle tracking object or false if action should be suppressed
263 FSWatcher.prototype._throttle = function(action, path, timeout) {
264 if (!(action in this._throttled)) {
265 this._throttled[action] = Object.create(null);
267 var throttled = this._throttled[action];
268 if (path in throttled) return false;
270 delete throttled[path];
271 clearTimeout(timeoutObject);
273 var timeoutObject = setTimeout(clear, timeout);
274 throttled[path] = {timeoutObject: timeoutObject, clear: clear};
275 return throttled[path];
278 // Private method: Awaits write operation to finish
280 // * path - string, path being acted upon
281 // * threshold - int, time in milliseconds a file size must be fixed before
282 // acknowledgeing write operation is finished
283 // * awfEmit - function, to be called when ready for event to be emitted
284 // Polls a newly created file for size variations. When files size does not
285 // change for 'threshold' milliseconds calls callback.
286 FSWatcher.prototype._awaitWriteFinish = function(path, threshold, event, awfEmit) {
290 if (this.options.cwd && !isAbsolute(path)) {
291 fullPath = sysPath.join(this.options.cwd, path);
294 var now = new Date();
296 var awaitWriteFinish = (function (prevStat) {
297 fs.stat(fullPath, function(err, curStat) {
299 if (err.code !== 'ENOENT') awfEmit(err);
303 var now = new Date();
305 if (prevStat && curStat.size != prevStat.size) {
306 this._pendingWrites[path].lastChange = now;
309 if (now - this._pendingWrites[path].lastChange >= threshold) {
310 delete this._pendingWrites[path];
311 awfEmit(null, curStat);
313 timeoutHandler = setTimeout(
314 awaitWriteFinish.bind(this, curStat),
315 this.options.awaitWriteFinish.pollInterval
321 if (!(path in this._pendingWrites)) {
322 this._pendingWrites[path] = {
324 cancelWait: function() {
325 delete this._pendingWrites[path];
326 clearTimeout(timeoutHandler);
330 timeoutHandler = setTimeout(
331 awaitWriteFinish.bind(this),
332 this.options.awaitWriteFinish.pollInterval
337 // Private method: Determines whether user has asked to ignore this path
339 // * path - string, path to file or directory
340 // * stats - object, result of fs.stat
343 var dotRe = /\..*\.(sw[px])$|\~$|\.subl.*\.tmp/;
344 FSWatcher.prototype._isIgnored = function(path, stats) {
345 if (this.options.atomic && dotRe.test(path)) return true;
347 if (!this._userIgnored) {
348 var cwd = this.options.cwd;
349 var ignored = this.options.ignored;
350 if (cwd && ignored) {
351 ignored = ignored.map(function (path) {
352 if (typeof path !== 'string') return path;
353 return isAbsolute(path) ? path : sysPath.join(cwd, path);
356 var paths = arrify(ignored)
357 .filter(function(path) {
358 return typeof path === 'string' && !isGlob(path);
359 }).map(function(path) {
362 this._userIgnored = anymatch(
363 this._globIgnored.concat(ignored).concat(paths)
367 return this._userIgnored([path, stats]);
370 // Private method: Provides a set of common helpers and properties relating to
371 // symlink and glob handling
373 // * path - string, file, directory, or glob pattern being watched
374 // * depth - int, at any depth > 0, this isn't a glob
376 // Returns object containing helpers for this path
377 var replacerRe = /^\.[\/\\]/;
378 FSWatcher.prototype._getWatchHelpers = function(path, depth) {
379 path = path.replace(replacerRe, '');
380 var watchPath = depth || !isGlob(path) ? path : globParent(path);
381 var fullWatchPath = sysPath.resolve(watchPath);
382 var hasGlob = watchPath !== path;
383 var globFilter = hasGlob ? anymatch(path) : false;
384 var follow = this.options.followSymlinks;
385 var globSymlink = hasGlob && follow ? null : false;
387 var checkGlobSymlink = function(entry) {
388 // only need to resolve once
389 // first entry should always have entry.parentDir === ''
390 if (globSymlink == null) {
391 globSymlink = entry.fullParentDir === fullWatchPath ? false : {
392 realPath: entry.fullParentDir,
393 linkPath: fullWatchPath
398 return entry.fullPath.replace(globSymlink.realPath, globSymlink.linkPath);
401 return entry.fullPath;
404 var entryPath = function(entry) {
405 return sysPath.join(watchPath,
406 sysPath.relative(watchPath, checkGlobSymlink(entry))
410 var filterPath = function(entry) {
411 if (entry.stat && entry.stat.isSymbolicLink()) return filterDir(entry);
412 var resolvedPath = entryPath(entry);
413 return (!hasGlob || globFilter(resolvedPath)) &&
414 this._isntIgnored(resolvedPath, entry.stat) &&
415 (this.options.ignorePermissionErrors ||
416 this._hasReadPermissions(entry.stat));
419 var getDirParts = function(path) {
420 if (!hasGlob) return false;
421 var parts = sysPath.relative(watchPath, path).split(/[\/\\]/);
425 var dirParts = getDirParts(path);
426 if (dirParts && dirParts.length > 1) dirParts.pop();
429 var filterDir = function(entry) {
431 var entryParts = getDirParts(checkGlobSymlink(entry));
432 var globstar = false;
433 unmatchedGlob = !dirParts.every(function(part, i) {
434 if (part === '**') globstar = true;
435 return globstar || !entryParts[i] || anymatch(part, entryParts[i]);
438 return !unmatchedGlob && this._isntIgnored(entryPath(entry), entry.stat);
442 followSymlinks: follow,
443 statMethod: follow ? 'stat' : 'lstat',
445 watchPath: watchPath,
446 entryPath: entryPath,
448 globFilter: globFilter,
449 filterPath: filterPath,
457 // Private method: Provides directory tracking objects
459 // * directory - string, path of the directory
461 // Returns the directory's tracking object
462 FSWatcher.prototype._getWatchedDir = function(directory) {
463 var dir = sysPath.resolve(directory);
464 var watcherRemove = this._remove.bind(this);
465 if (!(dir in this._watched)) this._watched[dir] = {
466 _items: Object.create(null),
467 add: function(item) {
468 if (item !== '.') this._items[item] = true;
470 remove: function(item) {
471 delete this._items[item];
472 if (!this.children().length) {
473 fs.readdir(dir, function(err) {
474 if (err) watcherRemove(sysPath.dirname(dir), sysPath.basename(dir));
478 has: function(item) {return item in this._items;},
479 children: function() {return Object.keys(this._items);}
481 return this._watched[dir];
487 // Private method: Check for read permissions
488 // Based on this answer on SO: http://stackoverflow.com/a/11781404/1358405
490 // * stats - object, result of fs.stat
493 FSWatcher.prototype._hasReadPermissions = function(stats) {
494 return Boolean(4 & parseInt(((stats && stats.mode) & 0x1ff).toString(8)[0], 10));
497 // Private method: Handles emitting unlink events for
498 // files and directories, and via recursion, for
499 // files and directories within directories that are unlinked
501 // * directory - string, directory within which the following item is located
502 // * item - string, base path of item/directory
505 FSWatcher.prototype._remove = function(directory, item) {
506 // if what is being deleted is a directory, get that directory's paths
507 // for recursive deleting and cleaning of watched object
508 // if it is not a directory, nestedDirectoryChildren will be empty array
509 var path = sysPath.join(directory, item);
510 var fullPath = sysPath.resolve(path);
511 var isDirectory = this._watched[path] || this._watched[fullPath];
513 // prevent duplicate handling in case of arriving here nearly simultaneously
514 // via multiple paths (such as _handleFile and _handleDir)
515 if (!this._throttle('remove', path, 100)) return;
517 // if the only watched file is removed, watch for its return
518 var watchedDirs = Object.keys(this._watched);
519 if (!isDirectory && !this.options.useFsEvents && watchedDirs.length === 1) {
520 this.add(directory, item, true);
523 // This will create a new entry in the watched object in either case
524 // so we got to do the directory check beforehand
525 var nestedDirectoryChildren = this._getWatchedDir(path).children();
527 // Recursively remove children directories / files.
528 nestedDirectoryChildren.forEach(function(nestedItem) {
529 this._remove(path, nestedItem);
532 // Check if item was on the watched list and remove it
533 var parent = this._getWatchedDir(directory);
534 var wasTracked = parent.has(item);
537 // If we wait for this file to be fully written, cancel the wait.
539 if (this.options.cwd) relPath = sysPath.relative(this.options.cwd, path);
540 if (this.options.awaitWriteFinish && this._pendingWrites[relPath]) {
541 var event = this._pendingWrites[relPath].cancelWait();
542 if (event === 'add') return;
545 // The Entry will either be a directory that just got removed
546 // or a bogus entry to a file, in either case we have to remove it
547 delete this._watched[path];
548 delete this._watched[fullPath];
549 var eventName = isDirectory ? 'unlinkDir' : 'unlink';
550 if (wasTracked && !this._isIgnored(path)) this._emit(eventName, path);
552 // Avoid conflicts if we later create another file with the same name
553 if (!this.options.useFsEvents) {
554 this._closePath(path);
558 FSWatcher.prototype._closePath = function(path) {
559 if (!this._closers[path]) return;
560 this._closers[path]();
561 delete this._closers[path];
562 this._getWatchedDir(sysPath.dirname(path)).remove(sysPath.basename(path));
565 // Public method: Adds paths to be watched on an existing FSWatcher instance
567 // * paths - string or array of strings, file/directory paths and/or globs
568 // * _origAdd - private boolean, for handling non-existent paths to be watched
569 // * _internal - private boolean, indicates a non-user add
571 // Returns an instance of FSWatcher for chaining.
572 FSWatcher.prototype.add = function(paths, _origAdd, _internal) {
573 var cwd = this.options.cwd;
575 paths = flatten(arrify(paths));
577 if (!paths.every(isString)) {
578 throw new TypeError('Non-string provided as watch path: ' + paths);
581 if (cwd) paths = paths.map(function(path) {
582 if (isAbsolute(path)) {
584 } else if (path[0] === '!') {
585 return '!' + sysPath.join(cwd, path.substring(1));
587 return sysPath.join(cwd, path);
591 // set aside negated glob strings
592 paths = paths.filter(function(path) {
593 if (path[0] === '!') {
594 this._ignoredPaths[path.substring(1)] = true;
596 // if a path is being added that was previously ignored, stop ignoring it
597 delete this._ignoredPaths[path];
598 delete this._ignoredPaths[path + '/**'];
600 // reset the cached userIgnored anymatch fn
601 // to make ignoredPaths changes effective
602 this._userIgnored = null;
608 if (this.options.useFsEvents && FsEventsHandler.canUse()) {
609 if (!this._readyCount) this._readyCount = paths.length;
610 if (this.options.persistent) this._readyCount *= 2;
611 paths.forEach(this._addToFsEvents, this);
613 if (!this._readyCount) this._readyCount = 0;
614 this._readyCount += paths.length;
615 asyncEach(paths, function(path, next) {
616 this._addToNodeFs(path, !_internal, 0, 0, _origAdd, function(err, res) {
617 if (res) this._emitReady();
620 }.bind(this), function(error, results) {
621 results.forEach(function(item) {
623 this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item));
631 // Public method: Close watchers or start ignoring events from specified paths.
633 // * paths - string or array of strings, file/directory paths and/or globs
635 // Returns instance of FSWatcher for chaining.
636 FSWatcher.prototype.unwatch = function(paths) {
637 if (this.closed) return this;
638 paths = flatten(arrify(paths));
640 paths.forEach(function(path) {
641 // convert to absolute path unless relative path already matches
642 if (!isAbsolute(path) && !this._closers[path]) {
643 if (this.options.cwd) path = sysPath.join(this.options.cwd, path);
644 path = sysPath.resolve(path);
647 this._closePath(path);
649 this._ignoredPaths[path] = true;
650 if (path in this._watched) {
651 this._ignoredPaths[path + '/**'] = true;
654 // reset the cached userIgnored anymatch fn
655 // to make ignoredPaths changes effective
656 this._userIgnored = null;
662 // Public method: Close watchers and remove all listeners from watched paths.
664 // Returns instance of FSWatcher for chaining.
665 FSWatcher.prototype.close = function() {
666 if (this.closed) return this;
669 Object.keys(this._closers).forEach(function(watchPath) {
670 this._closers[watchPath]();
671 delete this._closers[watchPath];
673 this._watched = Object.create(null);
675 this.removeAllListeners();
679 // Public method: Expose list of watched paths
681 // Returns object w/ dir paths as keys and arrays of contained paths as values.
682 FSWatcher.prototype.getWatched = function() {
684 Object.keys(this._watched).forEach(function(dir) {
685 var key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir;
686 watchList[key || '.'] = Object.keys(this._watched[dir]._items).sort();
691 // Attach watch handler prototype methods
692 function importHandler(handler) {
693 Object.keys(handler.prototype).forEach(function(method) {
694 FSWatcher.prototype[method] = handler.prototype[method];
697 importHandler(NodeFsHandler);
698 if (FsEventsHandler.canUse()) importHandler(FsEventsHandler);
700 // Export FSWatcher class
701 exports.FSWatcher = FSWatcher;
703 // Public function: Instantiates watcher with paths to be tracked.
705 // * paths - string or array of strings, file/directory paths and/or globs
706 // * options - object, chokidar options
708 // Returns an instance of FSWatcher for chaining.
709 exports.watch = function(paths, options) {
710 return new FSWatcher(options).add(paths);