1 | var debug = require('debug')('dev-server:watch');
|
2 | var fs = require('fs');
|
3 | var path = require('path');
|
4 |
|
5 | var notify = Function.prototype;
|
6 |
|
7 | Watcher.ignoreNames = ['.git', '.svn', '_svn', '_git'];
|
8 | Watcher.DEFER_TIMEOUT = 100;
|
9 |
|
10 | function 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 |
|
19 | Watcher.prototype._error = function(err) {
|
20 | console.error('Watcher error: ', err);
|
21 | };
|
22 |
|
23 | Watcher.prototype._decPending = function() {
|
24 | this._pendingDiscovery--;
|
25 | if (this._pendingDiscovery === 0) {
|
26 | debug('All discovered');
|
27 | notify('Watching for changes', 'info');
|
28 | }
|
29 | };
|
30 |
|
31 | Watcher.prototype._watch = function(name) {
|
32 | var self = this;
|
33 | self._pendingDiscovery++;
|
34 |
|
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 |
|
50 | Watcher.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 |
|
74 | Watcher._clearDeferTimeout = function() {
|
75 | Watcher._defered = {
|
76 | timeout: null,
|
77 | all: [],
|
78 | };
|
79 | };
|
80 | Watcher._clearDeferTimeout();
|
81 |
|
82 | Watcher.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 |
|
94 | Watcher.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 |
|
143 | Watcher.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 |
|
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 |
|
170 | Watcher.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 |
|
179 | Watcher.prototype.add = function(target, callback) {
|
180 | this._targets.push({ target: target, callback: callback });
|
181 | };
|
182 |
|
183 | module.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 | };
|