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