@expo/metro-file-map 55.0.0-0
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/README.md +1 -0
- package/build/Watcher.d.ts +53 -0
- package/build/Watcher.js +249 -0
- package/build/cache/DiskCacheManager.d.ts +25 -0
- package/build/cache/DiskCacheManager.js +110 -0
- package/build/constants.d.ts +23 -0
- package/build/constants.js +27 -0
- package/build/crawlers/node/fallback.d.ts +20 -0
- package/build/crawlers/node/fallback.js +148 -0
- package/build/crawlers/node/index.d.ts +10 -0
- package/build/crawlers/node/index.js +152 -0
- package/build/crawlers/watchman/index.d.ts +8 -0
- package/build/crawlers/watchman/index.js +288 -0
- package/build/crawlers/watchman/planQuery.d.ts +31 -0
- package/build/crawlers/watchman/planQuery.js +99 -0
- package/build/index.d.ts +140 -0
- package/build/index.js +836 -0
- package/build/lib/FileProcessor.d.ts +42 -0
- package/build/lib/FileProcessor.js +180 -0
- package/build/lib/FileSystemChangeAggregator.d.ts +18 -0
- package/build/lib/FileSystemChangeAggregator.js +114 -0
- package/build/lib/RootPathUtils.d.ts +24 -0
- package/build/lib/RootPathUtils.js +311 -0
- package/build/lib/TreeFS.d.ts +161 -0
- package/build/lib/TreeFS.js +1006 -0
- package/build/lib/checkWatchmanCapabilities.d.ts +9 -0
- package/build/lib/checkWatchmanCapabilities.js +50 -0
- package/build/lib/normalizePathSeparatorsToPosix.d.ts +8 -0
- package/build/lib/normalizePathSeparatorsToPosix.js +20 -0
- package/build/lib/normalizePathSeparatorsToSystem.d.ts +8 -0
- package/build/lib/normalizePathSeparatorsToSystem.js +20 -0
- package/build/lib/removeOverlappingRoots.d.ts +10 -0
- package/build/lib/removeOverlappingRoots.js +37 -0
- package/build/lib/rootRelativeCacheKeys.d.ts +11 -0
- package/build/lib/rootRelativeCacheKeys.js +56 -0
- package/build/lib/sorting.d.ts +8 -0
- package/build/lib/sorting.js +31 -0
- package/build/plugins/DependencyPlugin.d.ts +23 -0
- package/build/plugins/DependencyPlugin.js +61 -0
- package/build/plugins/FileDataPlugin.d.ts +28 -0
- package/build/plugins/FileDataPlugin.js +46 -0
- package/build/plugins/HastePlugin.d.ts +32 -0
- package/build/plugins/HastePlugin.js +358 -0
- package/build/plugins/MockPlugin.d.ts +27 -0
- package/build/plugins/MockPlugin.js +157 -0
- package/build/plugins/dependencies/dependencyExtractor.d.ts +7 -0
- package/build/plugins/dependencies/dependencyExtractor.js +66 -0
- package/build/plugins/dependencies/worker.d.ts +18 -0
- package/build/plugins/dependencies/worker.js +30 -0
- package/build/plugins/haste/DuplicateHasteCandidatesError.d.ts +14 -0
- package/build/plugins/haste/DuplicateHasteCandidatesError.js +51 -0
- package/build/plugins/haste/HasteConflictsError.d.ts +12 -0
- package/build/plugins/haste/HasteConflictsError.js +49 -0
- package/build/plugins/haste/computeConflicts.d.ts +19 -0
- package/build/plugins/haste/computeConflicts.js +74 -0
- package/build/plugins/haste/getPlatformExtension.d.ts +7 -0
- package/build/plugins/haste/getPlatformExtension.js +19 -0
- package/build/plugins/haste/worker.d.ts +16 -0
- package/build/plugins/haste/worker.js +48 -0
- package/build/plugins/mocks/getMockName.d.ts +8 -0
- package/build/plugins/mocks/getMockName.js +17 -0
- package/build/ts-declarations/fb-watchman.d.ts +20 -0
- package/build/ts-declarations/fb-watchman.js +2 -0
- package/build/types.d.ts +404 -0
- package/build/types.js +8 -0
- package/build/watchers/AbstractWatcher.d.ts +30 -0
- package/build/watchers/AbstractWatcher.js +97 -0
- package/build/watchers/FallbackWatcher.d.ts +18 -0
- package/build/watchers/FallbackWatcher.js +411 -0
- package/build/watchers/NativeWatcher.d.ts +39 -0
- package/build/watchers/NativeWatcher.js +156 -0
- package/build/watchers/RecrawlWarning.d.ts +18 -0
- package/build/watchers/RecrawlWarning.js +59 -0
- package/build/watchers/WatchmanWatcher.d.ts +22 -0
- package/build/watchers/WatchmanWatcher.js +263 -0
- package/build/watchers/common.d.ts +39 -0
- package/build/watchers/common.js +60 -0
- package/build/worker.d.ts +24 -0
- package/build/worker.js +80 -0
- package/build/workerExclusionList.d.ts +8 -0
- package/build/workerExclusionList.js +57 -0
- package/package.json +54 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.HastePlugin = exports.HasteConflictsError = exports.DuplicateHasteCandidatesError = exports.DependencyPlugin = exports.DiskCacheManager = void 0;
|
|
46
|
+
const events_1 = __importDefault(require("events"));
|
|
47
|
+
const fs_1 = require("fs");
|
|
48
|
+
const invariant_1 = __importDefault(require("invariant"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
51
|
+
const Watcher_1 = require("./Watcher");
|
|
52
|
+
const DiskCacheManager_1 = require("./cache/DiskCacheManager");
|
|
53
|
+
const constants_1 = __importDefault(require("./constants"));
|
|
54
|
+
const fallback_1 = __importDefault(require("./crawlers/node/fallback"));
|
|
55
|
+
const FileProcessor_1 = require("./lib/FileProcessor");
|
|
56
|
+
const FileSystemChangeAggregator_1 = require("./lib/FileSystemChangeAggregator");
|
|
57
|
+
const RootPathUtils_1 = require("./lib/RootPathUtils");
|
|
58
|
+
const TreeFS_1 = __importDefault(require("./lib/TreeFS"));
|
|
59
|
+
const checkWatchmanCapabilities_1 = __importDefault(require("./lib/checkWatchmanCapabilities"));
|
|
60
|
+
const normalizePathSeparatorsToPosix_1 = __importDefault(require("./lib/normalizePathSeparatorsToPosix"));
|
|
61
|
+
const normalizePathSeparatorsToSystem_1 = __importDefault(require("./lib/normalizePathSeparatorsToSystem"));
|
|
62
|
+
const removeOverlappingRoots_1 = __importDefault(require("./lib/removeOverlappingRoots"));
|
|
63
|
+
const debug = require('debug')('Metro:FileMap');
|
|
64
|
+
var DiskCacheManager_2 = require("./cache/DiskCacheManager");
|
|
65
|
+
Object.defineProperty(exports, "DiskCacheManager", { enumerable: true, get: function () { return DiskCacheManager_2.DiskCacheManager; } });
|
|
66
|
+
var DependencyPlugin_1 = require("./plugins/DependencyPlugin");
|
|
67
|
+
Object.defineProperty(exports, "DependencyPlugin", { enumerable: true, get: function () { return __importDefault(DependencyPlugin_1).default; } });
|
|
68
|
+
var DuplicateHasteCandidatesError_1 = require("./plugins/haste/DuplicateHasteCandidatesError");
|
|
69
|
+
Object.defineProperty(exports, "DuplicateHasteCandidatesError", { enumerable: true, get: function () { return DuplicateHasteCandidatesError_1.DuplicateHasteCandidatesError; } });
|
|
70
|
+
var HasteConflictsError_1 = require("./plugins/haste/HasteConflictsError");
|
|
71
|
+
Object.defineProperty(exports, "HasteConflictsError", { enumerable: true, get: function () { return HasteConflictsError_1.HasteConflictsError; } });
|
|
72
|
+
var HastePlugin_1 = require("./plugins/HastePlugin");
|
|
73
|
+
Object.defineProperty(exports, "HastePlugin", { enumerable: true, get: function () { return __importDefault(HastePlugin_1).default; } });
|
|
74
|
+
// This should be bumped whenever a code change to `metro-file-map` itself
|
|
75
|
+
// would cause a change to the cache data structure and/or content (for a given
|
|
76
|
+
// filesystem state and build parameters).
|
|
77
|
+
const CACHE_BREAKER = '12';
|
|
78
|
+
const CHANGE_INTERVAL = 30;
|
|
79
|
+
const NODE_MODULES = path.sep + 'node_modules' + path.sep;
|
|
80
|
+
const VCS_DIRECTORIES = /[/\\]\.(git|hg)[/\\]/.source;
|
|
81
|
+
const WATCHMAN_REQUIRED_CAPABILITIES = [
|
|
82
|
+
'field-content.sha1hex',
|
|
83
|
+
'relative_root',
|
|
84
|
+
'suffix-set',
|
|
85
|
+
'wildmatch',
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* FileMap includes a JavaScript implementation of Facebook's haste module system.
|
|
89
|
+
*
|
|
90
|
+
* This implementation is inspired by https://github.com/facebook/node-haste
|
|
91
|
+
* and was built with for high-performance in large code repositories with
|
|
92
|
+
* hundreds of thousands of files. This implementation is scalable and provides
|
|
93
|
+
* predictable performance.
|
|
94
|
+
*
|
|
95
|
+
* Because the file map creation and synchronization is critical to startup
|
|
96
|
+
* performance and most tasks are blocked by I/O this class makes heavy use of
|
|
97
|
+
* synchronous operations. It uses worker processes for parallelizing file
|
|
98
|
+
* access and metadata extraction.
|
|
99
|
+
*
|
|
100
|
+
* The data structures created by `metro-file-map` can be used directly from the
|
|
101
|
+
* cache without further processing. The metadata objects in the `files` and
|
|
102
|
+
* `map` objects contain cross-references: a metadata object from one can look
|
|
103
|
+
* up the corresponding metadata object in the other map. Note that in most
|
|
104
|
+
* projects, the number of files will be greater than the number of haste
|
|
105
|
+
* modules one module can refer to many files based on platform extensions.
|
|
106
|
+
*
|
|
107
|
+
* type CacheData = {
|
|
108
|
+
* clocks: WatchmanClocks,
|
|
109
|
+
* files: {[filepath: string]: FileMetadata},
|
|
110
|
+
* map: {[id: string]: HasteMapItem},
|
|
111
|
+
* mocks: {[id: string]: string},
|
|
112
|
+
* }
|
|
113
|
+
*
|
|
114
|
+
* // Watchman clocks are used for query synchronization and file system deltas.
|
|
115
|
+
* type WatchmanClocks = {[filepath: string]: string};
|
|
116
|
+
*
|
|
117
|
+
* type FileMetadata = {
|
|
118
|
+
* id: ?string, // used to look up module metadata objects in `map`.
|
|
119
|
+
* mtime: number, // check for outdated files.
|
|
120
|
+
* size: number, // size of the file in bytes.
|
|
121
|
+
* visited: boolean, // whether the file has been parsed or not.
|
|
122
|
+
* dependencies: Array<string>, // all relative dependencies of this file.
|
|
123
|
+
* sha1: ?string, // SHA-1 of the file, if requested via options.
|
|
124
|
+
* symlink: ?(1 | 0 | string), // Truthy if symlink, string is target
|
|
125
|
+
* };
|
|
126
|
+
*
|
|
127
|
+
* // Modules can be targeted to a specific platform based on the file name.
|
|
128
|
+
* // Example: platform.ios.js and Platform.android.js will both map to the same
|
|
129
|
+
* // `Platform` module. The platform should be specified during resolution.
|
|
130
|
+
* type HasteMapItem = {[platform: string]: ModuleMetadata};
|
|
131
|
+
*
|
|
132
|
+
* //
|
|
133
|
+
* type ModuleMetadata = {
|
|
134
|
+
* path: string, // the path to look up the file object in `files`.
|
|
135
|
+
* type: string, // the module type (either `package` or `module`).
|
|
136
|
+
* };
|
|
137
|
+
*
|
|
138
|
+
* Note that the data structures described above are conceptual only. The actual
|
|
139
|
+
* implementation uses arrays and constant keys for metadata storage. Instead of
|
|
140
|
+
* `{id: 'flatMap', mtime: 3421, size: 42, visited: true, dependencies: []}` the real
|
|
141
|
+
* representation is similar to `['flatMap', 3421, 42, 1, []]` to save storage space
|
|
142
|
+
* and reduce parse and write time of a big JSON blob.
|
|
143
|
+
*
|
|
144
|
+
* The FileMap is created as follows:
|
|
145
|
+
* 1. read data from the cache or create an empty structure.
|
|
146
|
+
*
|
|
147
|
+
* 2. crawl the file system.
|
|
148
|
+
* * empty cache: crawl the entire file system.
|
|
149
|
+
* * cache available:
|
|
150
|
+
* * if watchman is available: get file system delta changes.
|
|
151
|
+
* * if watchman is unavailable: crawl the entire file system.
|
|
152
|
+
* * build metadata objects for every file. This builds the `files` part of
|
|
153
|
+
* the `FileMap`.
|
|
154
|
+
*
|
|
155
|
+
* 3. visit and extract metadata from changed files, including sha1,
|
|
156
|
+
* depedendencies, and any plugins.
|
|
157
|
+
* * this is done in parallel over worker processes to improve performance.
|
|
158
|
+
* * the worst case is to visit all files.
|
|
159
|
+
* * the best case is no file system access and retrieving all data from
|
|
160
|
+
* the cache.
|
|
161
|
+
* * the average case is a small number of changed files.
|
|
162
|
+
*
|
|
163
|
+
* 4. serialize the new `FileMap` in a cache file.
|
|
164
|
+
*
|
|
165
|
+
*/
|
|
166
|
+
class FileMap extends events_1.default {
|
|
167
|
+
// NOTE(@kitten): Expo brand to recognize patched `metro-file-map -> @expo/metro-file-map`
|
|
168
|
+
__expo = true;
|
|
169
|
+
#buildPromise;
|
|
170
|
+
#cacheManager;
|
|
171
|
+
#canUseWatchmanPromise;
|
|
172
|
+
#changeID;
|
|
173
|
+
#changeInterval;
|
|
174
|
+
#console;
|
|
175
|
+
#crawlerAbortController;
|
|
176
|
+
#fileProcessor;
|
|
177
|
+
#healthCheckInterval;
|
|
178
|
+
#options;
|
|
179
|
+
#pathUtils;
|
|
180
|
+
#plugins;
|
|
181
|
+
#startupPerfLogger;
|
|
182
|
+
#watcher;
|
|
183
|
+
static create(options) {
|
|
184
|
+
return new FileMap(options);
|
|
185
|
+
}
|
|
186
|
+
constructor(options) {
|
|
187
|
+
super();
|
|
188
|
+
if (options.perfLoggerFactory) {
|
|
189
|
+
this.#startupPerfLogger = options.perfLoggerFactory?.('START_UP').subSpan('fileMap') ?? null;
|
|
190
|
+
this.#startupPerfLogger?.point('constructor_start');
|
|
191
|
+
}
|
|
192
|
+
// Add VCS_DIRECTORIES to provided ignorePattern
|
|
193
|
+
let ignorePattern;
|
|
194
|
+
if (options.ignorePattern) {
|
|
195
|
+
const inputIgnorePattern = options.ignorePattern;
|
|
196
|
+
if (inputIgnorePattern instanceof RegExp) {
|
|
197
|
+
ignorePattern = new RegExp(inputIgnorePattern.source.concat('|' + VCS_DIRECTORIES), inputIgnorePattern.flags);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
throw new Error('metro-file-map: the `ignorePattern` option must be a RegExp');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
ignorePattern = new RegExp(VCS_DIRECTORIES);
|
|
205
|
+
}
|
|
206
|
+
this.#console = options.console || globalThis.console;
|
|
207
|
+
let dataSlot = constants_1.default.PLUGINDATA;
|
|
208
|
+
const indexedPlugins = [];
|
|
209
|
+
const pluginWorkers = [];
|
|
210
|
+
const plugins = options.plugins ?? [];
|
|
211
|
+
for (const plugin of plugins) {
|
|
212
|
+
const maybeWorker = plugin.getWorker();
|
|
213
|
+
indexedPlugins.push({
|
|
214
|
+
plugin,
|
|
215
|
+
dataIdx: maybeWorker != null ? dataSlot++ : null,
|
|
216
|
+
});
|
|
217
|
+
if (maybeWorker != null) {
|
|
218
|
+
pluginWorkers.push(maybeWorker);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.#plugins = indexedPlugins;
|
|
222
|
+
const enableFallback = options.enableFallback ?? true;
|
|
223
|
+
const scopeFallback = options.scopeFallback ?? true;
|
|
224
|
+
const buildParameters = {
|
|
225
|
+
cacheBreaker: CACHE_BREAKER,
|
|
226
|
+
computeSha1: options.computeSha1 || false,
|
|
227
|
+
enableSymlinks: options.enableSymlinks || false,
|
|
228
|
+
extensions: options.extensions,
|
|
229
|
+
forceNodeFilesystemAPI: !!options.forceNodeFilesystemAPI,
|
|
230
|
+
ignorePattern,
|
|
231
|
+
plugins,
|
|
232
|
+
retainAllFiles: options.retainAllFiles,
|
|
233
|
+
rootDir: options.rootDir,
|
|
234
|
+
roots: (0, removeOverlappingRoots_1.default)(options.roots),
|
|
235
|
+
};
|
|
236
|
+
this.#options = {
|
|
237
|
+
...buildParameters,
|
|
238
|
+
healthCheck: options.healthCheck,
|
|
239
|
+
perfLoggerFactory: options.perfLoggerFactory,
|
|
240
|
+
resetCache: options.resetCache,
|
|
241
|
+
useWatchman: options.useWatchman ?? false,
|
|
242
|
+
watch: !!options.watch,
|
|
243
|
+
watchmanDeferStates: options.watchmanDeferStates ?? [],
|
|
244
|
+
enableFallback,
|
|
245
|
+
scopeFallback: enableFallback && scopeFallback,
|
|
246
|
+
serverRoot: options.serverRoot,
|
|
247
|
+
};
|
|
248
|
+
const cacheFactoryOptions = {
|
|
249
|
+
buildParameters,
|
|
250
|
+
};
|
|
251
|
+
this.#cacheManager = options.cacheManagerFactory
|
|
252
|
+
? options.cacheManagerFactory.call(null, cacheFactoryOptions)
|
|
253
|
+
: new DiskCacheManager_1.DiskCacheManager(cacheFactoryOptions, {});
|
|
254
|
+
this.#fileProcessor = new FileProcessor_1.FileProcessor({
|
|
255
|
+
maxFilesPerWorker: options.maxFilesPerWorker,
|
|
256
|
+
maxWorkers: options.maxWorkers,
|
|
257
|
+
perfLogger: this.#startupPerfLogger,
|
|
258
|
+
pluginWorkers,
|
|
259
|
+
rootDir: options.rootDir,
|
|
260
|
+
});
|
|
261
|
+
this.#buildPromise = null;
|
|
262
|
+
this.#pathUtils = new RootPathUtils_1.RootPathUtils(options.rootDir);
|
|
263
|
+
this.#startupPerfLogger?.point('constructor_end');
|
|
264
|
+
this.#crawlerAbortController = new AbortController();
|
|
265
|
+
this.#changeID = 0;
|
|
266
|
+
}
|
|
267
|
+
build() {
|
|
268
|
+
this.#startupPerfLogger?.point('build_start');
|
|
269
|
+
if (!this.#buildPromise) {
|
|
270
|
+
this.#buildPromise = (async () => {
|
|
271
|
+
let initialData;
|
|
272
|
+
if (this.#options.resetCache !== true) {
|
|
273
|
+
initialData = await this.read();
|
|
274
|
+
}
|
|
275
|
+
if (!initialData) {
|
|
276
|
+
debug('Not using a cache');
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
debug('Cache loaded (%d clock(s))', initialData.clocks.size);
|
|
280
|
+
}
|
|
281
|
+
const rootDir = this.#options.rootDir;
|
|
282
|
+
this.#startupPerfLogger?.point('constructFileSystem_start');
|
|
283
|
+
const processFile = async (normalPath, metadata, opts) => {
|
|
284
|
+
const result = await this.#fileProcessor.processRegularFile(normalPath, metadata, {
|
|
285
|
+
computeSha1: opts.computeSha1,
|
|
286
|
+
maybeReturnContent: true,
|
|
287
|
+
});
|
|
288
|
+
debug('Lazily processed file: %s', normalPath);
|
|
289
|
+
// Emit an event to inform caches that there is new data to save.
|
|
290
|
+
this.emit('metadata');
|
|
291
|
+
return result?.content;
|
|
292
|
+
};
|
|
293
|
+
const fallbackFilesystem = this.#options.enableFallback
|
|
294
|
+
? (0, fallback_1.default)({
|
|
295
|
+
rootPathUtils: this.#pathUtils,
|
|
296
|
+
extensions: this.#options.extensions,
|
|
297
|
+
ignore: (filePath) => this.#options.ignorePattern.test(filePath),
|
|
298
|
+
includeSymlinks: this.#options.enableSymlinks,
|
|
299
|
+
})
|
|
300
|
+
: null;
|
|
301
|
+
const { roots } = this.#options;
|
|
302
|
+
const serverRoot = this.#options.scopeFallback ? this.#options.serverRoot : null;
|
|
303
|
+
const fileSystem = initialData != null
|
|
304
|
+
? TreeFS_1.default.fromDeserializedSnapshot({
|
|
305
|
+
// Typed `mixed` because we've read this from an external
|
|
306
|
+
// source. It'd be too expensive to validate at runtime, so
|
|
307
|
+
// trust our cache manager that this is correct.
|
|
308
|
+
fileSystemData: initialData.fileSystemData,
|
|
309
|
+
processFile,
|
|
310
|
+
rootDir,
|
|
311
|
+
fallbackFilesystem,
|
|
312
|
+
roots,
|
|
313
|
+
serverRoot,
|
|
314
|
+
})
|
|
315
|
+
: new TreeFS_1.default({
|
|
316
|
+
processFile,
|
|
317
|
+
rootDir,
|
|
318
|
+
fallbackFilesystem,
|
|
319
|
+
roots,
|
|
320
|
+
serverRoot,
|
|
321
|
+
});
|
|
322
|
+
this.#startupPerfLogger?.point('constructFileSystem_end');
|
|
323
|
+
const plugins = this.#plugins;
|
|
324
|
+
// Initialize plugins from cached file system and plugin state while
|
|
325
|
+
// crawling to build a diff of current state vs cached. `fileSystem`
|
|
326
|
+
// is not mutated during either operation.
|
|
327
|
+
const [fileDelta] = await Promise.all([
|
|
328
|
+
this.#buildFileDelta({
|
|
329
|
+
clocks: initialData?.clocks ?? new Map(),
|
|
330
|
+
fileSystem,
|
|
331
|
+
}),
|
|
332
|
+
Promise.all(plugins.map(({ plugin, dataIdx }) => plugin.initialize({
|
|
333
|
+
files: {
|
|
334
|
+
lookup: (mixedPath) => {
|
|
335
|
+
const result = fileSystem.lookup(mixedPath);
|
|
336
|
+
if (!result.exists) {
|
|
337
|
+
return { exists: false };
|
|
338
|
+
}
|
|
339
|
+
if (result.type === 'd') {
|
|
340
|
+
return { exists: true, type: 'd' };
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
exists: true,
|
|
344
|
+
type: 'f',
|
|
345
|
+
pluginData: dataIdx != null ? result.metadata[dataIdx] : null,
|
|
346
|
+
};
|
|
347
|
+
},
|
|
348
|
+
fileIterator: (opts) => mapIterable(fileSystem.metadataIterator(opts), ({ baseName, canonicalPath, metadata }) => ({
|
|
349
|
+
baseName,
|
|
350
|
+
canonicalPath,
|
|
351
|
+
pluginData: dataIdx != null ? metadata[dataIdx] : null,
|
|
352
|
+
})),
|
|
353
|
+
},
|
|
354
|
+
pluginState: initialData?.plugins.get(plugin.name),
|
|
355
|
+
}))),
|
|
356
|
+
]);
|
|
357
|
+
// Update `fileSystem` and plugins based on the file delta.
|
|
358
|
+
const actualChanges = await this.#applyFileDelta(fileSystem, plugins, fileDelta);
|
|
359
|
+
const changeSize = actualChanges.getSize();
|
|
360
|
+
// Validate plugins before persisting them.
|
|
361
|
+
plugins.forEach(({ plugin }) => plugin.assertValid());
|
|
362
|
+
const watchmanClocks = new Map('clocks' in fileDelta ? fileDelta.clocks : []);
|
|
363
|
+
await this.#takeSnapshotAndPersist(fileSystem, watchmanClocks, plugins, changeSize > 0);
|
|
364
|
+
debug('Finished mapping files (%d changes).', changeSize);
|
|
365
|
+
await this.#watch(fileSystem, watchmanClocks, plugins);
|
|
366
|
+
return { fileSystem };
|
|
367
|
+
})();
|
|
368
|
+
}
|
|
369
|
+
return this.#buildPromise.then((result) => {
|
|
370
|
+
this.#startupPerfLogger?.point('build_end');
|
|
371
|
+
return result;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 1. read data from the cache or create an empty structure.
|
|
376
|
+
*/
|
|
377
|
+
async read() {
|
|
378
|
+
let data;
|
|
379
|
+
this.#startupPerfLogger?.point('read_start');
|
|
380
|
+
try {
|
|
381
|
+
data = await this.#cacheManager.read();
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
this.#console.warn('Error while reading cache, falling back to a full crawl:\n', e);
|
|
385
|
+
this.#startupPerfLogger?.annotate({
|
|
386
|
+
string: { cacheReadError: e.toString() },
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
this.#startupPerfLogger?.point('read_end');
|
|
390
|
+
return data;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 2. crawl the file system.
|
|
394
|
+
*/
|
|
395
|
+
async #buildFileDelta(previousState) {
|
|
396
|
+
this.#startupPerfLogger?.point('buildFileDelta_start');
|
|
397
|
+
const { computeSha1, enableSymlinks, extensions, forceNodeFilesystemAPI, ignorePattern, retainAllFiles, roots, rootDir, watch, watchmanDeferStates, } = this.#options;
|
|
398
|
+
this.#watcher = new Watcher_1.Watcher({
|
|
399
|
+
abortSignal: this.#crawlerAbortController.signal,
|
|
400
|
+
computeSha1,
|
|
401
|
+
console: this.#console,
|
|
402
|
+
enableSymlinks,
|
|
403
|
+
extensions,
|
|
404
|
+
forceNodeFilesystemAPI,
|
|
405
|
+
healthCheckFilePrefix: this.#options.healthCheck.filePrefix,
|
|
406
|
+
// TODO: Refactor out the two different ignore strategies here.
|
|
407
|
+
ignoreForCrawl: (filePath) => {
|
|
408
|
+
const ignoreMatched = ignorePattern.test(filePath);
|
|
409
|
+
return ignoreMatched || (!retainAllFiles && filePath.includes(NODE_MODULES));
|
|
410
|
+
},
|
|
411
|
+
ignorePatternForWatch: ignorePattern,
|
|
412
|
+
perfLogger: this.#startupPerfLogger,
|
|
413
|
+
previousState,
|
|
414
|
+
rootDir,
|
|
415
|
+
roots,
|
|
416
|
+
useWatchman: await this.#shouldUseWatchman(),
|
|
417
|
+
watch,
|
|
418
|
+
watchmanDeferStates,
|
|
419
|
+
});
|
|
420
|
+
const watcher = this.#watcher;
|
|
421
|
+
watcher.on('status', (status) => this.emit('status', status));
|
|
422
|
+
const result = await watcher.crawl();
|
|
423
|
+
this.#startupPerfLogger?.point('buildFileDelta_end');
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
#maybeReadLink(normalPath, fileMetadata) {
|
|
427
|
+
// If we only need to read a link, it's more efficient to do it in-band
|
|
428
|
+
// (with async file IO) than to have the overhead of worker IO.
|
|
429
|
+
if (fileMetadata[constants_1.default.SYMLINK] === 1) {
|
|
430
|
+
return fs_1.promises
|
|
431
|
+
.readlink(this.#pathUtils.normalToAbsolute(normalPath))
|
|
432
|
+
.then((symlinkTarget) => {
|
|
433
|
+
fileMetadata[constants_1.default.VISITED] = 1;
|
|
434
|
+
fileMetadata[constants_1.default.SYMLINK] = (0, normalizePathSeparatorsToPosix_1.default)(this.#pathUtils.resolveSymlinkToNormal(normalPath, symlinkTarget));
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
async #applyFileDelta(fileSystem, plugins, delta) {
|
|
440
|
+
this.#startupPerfLogger?.point('applyFileDelta_start');
|
|
441
|
+
const { changedFiles, removedFiles } = delta;
|
|
442
|
+
this.#startupPerfLogger?.point('applyFileDelta_preprocess_start');
|
|
443
|
+
// Remove files first so that we don't mistake moved modules
|
|
444
|
+
// modules as duplicates.
|
|
445
|
+
this.#startupPerfLogger?.point('applyFileDelta_remove_start');
|
|
446
|
+
const changeAggregator = new FileSystemChangeAggregator_1.FileSystemChangeAggregator();
|
|
447
|
+
for (const relativeFilePath of removedFiles) {
|
|
448
|
+
fileSystem.remove(relativeFilePath, changeAggregator);
|
|
449
|
+
}
|
|
450
|
+
this.#startupPerfLogger?.point('applyFileDelta_remove_end');
|
|
451
|
+
const readLinkPromises = [];
|
|
452
|
+
const readLinkErrors = [];
|
|
453
|
+
const filesToProcess = [];
|
|
454
|
+
for (const [normalFilePath, fileData] of changedFiles) {
|
|
455
|
+
// A crawler may preserve the H.VISITED flag to indicate that the file
|
|
456
|
+
// contents are unchaged and it doesn't need visiting again.
|
|
457
|
+
if (fileData[constants_1.default.VISITED] === 1) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (fileData[constants_1.default.SYMLINK] === 0) {
|
|
461
|
+
filesToProcess.push([normalFilePath, fileData]);
|
|
462
|
+
}
|
|
463
|
+
else if (fileData[constants_1.default.MTIME] != null && fileData[constants_1.default.MTIME] !== 0) {
|
|
464
|
+
// Symlink was previously resolved and its mtime changed — resolve
|
|
465
|
+
// eagerly to update the cached target. Symlinks with null mtime
|
|
466
|
+
// (cold start or never accessed) are deferred to lazy resolution
|
|
467
|
+
// in TreeFS.#resolveSymlinkTargetToNormalPath.
|
|
468
|
+
const maybeReadLink = this.#maybeReadLink(normalFilePath, fileData);
|
|
469
|
+
if (maybeReadLink) {
|
|
470
|
+
readLinkPromises.push(maybeReadLink.catch((error) => {
|
|
471
|
+
readLinkErrors.push({ normalFilePath, error });
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
this.#startupPerfLogger?.point('applyFileDelta_preprocess_end');
|
|
477
|
+
debug('Found %d added/modified files and %d symlinks.', filesToProcess.length, readLinkPromises.length);
|
|
478
|
+
this.#startupPerfLogger?.point('applyFileDelta_process_start');
|
|
479
|
+
const [batchResult] = await Promise.all([
|
|
480
|
+
this.#fileProcessor.processBatch(filesToProcess, {
|
|
481
|
+
computeSha1: this.#options.computeSha1,
|
|
482
|
+
maybeReturnContent: false,
|
|
483
|
+
}),
|
|
484
|
+
Promise.all(readLinkPromises),
|
|
485
|
+
]);
|
|
486
|
+
this.#startupPerfLogger?.point('applyFileDelta_process_end');
|
|
487
|
+
// It's possible that a file could be deleted between being seen by the
|
|
488
|
+
// crawler and our attempt to process it. For our purposes, this is
|
|
489
|
+
// equivalent to the file being deleted before the crawl, being absent
|
|
490
|
+
// from `changedFiles`, and (if we loaded from cache, and the file
|
|
491
|
+
// existed previously) possibly being reported in `removedFiles`.
|
|
492
|
+
//
|
|
493
|
+
// Treat the file accordingly - don't add it to `FileSystem`, and remove
|
|
494
|
+
// it if it already exists. We're not emitting events at this point in
|
|
495
|
+
// startup, so there's nothing more to do.
|
|
496
|
+
this.#startupPerfLogger?.point('applyFileDelta_missing_start');
|
|
497
|
+
for (const { normalFilePath, error } of batchResult.errors.concat(readLinkErrors)) {
|
|
498
|
+
if (['ENOENT', 'EACCESS'].includes(error.code ?? '')) {
|
|
499
|
+
delta.changedFiles.delete(normalFilePath);
|
|
500
|
+
fileSystem.remove(normalFilePath, changeAggregator);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
// Anything else is fatal.
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
this.#startupPerfLogger?.point('applyFileDelta_missing_end');
|
|
508
|
+
this.#startupPerfLogger?.point('applyFileDelta_add_start');
|
|
509
|
+
fileSystem.bulkAddOrModify(changedFiles, changeAggregator);
|
|
510
|
+
this.#startupPerfLogger?.point('applyFileDelta_add_end');
|
|
511
|
+
this.#startupPerfLogger?.point('applyFileDelta_updatePlugins_start');
|
|
512
|
+
this.#plugins.forEach(({ plugin, dataIdx }) => {
|
|
513
|
+
plugin.onChanged(changeAggregator.getMappedView(dataIdx != null ? (metadata) => metadata[dataIdx] : () => null));
|
|
514
|
+
});
|
|
515
|
+
this.#startupPerfLogger?.point('applyFileDelta_updatePlugins_end');
|
|
516
|
+
this.#startupPerfLogger?.point('applyFileDelta_end');
|
|
517
|
+
return changeAggregator;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* 4. Serialize a snapshot of our raw data via the configured cache manager
|
|
521
|
+
*/
|
|
522
|
+
async #takeSnapshotAndPersist(fileSystem, clocks, plugins, changedSinceCacheRead) {
|
|
523
|
+
this.#startupPerfLogger?.point('persist_start');
|
|
524
|
+
await this.#cacheManager.write(() => ({
|
|
525
|
+
clocks: new Map(clocks),
|
|
526
|
+
fileSystemData: fileSystem.getSerializableSnapshot(),
|
|
527
|
+
plugins: new Map(plugins.map(({ plugin }) => [plugin.name, plugin.getSerializableSnapshot()])),
|
|
528
|
+
}), {
|
|
529
|
+
changedSinceCacheRead,
|
|
530
|
+
eventSource: {
|
|
531
|
+
onChange: (cb) => {
|
|
532
|
+
// Inform the cache about changes to internal state, including:
|
|
533
|
+
// - File system changes
|
|
534
|
+
this.on('change', cb);
|
|
535
|
+
// - Changes to stored metadata, e.g. on lazy processing.
|
|
536
|
+
this.on('metadata', cb);
|
|
537
|
+
return () => {
|
|
538
|
+
this.removeListener('change', cb);
|
|
539
|
+
this.removeListener('metadata', cb);
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
onWriteError: (error) => {
|
|
544
|
+
this.#console.warn('[metro-file-map] Cache write error\n:', error);
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
this.#startupPerfLogger?.point('persist_end');
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Watch mode
|
|
551
|
+
*/
|
|
552
|
+
async #watch(fileSystem, clocks, plugins) {
|
|
553
|
+
this.#startupPerfLogger?.point('watch_start');
|
|
554
|
+
if (!this.#options.watch) {
|
|
555
|
+
this.#startupPerfLogger?.point('watch_end');
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const hasWatchedExtension = (filePath) => this.#options.extensions.some((ext) => filePath.endsWith(ext));
|
|
559
|
+
let nextEmit = null;
|
|
560
|
+
const emitChange = () => {
|
|
561
|
+
if (nextEmit == null) {
|
|
562
|
+
// Nothing to emit
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const { events, firstEventTimestamp, firstEnqueuedTimestamp } = nextEmit;
|
|
566
|
+
const changeAggregator = new FileSystemChangeAggregator_1.FileSystemChangeAggregator();
|
|
567
|
+
// Process a sequence of events. Note that preserving ordering is
|
|
568
|
+
// important here - a file may be both removed and added in the same
|
|
569
|
+
// batch.
|
|
570
|
+
// `changeAggregator` flattens this over time into the net change from
|
|
571
|
+
// this sequence.
|
|
572
|
+
for (const event of events) {
|
|
573
|
+
const { relativeFilePath, clock } = event;
|
|
574
|
+
if (event.type === 'delete') {
|
|
575
|
+
fileSystem.remove(relativeFilePath, changeAggregator);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
fileSystem.addOrModify(relativeFilePath, event.metadata, changeAggregator);
|
|
579
|
+
}
|
|
580
|
+
this.#updateClock(clocks, clock);
|
|
581
|
+
}
|
|
582
|
+
const changeSize = changeAggregator.getSize();
|
|
583
|
+
if (changeSize === 0) {
|
|
584
|
+
// We had events, but they've exactly cancelled each other out, reset
|
|
585
|
+
// so that timers are correct for the next change.
|
|
586
|
+
nextEmit = null;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.#plugins.forEach(({ plugin, dataIdx }) => {
|
|
590
|
+
plugin.onChanged(changeAggregator.getMappedView(dataIdx != null ? (metadata) => metadata[dataIdx] : () => null));
|
|
591
|
+
});
|
|
592
|
+
const toPublicMetadata = (metadata) => ({
|
|
593
|
+
isSymlink: metadata[constants_1.default.SYMLINK] !== 0,
|
|
594
|
+
modifiedTime: metadata[constants_1.default.MTIME] ?? null,
|
|
595
|
+
});
|
|
596
|
+
const changesWithMetadata = changeAggregator.getMappedView(toPublicMetadata);
|
|
597
|
+
const hmrPerfLogger = this.#options.perfLoggerFactory?.('HMR', {
|
|
598
|
+
key: this.#getNextChangeID(),
|
|
599
|
+
});
|
|
600
|
+
if (hmrPerfLogger != null) {
|
|
601
|
+
hmrPerfLogger.start({ timestamp: firstEventTimestamp });
|
|
602
|
+
hmrPerfLogger.point('waitingForChangeInterval_start', {
|
|
603
|
+
timestamp: firstEnqueuedTimestamp,
|
|
604
|
+
});
|
|
605
|
+
hmrPerfLogger.point('waitingForChangeInterval_end');
|
|
606
|
+
hmrPerfLogger.annotate({ int: { changeSize } });
|
|
607
|
+
hmrPerfLogger.point('fileChange_start');
|
|
608
|
+
}
|
|
609
|
+
const changeEvent = {
|
|
610
|
+
changes: changesWithMetadata,
|
|
611
|
+
logger: hmrPerfLogger,
|
|
612
|
+
rootDir: this.#options.rootDir,
|
|
613
|
+
};
|
|
614
|
+
this.emit('change', changeEvent);
|
|
615
|
+
nextEmit = null;
|
|
616
|
+
};
|
|
617
|
+
let changeQueue = Promise.resolve();
|
|
618
|
+
const onChange = (change) => {
|
|
619
|
+
// Recrawl events bypass normal filtering - they trigger a full subdirectory scan
|
|
620
|
+
if (change.event !== 'recrawl' &&
|
|
621
|
+
change.metadata &&
|
|
622
|
+
// Ignore all directory events
|
|
623
|
+
(change.metadata.type === 'd' ||
|
|
624
|
+
// Ignore regular files with unwatched extensions
|
|
625
|
+
(change.metadata.type === 'f' && !hasWatchedExtension(change.relativePath)) ||
|
|
626
|
+
// Don't emit events relating to symlinks if enableSymlinks: false
|
|
627
|
+
(!this.#options.enableSymlinks && change.metadata?.type === 'l'))) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const absoluteFilePath = path.join(change.root, (0, normalizePathSeparatorsToSystem_1.default)(change.relativePath));
|
|
631
|
+
// Ignore files (including symlinks) whose path matches ignorePattern
|
|
632
|
+
// (we don't ignore node_modules in watch mode)
|
|
633
|
+
if (this.#options.ignorePattern.test(absoluteFilePath)) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const relativeFilePath = this.#pathUtils.absoluteToNormal(absoluteFilePath);
|
|
637
|
+
const onChangeStartTime = perf_hooks_1.performance.timeOrigin + perf_hooks_1.performance.now();
|
|
638
|
+
const enqueueEvent = (event) => {
|
|
639
|
+
nextEmit ??= {
|
|
640
|
+
events: [],
|
|
641
|
+
firstEnqueuedTimestamp: perf_hooks_1.performance.timeOrigin + perf_hooks_1.performance.now(),
|
|
642
|
+
firstEventTimestamp: onChangeStartTime,
|
|
643
|
+
};
|
|
644
|
+
nextEmit.events.push(event);
|
|
645
|
+
};
|
|
646
|
+
changeQueue = changeQueue
|
|
647
|
+
.then(async () => {
|
|
648
|
+
// If we get duplicate events for the same file, ignore them.
|
|
649
|
+
if (nextEmit != null &&
|
|
650
|
+
nextEmit.events.find((event) => event.type === change.event &&
|
|
651
|
+
event.relativeFilePath === relativeFilePath &&
|
|
652
|
+
((!('metadata' in event) && !change.metadata) ||
|
|
653
|
+
('metadata' in event &&
|
|
654
|
+
change.metadata &&
|
|
655
|
+
event.metadata[constants_1.default.MTIME] != null &&
|
|
656
|
+
change.metadata.modifiedTime != null &&
|
|
657
|
+
event.metadata[constants_1.default.MTIME] === change.metadata.modifiedTime)))) {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
// If the file was added or modified,
|
|
661
|
+
// parse it and update the file map.
|
|
662
|
+
if (change.event === 'touch') {
|
|
663
|
+
(0, invariant_1.default)(change.metadata.size != null, 'since the file exists or changed, it should have known size');
|
|
664
|
+
const fileMetadata = [
|
|
665
|
+
change.metadata.modifiedTime ?? null,
|
|
666
|
+
change.metadata.size,
|
|
667
|
+
0,
|
|
668
|
+
null,
|
|
669
|
+
change.metadata.type === 'l' ? 1 : 0,
|
|
670
|
+
null,
|
|
671
|
+
];
|
|
672
|
+
try {
|
|
673
|
+
if (change.metadata.type === 'l') {
|
|
674
|
+
await this.#maybeReadLink(relativeFilePath, fileMetadata);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
await this.#fileProcessor.processRegularFile(relativeFilePath, fileMetadata, {
|
|
678
|
+
computeSha1: this.#options.computeSha1,
|
|
679
|
+
maybeReturnContent: false,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
enqueueEvent({
|
|
683
|
+
clock: change.clock,
|
|
684
|
+
relativeFilePath,
|
|
685
|
+
metadata: fileMetadata,
|
|
686
|
+
type: change.event,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
catch (e) {
|
|
690
|
+
if (!['ENOENT', 'EACCESS'].includes(e.code)) {
|
|
691
|
+
throw e;
|
|
692
|
+
}
|
|
693
|
+
// Swallow ENOENT/ACCESS errors silently. Safe because either:
|
|
694
|
+
// - We never knew about the file, so neither did any consumers.
|
|
695
|
+
// Or,
|
|
696
|
+
// - The watcher will soon (or has already) report a "delete"
|
|
697
|
+
// event for it, and we'll clean up in the usual way at that
|
|
698
|
+
// point.
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
else if (change.event === 'delete') {
|
|
702
|
+
enqueueEvent({
|
|
703
|
+
clock: change.clock,
|
|
704
|
+
relativeFilePath,
|
|
705
|
+
type: 'delete',
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
else if (change.event === 'recrawl') {
|
|
709
|
+
// Recrawl event: flush pending changes and re-crawl the directory
|
|
710
|
+
emitChange();
|
|
711
|
+
// The relativePath is relative to the watcher root (change.root),
|
|
712
|
+
// but we need a path relative to rootDir for the recrawl.
|
|
713
|
+
const absoluteDirPath = path.join(change.root, (0, normalizePathSeparatorsToSystem_1.default)(change.relativePath));
|
|
714
|
+
const subpath = this.#pathUtils.absoluteToNormal(absoluteDirPath);
|
|
715
|
+
// Crawl the specific subdirectory
|
|
716
|
+
const watcher = this.#watcher;
|
|
717
|
+
(0, invariant_1.default)(watcher != null, 'Watcher must be initialized');
|
|
718
|
+
const crawlResult = await watcher.recrawl(subpath, fileSystem);
|
|
719
|
+
// Skip if no changes
|
|
720
|
+
if (crawlResult.changedFiles.size === 0 && crawlResult.removedFiles.size === 0) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
// Reuse the same batch processing logic as build()
|
|
724
|
+
const recrawlChangeAggregator = await this.#applyFileDelta(fileSystem, this.#plugins, crawlResult);
|
|
725
|
+
// Update clock if provided
|
|
726
|
+
this.#updateClock(clocks, change.clock);
|
|
727
|
+
// Skip emit if no changes after processing
|
|
728
|
+
if (recrawlChangeAggregator.getSize() === 0) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
// Emit changes directly
|
|
732
|
+
const toPublicMetadata = (metadata) => ({
|
|
733
|
+
isSymlink: metadata[constants_1.default.SYMLINK] !== 0,
|
|
734
|
+
modifiedTime: metadata[constants_1.default.MTIME] ?? null,
|
|
735
|
+
});
|
|
736
|
+
const changesWithMetadata = recrawlChangeAggregator.getMappedView(toPublicMetadata);
|
|
737
|
+
const changeEvent = {
|
|
738
|
+
changes: changesWithMetadata,
|
|
739
|
+
logger: null,
|
|
740
|
+
rootDir: this.#options.rootDir,
|
|
741
|
+
};
|
|
742
|
+
this.emit('change', changeEvent);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
throw new Error(`metro-file-map: Unrecognized event type from watcher: ${change.event}`);
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
})
|
|
749
|
+
.catch((error) => {
|
|
750
|
+
this.#console.error(`metro-file-map: watch error:\n ${error.stack}\n`);
|
|
751
|
+
});
|
|
752
|
+
};
|
|
753
|
+
this.#changeInterval = setInterval(emitChange, CHANGE_INTERVAL);
|
|
754
|
+
(0, invariant_1.default)(this.#watcher != null, 'Expected #watcher to have been initialised by build()');
|
|
755
|
+
await this.#watcher.watch(onChange);
|
|
756
|
+
if (this.#options.healthCheck.enabled) {
|
|
757
|
+
const performHealthCheck = () => {
|
|
758
|
+
if (!this.#watcher) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
762
|
+
this.#watcher.checkHealth(this.#options.healthCheck.timeout).then((result) => {
|
|
763
|
+
this.emit('healthCheck', result);
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
performHealthCheck();
|
|
767
|
+
this.#healthCheckInterval = setInterval(performHealthCheck, this.#options.healthCheck.interval);
|
|
768
|
+
}
|
|
769
|
+
this.#startupPerfLogger?.point('watch_end');
|
|
770
|
+
}
|
|
771
|
+
async end() {
|
|
772
|
+
if (this.#changeInterval) {
|
|
773
|
+
clearInterval(this.#changeInterval);
|
|
774
|
+
}
|
|
775
|
+
if (this.#healthCheckInterval) {
|
|
776
|
+
clearInterval(this.#healthCheckInterval);
|
|
777
|
+
}
|
|
778
|
+
this.#crawlerAbortController.abort();
|
|
779
|
+
await Promise.all([
|
|
780
|
+
this.#fileProcessor.end(),
|
|
781
|
+
this.#watcher?.close(),
|
|
782
|
+
this.#cacheManager.end(),
|
|
783
|
+
]);
|
|
784
|
+
}
|
|
785
|
+
async #shouldUseWatchman() {
|
|
786
|
+
if (!this.#options.useWatchman) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
if (!this.#canUseWatchmanPromise) {
|
|
790
|
+
this.#canUseWatchmanPromise = (0, checkWatchmanCapabilities_1.default)(WATCHMAN_REQUIRED_CAPABILITIES)
|
|
791
|
+
.then(({ version }) => {
|
|
792
|
+
this.#startupPerfLogger?.annotate({
|
|
793
|
+
string: {
|
|
794
|
+
watchmanVersion: version,
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
return true;
|
|
798
|
+
})
|
|
799
|
+
.catch((e) => {
|
|
800
|
+
// TODO: Advise people to either install Watchman or set
|
|
801
|
+
// `useWatchman: false` here?
|
|
802
|
+
this.#startupPerfLogger?.annotate({
|
|
803
|
+
string: {
|
|
804
|
+
watchmanFailedCapabilityCheck: e?.message ?? '[missing]',
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
return false;
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return this.#canUseWatchmanPromise;
|
|
811
|
+
}
|
|
812
|
+
#getNextChangeID() {
|
|
813
|
+
if (this.#changeID >= Number.MAX_SAFE_INTEGER) {
|
|
814
|
+
this.#changeID = 0;
|
|
815
|
+
}
|
|
816
|
+
return ++this.#changeID;
|
|
817
|
+
}
|
|
818
|
+
#updateClock(clocks, newClock) {
|
|
819
|
+
if (newClock == null) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const [absoluteWatchRoot, clockSpec] = newClock;
|
|
823
|
+
const relativeFsRoot = this.#pathUtils.absoluteToNormal(absoluteWatchRoot);
|
|
824
|
+
clocks.set((0, normalizePathSeparatorsToPosix_1.default)(relativeFsRoot), clockSpec);
|
|
825
|
+
}
|
|
826
|
+
static H = constants_1.default;
|
|
827
|
+
}
|
|
828
|
+
exports.default = FileMap;
|
|
829
|
+
// TODO: Replace with it.map() from Node 22+
|
|
830
|
+
function mapIterable(it, fn) {
|
|
831
|
+
return (function* mapped() {
|
|
832
|
+
for (const item of it) {
|
|
833
|
+
yield fn(item);
|
|
834
|
+
}
|
|
835
|
+
})();
|
|
836
|
+
}
|