From 422055a5ccaca41edb1864ca07d4f810b3e03791 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 27 Sep 2022 03:47:19 -0700 Subject: [PATCH] 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 --- .../src/crawlers/__tests__/watchman-test.js | 86 +-------------- .../metro-file-map/src/crawlers/watchman.js | 90 +++------------ packages/metro-file-map/src/index.js | 11 +- .../src/watchers/WatchmanWatcher.js | 103 ++++-------------- 4 files changed, 48 insertions(+), 242 deletions(-) diff --git a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js index 77ce89537d..26f386e0ee 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js @@ -15,14 +15,6 @@ const path = require('path'); jest.mock('fb-watchman', () => { const normalizePathSep = require('../../lib/normalizePathSep').default; const Client = jest.fn(); - Client.prototype.capabilityCheck = jest.fn((args, callback) => - setImmediate(() => { - callback(null, { - capabilities: {'suffix-set': true}, - version: '2021.06.07.00', - }); - }), - ); Client.prototype.command = jest.fn((args, callback) => setImmediate(() => { const path = args[1] ? normalizePathSep(args[1]) : undefined; @@ -75,11 +67,6 @@ describe('watchman watch', () => { watchman = require('fb-watchman'); mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: ['field-content.sha1hex'], - }, - }, query: { [ROOT_MOCK]: { clock: 'c:fake-clock:1', @@ -179,11 +166,6 @@ describe('watchman watch', () => { test('updates file map and removedFiles when the clock is given', async () => { mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: ['field-content.sha1hex'], - }, - }, query: { [ROOT_MOCK]: { clock: 'c:fake-clock:2', @@ -257,11 +239,6 @@ describe('watchman watch', () => { const mockTomatoSha1 = '321f6b7e8bf7f29aab89c5e41a555b1b0baa41a9'; mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: ['field-content.sha1hex'], - }, - }, query: { [ROOT_MOCK]: { clock: 'c:fake-clock:3', @@ -350,11 +327,6 @@ describe('watchman watch', () => { test('properly resets the file map when only one watcher is reset', async () => { mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: ['field-content.sha1hex'], - }, - }, query: { [FRUITS]: { clock: 'c:fake-clock:3', @@ -435,11 +407,6 @@ describe('watchman watch', () => { test('does not add directory filters to query when watching a ROOT', async () => { mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: ['field-content.sha1hex'], - }, - }, query: { [ROOT_MOCK]: { clock: 'c:fake-clock:1', @@ -512,11 +479,6 @@ describe('watchman watch', () => { test('SHA-1 requested and available', async () => { mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: ['field-content.sha1hex'], - }, - }, query: { [ROOT_MOCK]: { clock: 'c:fake-clock:1', @@ -546,57 +508,11 @@ describe('watchman watch', () => { const client = watchman.Client.mock.instances[0]; const calls = client.command.mock.calls; - expect(calls[0][0]).toEqual(['list-capabilities']); - expect(calls[2][0][2].fields).toContain('content.sha1hex'); - }); - - test('SHA-1 requested and NOT available', async () => { - mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: [], - }, - }, - query: { - [ROOT_MOCK]: { - clock: 'c:fake-clock:1', - files: [], - is_fresh_instance: false, - version: '4.5.0', - }, - }, - 'watch-project': { - [ROOT_MOCK]: { - watch: forcePOSIXPaths(ROOT_MOCK), - }, - }, - }; - - await watchmanCrawl({ - computeSha1: true, - data: { - clocks: new Map(), - files: new Map(), - }, - extensions: ['js', 'json'], - rootDir: ROOT_MOCK, - roots: [ROOT_MOCK], - }); - - const client = watchman.Client.mock.instances[0]; - const calls = client.command.mock.calls; - - expect(calls[0][0]).toEqual(['list-capabilities']); - expect(calls[2][0][2].fields).not.toContain('content.sha1hex'); + expect(calls[1][0][2].fields).toContain('content.sha1hex'); }); test('source control query', async () => { mockResponse = { - 'list-capabilities': { - [undefined]: { - capabilities: ['field-content.sha1hex'], - }, - }, query: { [ROOT_MOCK]: { clock: { diff --git a/packages/metro-file-map/src/crawlers/watchman.js b/packages/metro-file-map/src/crawlers/watchman.js index 35f7a15b29..930f8d1fd7 100644 --- a/packages/metro-file-map/src/crawlers/watchman.js +++ b/packages/metro-file-map/src/crawlers/watchman.js @@ -29,17 +29,6 @@ type WatchmanQuery = any; type WatchmanRoots = Map>; -type WatchmanListCapabilitiesResponse = { - capabilities: Array, -}; - -type WatchmanCapabilityCheckResponse = { - // { 'suffix-set': true } - capabilities: $ReadOnly<{[string]: boolean}>, - // '2021.06.07.00' - version: string, -}; - type WatchmanWatchProjectResponse = { watch: string, relative_path: string, @@ -76,61 +65,30 @@ function makeWatchmanError(error: Error): Error { return error; } -/** - * Wrap watchman capabilityCheck method as a promise. - * - * @param client watchman client - * @param caps capabilities to verify - * @returns a promise resolving to a list of verified capabilities - */ -async function capabilityCheck( - client: watchman.Client, - caps: $ReadOnly<{optional?: $ReadOnlyArray}>, -): Promise { - return new Promise((resolve, reject) => { - client.capabilityCheck( - // @ts-expect-error: incorrectly typed - caps, - (error, response) => { - if (error != null || response == null) { - reject(error ?? new Error('capabilityCheck: Response missing')); - } else { - resolve(response); - } - }, - ); - }); -} - -module.exports = async function watchmanCrawl( - options: CrawlerOptions, -): Promise<{ +module.exports = async function watchmanCrawl({ + abortSignal, + computeSha1, + data, + extensions, + ignore, + rootDir, + roots, + perfLogger, +}: CrawlerOptions): Promise<{ changedFiles?: FileData, removedFiles: FileData, hasteMap: InternalData, }> { + perfLogger?.point('watchmanCrawl_start'); + const fields = ['name', 'exists', 'mtime_ms', 'size']; - const {data, extensions, ignore, rootDir, roots, perfLogger} = options; + if (computeSha1) { + fields.push('content.sha1hex'); + } const clocks = data.clocks; - perfLogger?.point('watchmanCrawl_start'); const client = new watchman.Client(); - options.abortSignal?.addEventListener('abort', () => client.end()); - - perfLogger?.point('watchmanCrawl/negotiateCapabilities_start'); - // https://facebook.github.io/watchman/docs/capabilities.html - // Check adds about ~28ms - const capabilities = await capabilityCheck(client, { - // If a required capability is missing then an error will be thrown, - // we don't need this assertion, so using optional instead. - optional: ['suffix-set'], - }); - - const suffixExpression = capabilities?.capabilities['suffix-set'] - ? // If available, use the optimized `suffix-set` operation: - // https://facebook.github.io/watchman/docs/expr/suffix.html#suffix-set - ['suffix', extensions] - : ['anyof', ...extensions.map(extension => ['suffix', extension])]; + abortSignal?.addEventListener('abort', () => client.end()); let clientError; // $FlowFixMe[prop-missing] - Client is not typed as an EventEmitter @@ -164,18 +122,6 @@ module.exports = async function watchmanCrawl( } }; - if (options.computeSha1) { - const {capabilities} = await cmd( - 'list-capabilities', - ); - - if (capabilities.indexOf('field-content.sha1hex') !== -1) { - fields.push('content.sha1hex'); - } - } - - perfLogger?.point('watchmanCrawl/negotiateCapabilities_end'); - async function getWatchmanRoots( roots: $ReadOnlyArray, ): Promise { @@ -277,7 +223,7 @@ module.exports = async function watchmanCrawl( queryGenerator = 'since'; query.expression.push( ['anyof', ...directoryFilters.map(dir => ['dirname', dir])], - suffixExpression, + ['suffix', extensions], ); } else if (directoryFilters.length > 0) { // Use the `glob` generator and filter only by extension. @@ -285,7 +231,7 @@ module.exports = async function watchmanCrawl( query.glob_includedotfiles = true; queryGenerator = 'glob'; - query.expression.push(suffixExpression); + query.expression.push(['suffix', extensions]); } else { // Use the `suffix` generator with no path/extension filtering. query.suffix = extensions; diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index eaaac07aa0..b774de7b93 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -145,6 +145,12 @@ const PACKAGE_JSON = path.sep + 'package.json'; const VCS_DIRECTORIES = ['.git', '.hg'] .map(vcs => escapePathForRegex(path.sep + vcs + path.sep)) .join('|'); +const WATCHMAN_REQUIRED_CAPABILITIES = [ + 'field-content.sha1hex', + 'relative_root', + 'suffix-set', + 'wildmatch', +]; /** * HasteMap is a JavaScript implementation of Facebook's haste module system. @@ -1158,8 +1164,9 @@ export default class HasteMap extends EventEmitter { return false; } if (!this._canUseWatchmanPromise) { - // TODO: Ensure minimum capabilities here - this._canUseWatchmanPromise = checkWatchmanCapabilities([]) + this._canUseWatchmanPromise = checkWatchmanCapabilities( + WATCHMAN_REQUIRED_CAPABILITIES, + ) .then(() => true) .catch(e => { // TODO: Advise people to either install Watchman or set diff --git a/packages/metro-file-map/src/watchers/WatchmanWatcher.js b/packages/metro-file-map/src/watchers/WatchmanWatcher.js index 5d0f1ea610..f02c981145 100644 --- a/packages/metro-file-map/src/watchers/WatchmanWatcher.js +++ b/packages/metro-file-map/src/watchers/WatchmanWatcher.js @@ -70,23 +70,6 @@ WatchmanWatcher.prototype.init = function () { return self.watchProjectInfo ? self.watchProjectInfo.root : self.root; } - function onCapability(error, resp) { - if (handleError(self, error)) { - // The Watchman watcher is unusable on this system, we cannot continue - return; - } - - handleWarning(resp); - - self.capabilities = resp.capabilities; - - if (self.capabilities.relative_root) { - self.client.command(['watch-project', getWatchRoot()], onWatchProject); - } else { - self.client.command(['watch', getWatchRoot()], onWatch); - } - } - function onWatchProject(error, resp) { if (handleError(self, error)) { return; @@ -102,16 +85,6 @@ WatchmanWatcher.prototype.init = function () { self.client.command(['clock', getWatchRoot()], onClock); } - function onWatch(error, resp) { - if (handleError(self, error)) { - return; - } - - handleWarning(resp); - - self.client.command(['clock', getWatchRoot()], onClock); - } - function onClock(error, resp) { if (handleError(self, error)) { return; @@ -123,43 +96,19 @@ WatchmanWatcher.prototype.init = function () { fields: ['name', 'exists', 'new'], since: resp.clock, defer: self.watchmanDeferStates, + relative_root: self.watchProjectInfo.relativePath, }; - // If the server has the wildmatch capability available it supports - // the recursive **/*.foo style match and we can offload our globs - // to the watchman server. This saves both on data size to be - // communicated back to us and compute for evaluating the globs - // in our node process. - if (self.capabilities.wildmatch) { - if (self.globs.length === 0) { - if (!self.dot) { - // Make sure we honor the dot option if even we're not using globs. - options.expression = [ - 'match', - '**', - 'wholename', - { - includedotfiles: false, - }, - ]; - } - } else { - options.expression = ['anyof']; - for (const i in self.globs) { - options.expression.push([ - 'match', - self.globs[i], - 'wholename', - { - includedotfiles: self.dot, - }, - ]); - } - } - } - - if (self.capabilities.relative_root) { - options.relative_root = self.watchProjectInfo.relativePath; + // Make sure we honor the dot option if even we're not using globs. + if (self.globs.length === 0 && !self.dot) { + options.expression = [ + 'match', + '**', + 'wholename', + { + includedotfiles: false, + }, + ]; } self.client.command( @@ -178,12 +127,7 @@ WatchmanWatcher.prototype.init = function () { self.emit('ready'); } - self.client.capabilityCheck( - { - optional: ['wildmatch', 'relative_root'], - }, - onCapability, - ); + self.client.command(['watch-project', getWatchRoot()], onWatchProject); }; /** @@ -225,23 +169,16 @@ WatchmanWatcher.prototype.handleChangeEvent = function (resp) { WatchmanWatcher.prototype.handleFileChange = function (changeDescriptor) { const self = this; - let absPath; - let relativePath; - - if (this.capabilities.relative_root) { - relativePath = changeDescriptor.name; - absPath = path.join( - this.watchProjectInfo.root, - this.watchProjectInfo.relativePath, - relativePath, - ); - } else { - absPath = path.join(this.root, changeDescriptor.name); - relativePath = changeDescriptor.name; - } + + const relativePath = changeDescriptor.name; + const absPath = path.join( + this.watchProjectInfo.root, + this.watchProjectInfo.relativePath, + relativePath, + ); if ( - !(self.capabilities.wildmatch && !this.hasIgnore) && + this.hasIgnore && !common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath) ) { return;