1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | 'use strict';
|
10 |
|
11 |
|
12 | var util = require('util');
|
13 | var EE = require('events').EventEmitter;
|
14 | var fs = require('fs');
|
15 | var path = require('path');
|
16 | var globule = require('globule');
|
17 | var helper = require('./helper');
|
18 |
|
19 |
|
20 | var setImmediate = require('timers').setImmediate;
|
21 | if (typeof setImmediate !== 'function') {
|
22 | setImmediate = process.nextTick;
|
23 | }
|
24 |
|
25 |
|
26 | var delay = 10;
|
27 |
|
28 |
|
29 | function Gaze (patterns, opts, done) {
|
30 | var self = this;
|
31 | EE.call(self);
|
32 |
|
33 |
|
34 | if (typeof opts === 'function') {
|
35 | done = opts;
|
36 | opts = {};
|
37 | }
|
38 |
|
39 |
|
40 | opts = opts || {};
|
41 | opts.mark = true;
|
42 | opts.interval = opts.interval || 100;
|
43 | opts.debounceDelay = opts.debounceDelay || 500;
|
44 | opts.cwd = opts.cwd || process.cwd();
|
45 | this.options = opts;
|
46 |
|
47 |
|
48 | done = done || function () {};
|
49 |
|
50 |
|
51 | this._watched = Object.create(null);
|
52 |
|
53 |
|
54 | this._watchers = Object.create(null);
|
55 |
|
56 |
|
57 | this._pollers = Object.create(null);
|
58 |
|
59 |
|
60 | this._patterns = [];
|
61 |
|
62 |
|
63 | this._cached = Object.create(null);
|
64 |
|
65 |
|
66 | if (this.options.maxListeners != null) {
|
67 | this.setMaxListeners(this.options.maxListeners);
|
68 | Gaze.super_.prototype.setMaxListeners(this.options.maxListeners);
|
69 | delete this.options.maxListeners;
|
70 | }
|
71 |
|
72 |
|
73 | if (patterns) {
|
74 | this.add(patterns, done);
|
75 | }
|
76 |
|
77 |
|
78 | this._keepalive = setInterval(function () {}, 200);
|
79 |
|
80 | return this;
|
81 | }
|
82 | util.inherits(Gaze, EE);
|
83 |
|
84 |
|
85 | module.exports = function gaze (patterns, opts, done) {
|
86 | return new Gaze(patterns, opts, done);
|
87 | };
|
88 | module.exports.Gaze = Gaze;
|
89 |
|
90 |
|
91 |
|
92 | Gaze.prototype.emit = function () {
|
93 | var self = this;
|
94 | var args = arguments;
|
95 |
|
96 | var e = args[0];
|
97 | var filepath = args[1];
|
98 | var timeoutId;
|
99 |
|
100 |
|
101 | if (e.slice(-2) !== 'ed') {
|
102 | Gaze.super_.prototype.emit.apply(self, args);
|
103 | return this;
|
104 | }
|
105 |
|
106 |
|
107 | if (e === 'added') {
|
108 | Object.keys(this._cached).forEach(function (oldFile) {
|
109 | if (self._cached[oldFile].indexOf('deleted') !== -1) {
|
110 | args[0] = e = 'renamed';
|
111 | [].push.call(args, oldFile);
|
112 | delete self._cached[oldFile];
|
113 | return false;
|
114 | }
|
115 | });
|
116 | }
|
117 |
|
118 |
|
119 |
|
120 | var cache = this._cached[filepath] || [];
|
121 | if (cache.indexOf(e) === -1) {
|
122 | helper.objectPush(self._cached, filepath, e);
|
123 | clearTimeout(timeoutId);
|
124 | timeoutId = setTimeout(function () {
|
125 | delete self._cached[filepath];
|
126 | }, this.options.debounceDelay);
|
127 |
|
128 | Gaze.super_.prototype.emit.apply(self, args);
|
129 | Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
|
130 | }
|
131 |
|
132 |
|
133 | if (e === 'added') {
|
134 | if (helper.isDir(filepath)) {
|
135 |
|
136 |
|
137 |
|
138 | var files;
|
139 |
|
140 | try {
|
141 | files = fs.readdirSync(filepath);
|
142 | } catch (e) {
|
143 |
|
144 | if (e.code !== 'ENOENT') {
|
145 | throw e;
|
146 | }
|
147 |
|
148 | files = [];
|
149 | }
|
150 |
|
151 | files.map(function (file) {
|
152 | return path.join(filepath, file);
|
153 | }).filter(function (file) {
|
154 | return globule.isMatch(self._patterns, file, self.options);
|
155 | }).forEach(function (file) {
|
156 | self.emit('added', file);
|
157 | });
|
158 | }
|
159 | }
|
160 |
|
161 | return this;
|
162 | };
|
163 |
|
164 |
|
165 | Gaze.prototype.close = function (_reset) {
|
166 | var self = this;
|
167 | Object.keys(self._watchers).forEach(function (file) {
|
168 | self._watchers[file].close();
|
169 | });
|
170 | self._watchers = Object.create(null);
|
171 | Object.keys(this._watched).forEach(function (dir) {
|
172 | self._unpollDir(dir);
|
173 | });
|
174 | if (_reset !== false) {
|
175 | self._watched = Object.create(null);
|
176 | setTimeout(function () {
|
177 | self.emit('end');
|
178 | self.removeAllListeners();
|
179 | clearInterval(self._keepalive);
|
180 | }, delay + 100);
|
181 | }
|
182 | return self;
|
183 | };
|
184 |
|
185 |
|
186 | Gaze.prototype.add = function (files, done) {
|
187 | if (typeof files === 'string') { files = [files]; }
|
188 | this._patterns = helper.unique.apply(null, [this._patterns, files]);
|
189 | files = globule.find(this._patterns, this.options);
|
190 | this._addToWatched(files);
|
191 | this.close(false);
|
192 | this._initWatched(done);
|
193 | };
|
194 |
|
195 |
|
196 | Gaze.prototype._internalAdd = function (file, done) {
|
197 | var files = [];
|
198 | if (helper.isDir(file)) {
|
199 | files = [helper.markDir(file)].concat(globule.find(this._patterns, this.options));
|
200 | } else {
|
201 | if (globule.isMatch(this._patterns, file, this.options)) {
|
202 | files = [file];
|
203 | }
|
204 | }
|
205 | if (files.length > 0) {
|
206 | this._addToWatched(files);
|
207 | this.close(false);
|
208 | this._initWatched(done);
|
209 | }
|
210 | };
|
211 |
|
212 |
|
213 | Gaze.prototype.remove = function (file) {
|
214 | var self = this;
|
215 | if (this._watched[file]) {
|
216 |
|
217 | this._unpollDir(file);
|
218 | delete this._watched[file];
|
219 | } else {
|
220 |
|
221 | Object.keys(this._watched).forEach(function (dir) {
|
222 | var index = self._watched[dir].indexOf(file);
|
223 | if (index !== -1) {
|
224 | self._unpollFile(file);
|
225 | self._watched[dir].splice(index, 1);
|
226 | return false;
|
227 | }
|
228 | });
|
229 | }
|
230 | if (this._watchers[file]) {
|
231 | this._watchers[file].close();
|
232 | }
|
233 | return this;
|
234 | };
|
235 |
|
236 |
|
237 | Gaze.prototype.watched = function () {
|
238 | return this._watched;
|
239 | };
|
240 |
|
241 |
|
242 | Gaze.prototype.relative = function (dir, unixify) {
|
243 | var self = this;
|
244 | var relative = Object.create(null);
|
245 | var relDir, relFile, unixRelDir;
|
246 | var cwd = this.options.cwd || process.cwd();
|
247 | if (dir === '') { dir = '.'; }
|
248 | dir = helper.markDir(dir);
|
249 | unixify = unixify || false;
|
250 | Object.keys(this._watched).forEach(function (dir) {
|
251 | relDir = path.relative(cwd, dir) + path.sep;
|
252 | if (relDir === path.sep) { relDir = '.'; }
|
253 | unixRelDir = unixify ? helper.unixifyPathSep(relDir) : relDir;
|
254 | relative[unixRelDir] = self._watched[dir].map(function (file) {
|
255 | relFile = path.relative(path.join(cwd, relDir) || '', file || '');
|
256 | if (helper.isDir(file)) {
|
257 | relFile = helper.markDir(relFile);
|
258 | }
|
259 | if (unixify) {
|
260 | relFile = helper.unixifyPathSep(relFile);
|
261 | }
|
262 | return relFile;
|
263 | });
|
264 | });
|
265 | if (dir && unixify) {
|
266 | dir = helper.unixifyPathSep(dir);
|
267 | }
|
268 | return dir ? relative[dir] || [] : relative;
|
269 | };
|
270 |
|
271 |
|
272 | Gaze.prototype._addToWatched = function (files) {
|
273 | var dirs = [];
|
274 |
|
275 | for (var i = 0; i < files.length; i++) {
|
276 | var file = files[i];
|
277 | var filepath = path.resolve(this.options.cwd, file);
|
278 |
|
279 | var dirname = (helper.isDir(file)) ? filepath : path.dirname(filepath);
|
280 | dirname = helper.markDir(dirname);
|
281 |
|
282 |
|
283 | if (helper.isDir(file) && !(dirname in this._watched)) {
|
284 | helper.objectPush(this._watched, dirname, []);
|
285 | }
|
286 |
|
287 | if (file.slice(-1) === '/') { filepath += path.sep; }
|
288 | helper.objectPush(this._watched, path.dirname(filepath) + path.sep, filepath);
|
289 |
|
290 | dirs.push(dirname);
|
291 | }
|
292 |
|
293 | dirs = helper.unique(dirs);
|
294 |
|
295 | for (var k = 0; k < dirs.length; k++) {
|
296 | dirname = dirs[k];
|
297 |
|
298 | var readdir = fs.readdirSync(dirname);
|
299 | for (var j = 0; j < readdir.length; j++) {
|
300 | var dirfile = path.join(dirname, readdir[j]);
|
301 | if (fs.lstatSync(dirfile).isDirectory()) {
|
302 | helper.objectPush(this._watched, dirname, dirfile + path.sep);
|
303 | }
|
304 | }
|
305 | }
|
306 |
|
307 | return this;
|
308 | };
|
309 |
|
310 | Gaze.prototype._watchDir = function (dir, done) {
|
311 | var self = this;
|
312 | var timeoutId;
|
313 | try {
|
314 | this._watchers[dir] = fs.watch(dir, function (event) {
|
315 |
|
316 |
|
317 | clearTimeout(timeoutId);
|
318 | timeoutId = setTimeout(function () {
|
319 |
|
320 |
|
321 | if ((dir in self._watchers) && fs.existsSync(dir)) {
|
322 | done(null, dir);
|
323 | }
|
324 | }, delay + 100);
|
325 | });
|
326 |
|
327 | this._watchers[dir].on('error', function (err) {
|
328 | self._handleError(err);
|
329 | });
|
330 | } catch (err) {
|
331 | return this._handleError(err);
|
332 | }
|
333 | return this;
|
334 | };
|
335 |
|
336 | Gaze.prototype._unpollFile = function (file) {
|
337 | if (this._pollers[file]) {
|
338 | fs.unwatchFile(file, this._pollers[file]);
|
339 | delete this._pollers[file];
|
340 | }
|
341 | return this;
|
342 | };
|
343 |
|
344 | Gaze.prototype._unpollDir = function (dir) {
|
345 | this._unpollFile(dir);
|
346 | for (var i = 0; i < this._watched[dir].length; i++) {
|
347 | this._unpollFile(this._watched[dir][i]);
|
348 | }
|
349 | };
|
350 |
|
351 | Gaze.prototype._pollFile = function (file, done) {
|
352 | var opts = { persistent: true, interval: this.options.interval };
|
353 | if (!this._pollers[file]) {
|
354 | this._pollers[file] = function (curr, prev) {
|
355 | done(null, file);
|
356 | };
|
357 | try {
|
358 | fs.watchFile(file, opts, this._pollers[file]);
|
359 | } catch (err) {
|
360 | return this._handleError(err);
|
361 | }
|
362 | }
|
363 | return this;
|
364 | };
|
365 |
|
366 |
|
367 | Gaze.prototype._initWatched = function (done) {
|
368 | var self = this;
|
369 | var cwd = this.options.cwd || process.cwd();
|
370 | var curWatched = Object.keys(self._watched);
|
371 |
|
372 |
|
373 | if (curWatched.length < 1) {
|
374 |
|
375 | setImmediate(function () {
|
376 | self.emit('ready', self);
|
377 | if (done) { done.call(self, null, self); }
|
378 | self.emit('nomatch');
|
379 | });
|
380 | return;
|
381 | }
|
382 |
|
383 | helper.forEachSeries(curWatched, function (dir, next) {
|
384 | dir = dir || '';
|
385 | var files = self._watched[dir];
|
386 |
|
387 | self._watchDir(dir, function (event, dirpath) {
|
388 | var relDir = cwd === dir ? '.' : path.relative(cwd, dir);
|
389 | relDir = relDir || '';
|
390 |
|
391 | fs.readdir(dirpath, function (err, current) {
|
392 | if (err) { return self.emit('error', err); }
|
393 | if (!current) { return; }
|
394 |
|
395 | try {
|
396 |
|
397 | current = current.map(function (curPath) {
|
398 | if (fs.existsSync(path.join(dir, curPath)) && fs.lstatSync(path.join(dir, curPath)).isDirectory()) {
|
399 | return curPath + path.sep;
|
400 | } else {
|
401 | return curPath;
|
402 | }
|
403 | });
|
404 | } catch (err) {
|
405 |
|
406 | }
|
407 |
|
408 |
|
409 | var previous = self.relative(relDir);
|
410 |
|
411 |
|
412 | previous.filter(function (file) {
|
413 | return current.indexOf(file) < 0;
|
414 | }).forEach(function (file) {
|
415 | if (!helper.isDir(file)) {
|
416 | var filepath = path.join(dir, file);
|
417 | self.remove(filepath);
|
418 | self.emit('deleted', filepath);
|
419 | }
|
420 | });
|
421 |
|
422 |
|
423 | current.filter(function (file) {
|
424 | return previous.indexOf(file) < 0;
|
425 | }).forEach(function (file) {
|
426 |
|
427 | var relFile = path.join(relDir, file);
|
428 |
|
429 | self._internalAdd(relFile, function () {
|
430 | self.emit('added', path.join(dir, file));
|
431 | });
|
432 | });
|
433 | });
|
434 | });
|
435 |
|
436 |
|
437 | files.forEach(function (file) {
|
438 | if (helper.isDir(file)) { return; }
|
439 | self._pollFile(file, function (err, filepath) {
|
440 | if (err) {
|
441 | self.emit('error', err);
|
442 | return;
|
443 | }
|
444 |
|
445 |
|
446 | if (fs.existsSync(filepath)) {
|
447 | self.emit('changed', filepath);
|
448 | }
|
449 | });
|
450 | });
|
451 |
|
452 | next();
|
453 | }, function () {
|
454 |
|
455 |
|
456 | setTimeout(function () {
|
457 | self.emit('ready', self);
|
458 | if (done) { done.call(self, null, self); }
|
459 | }, delay + 100);
|
460 | });
|
461 | };
|
462 |
|
463 |
|
464 | Gaze.prototype._handleError = function (err) {
|
465 | if (err.code === 'EMFILE') {
|
466 | return this.emit('error', new Error('EMFILE: Too many opened files.'));
|
467 | }
|
468 | return this.emit('error', err);
|
469 | };
|