metro-file-map 0.73.1 → 0.73.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/ModuleMap.js.flow +2 -2
- package/src/Watcher.js +344 -0
- package/src/Watcher.js.flow +305 -0
- package/src/crawlers/node.js.flow +2 -1
- package/src/crawlers/watchman/index.js.flow +3 -3
- package/src/index.js +81 -161
- package/src/index.js.flow +79 -132
- package/src/watchers/FSEventsWatcher.js +4 -0
- package/src/watchers/FSEventsWatcher.js.flow +4 -0
- package/src/watchers/NodeWatcher.js +112 -141
- package/src/watchers/NodeWatcher.js.flow +347 -0
- package/src/watchers/WatchmanWatcher.js +37 -8
- package/src/watchers/WatchmanWatcher.js.flow +31 -8
- package/src/watchers/common.js.flow +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro-file-map",
|
|
3
|
-
"version": "0.73.
|
|
3
|
+
"version": "0.73.3",
|
|
4
4
|
"description": "[Experimental] - 🚇 File crawling, watching and mapping for Metro",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"jest-util": "^27.2.0",
|
|
25
25
|
"jest-worker": "^27.2.0",
|
|
26
26
|
"micromatch": "^4.0.4",
|
|
27
|
+
"nullthrows": "^1.1.1",
|
|
27
28
|
"walker": "^1.0.7"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
package/src/ModuleMap.js.flow
CHANGED
|
@@ -23,7 +23,7 @@ import H from './constants';
|
|
|
23
23
|
import * as fastPath from './lib/fast_path';
|
|
24
24
|
|
|
25
25
|
const EMPTY_OBJ: {[string]: ModuleMetaData} = {};
|
|
26
|
-
const EMPTY_MAP = new Map();
|
|
26
|
+
const EMPTY_MAP = new Map<'g' | 'native' | string, ?DuplicatesSet>();
|
|
27
27
|
|
|
28
28
|
export default class ModuleMap implements IModuleMap<SerializableModuleMap> {
|
|
29
29
|
static DuplicateHasteCandidatesError: Class<DuplicateHasteCandidatesError>;
|
|
@@ -191,7 +191,7 @@ export default class ModuleMap implements IModuleMap<SerializableModuleMap> {
|
|
|
191
191
|
}
|
|
192
192
|
// Force flow refinement
|
|
193
193
|
const previousSet = relativePathSet;
|
|
194
|
-
const duplicates = new Map();
|
|
194
|
+
const duplicates = new Map<string, number>();
|
|
195
195
|
|
|
196
196
|
for (const [relativePath, type] of previousSet) {
|
|
197
197
|
const duplicatePath = fastPath.resolve(this._raw.rootDir, relativePath);
|
package/src/Watcher.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true,
|
|
5
|
+
});
|
|
6
|
+
exports.Watcher = void 0;
|
|
7
|
+
|
|
8
|
+
var _watchman = _interopRequireDefault(require("./crawlers/watchman"));
|
|
9
|
+
|
|
10
|
+
var _node = _interopRequireDefault(require("./crawlers/node"));
|
|
11
|
+
|
|
12
|
+
var _WatchmanWatcher = _interopRequireDefault(
|
|
13
|
+
require("./watchers/WatchmanWatcher")
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
var _FSEventsWatcher = _interopRequireDefault(
|
|
17
|
+
require("./watchers/FSEventsWatcher")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
var _NodeWatcher = _interopRequireDefault(require("./watchers/NodeWatcher"));
|
|
21
|
+
|
|
22
|
+
var path = _interopRequireWildcard(require("path"));
|
|
23
|
+
|
|
24
|
+
var fs = _interopRequireWildcard(require("fs"));
|
|
25
|
+
|
|
26
|
+
var _common = require("./watchers/common");
|
|
27
|
+
|
|
28
|
+
var _perf_hooks = require("perf_hooks");
|
|
29
|
+
|
|
30
|
+
var _nullthrows = _interopRequireDefault(require("nullthrows"));
|
|
31
|
+
|
|
32
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
33
|
+
if (typeof WeakMap !== "function") return null;
|
|
34
|
+
var cacheBabelInterop = new WeakMap();
|
|
35
|
+
var cacheNodeInterop = new WeakMap();
|
|
36
|
+
return (_getRequireWildcardCache = function (nodeInterop) {
|
|
37
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
38
|
+
})(nodeInterop);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _interopRequireWildcard(obj, nodeInterop) {
|
|
42
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
45
|
+
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
|
|
46
|
+
return { default: obj };
|
|
47
|
+
}
|
|
48
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
49
|
+
if (cache && cache.has(obj)) {
|
|
50
|
+
return cache.get(obj);
|
|
51
|
+
}
|
|
52
|
+
var newObj = {};
|
|
53
|
+
var hasPropertyDescriptor =
|
|
54
|
+
Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
55
|
+
for (var key in obj) {
|
|
56
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
57
|
+
var desc = hasPropertyDescriptor
|
|
58
|
+
? Object.getOwnPropertyDescriptor(obj, key)
|
|
59
|
+
: null;
|
|
60
|
+
if (desc && (desc.get || desc.set)) {
|
|
61
|
+
Object.defineProperty(newObj, key, desc);
|
|
62
|
+
} else {
|
|
63
|
+
newObj[key] = obj[key];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
newObj.default = obj;
|
|
68
|
+
if (cache) {
|
|
69
|
+
cache.set(obj, newObj);
|
|
70
|
+
}
|
|
71
|
+
return newObj;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _interopRequireDefault(obj) {
|
|
75
|
+
return obj && obj.__esModule ? obj : { default: obj };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
80
|
+
*
|
|
81
|
+
* This source code is licensed under the MIT license found in the
|
|
82
|
+
* LICENSE file in the root directory of this source tree.
|
|
83
|
+
*
|
|
84
|
+
* @format
|
|
85
|
+
*
|
|
86
|
+
*/
|
|
87
|
+
// $FlowFixMe[untyped-import] - it's a fork: https://github.com/facebook/jest/pull/10919
|
|
88
|
+
const debug = require("debug")("Metro:Watcher");
|
|
89
|
+
|
|
90
|
+
const MAX_WAIT_TIME = 240000;
|
|
91
|
+
let nextInstanceId = 0;
|
|
92
|
+
|
|
93
|
+
class Watcher {
|
|
94
|
+
_backends = [];
|
|
95
|
+
_nextHealthCheckId = 0;
|
|
96
|
+
_pendingHealthChecks = new Map();
|
|
97
|
+
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this._options = options;
|
|
100
|
+
this._instanceId = nextInstanceId++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async crawl() {
|
|
104
|
+
var _this$_options$perfLo;
|
|
105
|
+
|
|
106
|
+
(_this$_options$perfLo = this._options.perfLogger) === null ||
|
|
107
|
+
_this$_options$perfLo === void 0
|
|
108
|
+
? void 0
|
|
109
|
+
: _this$_options$perfLo.point("crawl_start");
|
|
110
|
+
const options = this._options;
|
|
111
|
+
|
|
112
|
+
const ignore = (filePath) =>
|
|
113
|
+
options.ignore(filePath) ||
|
|
114
|
+
path.basename(filePath).startsWith(this._options.healthCheckFilePrefix);
|
|
115
|
+
|
|
116
|
+
const crawl = options.useWatchman ? _watchman.default : _node.default;
|
|
117
|
+
const crawlerOptions = {
|
|
118
|
+
abortSignal: options.abortSignal,
|
|
119
|
+
computeSha1: options.computeSha1,
|
|
120
|
+
data: options.initialData,
|
|
121
|
+
enableSymlinks: options.enableSymlinks,
|
|
122
|
+
extensions: options.extensions,
|
|
123
|
+
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
|
|
124
|
+
ignore,
|
|
125
|
+
perfLogger: options.perfLogger,
|
|
126
|
+
rootDir: options.rootDir,
|
|
127
|
+
roots: options.roots,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const retry = (error) => {
|
|
131
|
+
if (crawl === _watchman.default) {
|
|
132
|
+
options.console.warn(
|
|
133
|
+
"metro-file-map: Watchman crawl failed. Retrying once with node " +
|
|
134
|
+
"crawler.\n" +
|
|
135
|
+
" Usually this happens when watchman isn't running. Create an " +
|
|
136
|
+
"empty `.watchmanconfig` file in your project's root folder or " +
|
|
137
|
+
"initialize a git or hg repository in your project.\n" +
|
|
138
|
+
" " +
|
|
139
|
+
error.toString()
|
|
140
|
+
);
|
|
141
|
+
return (0, _node.default)(crawlerOptions).catch((e) => {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"Crawler retry failed:\n" +
|
|
144
|
+
` Original error: ${error.message}\n` +
|
|
145
|
+
` Retry error: ${e.message}\n`
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw error;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const logEnd = (result) => {
|
|
154
|
+
var _this$_options$perfLo2;
|
|
155
|
+
|
|
156
|
+
(_this$_options$perfLo2 = this._options.perfLogger) === null ||
|
|
157
|
+
_this$_options$perfLo2 === void 0
|
|
158
|
+
? void 0
|
|
159
|
+
: _this$_options$perfLo2.point("crawl_end");
|
|
160
|
+
return result;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
return crawl(crawlerOptions).catch(retry).then(logEnd);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return retry(error).then(logEnd);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async watch(onChange) {
|
|
171
|
+
var _this$_options$perfLo3;
|
|
172
|
+
|
|
173
|
+
const { extensions, ignorePattern, useWatchman } = this._options; // WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
|
|
174
|
+
|
|
175
|
+
const WatcherImpl = useWatchman
|
|
176
|
+
? _WatchmanWatcher.default
|
|
177
|
+
: _FSEventsWatcher.default.isSupported()
|
|
178
|
+
? _FSEventsWatcher.default
|
|
179
|
+
: _NodeWatcher.default;
|
|
180
|
+
let watcher = "node";
|
|
181
|
+
|
|
182
|
+
if (WatcherImpl === _WatchmanWatcher.default) {
|
|
183
|
+
watcher = "watchman";
|
|
184
|
+
} else if (WatcherImpl === _FSEventsWatcher.default) {
|
|
185
|
+
watcher = "fsevents";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
debug(`Using watcher: ${watcher}`);
|
|
189
|
+
(_this$_options$perfLo3 = this._options.perfLogger) === null ||
|
|
190
|
+
_this$_options$perfLo3 === void 0
|
|
191
|
+
? void 0
|
|
192
|
+
: _this$_options$perfLo3.annotate({
|
|
193
|
+
string: {
|
|
194
|
+
watcher,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
this._activeWatcher = watcher;
|
|
198
|
+
|
|
199
|
+
const createWatcherBackend = (root) => {
|
|
200
|
+
const watcherOptions = {
|
|
201
|
+
dot: true,
|
|
202
|
+
glob: [
|
|
203
|
+
// Ensure we always include package.json files, which are crucial for
|
|
204
|
+
/// module resolution.
|
|
205
|
+
"**/package.json", // Ensure we always watch any health check files
|
|
206
|
+
"**/" + this._options.healthCheckFilePrefix + "*",
|
|
207
|
+
...extensions.map((extension) => "**/*." + extension),
|
|
208
|
+
],
|
|
209
|
+
ignored: ignorePattern,
|
|
210
|
+
watchmanDeferStates: this._options.watchmanDeferStates,
|
|
211
|
+
};
|
|
212
|
+
const watcher = new WatcherImpl(root, watcherOptions);
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const rejectTimeout = setTimeout(
|
|
215
|
+
() => reject(new Error("Failed to start watch mode.")),
|
|
216
|
+
MAX_WAIT_TIME
|
|
217
|
+
);
|
|
218
|
+
watcher.once("ready", () => {
|
|
219
|
+
clearTimeout(rejectTimeout);
|
|
220
|
+
watcher.on("all", (type, filePath, root, stat) => {
|
|
221
|
+
const basename = path.basename(filePath);
|
|
222
|
+
|
|
223
|
+
if (basename.startsWith(this._options.healthCheckFilePrefix)) {
|
|
224
|
+
if (type === _common.ADD_EVENT || type === _common.CHANGE_EVENT) {
|
|
225
|
+
debug(
|
|
226
|
+
"Observed possible health check cookie: %s in %s",
|
|
227
|
+
filePath,
|
|
228
|
+
root
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
this._handleHealthCheckObservation(basename);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
onChange(type, filePath, root, stat);
|
|
238
|
+
});
|
|
239
|
+
resolve(watcher);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
this._backends = await Promise.all(
|
|
245
|
+
this._options.roots.map(createWatcherBackend)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_handleHealthCheckObservation(basename) {
|
|
250
|
+
const resolveHealthCheck = this._pendingHealthChecks.get(basename);
|
|
251
|
+
|
|
252
|
+
if (!resolveHealthCheck) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
resolveHealthCheck();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async close() {
|
|
260
|
+
await Promise.all(this._backends.map((watcher) => watcher.close()));
|
|
261
|
+
this._activeWatcher = null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async checkHealth(timeout) {
|
|
265
|
+
const healthCheckId = this._nextHealthCheckId++;
|
|
266
|
+
|
|
267
|
+
if (healthCheckId === Number.MAX_SAFE_INTEGER) {
|
|
268
|
+
this._nextHealthCheckId = 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const watcher = this._activeWatcher;
|
|
272
|
+
const basename =
|
|
273
|
+
this._options.healthCheckFilePrefix +
|
|
274
|
+
"-" +
|
|
275
|
+
process.pid +
|
|
276
|
+
"-" +
|
|
277
|
+
this._instanceId +
|
|
278
|
+
"-" +
|
|
279
|
+
healthCheckId;
|
|
280
|
+
const healthCheckPath = path.join(this._options.rootDir, basename);
|
|
281
|
+
let result;
|
|
282
|
+
const timeoutPromise = new Promise((resolve) =>
|
|
283
|
+
setTimeout(resolve, timeout)
|
|
284
|
+
).then(() => {
|
|
285
|
+
if (!result) {
|
|
286
|
+
var _this$_backends$;
|
|
287
|
+
|
|
288
|
+
result = {
|
|
289
|
+
type: "timeout",
|
|
290
|
+
pauseReason:
|
|
291
|
+
(_this$_backends$ = this._backends[0]) === null ||
|
|
292
|
+
_this$_backends$ === void 0
|
|
293
|
+
? void 0
|
|
294
|
+
: _this$_backends$.getPauseReason(),
|
|
295
|
+
timeout,
|
|
296
|
+
watcher,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const startTime = _perf_hooks.performance.now();
|
|
302
|
+
|
|
303
|
+
debug("Creating health check cookie: %s", healthCheckPath);
|
|
304
|
+
const creationPromise = fs.promises
|
|
305
|
+
.writeFile(healthCheckPath, String(startTime))
|
|
306
|
+
.catch((error) => {
|
|
307
|
+
if (!result) {
|
|
308
|
+
result = {
|
|
309
|
+
type: "error",
|
|
310
|
+
error,
|
|
311
|
+
timeout,
|
|
312
|
+
watcher,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
const observationPromise = new Promise((resolve) => {
|
|
317
|
+
this._pendingHealthChecks.set(basename, resolve);
|
|
318
|
+
}).then(() => {
|
|
319
|
+
if (!result) {
|
|
320
|
+
result = {
|
|
321
|
+
type: "success",
|
|
322
|
+
timeElapsed: _perf_hooks.performance.now() - startTime,
|
|
323
|
+
timeout,
|
|
324
|
+
watcher,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
await Promise.race([
|
|
329
|
+
timeoutPromise,
|
|
330
|
+
creationPromise.then(() => observationPromise),
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
this._pendingHealthChecks.delete(basename); // Chain a deletion to the creation promise (which may not have even settled yet!),
|
|
334
|
+
// don't await it, and swallow errors. This is just best-effort cleanup.
|
|
335
|
+
|
|
336
|
+
creationPromise.then(() =>
|
|
337
|
+
fs.promises.unlink(healthCheckPath).catch(() => {})
|
|
338
|
+
);
|
|
339
|
+
debug("Health check result: %o", result);
|
|
340
|
+
return (0, _nullthrows.default)(result);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
exports.Watcher = Watcher;
|
|
@@ -0,0 +1,305 @@
|
|
|
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
|
+
import * as path from 'path';
|
|
29
|
+
import * as fs from 'fs';
|
|
30
|
+
import {ADD_EVENT, CHANGE_EVENT} from './watchers/common';
|
|
31
|
+
import {performance} from 'perf_hooks';
|
|
32
|
+
import nullthrows from 'nullthrows';
|
|
33
|
+
|
|
34
|
+
const debug = require('debug')('Metro:Watcher');
|
|
35
|
+
|
|
36
|
+
const MAX_WAIT_TIME = 240000;
|
|
37
|
+
|
|
38
|
+
type WatcherOptions = {
|
|
39
|
+
abortSignal: AbortSignal,
|
|
40
|
+
computeSha1: boolean,
|
|
41
|
+
console: Console,
|
|
42
|
+
enableSymlinks: boolean,
|
|
43
|
+
extensions: $ReadOnlyArray<string>,
|
|
44
|
+
forceNodeFilesystemAPI: boolean,
|
|
45
|
+
healthCheckFilePrefix: string,
|
|
46
|
+
ignore: string => boolean,
|
|
47
|
+
ignorePattern: RegExp,
|
|
48
|
+
initialData: InternalData,
|
|
49
|
+
perfLogger: ?PerfLogger,
|
|
50
|
+
roots: $ReadOnlyArray<string>,
|
|
51
|
+
rootDir: string,
|
|
52
|
+
useWatchman: boolean,
|
|
53
|
+
watch: boolean,
|
|
54
|
+
watchmanDeferStates: $ReadOnlyArray<string>,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface WatcherBackend {
|
|
58
|
+
getPauseReason(): ?string;
|
|
59
|
+
close(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let nextInstanceId = 0;
|
|
63
|
+
|
|
64
|
+
export type HealthCheckResult =
|
|
65
|
+
| {type: 'error', timeout: number, error: Error, watcher: ?string}
|
|
66
|
+
| {type: 'success', timeout: number, timeElapsed: number, watcher: ?string}
|
|
67
|
+
| {type: 'timeout', timeout: number, watcher: ?string, pauseReason: ?string};
|
|
68
|
+
|
|
69
|
+
export class Watcher {
|
|
70
|
+
_options: WatcherOptions;
|
|
71
|
+
_backends: $ReadOnlyArray<WatcherBackend> = [];
|
|
72
|
+
_instanceId: number;
|
|
73
|
+
_nextHealthCheckId: number = 0;
|
|
74
|
+
_pendingHealthChecks: Map</* basename */ string, /* resolve */ () => void> =
|
|
75
|
+
new Map();
|
|
76
|
+
_activeWatcher: ?string;
|
|
77
|
+
|
|
78
|
+
constructor(options: WatcherOptions) {
|
|
79
|
+
this._options = options;
|
|
80
|
+
this._instanceId = nextInstanceId++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async crawl(): Promise<?(
|
|
84
|
+
| Promise<{
|
|
85
|
+
changedFiles?: FileData,
|
|
86
|
+
hasteMap: InternalData,
|
|
87
|
+
removedFiles: FileData,
|
|
88
|
+
}>
|
|
89
|
+
| {changedFiles?: FileData, hasteMap: InternalData, removedFiles: FileData}
|
|
90
|
+
)> {
|
|
91
|
+
this._options.perfLogger?.point('crawl_start');
|
|
92
|
+
|
|
93
|
+
const options = this._options;
|
|
94
|
+
const ignore = (filePath: string) =>
|
|
95
|
+
options.ignore(filePath) ||
|
|
96
|
+
path.basename(filePath).startsWith(this._options.healthCheckFilePrefix);
|
|
97
|
+
const crawl = options.useWatchman ? watchmanCrawl : nodeCrawl;
|
|
98
|
+
const crawlerOptions: CrawlerOptions = {
|
|
99
|
+
abortSignal: options.abortSignal,
|
|
100
|
+
computeSha1: options.computeSha1,
|
|
101
|
+
data: options.initialData,
|
|
102
|
+
enableSymlinks: options.enableSymlinks,
|
|
103
|
+
extensions: options.extensions,
|
|
104
|
+
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
|
|
105
|
+
ignore,
|
|
106
|
+
perfLogger: options.perfLogger,
|
|
107
|
+
rootDir: options.rootDir,
|
|
108
|
+
roots: options.roots,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const retry = (error: Error) => {
|
|
112
|
+
if (crawl === watchmanCrawl) {
|
|
113
|
+
options.console.warn(
|
|
114
|
+
'metro-file-map: Watchman crawl failed. Retrying once with node ' +
|
|
115
|
+
'crawler.\n' +
|
|
116
|
+
" Usually this happens when watchman isn't running. Create an " +
|
|
117
|
+
"empty `.watchmanconfig` file in your project's root folder or " +
|
|
118
|
+
'initialize a git or hg repository in your project.\n' +
|
|
119
|
+
' ' +
|
|
120
|
+
error.toString(),
|
|
121
|
+
);
|
|
122
|
+
return nodeCrawl(crawlerOptions).catch(e => {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'Crawler retry failed:\n' +
|
|
125
|
+
` Original error: ${error.message}\n` +
|
|
126
|
+
` Retry error: ${e.message}\n`,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw error;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const logEnd = <T>(result: T): T => {
|
|
135
|
+
this._options.perfLogger?.point('crawl_end');
|
|
136
|
+
return result;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
return crawl(crawlerOptions).catch(retry).then(logEnd);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return retry(error).then(logEnd);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async watch(
|
|
147
|
+
onChange: (
|
|
148
|
+
type: string,
|
|
149
|
+
filePath: string,
|
|
150
|
+
root: string,
|
|
151
|
+
stat?: Stats,
|
|
152
|
+
) => void,
|
|
153
|
+
) {
|
|
154
|
+
const {extensions, ignorePattern, useWatchman} = this._options;
|
|
155
|
+
|
|
156
|
+
// WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
|
|
157
|
+
const WatcherImpl = useWatchman
|
|
158
|
+
? WatchmanWatcher
|
|
159
|
+
: FSEventsWatcher.isSupported()
|
|
160
|
+
? FSEventsWatcher
|
|
161
|
+
: NodeWatcher;
|
|
162
|
+
|
|
163
|
+
let watcher = 'node';
|
|
164
|
+
if (WatcherImpl === WatchmanWatcher) {
|
|
165
|
+
watcher = 'watchman';
|
|
166
|
+
} else if (WatcherImpl === FSEventsWatcher) {
|
|
167
|
+
watcher = 'fsevents';
|
|
168
|
+
}
|
|
169
|
+
debug(`Using watcher: ${watcher}`);
|
|
170
|
+
this._options.perfLogger?.annotate({string: {watcher}});
|
|
171
|
+
this._activeWatcher = watcher;
|
|
172
|
+
|
|
173
|
+
const createWatcherBackend = (root: Path): Promise<WatcherBackend> => {
|
|
174
|
+
const watcherOptions: WatcherBackendOptions = {
|
|
175
|
+
dot: true,
|
|
176
|
+
glob: [
|
|
177
|
+
// Ensure we always include package.json files, which are crucial for
|
|
178
|
+
/// module resolution.
|
|
179
|
+
'**/package.json',
|
|
180
|
+
// Ensure we always watch any health check files
|
|
181
|
+
'**/' + this._options.healthCheckFilePrefix + '*',
|
|
182
|
+
...extensions.map(extension => '**/*.' + extension),
|
|
183
|
+
],
|
|
184
|
+
ignored: ignorePattern,
|
|
185
|
+
watchmanDeferStates: this._options.watchmanDeferStates,
|
|
186
|
+
};
|
|
187
|
+
const watcher = new WatcherImpl(root, watcherOptions);
|
|
188
|
+
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const rejectTimeout = setTimeout(
|
|
191
|
+
() => reject(new Error('Failed to start watch mode.')),
|
|
192
|
+
MAX_WAIT_TIME,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
watcher.once('ready', () => {
|
|
196
|
+
clearTimeout(rejectTimeout);
|
|
197
|
+
watcher.on(
|
|
198
|
+
'all',
|
|
199
|
+
(type: string, filePath: string, root: string, stat?: Stats) => {
|
|
200
|
+
const basename = path.basename(filePath);
|
|
201
|
+
if (basename.startsWith(this._options.healthCheckFilePrefix)) {
|
|
202
|
+
if (type === ADD_EVENT || type === CHANGE_EVENT) {
|
|
203
|
+
debug(
|
|
204
|
+
'Observed possible health check cookie: %s in %s',
|
|
205
|
+
filePath,
|
|
206
|
+
root,
|
|
207
|
+
);
|
|
208
|
+
this._handleHealthCheckObservation(basename);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
onChange(type, filePath, root, stat);
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
resolve(watcher);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
this._backends = await Promise.all(
|
|
221
|
+
this._options.roots.map(createWatcherBackend),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_handleHealthCheckObservation(basename: string) {
|
|
226
|
+
const resolveHealthCheck = this._pendingHealthChecks.get(basename);
|
|
227
|
+
if (!resolveHealthCheck) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
resolveHealthCheck();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async close() {
|
|
234
|
+
await Promise.all(this._backends.map(watcher => watcher.close()));
|
|
235
|
+
this._activeWatcher = null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async checkHealth(timeout: number): Promise<HealthCheckResult> {
|
|
239
|
+
const healthCheckId = this._nextHealthCheckId++;
|
|
240
|
+
if (healthCheckId === Number.MAX_SAFE_INTEGER) {
|
|
241
|
+
this._nextHealthCheckId = 0;
|
|
242
|
+
}
|
|
243
|
+
const watcher = this._activeWatcher;
|
|
244
|
+
const basename =
|
|
245
|
+
this._options.healthCheckFilePrefix +
|
|
246
|
+
'-' +
|
|
247
|
+
process.pid +
|
|
248
|
+
'-' +
|
|
249
|
+
this._instanceId +
|
|
250
|
+
'-' +
|
|
251
|
+
healthCheckId;
|
|
252
|
+
const healthCheckPath = path.join(this._options.rootDir, basename);
|
|
253
|
+
let result: ?HealthCheckResult;
|
|
254
|
+
const timeoutPromise = new Promise(resolve =>
|
|
255
|
+
setTimeout(resolve, timeout),
|
|
256
|
+
).then(() => {
|
|
257
|
+
if (!result) {
|
|
258
|
+
result = {
|
|
259
|
+
type: 'timeout',
|
|
260
|
+
pauseReason: this._backends[0]?.getPauseReason(),
|
|
261
|
+
timeout,
|
|
262
|
+
watcher,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
const startTime = performance.now();
|
|
267
|
+
debug('Creating health check cookie: %s', healthCheckPath);
|
|
268
|
+
const creationPromise = fs.promises
|
|
269
|
+
.writeFile(healthCheckPath, String(startTime))
|
|
270
|
+
.catch(error => {
|
|
271
|
+
if (!result) {
|
|
272
|
+
result = {
|
|
273
|
+
type: 'error',
|
|
274
|
+
error,
|
|
275
|
+
timeout,
|
|
276
|
+
watcher,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
const observationPromise = new Promise(resolve => {
|
|
281
|
+
this._pendingHealthChecks.set(basename, resolve);
|
|
282
|
+
}).then(() => {
|
|
283
|
+
if (!result) {
|
|
284
|
+
result = {
|
|
285
|
+
type: 'success',
|
|
286
|
+
timeElapsed: performance.now() - startTime,
|
|
287
|
+
timeout,
|
|
288
|
+
watcher,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
await Promise.race([
|
|
293
|
+
timeoutPromise,
|
|
294
|
+
creationPromise.then(() => observationPromise),
|
|
295
|
+
]);
|
|
296
|
+
this._pendingHealthChecks.delete(basename);
|
|
297
|
+
// Chain a deletion to the creation promise (which may not have even settled yet!),
|
|
298
|
+
// don't await it, and swallow errors. This is just best-effort cleanup.
|
|
299
|
+
creationPromise.then(() =>
|
|
300
|
+
fs.promises.unlink(healthCheckPath).catch(() => {}),
|
|
301
|
+
);
|
|
302
|
+
debug('Health check result: %o', result);
|
|
303
|
+
return nullthrows(result);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @oncall react_native
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type {Path, FileMetaData} from '../flow-types';
|
|
12
13
|
import type {
|
|
13
14
|
CrawlerOptions,
|
|
14
15
|
FileData,
|
|
@@ -224,7 +225,7 @@ module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{
|
|
|
224
225
|
|
|
225
226
|
return new Promise(resolve => {
|
|
226
227
|
const callback = (list: Result) => {
|
|
227
|
-
const files = new Map();
|
|
228
|
+
const files = new Map<Path, FileMetaData>();
|
|
228
229
|
const removedFiles = new Map(data.files);
|
|
229
230
|
list.forEach(fileData => {
|
|
230
231
|
const [filePath, mtime, size] = fileData;
|