UNPKG

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