Skip to content

Commit 422055a

Browse files
robhoganfacebook-github-bot
authored andcommittedSep 27, 2022
Drop support for Watchman <=4.9.0, remove dead code
Summary: We have various capability checks across the Watchman watcher and crawler at the moment, checking for features that have been available in Watchman for a long time (before 2017 in most cases). This drops support for any version of Watchman too old to support the features we prefer to use, by asserting on specific capabilities and removing the fallback code paths. If the capability check fails we'll still seamlessly fall back to a zero-dependency option via the "node" crawler+watcher. Changelog: [Breaking]: Drop support for old (pre-CalVer) Watchman versions Reviewed By: jacdebug Differential Revision: D39771009 fbshipit-source-id: 2d0f84157d8daf0d7337d535be28cd92bdf048e1
1 parent d831400 commit 422055a

File tree

4 files changed

+48
-242
lines changed

4 files changed

+48
-242
lines changed
 

‎packages/metro-file-map/src/crawlers/__tests__/watchman-test.js

+1-85
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,6 @@ const path = require('path');
1515
jest.mock('fb-watchman', () => {
1616
const normalizePathSep = require('../../lib/normalizePathSep').default;
1717
const Client = jest.fn();
18-
Client.prototype.capabilityCheck = jest.fn((args, callback) =>
19-
setImmediate(() => {
20-
callback(null, {
21-
capabilities: {'suffix-set': true},
22-
version: '2021.06.07.00',
23-
});
24-
}),
25-
);
2618
Client.prototype.command = jest.fn((args, callback) =>
2719
setImmediate(() => {
2820
const path = args[1] ? normalizePathSep(args[1]) : undefined;
@@ -75,11 +67,6 @@ describe('watchman watch', () => {
7567
watchman = require('fb-watchman');
7668

7769
mockResponse = {
78-
'list-capabilities': {
79-
[undefined]: {
80-
capabilities: ['field-content.sha1hex'],
81-
},
82-
},
8370
query: {
8471
[ROOT_MOCK]: {
8572
clock: 'c:fake-clock:1',
@@ -179,11 +166,6 @@ describe('watchman watch', () => {
179166

180167
test('updates file map and removedFiles when the clock is given', async () => {
181168
mockResponse = {
182-
'list-capabilities': {
183-
[undefined]: {
184-
capabilities: ['field-content.sha1hex'],
185-
},
186-
},
187169
query: {
188170
[ROOT_MOCK]: {
189171
clock: 'c:fake-clock:2',
@@ -257,11 +239,6 @@ describe('watchman watch', () => {
257239
const mockTomatoSha1 = '321f6b7e8bf7f29aab89c5e41a555b1b0baa41a9';
258240

259241
mockResponse = {
260-
'list-capabilities': {
261-
[undefined]: {
262-
capabilities: ['field-content.sha1hex'],
263-
},
264-
},
265242
query: {
266243
[ROOT_MOCK]: {
267244
clock: 'c:fake-clock:3',
@@ -350,11 +327,6 @@ describe('watchman watch', () => {
350327

351328
test('properly resets the file map when only one watcher is reset', async () => {
352329
mockResponse = {
353-
'list-capabilities': {
354-
[undefined]: {
355-
capabilities: ['field-content.sha1hex'],
356-
},
357-
},
358330
query: {
359331
[FRUITS]: {
360332
clock: 'c:fake-clock:3',
@@ -435,11 +407,6 @@ describe('watchman watch', () => {
435407

436408
test('does not add directory filters to query when watching a ROOT', async () => {
437409
mockResponse = {
438-
'list-capabilities': {
439-
[undefined]: {
440-
capabilities: ['field-content.sha1hex'],
441-
},
442-
},
443410
query: {
444411
[ROOT_MOCK]: {
445412
clock: 'c:fake-clock:1',
@@ -512,11 +479,6 @@ describe('watchman watch', () => {
512479

513480
test('SHA-1 requested and available', async () => {
514481
mockResponse = {
515-
'list-capabilities': {
516-
[undefined]: {
517-
capabilities: ['field-content.sha1hex'],
518-
},
519-
},
520482
query: {
521483
[ROOT_MOCK]: {
522484
clock: 'c:fake-clock:1',
@@ -546,57 +508,11 @@ describe('watchman watch', () => {
546508
const client = watchman.Client.mock.instances[0];
547509
const calls = client.command.mock.calls;
548510

549-
expect(calls[0][0]).toEqual(['list-capabilities']);
550-
expect(calls[2][0][2].fields).toContain('content.sha1hex');
551-
});
552-
553-
test('SHA-1 requested and NOT available', async () => {
554-
mockResponse = {
555-
'list-capabilities': {
556-
[undefined]: {
557-
capabilities: [],
558-
},
559-
},
560-
query: {
561-
[ROOT_MOCK]: {
562-
clock: 'c:fake-clock:1',
563-
files: [],
564-
is_fresh_instance: false,
565-
version: '4.5.0',
566-
},
567-
},
568-
'watch-project': {
569-
[ROOT_MOCK]: {
570-
watch: forcePOSIXPaths(ROOT_MOCK),
571-
},
572-
},
573-
};
574-
575-
await watchmanCrawl({
576-
computeSha1: true,
577-
data: {
578-
clocks: new Map(),
579-
files: new Map(),
580-
},
581-
extensions: ['js', 'json'],
582-
rootDir: ROOT_MOCK,
583-
roots: [ROOT_MOCK],
584-
});
585-
586-
const client = watchman.Client.mock.instances[0];
587-
const calls = client.command.mock.calls;
588-
589-
expect(calls[0][0]).toEqual(['list-capabilities']);
590-
expect(calls[2][0][2].fields).not.toContain('content.sha1hex');
511+
expect(calls[1][0][2].fields).toContain('content.sha1hex');
591512
});
592513

593514
test('source control query', async () => {
594515
mockResponse = {
595-
'list-capabilities': {
596-
[undefined]: {
597-
capabilities: ['field-content.sha1hex'],
598-
},
599-
},
600516
query: {
601517
[ROOT_MOCK]: {
602518
clock: {

‎packages/metro-file-map/src/crawlers/watchman.js

+18-72
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,6 @@ type WatchmanQuery = any;
2929

3030
type WatchmanRoots = Map<string, Array<string>>;
3131

32-
type WatchmanListCapabilitiesResponse = {
33-
capabilities: Array<string>,
34-
};
35-
36-
type WatchmanCapabilityCheckResponse = {
37-
// { 'suffix-set': true }
38-
capabilities: $ReadOnly<{[string]: boolean}>,
39-
// '2021.06.07.00'
40-
version: string,
41-
};
42-
4332
type WatchmanWatchProjectResponse = {
4433
watch: string,
4534
relative_path: string,
@@ -76,61 +65,30 @@ function makeWatchmanError(error: Error): Error {
7665
return error;
7766
}
7867

79-
/**
80-
* Wrap watchman capabilityCheck method as a promise.
81-
*
82-
* @param client watchman client
83-
* @param caps capabilities to verify
84-
* @returns a promise resolving to a list of verified capabilities
85-
*/
86-
async function capabilityCheck(
87-
client: watchman.Client,
88-
caps: $ReadOnly<{optional?: $ReadOnlyArray<string>}>,
89-
): Promise<WatchmanCapabilityCheckResponse> {
90-
return new Promise((resolve, reject) => {
91-
client.capabilityCheck(
92-
// @ts-expect-error: incorrectly typed
93-
caps,
94-
(error, response) => {
95-
if (error != null || response == null) {
96-
reject(error ?? new Error('capabilityCheck: Response missing'));
97-
} else {
98-
resolve(response);
99-
}
100-
},
101-
);
102-
});
103-
}
104-
105-
module.exports = async function watchmanCrawl(
106-
options: CrawlerOptions,
107-
): Promise<{
68+
module.exports = async function watchmanCrawl({
69+
abortSignal,
70+
computeSha1,
71+
data,
72+
extensions,
73+
ignore,
74+
rootDir,
75+
roots,
76+
perfLogger,
77+
}: CrawlerOptions): Promise<{
10878
changedFiles?: FileData,
10979
removedFiles: FileData,
11080
hasteMap: InternalData,
11181
}> {
82+
perfLogger?.point('watchmanCrawl_start');
83+
11284
const fields = ['name', 'exists', 'mtime_ms', 'size'];
113-
const {data, extensions, ignore, rootDir, roots, perfLogger} = options;
85+
if (computeSha1) {
86+
fields.push('content.sha1hex');
87+
}
11488
const clocks = data.clocks;
11589

116-
perfLogger?.point('watchmanCrawl_start');
11790
const client = new watchman.Client();
118-
options.abortSignal?.addEventListener('abort', () => client.end());
119-
120-
perfLogger?.point('watchmanCrawl/negotiateCapabilities_start');
121-
// https://facebook.github.io/watchman/docs/capabilities.html
122-
// Check adds about ~28ms
123-
const capabilities = await capabilityCheck(client, {
124-
// If a required capability is missing then an error will be thrown,
125-
// we don't need this assertion, so using optional instead.
126-
optional: ['suffix-set'],
127-
});
128-
129-
const suffixExpression = capabilities?.capabilities['suffix-set']
130-
? // If available, use the optimized `suffix-set` operation:
131-
// https://facebook.github.io/watchman/docs/expr/suffix.html#suffix-set
132-
['suffix', extensions]
133-
: ['anyof', ...extensions.map(extension => ['suffix', extension])];
91+
abortSignal?.addEventListener('abort', () => client.end());
13492

13593
let clientError;
13694
// $FlowFixMe[prop-missing] - Client is not typed as an EventEmitter
@@ -164,18 +122,6 @@ module.exports = async function watchmanCrawl(
164122
}
165123
};
166124

167-
if (options.computeSha1) {
168-
const {capabilities} = await cmd<WatchmanListCapabilitiesResponse>(
169-
'list-capabilities',
170-
);
171-
172-
if (capabilities.indexOf('field-content.sha1hex') !== -1) {
173-
fields.push('content.sha1hex');
174-
}
175-
}
176-
177-
perfLogger?.point('watchmanCrawl/negotiateCapabilities_end');
178-
179125
async function getWatchmanRoots(
180126
roots: $ReadOnlyArray<Path>,
181127
): Promise<WatchmanRoots> {
@@ -277,15 +223,15 @@ module.exports = async function watchmanCrawl(
277223
queryGenerator = 'since';
278224
query.expression.push(
279225
['anyof', ...directoryFilters.map(dir => ['dirname', dir])],
280-
suffixExpression,
226+
['suffix', extensions],
281227
);
282228
} else if (directoryFilters.length > 0) {
283229
// Use the `glob` generator and filter only by extension.
284230
query.glob = directoryFilters.map(directory => `${directory}/**`);
285231
query.glob_includedotfiles = true;
286232
queryGenerator = 'glob';
287233

288-
query.expression.push(suffixExpression);
234+
query.expression.push(['suffix', extensions]);
289235
} else {
290236
// Use the `suffix` generator with no path/extension filtering.
291237
query.suffix = extensions;

‎packages/metro-file-map/src/index.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ const PACKAGE_JSON = path.sep + 'package.json';
145145
const VCS_DIRECTORIES = ['.git', '.hg']
146146
.map(vcs => escapePathForRegex(path.sep + vcs + path.sep))
147147
.join('|');
148+
const WATCHMAN_REQUIRED_CAPABILITIES = [
149+
'field-content.sha1hex',
150+
'relative_root',
151+
'suffix-set',
152+
'wildmatch',
153+
];
148154

149155
/**
150156
* HasteMap is a JavaScript implementation of Facebook's haste module system.
@@ -1158,8 +1164,9 @@ export default class HasteMap extends EventEmitter {
11581164
return false;
11591165
}
11601166
if (!this._canUseWatchmanPromise) {
1161-
// TODO: Ensure minimum capabilities here
1162-
this._canUseWatchmanPromise = checkWatchmanCapabilities([])
1167+
this._canUseWatchmanPromise = checkWatchmanCapabilities(
1168+
WATCHMAN_REQUIRED_CAPABILITIES,
1169+
)
11631170
.then(() => true)
11641171
.catch(e => {
11651172
// TODO: Advise people to either install Watchman or set

‎packages/metro-file-map/src/watchers/WatchmanWatcher.js

+20-83
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,6 @@ WatchmanWatcher.prototype.init = function () {
7070
return self.watchProjectInfo ? self.watchProjectInfo.root : self.root;
7171
}
7272

73-
function onCapability(error, resp) {
74-
if (handleError(self, error)) {
75-
// The Watchman watcher is unusable on this system, we cannot continue
76-
return;
77-
}
78-
79-
handleWarning(resp);
80-
81-
self.capabilities = resp.capabilities;
82-
83-
if (self.capabilities.relative_root) {
84-
self.client.command(['watch-project', getWatchRoot()], onWatchProject);
85-
} else {
86-
self.client.command(['watch', getWatchRoot()], onWatch);
87-
}
88-
}
89-
9073
function onWatchProject(error, resp) {
9174
if (handleError(self, error)) {
9275
return;
@@ -102,16 +85,6 @@ WatchmanWatcher.prototype.init = function () {
10285
self.client.command(['clock', getWatchRoot()], onClock);
10386
}
10487

105-
function onWatch(error, resp) {
106-
if (handleError(self, error)) {
107-
return;
108-
}
109-
110-
handleWarning(resp);
111-
112-
self.client.command(['clock', getWatchRoot()], onClock);
113-
}
114-
11588
function onClock(error, resp) {
11689
if (handleError(self, error)) {
11790
return;
@@ -123,43 +96,19 @@ WatchmanWatcher.prototype.init = function () {
12396
fields: ['name', 'exists', 'new'],
12497
since: resp.clock,
12598
defer: self.watchmanDeferStates,
99+
relative_root: self.watchProjectInfo.relativePath,
126100
};
127101

128-
// If the server has the wildmatch capability available it supports
129-
// the recursive **/*.foo style match and we can offload our globs
130-
// to the watchman server. This saves both on data size to be
131-
// communicated back to us and compute for evaluating the globs
132-
// in our node process.
133-
if (self.capabilities.wildmatch) {
134-
if (self.globs.length === 0) {
135-
if (!self.dot) {
136-
// Make sure we honor the dot option if even we're not using globs.
137-
options.expression = [
138-
'match',
139-
'**',
140-
'wholename',
141-
{
142-
includedotfiles: false,
143-
},
144-
];
145-
}
146-
} else {
147-
options.expression = ['anyof'];
148-
for (const i in self.globs) {
149-
options.expression.push([
150-
'match',
151-
self.globs[i],
152-
'wholename',
153-
{
154-
includedotfiles: self.dot,
155-
},
156-
]);
157-
}
158-
}
159-
}
160-
161-
if (self.capabilities.relative_root) {
162-
options.relative_root = self.watchProjectInfo.relativePath;
102+
// Make sure we honor the dot option if even we're not using globs.
103+
if (self.globs.length === 0 && !self.dot) {
104+
options.expression = [
105+
'match',
106+
'**',
107+
'wholename',
108+
{
109+
includedotfiles: false,
110+
},
111+
];
163112
}
164113

165114
self.client.command(
@@ -178,12 +127,7 @@ WatchmanWatcher.prototype.init = function () {
178127
self.emit('ready');
179128
}
180129

181-
self.client.capabilityCheck(
182-
{
183-
optional: ['wildmatch', 'relative_root'],
184-
},
185-
onCapability,
186-
);
130+
self.client.command(['watch-project', getWatchRoot()], onWatchProject);
187131
};
188132

189133
/**
@@ -225,23 +169,16 @@ WatchmanWatcher.prototype.handleChangeEvent = function (resp) {
225169

226170
WatchmanWatcher.prototype.handleFileChange = function (changeDescriptor) {
227171
const self = this;
228-
let absPath;
229-
let relativePath;
230-
231-
if (this.capabilities.relative_root) {
232-
relativePath = changeDescriptor.name;
233-
absPath = path.join(
234-
this.watchProjectInfo.root,
235-
this.watchProjectInfo.relativePath,
236-
relativePath,
237-
);
238-
} else {
239-
absPath = path.join(this.root, changeDescriptor.name);
240-
relativePath = changeDescriptor.name;
241-
}
172+
173+
const relativePath = changeDescriptor.name;
174+
const absPath = path.join(
175+
this.watchProjectInfo.root,
176+
this.watchProjectInfo.relativePath,
177+
relativePath,
178+
);
242179

243180
if (
244-
!(self.capabilities.wildmatch && !this.hasIgnore) &&
181+
this.hasIgnore &&
245182
!common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
246183
) {
247184
return;

0 commit comments

Comments
 (0)
Please sign in to comment.