@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.
Files changed (82) hide show
  1. package/README.md +1 -0
  2. package/build/Watcher.d.ts +53 -0
  3. package/build/Watcher.js +249 -0
  4. package/build/cache/DiskCacheManager.d.ts +25 -0
  5. package/build/cache/DiskCacheManager.js +110 -0
  6. package/build/constants.d.ts +23 -0
  7. package/build/constants.js +27 -0
  8. package/build/crawlers/node/fallback.d.ts +20 -0
  9. package/build/crawlers/node/fallback.js +148 -0
  10. package/build/crawlers/node/index.d.ts +10 -0
  11. package/build/crawlers/node/index.js +152 -0
  12. package/build/crawlers/watchman/index.d.ts +8 -0
  13. package/build/crawlers/watchman/index.js +288 -0
  14. package/build/crawlers/watchman/planQuery.d.ts +31 -0
  15. package/build/crawlers/watchman/planQuery.js +99 -0
  16. package/build/index.d.ts +140 -0
  17. package/build/index.js +836 -0
  18. package/build/lib/FileProcessor.d.ts +42 -0
  19. package/build/lib/FileProcessor.js +180 -0
  20. package/build/lib/FileSystemChangeAggregator.d.ts +18 -0
  21. package/build/lib/FileSystemChangeAggregator.js +114 -0
  22. package/build/lib/RootPathUtils.d.ts +24 -0
  23. package/build/lib/RootPathUtils.js +311 -0
  24. package/build/lib/TreeFS.d.ts +161 -0
  25. package/build/lib/TreeFS.js +1006 -0
  26. package/build/lib/checkWatchmanCapabilities.d.ts +9 -0
  27. package/build/lib/checkWatchmanCapabilities.js +50 -0
  28. package/build/lib/normalizePathSeparatorsToPosix.d.ts +8 -0
  29. package/build/lib/normalizePathSeparatorsToPosix.js +20 -0
  30. package/build/lib/normalizePathSeparatorsToSystem.d.ts +8 -0
  31. package/build/lib/normalizePathSeparatorsToSystem.js +20 -0
  32. package/build/lib/removeOverlappingRoots.d.ts +10 -0
  33. package/build/lib/removeOverlappingRoots.js +37 -0
  34. package/build/lib/rootRelativeCacheKeys.d.ts +11 -0
  35. package/build/lib/rootRelativeCacheKeys.js +56 -0
  36. package/build/lib/sorting.d.ts +8 -0
  37. package/build/lib/sorting.js +31 -0
  38. package/build/plugins/DependencyPlugin.d.ts +23 -0
  39. package/build/plugins/DependencyPlugin.js +61 -0
  40. package/build/plugins/FileDataPlugin.d.ts +28 -0
  41. package/build/plugins/FileDataPlugin.js +46 -0
  42. package/build/plugins/HastePlugin.d.ts +32 -0
  43. package/build/plugins/HastePlugin.js +358 -0
  44. package/build/plugins/MockPlugin.d.ts +27 -0
  45. package/build/plugins/MockPlugin.js +157 -0
  46. package/build/plugins/dependencies/dependencyExtractor.d.ts +7 -0
  47. package/build/plugins/dependencies/dependencyExtractor.js +66 -0
  48. package/build/plugins/dependencies/worker.d.ts +18 -0
  49. package/build/plugins/dependencies/worker.js +30 -0
  50. package/build/plugins/haste/DuplicateHasteCandidatesError.d.ts +14 -0
  51. package/build/plugins/haste/DuplicateHasteCandidatesError.js +51 -0
  52. package/build/plugins/haste/HasteConflictsError.d.ts +12 -0
  53. package/build/plugins/haste/HasteConflictsError.js +49 -0
  54. package/build/plugins/haste/computeConflicts.d.ts +19 -0
  55. package/build/plugins/haste/computeConflicts.js +74 -0
  56. package/build/plugins/haste/getPlatformExtension.d.ts +7 -0
  57. package/build/plugins/haste/getPlatformExtension.js +19 -0
  58. package/build/plugins/haste/worker.d.ts +16 -0
  59. package/build/plugins/haste/worker.js +48 -0
  60. package/build/plugins/mocks/getMockName.d.ts +8 -0
  61. package/build/plugins/mocks/getMockName.js +17 -0
  62. package/build/ts-declarations/fb-watchman.d.ts +20 -0
  63. package/build/ts-declarations/fb-watchman.js +2 -0
  64. package/build/types.d.ts +404 -0
  65. package/build/types.js +8 -0
  66. package/build/watchers/AbstractWatcher.d.ts +30 -0
  67. package/build/watchers/AbstractWatcher.js +97 -0
  68. package/build/watchers/FallbackWatcher.d.ts +18 -0
  69. package/build/watchers/FallbackWatcher.js +411 -0
  70. package/build/watchers/NativeWatcher.d.ts +39 -0
  71. package/build/watchers/NativeWatcher.js +156 -0
  72. package/build/watchers/RecrawlWarning.d.ts +18 -0
  73. package/build/watchers/RecrawlWarning.js +59 -0
  74. package/build/watchers/WatchmanWatcher.d.ts +22 -0
  75. package/build/watchers/WatchmanWatcher.js +263 -0
  76. package/build/watchers/common.d.ts +39 -0
  77. package/build/watchers/common.js +60 -0
  78. package/build/worker.d.ts +24 -0
  79. package/build/worker.js +80 -0
  80. package/build/workerExclusionList.d.ts +8 -0
  81. package/build/workerExclusionList.js +57 -0
  82. 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
+ }