Skip to content

Commit

Permalink
Drop support for Watchman <=4.9.0, remove dead code
Browse files Browse the repository at this point in the history
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
  • Loading branch information
robhogan authored and facebook-github-bot committed Sep 27, 2022
1 parent d831400 commit 422055a
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 242 deletions.
86 changes: 1 addition & 85 deletions packages/metro-file-map/src/crawlers/__tests__/watchman-test.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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: {
Expand Down
90 changes: 18 additions & 72 deletions packages/metro-file-map/src/crawlers/watchman.js
Expand Up @@ -29,17 +29,6 @@ type WatchmanQuery = any;

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

type WatchmanListCapabilitiesResponse = {
capabilities: Array<string>,
};

type WatchmanCapabilityCheckResponse = {
// { 'suffix-set': true }
capabilities: $ReadOnly<{[string]: boolean}>,
// '2021.06.07.00'
version: string,
};

type WatchmanWatchProjectResponse = {
watch: string,
relative_path: string,
Expand Down Expand Up @@ -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<string>}>,
): Promise<WatchmanCapabilityCheckResponse> {
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
Expand Down Expand Up @@ -164,18 +122,6 @@ module.exports = async function watchmanCrawl(
}
};

if (options.computeSha1) {
const {capabilities} = await cmd<WatchmanListCapabilitiesResponse>(
'list-capabilities',
);

if (capabilities.indexOf('field-content.sha1hex') !== -1) {
fields.push('content.sha1hex');
}
}

perfLogger?.point('watchmanCrawl/negotiateCapabilities_end');

async function getWatchmanRoots(
roots: $ReadOnlyArray<Path>,
): Promise<WatchmanRoots> {
Expand Down Expand Up @@ -277,15 +223,15 @@ 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.
query.glob = directoryFilters.map(directory => `${directory}/**`);
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;
Expand Down
11 changes: 9 additions & 2 deletions packages/metro-file-map/src/index.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 422055a

Please sign in to comment.