Skip to content

Commit defbc05

Browse files
authored
fix(jest-haste-map): Make watchman existence check lazy+async (#12675)
1 parent 06040d3 commit defbc05

File tree

5 files changed

+87
-22
lines changed

5 files changed

+87
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
- `[jest-environment-node]` Add `structuredClone` to globals ([#12631](https://github.com/facebook/jest/pull/12631))
7575
- `[@jest/expect-utils]` [**BREAKING**] Fix false positives when looking for `undefined` prop ([#8923](https://github.com/facebook/jest/pull/8923))
7676
- `[jest-haste-map]` Don't use partial results if file crawl errors ([#12420](https://github.com/facebook/jest/pull/12420))
77+
- `[jest-haste-map]` Make watchman existence check lazy+async ([#12675](https://github.com/facebook/jest/pull/12675))
7778
- `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125))
7879
- `[jest-jasmine2]` Do not set `duration` to `0` for skipped tests ([#12518](https://github.com/facebook/jest/pull/12518))
7980
- `[jest-matcher-utils]` Pass maxWidth to `pretty-format` to avoid printing every element in arrays by default ([#12402](https://github.com/facebook/jest/pull/12402))

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ function mockHashContents(contents) {
1313
return crypto.createHash('sha1').update(contents).digest('hex');
1414
}
1515

16-
jest.mock('child_process', () => ({
17-
// If this does not throw, we'll use the (mocked) watchman crawler
18-
execSync() {},
16+
const mockIsWatchmanInstalled = jest.fn().mockResolvedValue(true);
17+
18+
jest.mock('../lib/isWatchmanInstalled', () => ({
19+
__esModule: true,
20+
default: mockIsWatchmanInstalled,
1921
}));
2022

2123
jest.mock('jest-worker', () => ({
@@ -612,6 +614,8 @@ describe('HasteMap', () => {
612614
});
613615
});
614616

617+
mockIsWatchmanInstalled.mockClear();
618+
615619
const hasteMap = await HasteMap.create({
616620
...defaultConfig,
617621
computeSha1: true,
@@ -621,6 +625,10 @@ describe('HasteMap', () => {
621625

622626
const data = (await hasteMap.build()).__hasteMapForTest;
623627

628+
expect(mockIsWatchmanInstalled).toHaveBeenCalledTimes(
629+
useWatchman ? 1 : 0,
630+
);
631+
624632
expect(data.files).toEqual(
625633
createMap({
626634
[path.join('fruits', 'Banana.js')]: [

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

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
/* eslint-disable local/ban-types-eventually */
99

10-
import {execSync} from 'child_process';
1110
import {createHash} from 'crypto';
1211
import {EventEmitter} from 'events';
1312
import {tmpdir} from 'os';
@@ -26,6 +25,7 @@ import {watchmanCrawl} from './crawlers/watchman';
2625
import getMockName from './getMockName';
2726
import * as fastPath from './lib/fast_path';
2827
import getPlatformExtension from './lib/getPlatformExtension';
28+
import isWatchmanInstalled from './lib/isWatchmanInstalled';
2929
import normalizePathSep from './lib/normalizePathSep';
3030
import type {
3131
ChangeEvent,
@@ -124,14 +124,6 @@ const VCS_DIRECTORIES = ['.git', '.hg']
124124
.map(vcs => escapePathForRegex(path.sep + vcs + path.sep))
125125
.join('|');
126126

127-
const canUseWatchman = ((): boolean => {
128-
try {
129-
execSync('watchman --version', {stdio: ['ignore']});
130-
return true;
131-
} catch {}
132-
return false;
133-
})();
134-
135127
function invariant(condition: unknown, message?: string): asserts condition {
136128
if (!condition) {
137129
throw new Error(message);
@@ -221,6 +213,7 @@ export default class HasteMap extends EventEmitter {
221213
private _cachePath: string;
222214
private _changeInterval?: ReturnType<typeof setInterval>;
223215
private _console: Console;
216+
private _isWatchmanInstalledPromise: Promise<boolean> | null = null;
224217
private _options: InternalOptions;
225218
private _watchers: Array<Watcher>;
226219
private _worker: WorkerInterface | null;
@@ -763,11 +756,10 @@ export default class HasteMap extends EventEmitter {
763756
return this._worker;
764757
}
765758

766-
private _crawl(hasteMap: InternalHasteMap) {
759+
private async _crawl(hasteMap: InternalHasteMap) {
767760
const options = this._options;
768761
const ignore = this._ignore.bind(this);
769-
const crawl =
770-
canUseWatchman && this._options.useWatchman ? watchmanCrawl : nodeCrawl;
762+
const crawl = (await this._shouldUseWatchman()) ? watchmanCrawl : nodeCrawl;
771763
const crawlerOptions: CrawlerOptions = {
772764
computeSha1: options.computeSha1,
773765
data: hasteMap,
@@ -811,7 +803,7 @@ export default class HasteMap extends EventEmitter {
811803
/**
812804
* Watch mode
813805
*/
814-
private _watch(hasteMap: InternalHasteMap): Promise<void> {
806+
private async _watch(hasteMap: InternalHasteMap): Promise<void> {
815807
if (!this._options.watch) {
816808
return Promise.resolve();
817809
}
@@ -822,12 +814,11 @@ export default class HasteMap extends EventEmitter {
822814
this._options.retainAllFiles = true;
823815

824816
// WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
825-
const Watcher =
826-
canUseWatchman && this._options.useWatchman
827-
? WatchmanWatcher
828-
: FSEventsWatcher.isSupported()
829-
? FSEventsWatcher
830-
: NodeWatcher;
817+
const Watcher = (await this._shouldUseWatchman())
818+
? WatchmanWatcher
819+
: FSEventsWatcher.isSupported()
820+
? FSEventsWatcher
821+
: NodeWatcher;
831822

832823
const extensions = this._options.extensions;
833824
const ignorePattern = this._options.ignorePattern;
@@ -1110,6 +1101,16 @@ export default class HasteMap extends EventEmitter {
11101101
);
11111102
}
11121103

1104+
private async _shouldUseWatchman(): Promise<boolean> {
1105+
if (!this._options.useWatchman) {
1106+
return false;
1107+
}
1108+
if (!this._isWatchmanInstalledPromise) {
1109+
this._isWatchmanInstalledPromise = isWatchmanInstalled();
1110+
}
1111+
return this._isWatchmanInstalledPromise;
1112+
}
1113+
11131114
private _createEmptyMap(): InternalHasteMap {
11141115
return {
11151116
clocks: new Map(),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. 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 {execFile} from 'child_process';
9+
import isWatchmanInstalled from '../isWatchmanInstalled';
10+
11+
jest.mock('child_process');
12+
13+
describe('isWatchmanInstalled', () => {
14+
beforeEach(() => jest.clearAllMocks());
15+
16+
it('executes watchman --version and returns true on success', async () => {
17+
execFile.mockImplementation((file, args, cb) => {
18+
expect(file).toBe('watchman');
19+
expect(args).toStrictEqual(['--version']);
20+
cb(null, {stdout: 'v123'});
21+
});
22+
expect(await isWatchmanInstalled()).toBe(true);
23+
expect(execFile).toHaveBeenCalledWith(
24+
'watchman',
25+
['--version'],
26+
expect.any(Function),
27+
);
28+
});
29+
30+
it('returns false when execFile fails', async () => {
31+
execFile.mockImplementation((file, args, cb) => {
32+
cb(new Error());
33+
});
34+
expect(await isWatchmanInstalled()).toBe(false);
35+
expect(execFile).toHaveBeenCalled();
36+
});
37+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. 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 {execFile} from 'child_process';
9+
import {promisify} from 'util';
10+
11+
export default async function isWatchmanInstalled(): Promise<boolean> {
12+
try {
13+
await promisify(execFile)('watchman', ['--version']);
14+
return true;
15+
} catch {
16+
return false;
17+
}
18+
}

0 commit comments

Comments
 (0)