UNPKG

5.08 kBJavaScriptView Raw
1
2var debug = require('debug')('dev-server:watch');
3var fs = require('fs');
4var path = require('path');
5
6var notify = Function.prototype;
7
8
9
10Watcher.ignoreNames = ['.git', '.svn', '_svn', '_git'];
11Watcher.DEFER_TIMEOUT = 100; // ms
12
13
14
15function Watcher(root) {
16 this.root = root;
17 this._pendingDiscovery = 0;
18 this._watch(root);
19 this._seenStat = {};
20 this._targets = [];
21 debug('Watcher started in ' + root);
22}
23
24
25Watcher.prototype._error = function(err) {
26 console.error('Watcher error: ', err);
27};
28
29
30
31Watcher.prototype._decPending = function() {
32 this._pendingDiscovery--;
33 if (this._pendingDiscovery === 0) {
34 debug('All discovered');
35 notify('Watching for changes', 'info');
36 }
37};
38
39
40
41Watcher.prototype._watch = function(name) {
42 var self = this;
43 self._pendingDiscovery++;
44 // lstat accepts symlinks not targets, this prevents deadly recursion
45 fs.lstat(name, function(err, stat) {
46 if (err) {
47 self._decPending();
48 return self._error(err);
49 }
50 var relative = path.relative(self.root, name);
51 self._seenStat[relative] = stat || null;
52 if (stat.isDirectory()) {
53 self._watchDir(name);
54 } else {
55 self._decPending();
56 }
57 });
58};
59
60
61
62Watcher.prototype._watchDir = function(root) {
63 var self = this;
64
65 fs.watch(root, function(ev, filename) {
66 debug('fs.watch', arguments);
67 self._change(root, filename, ev);
68 });
69
70 fs.readdir(root, function(err, files) {
71 if (err) {
72 self._decPending();
73 return self._error(err);
74 }
75
76 files.forEach(function(name) {
77 if (Watcher.ignoreNames.indexOf(name) !== -1)
78 return;
79
80 var fullname = path.join(root, name);
81 self._watch(fullname);
82 });
83 self._decPending();
84 });
85};
86
87
88Watcher._clearDeferTimeout = function() {
89 Watcher._defered = {
90 timeout: null,
91 all: []
92 };
93};
94Watcher._clearDeferTimeout();
95
96
97
98Watcher.prototype._defer = function(method, relative, stat, oldStat) {
99 var data = Watcher._defered;
100 if (data.timeout) clearTimeout(data.timeout);
101 data.all.push({
102 method: method,
103 path: relative,
104 stat: stat,
105 oldStat: oldStat
106 });
107 data.timeout = setTimeout(
108 this._deferTimeout.bind(this), Watcher.DEFER_TIMEOUT);
109};
110
111
112
113Watcher.prototype._deferTimeout = function() {
114 var data = Watcher._defered;
115 var self = this;
116 var byInode = {};
117
118 data.all.forEach(function(item) {
119 if (item.stat && item.stat.ino) {
120 var key = item.stat.dev + ':' + item.stat.ino;
121 if (byInode[key]) {
122 var item2 = byInode[key];
123
124 if (item.method === item2.method) return;
125
126 if (item2.method === 'deleted') {
127 var tmp = item; item = item2; item2 = tmp;
128 }
129
130 if (item.method === 'deleted' && item2.method === 'created') {
131 item2.method = 'moved';
132 item2.from = item.path;
133 byInode[key] = item2;
134 } else {
135 debug('Multiple events?', [item, item2]);
136 }
137 } else {
138 byInode[key] = item;
139 }
140 } else {
141 debug('Item has no stat or inode info', item);
142 }
143 });
144
145 Object.getOwnPropertyNames(byInode).forEach(function(key) {
146 var ev = byInode[key];
147 self._targets.forEach(function(target) {
148 if (target.target instanceof RegExp) {
149 if (target.target.test(ev.path)) {
150 debug('Invoking custom hook for ' + ev.path);
151 target.callback(ev);
152 }
153 }
154 });
155 });
156
157 Watcher._clearDeferTimeout();
158};
159
160
161
162Watcher.prototype._updateStat = function(absolute, relative, err, stat) {
163 var oldStat = this._seenStat[relative];
164 var newStat = this._seenStat[relative] = stat || null;
165 if (newStat && !oldStat) {
166 this._defer('created', relative, newStat);
167
168 if (newStat.isDirectory())
169 this._watchDir(absolute);
170
171 } else if (!newStat && oldStat) {
172 this._defer('deleted', relative, oldStat);
173 } else if (!newStat && !oldStat) {
174 // stated after delete or move...
175 if (err) {
176 if (err.code === 'ENOENT') {
177 this._defer('deleted', relative);
178 } else {
179 notify('Error stating ' + relative + ': ' + err.toString());
180 }
181 } else {
182 notify('No old, no new and no error, WTF?? ' + relative);
183 }
184 } else {
185 if ((newStat.mtime.getTime() !== oldStat.mtime.getTime()) ||
186 (newStat.size !== oldStat.size))
187 {
188 this._defer('updated', relative, newStat, oldStat);
189 }
190 }
191};
192
193
194
195Watcher.prototype._change = function(root, filename, ev) {
196 var absolute = path.join(root, filename);
197 var relative = path.relative(this.root, absolute);
198 var self = this;
199 fs.lstat(absolute, function(err, stat) {
200 self._updateStat(absolute, relative, err, stat);
201 });
202};
203
204
205
206Watcher.prototype.add = function(target, callback) {
207 this._targets.push({target: target, callback: callback});
208};
209
210
211
212module.exports = function(app) {
213 app.watch = function(target, callback) {
214 if (!app._watcher)
215 app._watcher = new Watcher(app.root);
216 return app._watcher.add.apply(app._watcher, arguments);
217 };
218 notify = function() {
219 debug(arguments);
220 return app.notify.apply(null, arguments);
221 }
222};