forked from paulmillr/chokidar
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
440 lines (377 loc) · 14 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
'use strict';
var EventEmitter = require('events').EventEmitter;
var fs = require('fs');
var os = require('os');
var sysPath = require('path');
var fsevents, recursiveReaddir;
try {
fsevents = require('fsevents');
recursiveReaddir = require('recursive-readdir');
} catch (error) {}
var isWindows = os.platform() === 'win32';
var canUseFsEvents = os.platform() === 'darwin' && fsevents;
// To disable FSEvents completely.
// var canUseFsEvents = false;
// Binary file handling code.
var _binExts = ['adp', 'au', 'mid', 'mp4a', 'mpga', 'oga', 's3m', 'sil', 'eol', 'dra', 'dts', 'dtshd', 'lvp', 'pya', 'ecelp4800', 'ecelp7470', 'ecelp9600', 'rip', 'weba', 'aac', 'aif', 'caf', 'flac', 'mka', 'm3u', 'wax', 'wma', 'wav', 'xm', 'flac', '3gp', '3g2', 'h261', 'h263', 'h264', 'jpgv', 'jpm', 'mj2', 'mp4', 'mpeg', 'ogv', 'qt', 'uvh', 'uvm', 'uvp', 'uvs', 'dvb', 'fvt', 'mxu', 'pyv', 'uvu', 'viv', 'webm', 'f4v', 'fli', 'flv', 'm4v', 'mkv', 'mng', 'asf', 'vob', 'wm', 'wmv', 'wmx', 'wvx', 'movie', 'smv', 'ts', 'bmp', 'cgm', 'g3', 'gif', 'ief', 'jpg', 'jpeg', 'ktx', 'png', 'btif', 'sgi', 'svg', 'tiff', 'psd', 'uvi', 'sub', 'djvu', 'dwg', 'dxf', 'fbs', 'fpx', 'fst', 'mmr', 'rlc', 'mdi', 'wdp', 'npx', 'wbmp', 'xif', 'webp', '3ds', 'ras', 'cmx', 'fh', 'ico', 'pcx', 'pic', 'pnm', 'pbm', 'pgm', 'ppm', 'rgb', 'tga', 'xbm', 'xpm', 'xwd', 'zip', 'rar', 'tar', 'bz2', 'eot', 'ttf', 'woff'];
var binExts = Object.create(null);
_binExts.forEach(function(extension) {
return binExts[extension] = true;
});
var isBinary = function(extension) {
if (extension === '') return false;
return !!binExts[extension];
}
var isBinaryPath = function(path) {
return isBinary(sysPath.extname(path).slice(1));
};
exports.isBinaryPath = isBinaryPath;
// Main code.
//
// Watches files & directories for changes.
//
// Emitted events: `add`, `change`, `unlink`, `error`.
//
// Examples
//
// var watcher = new FSWatcher()
// .add(directories)
// .on('add', function(path) {console.log('File', path, 'was added');})
// .on('change', function(path) {console.log('File', path, 'was changed');})
// .on('unlink', function(path) {console.log('File', path, 'was removed');})
//
function FSWatcher(_opts) {
if (_opts == null) _opts = {};
var opts = {};
for (var opt in _opts) opts[opt] = _opts[opt]
this.close = this.close.bind(this);
EventEmitter.call(this);
this.watched = Object.create(null);
this.watchers = [];
// Set up default options.
if (opts.persistent == null) opts.persistent = false;
if (opts.ignoreInitial == null) opts.ignoreInitial = false;
if (opts.interval == null) opts.interval = 100;
if (opts.binaryInterval == null) opts.binaryInterval = 300;
if (opts.usePolling == null) opts.usePolling = !isWindows;
if (opts.useFsEvents == null) {
opts.useFsEvents = !opts.usePolling && canUseFsEvents;
} else {
if (!canUseFsEvents) opts.useFsEvents = false;
}
if (opts.ignorePermissionErrors == null) {
opts.ignorePermissionErrors = false;
}
this.enableBinaryInterval = opts.binaryInterval !== opts.interval;
this._isIgnored = (function(ignored) {
switch (toString.call(ignored)) {
case '[object RegExp]':
return function(string) {
return ignored.test(string);
};
case '[object Function]':
return ignored;
default:
return function() {
return false;
};
}
})(opts.ignored);
this.options = opts;
// You’re frozen when your heart’s not open.
Object.freeze(opts);
}
FSWatcher.prototype = Object.create(EventEmitter.prototype);
// Directory helpers
// -----------------
var directoryEndRe = /[\\\/]$/;
FSWatcher.prototype._getWatchedDir = function(directory) {
var _base;
var dir = directory.replace(directoryEndRe, '');
return (_base = this.watched)[dir] != null ? (_base = this.watched)[dir] : _base[dir] = [];
};
FSWatcher.prototype._addToWatchedDir = function(directory, basename) {
var watchedFiles = this._getWatchedDir(directory);
return watchedFiles.push(basename);
};
FSWatcher.prototype._removeFromWatchedDir = function(directory, file) {
var watchedFiles = this._getWatchedDir(directory);
return watchedFiles.some(function(watchedFile, index) {
if (watchedFile === file) {
watchedFiles.splice(index, 1);
return true;
}
});
};
// File helpers
// ------------
// Private: Check for read permissions
// Based on this answer on SO: http://stackoverflow.com/a/11781404/1358405
//
// stats - fs.Stats object
//
// Returns Boolean
FSWatcher.prototype._hasReadPermissions = function(stats) {
return Boolean(4 & parseInt((stats.mode & 0x1ff).toString(8)[0]));
};
// Private: Handles emitting unlink events for
// files and directories, and via recursion, for
// files and directories within directories that are unlinked
//
// directory - string, directory within which the following item is located
// item - string, base path of item/directory
//
// Returns nothing.
FSWatcher.prototype._remove = function(directory, item) {
// if what is being deleted is a directory, get that directory's paths
// for recursive deleting and cleaning of watched object
// if it is not a directory, nestedDirectoryChildren will be empty array
var fullPath = sysPath.join(directory, item);
var isDirectory = this.watched[fullPath];
// This will create a new entry in the watched object in either case
// so we got to do the directory check beforehand
var nestedDirectoryChildren = this._getWatchedDir(fullPath).slice();
// Remove directory / file from watched list.
this._removeFromWatchedDir(directory, item);
// Recursively remove children directories / files.
nestedDirectoryChildren.forEach(function(nestedItem) {
return this._remove(fullPath, nestedItem);
}, this);
if (this.options.usePolling) fs.unwatchFile(fullPath);
// The Entry will either be a directory that just got removed
// or a bogus entry to a file, in either case we have to remove it
delete this.watched[fullPath];
var eventName = isDirectory ? 'unlinkDir' : 'unlink';
this.emit(eventName, fullPath);
};
// FS Events helper.
var createFSEventsInstance = function(path, callback) {
var watcher = new fsevents.FSEvents(path);
watcher.on('fsevent', callback);
return watcher;
};
FSWatcher.prototype._watchWithFsEvents = function(path) {
var _this = this;
var watcher = createFSEventsInstance(path, function(path, flags) {
var emit, info;
if (_this._isIgnored(path)) {
return;
}
info = fsevents.getInfo(path, flags);
emit = function(event) {
var name;
name = info.type === 'file' ? event : "" + event + "Dir";
if (event === 'add' || event === 'addDir') {
_this._addToWatchedDir(sysPath.dirname(path), sysPath.basename(path));
} else if (event === 'unlink' || event === 'unlinkDir') {
_this._remove(sysPath.dirname(path), sysPath.basename(path));
return; // Don't emit event twice.
}
return _this.emit(name, path);
};
switch (info.event) {
case 'created':
return emit('add');
case 'modified':
return emit('change');
case 'deleted':
return emit('unlink');
case 'moved':
return fs.stat(path, function(error, stats) {
return emit((error || !stats ? 'unlink' : 'add'));
});
}
});
return this.watchers.push(watcher);
};
// Private: Watch file for changes with fs.watchFile or fs.watch.
// item - string, path to file or directory.
// callback - function that will be executed on fs change.
// Returns nothing.
FSWatcher.prototype._watch = function(item, callback) {
var basename, directory, options, parent, watcher;
if (callback == null) callback = Function.prototype; // empty function
directory = sysPath.dirname(item);
basename = sysPath.basename(item);
parent = this._getWatchedDir(directory);
if (parent.indexOf(basename) !== -1) return;
this._addToWatchedDir(directory, basename);
options = {persistent: this.options.persistent};
if (this.options.usePolling) {
options.interval = this.enableBinaryInterval && isBinaryPath(basename) ?
this.options.binaryInterval : this.options.interval;
fs.watchFile(item, options, function(curr, prev) {
if (curr.mtime.getTime() > prev.mtime.getTime()) {
callback(item, curr);
}
});
} else {
watcher = fs.watch(item, options, function(event, path) {
callback(item);
});
this.watchers.push(watcher);
}
};
// Workaround for the "Windows rough edge" regarding the deletion of directories
// (https://github.com/joyent/node/issues/4337)
FSWatcher.prototype._emitError = function(error) {
var emit = (function() {
this.emit('error', error);
}).bind(this);
if (isWindows && error.code === 'EPERM') {
fs.exists(item, function(exists) {
if (exists) emit();
});
} else {
emit();
}
};
// Private: Emit `change` event once and watch file to emit it in the future
// once the file is changed.
// file - string, fs path.
// stats - object, result of executing stat(1) on file.
// initialAdd - boolean, was the file added at the launch?
// Returns nothing.
FSWatcher.prototype._handleFile = function(file, stats, initialAdd) {
var _this = this;
if (initialAdd == null) initialAdd = false;
this._watch(file, function(file, newStats) {
return _this.emit('change', file, newStats);
});
if (!(initialAdd && this.options.ignoreInitial)) {
return this.emit('add', file, stats);
}
};
// Private: Read directory to add / remove files from `@watched` list
// and re-read it on change.
// directory - string, fs path.
// Returns nothing.
FSWatcher.prototype._handleDir = function(directory, stats, initialAdd) {
var _this = this;
var read = function(directory, initialAdd) {
return fs.readdir(directory, function(error, current) {
if (error != null) return _this._emitError(error);
if (!current) return;
var previous = _this._getWatchedDir(directory);
// Files that absent in current directory snapshot
// but present in previous emit `remove` event
// and are removed from @watched[directory].
previous.filter(function(file) {
return current.indexOf(file) === -1;
}).forEach(function(file) {
return _this._remove(directory, file);
});
// Files that present in current directory snapshot
// but absent in previous are added to watch list and
// emit `add` event.
current.filter(function(file) {
return previous.indexOf(file) === -1;
}).forEach(function(file) {
_this._handle(sysPath.join(directory, file), initialAdd);
});
});
};
read(directory, initialAdd);
this._watch(directory, function(dir) {
return read(dir, false);
});
if (!(initialAdd && this.options.ignoreInitial)) {
return this.emit('addDir', directory, stats);
}
};
// Private: Handle added file or directory.
// Delegates call to _handleFile / _handleDir after checks.
// item - string, path to file or directory.
// Returns nothing.
FSWatcher.prototype._handle = function(item, initialAdd) {
var _this = this;
if (this._isIgnored(item)) return;
return fs.realpath(item, function(error, path) {
if (error && error.code === 'ENOENT') return;
if (error != null) return _this._emitError(error);
fs.stat(path, function(error, stats) {
if (error != null) return _this._emitError(error);
if (_this.options.ignorePermissionErrors && (!_this._hasReadPermissions(stats))) {
return;
}
if (_this._isIgnored.length === 2 && _this._isIgnored(item, stats)) {
return;
}
if (stats.isFile()) _this._handleFile(item, stats, initialAdd);
if (stats.isDirectory()) _this._handleDir(item, stats, initialAdd);
});
});
};
FSWatcher.prototype.emit = function(event, arg1) {
var data = arguments.length === 2 ? [arg1] : [].slice.call(arguments, 1);
var args = [event].concat(data);
EventEmitter.prototype.emit.apply(this, args);
if (event === 'add' || event === 'addDir' || event === 'change' ||
event === 'unlink' || event === 'unlinkDir') {
EventEmitter.prototype.emit.apply(this, ['all'].concat(args));
}
};
FSWatcher.prototype._addToFsEvents = function(files) {
var _this = this;
var handle = function(path) {
return _this.emit('add', path);
};
files.forEach(function(file) {
if (!_this.options.ignoreInitial) {
fs.stat(file, function(error, stats) {
if (error != null) return _this._emitError(error);
if (stats.isDirectory()) {
recursiveReaddir(file, function(error, dirFiles) {
if (error != null) return _this._emitError(error);
dirFiles.filter(function(path) {
return !_this._isIgnored(path);
}).forEach(handle);
});
} else {
handle(file);
}
});
}
_this._watchWithFsEvents(file);
});
return this;
};
// Public: Adds directories / files for tracking.
// * files - array of strings (file paths).
// Examples
// add ['app', 'vendor']
// Returns an instance of FSWatcher for chaning.
FSWatcher.prototype.add = function(files) {
if (this._initialAdd == null) this._initialAdd = true;
if (!Array.isArray(files)) files = [files];
if (this.options.useFsEvents) return this._addToFsEvents(files);
files.forEach(function(file) {
return this._handle(file, this._initialAdd);
}, this);
this._initialAdd = false;
return this;
};
// Public: Remove all listeners from watched files.
// Returns an instance of FSWatcher for chaning.
FSWatcher.prototype.close = function() {
var useFsEvents = this.options.useFsEvents;
var method = useFsEvents ? 'stop' : 'close';
this.watchers.forEach(function(watcher) {
watcher[method]();
});
if (this.options.usePolling) {
var watched = this.watched;
Object.keys(watched).forEach(function(directory) {
return watched[directory].forEach(function(file) {
return fs.unwatchFile(sysPath.join(directory, file));
});
});
}
this.watched = Object.create(null);
this.removeAllListeners();
return this;
};
exports.FSWatcher = FSWatcher;
exports.watch = function(files, options) {
return new FSWatcher(options).add(files);
};