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;