Skip to content

Commit dc02eac

Browse files
robhoganfacebook-github-bot
authored andcommitted
Extract some crawl and watch management out of index.js
Summary: Refactoring to begin the work of bringing crawl+watch closer together. This simply pulls some code out of the bloated `index.js` into a `Watcher` class, which is responsible for managing crawl and watch backends. The API is likely to change, but for now sticks to `crawl()` / `watch()` as separate instance methods (reflecting the current architecture) to minimise the churn in this diff. Changelog: Internal Reviewed By: motiz88 Differential Revision: D39891465 fbshipit-source-id: 8e3616847380bea7acf7117479a0a9a90526e19e
1 parent 5177582 commit dc02eac

File tree

2 files changed

+230
-132
lines changed

2 files changed

+230
-132
lines changed
+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
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+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
import type {
12+
Console,
13+
CrawlerOptions,
14+
FileData,
15+
InternalData,
16+
Path,
17+
PerfLogger,
18+
} from './flow-types';
19+
import type {WatcherOptions as WatcherBackendOptions} from './watchers/common';
20+
import type {Stats} from 'fs';
21+
22+
import watchmanCrawl from './crawlers/watchman';
23+
import nodeCrawl from './crawlers/node';
24+
import WatchmanWatcher from './watchers/WatchmanWatcher';
25+
import FSEventsWatcher from './watchers/FSEventsWatcher';
26+
// $FlowFixMe[untyped-import] - it's a fork: https://github.com/facebook/jest/pull/10919
27+
import NodeWatcher from './watchers/NodeWatcher';
28+
29+
const debug = require('debug')('Metro:Watcher');
30+
31+
const MAX_WAIT_TIME = 240000;
32+
33+
type WatcherOptions = {
34+
abortSignal: AbortSignal,
35+
computeSha1: boolean,
36+
console: Console,
37+
enableSymlinks: boolean,
38+
extensions: $ReadOnlyArray<string>,
39+
forceNodeFilesystemAPI: boolean,
40+
ignore: string => boolean,
41+
ignorePattern: RegExp,
42+
initialData: InternalData,
43+
perfLogger: ?PerfLogger,
44+
roots: $ReadOnlyArray<string>,
45+
rootDir: string,
46+
useWatchman: boolean,
47+
watch: boolean,
48+
watchmanDeferStates: $ReadOnlyArray<string>,
49+
};
50+
51+
interface WatcherBackend {
52+
close(): Promise<void>;
53+
}
54+
55+
export class Watcher {
56+
_options: WatcherOptions;
57+
_backends: $ReadOnlyArray<WatcherBackend> = [];
58+
59+
constructor(options: WatcherOptions) {
60+
this._options = options;
61+
}
62+
63+
async crawl(): Promise<?(
64+
| Promise<{
65+
changedFiles?: FileData,
66+
hasteMap: InternalData,
67+
removedFiles: FileData,
68+
}>
69+
| {changedFiles?: FileData, hasteMap: InternalData, removedFiles: FileData}
70+
)> {
71+
this._options.perfLogger?.point('crawl_start');
72+
73+
const options = this._options;
74+
const ignore = (filePath: string) => options.ignore(filePath);
75+
const crawl = options.useWatchman ? watchmanCrawl : nodeCrawl;
76+
const crawlerOptions: CrawlerOptions = {
77+
abortSignal: options.abortSignal,
78+
computeSha1: options.computeSha1,
79+
data: options.initialData,
80+
enableSymlinks: options.enableSymlinks,
81+
extensions: options.extensions,
82+
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
83+
ignore,
84+
perfLogger: options.perfLogger,
85+
rootDir: options.rootDir,
86+
roots: options.roots,
87+
};
88+
89+
const retry = (error: Error) => {
90+
if (crawl === watchmanCrawl) {
91+
options.console.warn(
92+
'metro-file-map: Watchman crawl failed. Retrying once with node ' +
93+
'crawler.\n' +
94+
" Usually this happens when watchman isn't running. Create an " +
95+
"empty `.watchmanconfig` file in your project's root folder or " +
96+
'initialize a git or hg repository in your project.\n' +
97+
' ' +
98+
error.toString(),
99+
);
100+
return nodeCrawl(crawlerOptions).catch(e => {
101+
throw new Error(
102+
'Crawler retry failed:\n' +
103+
` Original error: ${error.message}\n` +
104+
` Retry error: ${e.message}\n`,
105+
);
106+
});
107+
}
108+
109+
throw error;
110+
};
111+
112+
const logEnd = <T>(result: T): T => {
113+
this._options.perfLogger?.point('crawl_end');
114+
return result;
115+
};
116+
117+
try {
118+
return crawl(crawlerOptions).catch(retry).then(logEnd);
119+
} catch (error) {
120+
return retry(error).then(logEnd);
121+
}
122+
}
123+
124+
async watch(
125+
onChange: (
126+
type: string,
127+
filePath: string,
128+
root: string,
129+
stat?: Stats,
130+
) => void,
131+
) {
132+
const {extensions, ignorePattern, useWatchman} = this._options;
133+
134+
// WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
135+
const WatcherImpl = useWatchman
136+
? WatchmanWatcher
137+
: FSEventsWatcher.isSupported()
138+
? FSEventsWatcher
139+
: NodeWatcher;
140+
141+
let watcher = 'node';
142+
if (WatcherImpl === WatchmanWatcher) {
143+
watcher = 'watchman';
144+
} else if (WatcherImpl === FSEventsWatcher) {
145+
watcher = 'fsevents';
146+
}
147+
debug(`Using watcher: ${watcher}`);
148+
this._options.perfLogger?.annotate({string: {watcher}});
149+
150+
const createWatcherBackend = (root: Path): Promise<WatcherBackend> => {
151+
const watcherOptions: WatcherBackendOptions = {
152+
dot: true,
153+
glob: [
154+
// Ensure we always include package.json files, which are crucial for
155+
/// module resolution.
156+
'**/package.json',
157+
...extensions.map(extension => '**/*.' + extension),
158+
],
159+
ignored: ignorePattern,
160+
watchmanDeferStates: this._options.watchmanDeferStates,
161+
};
162+
const watcher = new WatcherImpl(root, watcherOptions);
163+
164+
return new Promise((resolve, reject) => {
165+
const rejectTimeout = setTimeout(
166+
() => reject(new Error('Failed to start watch mode.')),
167+
MAX_WAIT_TIME,
168+
);
169+
170+
watcher.once('ready', () => {
171+
clearTimeout(rejectTimeout);
172+
watcher.on('all', onChange);
173+
resolve(watcher);
174+
});
175+
});
176+
};
177+
178+
this._backends = await Promise.all(
179+
this._options.roots.map(createWatcherBackend),
180+
);
181+
}
182+
183+
async close() {
184+
await Promise.all(this._backends.map(watcher => watcher.close()));
185+
}
186+
}

0 commit comments

Comments
 (0)