Skip to content

Commit faf8883

Browse files
authored
jest-haste-map: fork watchman watcher from sane to enable custom features (#5387)
1 parent 4561959 commit faf8883

3 files changed

Lines changed: 341 additions & 10 deletions

File tree

packages/jest-haste-map/src/__tests__/index.test.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,25 @@ jest.mock('../crawlers/watchman', () =>
4646
}),
4747
);
4848

49+
const mockWatcherConstructor = jest.fn(root => {
50+
const EventEmitter = require('events').EventEmitter;
51+
mockEmitters[root] = new EventEmitter();
52+
mockEmitters[root].close = jest.fn(callback => callback());
53+
setTimeout(() => mockEmitters[root].emit('ready'), 0);
54+
return mockEmitters[root];
55+
});
56+
4957
jest.mock('sane', () => {
50-
const watcher = jest.fn(root => {
51-
const EventEmitter = require('events').EventEmitter;
52-
mockEmitters[root] = new EventEmitter();
53-
mockEmitters[root].close = jest.fn(callback => callback());
54-
setTimeout(() => mockEmitters[root].emit('ready'), 0);
55-
return mockEmitters[root];
56-
});
5758
return {
58-
NodeWatcher: watcher,
59-
WatchmanWatcher: watcher,
59+
NodeWatcher: mockWatcherConstructor,
60+
WatchmanWatcher: mockWatcherConstructor,
6061
};
6162
});
6263

64+
jest.mock('../lib/watchman_watcher.js', () => {
65+
return mockWatcherConstructor;
66+
});
67+
6368
let mockChangedFiles;
6469
let mockFs;
6570

packages/jest-haste-map/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import getMockName from './get_mock_name';
3939
import getPlatformExtension from './lib/get_platform_extension';
4040
import normalizePathSep from './lib/normalize_path_sep';
4141
import Worker from 'jest-worker';
42+
import WatchmanWatcher from './lib/watchman_watcher';
4243

4344
// eslint-disable-next-line import/default
4445
import nodeCrawl from './crawlers/node';
@@ -608,7 +609,7 @@ class HasteMap extends EventEmitter {
608609

609610
const Watcher =
610611
canUseWatchman && this._options.useWatchman
611-
? sane.WatchmanWatcher
612+
? WatchmanWatcher
612613
: os.platform() === 'darwin' ? sane.FSEventsWatcher : sane.NodeWatcher;
613614
const extensions = this._options.extensions;
614615
const ignorePattern = this._options.ignorePattern;
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import fs from 'fs';
9+
import path from 'path';
10+
import assert from 'assert';
11+
import common from 'sane/src/common';
12+
import watchman from 'fb-watchman';
13+
import {EventEmitter} from 'events';
14+
import RecrawlWarning from 'sane/src/utils/recrawl-warning-dedupe';
15+
16+
const CHANGE_EVENT = common.CHANGE_EVENT;
17+
const DELETE_EVENT = common.DELETE_EVENT;
18+
const ADD_EVENT = common.ADD_EVENT;
19+
const ALL_EVENT = common.ALL_EVENT;
20+
const SUB_NAME = 'sane-sub';
21+
22+
/**
23+
* Watches `dir`.
24+
*
25+
* @class PollWatcher
26+
* @param String dir
27+
* @param {Object} opts
28+
* @public
29+
*/
30+
31+
export default function WatchmanWatcher(dir, opts) {
32+
common.assignOptions(this, opts);
33+
this.root = path.resolve(dir);
34+
this.init();
35+
}
36+
37+
// eslint-disable-next-line no-proto
38+
WatchmanWatcher.prototype.__proto__ = EventEmitter.prototype;
39+
40+
/**
41+
* Run the watchman `watch` command on the root and subscribe to changes.
42+
*
43+
* @private
44+
*/
45+
46+
WatchmanWatcher.prototype.init = function() {
47+
if (this.client) {
48+
this.client.removeAllListeners();
49+
}
50+
51+
const self = this;
52+
this.client = new watchman.Client();
53+
this.client.on('error', error => {
54+
self.emit('error', error);
55+
});
56+
this.client.on('subscription', this.handleChangeEvent.bind(this));
57+
this.client.on('end', () => {
58+
console.warn('[sane] Warning: Lost connection to watchman, reconnecting..');
59+
self.init();
60+
});
61+
62+
this.watchProjectInfo = null;
63+
64+
function getWatchRoot() {
65+
return self.watchProjectInfo ? self.watchProjectInfo.root : self.root;
66+
}
67+
68+
function onCapability(error, resp) {
69+
if (handleError(self, error)) {
70+
// The Watchman watcher is unusable on this system, we cannot continue
71+
return;
72+
}
73+
74+
handleWarning(resp);
75+
76+
self.capabilities = resp.capabilities;
77+
78+
if (self.capabilities.relative_root) {
79+
self.client.command(['watch-project', getWatchRoot()], onWatchProject);
80+
} else {
81+
self.client.command(['watch', getWatchRoot()], onWatch);
82+
}
83+
}
84+
85+
function onWatchProject(error, resp) {
86+
if (handleError(self, error)) {
87+
return;
88+
}
89+
90+
handleWarning(resp);
91+
92+
self.watchProjectInfo = {
93+
relativePath: resp.relative_path ? resp.relative_path : '',
94+
root: resp.watch,
95+
};
96+
97+
self.client.command(['clock', getWatchRoot()], onClock);
98+
}
99+
100+
function onWatch(error, resp) {
101+
if (handleError(self, error)) {
102+
return;
103+
}
104+
105+
handleWarning(resp);
106+
107+
self.client.command(['clock', getWatchRoot()], onClock);
108+
}
109+
110+
function onClock(error, resp) {
111+
if (handleError(self, error)) {
112+
return;
113+
}
114+
115+
handleWarning(resp);
116+
117+
const options = {
118+
fields: ['name', 'exists', 'new'],
119+
since: resp.clock,
120+
};
121+
122+
// If the server has the wildmatch capability available it supports
123+
// the recursive **/*.foo style match and we can offload our globs
124+
// to the watchman server. This saves both on data size to be
125+
// communicated back to us and compute for evaluating the globs
126+
// in our node process.
127+
if (self.capabilities.wildmatch) {
128+
if (self.globs.length === 0) {
129+
if (!self.dot) {
130+
// Make sure we honor the dot option if even we're not using globs.
131+
options.expression = [
132+
'match',
133+
'**',
134+
'wholename',
135+
{
136+
includedotfiles: false,
137+
},
138+
];
139+
}
140+
} else {
141+
options.expression = ['anyof'];
142+
for (const i in self.globs) {
143+
options.expression.push([
144+
'match',
145+
self.globs[i],
146+
'wholename',
147+
{
148+
includedotfiles: self.dot,
149+
},
150+
]);
151+
}
152+
}
153+
}
154+
155+
if (self.capabilities.relative_root) {
156+
options.relative_root = self.watchProjectInfo.relativePath;
157+
}
158+
159+
self.client.command(
160+
['subscribe', getWatchRoot(), SUB_NAME, options],
161+
onSubscribe,
162+
);
163+
}
164+
165+
function onSubscribe(error, resp) {
166+
if (handleError(self, error)) {
167+
return;
168+
}
169+
170+
handleWarning(resp);
171+
172+
self.emit('ready');
173+
}
174+
175+
self.client.capabilityCheck(
176+
{
177+
optional: ['wildmatch', 'relative_root'],
178+
},
179+
onCapability,
180+
);
181+
};
182+
183+
/**
184+
* Handles a change event coming from the subscription.
185+
*
186+
* @param {Object} resp
187+
* @private
188+
*/
189+
190+
WatchmanWatcher.prototype.handleChangeEvent = function(resp) {
191+
assert.equal(resp.subscription, SUB_NAME, 'Invalid subscription event.');
192+
if (resp.is_fresh_instance) {
193+
this.emit('fresh_instance');
194+
}
195+
if (resp.is_fresh_instance) {
196+
this.emit('fresh_instance');
197+
}
198+
if (Array.isArray(resp.files)) {
199+
resp.files.forEach(this.handleFileChange, this);
200+
}
201+
};
202+
203+
/**
204+
* Handles a single change event record.
205+
*
206+
* @param {Object} changeDescriptor
207+
* @private
208+
*/
209+
210+
WatchmanWatcher.prototype.handleFileChange = function(changeDescriptor) {
211+
const self = this;
212+
let absPath;
213+
let relativePath;
214+
215+
if (this.capabilities.relative_root) {
216+
relativePath = changeDescriptor.name;
217+
absPath = path.join(
218+
this.watchProjectInfo.root,
219+
this.watchProjectInfo.relativePath,
220+
relativePath,
221+
);
222+
} else {
223+
absPath = path.join(this.root, changeDescriptor.name);
224+
relativePath = changeDescriptor.name;
225+
}
226+
227+
if (
228+
!(self.capabilities.wildmatch && !this.hasIgnore) &&
229+
!common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
230+
) {
231+
return;
232+
}
233+
234+
if (!changeDescriptor.exists) {
235+
self.emitEvent(DELETE_EVENT, relativePath, self.root);
236+
} else {
237+
fs.lstat(absPath, (error, stat) => {
238+
// Files can be deleted between the event and the lstat call
239+
// the most reliable thing to do here is to ignore the event.
240+
if (error && error.code === 'ENOENT') {
241+
return;
242+
}
243+
244+
if (handleError(self, error)) {
245+
return;
246+
}
247+
248+
const eventType = changeDescriptor.new ? ADD_EVENT : CHANGE_EVENT;
249+
250+
// Change event on dirs are mostly useless.
251+
if (!(eventType === CHANGE_EVENT && stat.isDirectory())) {
252+
self.emitEvent(eventType, relativePath, self.root, stat);
253+
}
254+
});
255+
}
256+
};
257+
258+
/**
259+
* Dispatches the event.
260+
*
261+
* @param {string} eventType
262+
* @param {string} filepath
263+
* @param {string} root
264+
* @param {fs.Stat} stat
265+
* @private
266+
*/
267+
268+
WatchmanWatcher.prototype.emitEvent = function(
269+
eventType,
270+
filepath,
271+
root,
272+
stat,
273+
) {
274+
this.emit(eventType, filepath, root, stat);
275+
this.emit(ALL_EVENT, eventType, filepath, root, stat);
276+
};
277+
278+
/**
279+
* Closes the watcher.
280+
*
281+
* @param {function} callback
282+
* @private
283+
*/
284+
285+
WatchmanWatcher.prototype.close = function(callback) {
286+
this.client.removeAllListeners();
287+
this.client.end();
288+
callback && callback(null, true);
289+
};
290+
291+
/**
292+
* Handles an error and returns true if exists.
293+
*
294+
* @param {WatchmanWatcher} self
295+
* @param {Error} error
296+
* @private
297+
*/
298+
299+
function handleError(self, error) {
300+
if (error != null) {
301+
self.emit('error', error);
302+
return true;
303+
} else {
304+
return false;
305+
}
306+
}
307+
308+
/**
309+
* Handles a warning in the watchman resp object.
310+
*
311+
* @param {object} resp
312+
* @private
313+
*/
314+
315+
function handleWarning(resp) {
316+
if ('warning' in resp) {
317+
if (RecrawlWarning.isRecrawlWarningDupe(resp.warning)) {
318+
return true;
319+
}
320+
console.warn(resp.warning);
321+
return true;
322+
} else {
323+
return false;
324+
}
325+
}

0 commit comments

Comments
 (0)