@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
@@ -0,0 +1,1006 @@
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 __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const invariant_1 = __importDefault(require("invariant"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const constants_1 = __importDefault(require("../constants"));
16
+ const normalizePathSeparatorsToPosix_1 = __importDefault(require("./normalizePathSeparatorsToPosix"));
17
+ const normalizePathSeparatorsToSystem_1 = __importDefault(require("./normalizePathSeparatorsToSystem"));
18
+ const fallback_1 = require("../crawlers/node/fallback");
19
+ const RootPathUtils_1 = require("./RootPathUtils");
20
+ function isDirectory(node) {
21
+ return node instanceof Map;
22
+ }
23
+ function isRegularFile(node) {
24
+ return node != null && node[constants_1.default.SYMLINK] === 0;
25
+ }
26
+ /**
27
+ * OVERVIEW:
28
+ *
29
+ * TreeFS is Metro's in-memory representation of the file system. It is
30
+ * structured as a tree of non-empty maps and leaves (tuples), with the root
31
+ * node representing the given `rootDir`, typically Metro's _project root_
32
+ * (not a filesystem root). Map keys are path segments, and branches outside
33
+ * the project root are accessed via `'..'`.
34
+ *
35
+ * EXAMPLE:
36
+ *
37
+ * For a root dir '/data/project', the file '/data/other/app/index.js' would
38
+ * have metadata at #rootNode.get('..').get('other').get('app').get('index.js')
39
+ *
40
+ * SERIALISATION:
41
+ *
42
+ * #rootNode is designed to be directly serialisable and directly portable (for
43
+ * a given project) between different root directories and operating systems.
44
+ *
45
+ * SYMLINKS:
46
+ *
47
+ * Symlinks are represented as nodes whose metadata contains their literal
48
+ * target. Literal targets are resolved to normal paths at runtime, and cached.
49
+ * If a symlink is encountered during traversal, we restart traversal at the
50
+ * root node targeting join(normal symlink target, remaining path suffix).
51
+ *
52
+ * NODE TYPES:
53
+ *
54
+ * - A directory (including a parent directory at '..') is represented by a
55
+ * `Map` of basenames to any other node type.
56
+ * - A file is represented by an `Array` (tuple) of metadata, of which:
57
+ * - A regular file has node[H.SYMLINK] === 0
58
+ * - A symlink has node[H.SYMLINK] === 1 or
59
+ * typeof node[H.SYMLINK] === 'string', where a string is the literal
60
+ * content of the symlink (i.e. from readlink), if known.
61
+ *
62
+ * TERMINOLOGY:
63
+ *
64
+ * - mixedPath
65
+ * A root-relative or absolute path
66
+ * - relativePath
67
+ * A root-relative path
68
+ * - normalPath
69
+ * A root-relative, normalised path (no extraneous '.' or '..'), may have a
70
+ * single trailing slash
71
+ * - canonicalPath
72
+ * A root-relative, normalised, real path (no symlinks in dirname), never has
73
+ * a trailing slash
74
+ */
75
+ class TreeFS {
76
+ #fallbackBoundaryDepth;
77
+ #fallbackFilesystem;
78
+ #pathUtils;
79
+ #processFile;
80
+ #rootDir;
81
+ #rootPattern;
82
+ #rootNode = new Map();
83
+ constructor(opts) {
84
+ const { rootDir, files, processFile, fallbackFilesystem, roots, serverRoot } = opts;
85
+ this.#rootDir = rootDir;
86
+ this.#pathUtils = new RootPathUtils_1.RootPathUtils(rootDir);
87
+ this.#processFile = processFile;
88
+ this.#fallbackFilesystem = fallbackFilesystem ?? null;
89
+ if (serverRoot != null) {
90
+ this.#fallbackBoundaryDepth = (0, RootPathUtils_1.getAncestorOfRootIdx)(this.#pathUtils.absoluteToNormal(serverRoot));
91
+ }
92
+ else {
93
+ this.#fallbackBoundaryDepth = null;
94
+ }
95
+ this.#rootPattern = (0, RootPathUtils_1.pathsToPattern)(roots ?? [], this.#pathUtils);
96
+ if (files != null) {
97
+ this.bulkAddOrModify(files);
98
+ }
99
+ }
100
+ getSerializableSnapshot() {
101
+ return this.#cloneTree(this.#rootNode, '');
102
+ }
103
+ static fromDeserializedSnapshot(args) {
104
+ const { rootDir, fileSystemData, processFile, fallbackFilesystem, roots, serverRoot } = args;
105
+ const tfs = new TreeFS({
106
+ processFile,
107
+ rootDir,
108
+ fallbackFilesystem,
109
+ roots,
110
+ serverRoot,
111
+ });
112
+ tfs.#rootNode = fileSystemData;
113
+ return tfs;
114
+ }
115
+ getSize(mixedPath) {
116
+ const fileMetadata = this.#getFileData(mixedPath);
117
+ return (fileMetadata && fileMetadata[constants_1.default.SIZE]) ?? null;
118
+ }
119
+ getDifference(files, options) {
120
+ const changedFiles = new Map(files);
121
+ const removedFiles = new Set();
122
+ const subpath = options?.subpath;
123
+ // If a subpath is specified, start iteration from that node
124
+ let rootNode = this.#rootNode;
125
+ let prefix = '';
126
+ if (subpath != null && subpath !== '') {
127
+ const lookupResult = this.#lookupByNormalPath(subpath, {
128
+ followLeaf: true,
129
+ });
130
+ if (!lookupResult.exists || !isDirectory(lookupResult.node)) {
131
+ // Directory doesn't exist, nothing to compare - all files are new
132
+ return { changedFiles, removedFiles };
133
+ }
134
+ rootNode = lookupResult.node;
135
+ prefix = lookupResult.canonicalPath;
136
+ }
137
+ for (const { canonicalPath, metadata } of this.#metadataIterator(rootNode, {
138
+ includeNodeModules: true,
139
+ includeSymlinks: true,
140
+ }, prefix)) {
141
+ const newMetadata = files.get(canonicalPath);
142
+ if (newMetadata) {
143
+ if (isRegularFile(newMetadata) !== isRegularFile(metadata)) {
144
+ // Types differ, file has changed
145
+ continue;
146
+ }
147
+ if (newMetadata[constants_1.default.MTIME] != null &&
148
+ newMetadata[constants_1.default.MTIME] !== 0 &&
149
+ newMetadata[constants_1.default.MTIME] === metadata[constants_1.default.MTIME]) {
150
+ // Types and modified time match - not changed.
151
+ changedFiles.delete(canonicalPath);
152
+ }
153
+ else if ((newMetadata[constants_1.default.MTIME] == null || newMetadata[constants_1.default.MTIME] === 0) &&
154
+ (metadata[constants_1.default.MTIME] == null || metadata[constants_1.default.MTIME] === 0)) {
155
+ // If file is still untouched then mark it as unchanged
156
+ changedFiles.delete(canonicalPath);
157
+ }
158
+ else if (newMetadata[constants_1.default.SHA1] != null &&
159
+ newMetadata[constants_1.default.SHA1] === metadata[constants_1.default.SHA1] &&
160
+ metadata[constants_1.default.VISITED] === 1) {
161
+ // Content matches - update modified time but don't revisit
162
+ const updatedMetadata = [...metadata];
163
+ updatedMetadata[constants_1.default.MTIME] = newMetadata[constants_1.default.MTIME];
164
+ changedFiles.set(canonicalPath, updatedMetadata);
165
+ }
166
+ }
167
+ else {
168
+ removedFiles.add(canonicalPath);
169
+ }
170
+ }
171
+ return {
172
+ changedFiles,
173
+ removedFiles,
174
+ };
175
+ }
176
+ getMtimeByNormalPath(normalPath) {
177
+ const result = this.#lookupByNormalPath(normalPath, {
178
+ followLeaf: false,
179
+ skipFallback: true,
180
+ });
181
+ return result.exists && !isDirectory(result.node) ? result.node[constants_1.default.MTIME] : null;
182
+ }
183
+ getSha1(mixedPath) {
184
+ const fileMetadata = this.#getFileData(mixedPath);
185
+ return (fileMetadata && fileMetadata[constants_1.default.SHA1]) ?? null;
186
+ }
187
+ async getOrComputeSha1(mixedPath) {
188
+ const normalPath = this.#normalizePath(mixedPath);
189
+ const result = this.#lookupByNormalPath(normalPath, {
190
+ followLeaf: true,
191
+ });
192
+ if (!result.exists || isDirectory(result.node)) {
193
+ return null;
194
+ }
195
+ const { canonicalPath, node: fileMetadata } = result;
196
+ // Populate mtime and size on demand
197
+ if (fileMetadata[constants_1.default.MTIME] == null || fileMetadata[constants_1.default.MTIME] === 0) {
198
+ fileMetadata[constants_1.default.SHA1] = null;
199
+ const absolutePath = this.#pathUtils.normalToAbsolute(canonicalPath);
200
+ try {
201
+ const stat = await fs_1.default.promises.lstat(absolutePath);
202
+ const diskMtime = stat.mtime.getTime();
203
+ fileMetadata[constants_1.default.MTIME] = diskMtime;
204
+ fileMetadata[constants_1.default.SIZE] = stat.size;
205
+ }
206
+ catch { }
207
+ }
208
+ // Empty strings
209
+ const existing = fileMetadata[constants_1.default.SHA1];
210
+ if (existing != null && existing.length > 0) {
211
+ return { sha1: existing };
212
+ }
213
+ // Mutate the metadata we first retrieved. This may be orphaned or about
214
+ // to be overwritten if the file changes while we are processing it -
215
+ // by only mutating the original metadata, we don't risk caching a stale
216
+ // SHA-1 after a change event.
217
+ const maybeContent = await this.#processFile(canonicalPath, fileMetadata, {
218
+ computeSha1: true,
219
+ });
220
+ const sha1 = fileMetadata[constants_1.default.SHA1];
221
+ (0, invariant_1.default)(sha1 != null && sha1.length > 0, "File processing didn't populate a SHA-1 hash for %s", canonicalPath);
222
+ return maybeContent
223
+ ? {
224
+ content: maybeContent,
225
+ sha1,
226
+ }
227
+ : { sha1 };
228
+ }
229
+ exists(mixedPath) {
230
+ const result = this.#getFileData(mixedPath);
231
+ return result != null;
232
+ }
233
+ lookup(mixedPath) {
234
+ const normalPath = this.#normalizePath(mixedPath);
235
+ const links = new Set();
236
+ const result = this.#lookupByNormalPath(normalPath, {
237
+ collectLinkPaths: links,
238
+ followLeaf: true,
239
+ });
240
+ if (!result.exists) {
241
+ const { canonicalMissingPath } = result;
242
+ return {
243
+ exists: false,
244
+ links,
245
+ missing: this.#pathUtils.normalToAbsolute(canonicalMissingPath),
246
+ };
247
+ }
248
+ const { canonicalPath, node } = result;
249
+ const realPath = this.#pathUtils.normalToAbsolute(canonicalPath);
250
+ if (isDirectory(node)) {
251
+ return { exists: true, links, realPath, type: 'd' };
252
+ }
253
+ (0, invariant_1.default)(isRegularFile(node), 'lookup follows symlinks, so should never return one (%s -> %s)', mixedPath, canonicalPath);
254
+ return { exists: true, links, realPath, type: 'f', metadata: node };
255
+ }
256
+ getAllFiles() {
257
+ return Array.from(this.metadataIterator({
258
+ includeNodeModules: true,
259
+ includeSymlinks: false,
260
+ }), ({ canonicalPath }) => this.#pathUtils.normalToAbsolute(canonicalPath));
261
+ }
262
+ linkStats(mixedPath) {
263
+ const fileMetadata = this.#getFileData(mixedPath, { followLeaf: false });
264
+ if (fileMetadata == null) {
265
+ return null;
266
+ }
267
+ const fileType = isRegularFile(fileMetadata) ? 'f' : 'l';
268
+ return {
269
+ fileType,
270
+ modifiedTime: fileMetadata[constants_1.default.MTIME],
271
+ size: fileMetadata[constants_1.default.SIZE],
272
+ };
273
+ }
274
+ /**
275
+ * Given a search context, return a list of file paths matching the query.
276
+ * The query matches against normalized paths which start with `./`,
277
+ * for example: `a/b.js` -> `./a/b.js`
278
+ */
279
+ *matchFiles(opts) {
280
+ const { filter = null, filterCompareAbsolute = false, filterComparePosix = false, follow = false, recursive = true, rootDir = null, } = opts;
281
+ const normalRoot = rootDir == null ? '' : this.#normalizePath(rootDir);
282
+ const contextRootResult = this.#lookupByNormalPath(normalRoot);
283
+ if (!contextRootResult.exists) {
284
+ return;
285
+ }
286
+ const { ancestorOfRootIdx, canonicalPath: rootRealPath, node: contextRoot, parentNode: contextRootParent, } = contextRootResult;
287
+ if (!isDirectory(contextRoot)) {
288
+ return;
289
+ }
290
+ const contextRootAbsolutePath = rootRealPath === '' ? this.#rootDir : path_1.default.join(this.#rootDir, rootRealPath);
291
+ const prefix = filterComparePosix ? './' : '.' + path_1.default.sep;
292
+ const contextRootAbsolutePathForComparison = filterComparePosix && path_1.default.sep !== '/'
293
+ ? contextRootAbsolutePath.replaceAll(path_1.default.sep, '/')
294
+ : contextRootAbsolutePath;
295
+ for (const relativePathForComparison of this.#pathIterator(contextRoot, contextRootParent, ancestorOfRootIdx, {
296
+ alwaysYieldPosix: filterComparePosix,
297
+ canonicalPathOfRoot: rootRealPath,
298
+ follow,
299
+ recursive,
300
+ subtreeOnly: rootDir != null,
301
+ })) {
302
+ if (filter == null ||
303
+ filter.test(
304
+ // NOTE(EvanBacon): Ensure files start with `./` for matching purposes
305
+ // this ensures packages work across Metro and Webpack (ex: Storybook for React DOM / React Native).
306
+ // `a/b.js` -> `./a/b.js`
307
+ filterCompareAbsolute === true
308
+ ? path_1.default.join(contextRootAbsolutePathForComparison, relativePathForComparison)
309
+ : prefix + relativePathForComparison)) {
310
+ const relativePath = filterComparePosix === true && path_1.default.sep !== '/'
311
+ ? relativePathForComparison.replaceAll('/', path_1.default.sep)
312
+ : relativePathForComparison;
313
+ yield path_1.default.join(contextRootAbsolutePath, relativePath);
314
+ }
315
+ }
316
+ }
317
+ addOrModify(mixedPath, metadata, changeListener) {
318
+ const normalPath = this.#normalizePath(mixedPath);
319
+ // Walk the tree to find the *real* path of the parent node, creating
320
+ // directories as we need.
321
+ const parentDirNode = this.#lookupByNormalPath(path_1.default.dirname(normalPath), {
322
+ changeListener,
323
+ makeDirectories: true,
324
+ });
325
+ if (!parentDirNode.exists) {
326
+ throw new Error(`TreeFS: Failed to make parent directory entry for ${mixedPath}`);
327
+ }
328
+ // Normalize the resulting path to account for the parent node being root.
329
+ const canonicalPath = this.#normalizePath(parentDirNode.canonicalPath + path_1.default.sep + path_1.default.basename(normalPath));
330
+ this.bulkAddOrModify(new Map([[canonicalPath, metadata]]), changeListener);
331
+ }
332
+ bulkAddOrModify(addedOrModifiedFiles, changeListener) {
333
+ // Optimisation: Bulk FileData are typically clustered by directory, so we
334
+ // optimise for that case by remembering the last directory we looked up.
335
+ // Experiments with large result sets show this to be significantly (~30%)
336
+ // faster than caching all lookups in a Map, and 70% faster than no cache.
337
+ let lastDir;
338
+ let directoryNode;
339
+ for (const [normalPath, metadata] of addedOrModifiedFiles) {
340
+ const lastSepIdx = normalPath.lastIndexOf(path_1.default.sep);
341
+ const dirname = lastSepIdx === -1 ? '' : normalPath.slice(0, lastSepIdx);
342
+ const basename = lastSepIdx === -1 ? normalPath : normalPath.slice(lastSepIdx + 1);
343
+ if (directoryNode == null || dirname !== lastDir) {
344
+ const lookup = this.#lookupByNormalPath(dirname, {
345
+ changeListener,
346
+ followLeaf: false,
347
+ makeDirectories: true,
348
+ });
349
+ if (!lookup.exists) {
350
+ // This should only be possible if the input is non-real and
351
+ // lookup hits a broken symlink.
352
+ throw new Error(`TreeFS: Unexpected error adding ${normalPath}.\nMissing: ` +
353
+ lookup.canonicalMissingPath);
354
+ }
355
+ if (!isDirectory(lookup.node)) {
356
+ throw new Error(`TreeFS: Could not add directory ${dirname}, adding ${normalPath}. ` +
357
+ `${dirname} already exists in the file map as a file.`);
358
+ }
359
+ lastDir = dirname;
360
+ directoryNode = lookup.node;
361
+ }
362
+ if (changeListener != null) {
363
+ const existingNode = directoryNode.get(basename);
364
+ if (existingNode != null) {
365
+ (0, invariant_1.default)(!isDirectory(existingNode), 'Detected addition or modification of file %s, but it is tracked as a non-empty directory', normalPath);
366
+ // File already exists - this is a modification
367
+ changeListener.fileModified(normalPath, existingNode, metadata);
368
+ }
369
+ else {
370
+ // New file
371
+ changeListener.fileAdded(normalPath, metadata);
372
+ }
373
+ }
374
+ directoryNode.set(basename, metadata);
375
+ }
376
+ }
377
+ remove(mixedPath, changeListener) {
378
+ const normalPath = this.#normalizePath(mixedPath);
379
+ const result = this.#lookupByNormalPath(normalPath, { followLeaf: false });
380
+ if (!result.exists) {
381
+ return;
382
+ }
383
+ const { parentNode, canonicalPath, node } = result;
384
+ if (isDirectory(node) && node.size > 0) {
385
+ for (const basename of node.keys()) {
386
+ this.remove(canonicalPath + path_1.default.sep + basename, changeListener);
387
+ }
388
+ // Removing the last file will delete this directory
389
+ return;
390
+ }
391
+ if (parentNode != null) {
392
+ if (changeListener != null) {
393
+ if (isDirectory(node)) {
394
+ changeListener.directoryRemoved(canonicalPath);
395
+ }
396
+ else {
397
+ changeListener.fileRemoved(canonicalPath, node);
398
+ }
399
+ }
400
+ parentNode.delete(path_1.default.basename(canonicalPath));
401
+ if (parentNode.size === 0 && parentNode !== this.#rootNode) {
402
+ // NB: This isn't the most efficient algorithm - in the case of
403
+ // removing the last file in a deep hierarchy it's O(depth^2), but
404
+ // that's not expected to be a case common enough to justify
405
+ // implementation complexity, or slowing down more common uses of
406
+ // _lookupByNormalPath.
407
+ this.remove(path_1.default.dirname(canonicalPath), changeListener);
408
+ }
409
+ }
410
+ }
411
+ /**
412
+ * The core traversal algorithm of TreeFS - takes a normal path and traverses
413
+ * through a tree of maps keyed on path segments, returning the node,
414
+ * canonical path, and other metadata if successful, or the first missing
415
+ * segment otherwise.
416
+ *
417
+ * When a symlink is encountered, we set a new target of the symlink's
418
+ * normalised target path plus the remainder of the original target path. In
419
+ * this way, the eventual target path in a successful lookup has all symlinks
420
+ * resolved, and gives us the real path "for free". Similarly if a traversal
421
+ * fails, we automatically have the real path of the first non-existent node.
422
+ *
423
+ * Note that this code is extremely hot during resolution, being the most
424
+ * expensive part of a file existence check. Benchmark any modifications!
425
+ */
426
+ #lookupByNormalPath(requestedNormalPath, opts = { followLeaf: true, makeDirectories: false }) {
427
+ // We'll update the target if we hit a symlink.
428
+ let targetNormalPath = requestedNormalPath;
429
+ // Lazy-initialised set of seen target paths, to detect symlink cycles.
430
+ let seen;
431
+ // Pointer to the first character of the current path segment in
432
+ // targetNormalPath.
433
+ let fromIdx = opts.start?.pathIdx ?? 0;
434
+ // The parent of the current segment.
435
+ let parentNode = opts.start?.node ?? this.#rootNode;
436
+ // If a returned node is (an ancestor of) the root, this is the number of
437
+ // levels below the root, i.e. '' is 0, '..' is 1, '../..' is 2, otherwise
438
+ // null.
439
+ let ancestorOfRootIdx = opts.start?.ancestorOfRootIdx ?? 0;
440
+ const { collectAncestors, changeListener } = opts;
441
+ // Used only when collecting ancestors, to avoid double-counting nodes and
442
+ // paths when traversing a symlink takes us back to rootNode and out again.
443
+ // This tracks the first character of the first segment not already
444
+ // collected.
445
+ let unseenPathFromIdx = 0;
446
+ while (targetNormalPath.length > fromIdx) {
447
+ const nextSepIdx = targetNormalPath.indexOf(path_1.default.sep, fromIdx);
448
+ const isLastSegment = nextSepIdx === -1;
449
+ const segmentName = isLastSegment
450
+ ? targetNormalPath.slice(fromIdx)
451
+ : targetNormalPath.slice(fromIdx, nextSepIdx);
452
+ const isUnseen = fromIdx >= unseenPathFromIdx;
453
+ fromIdx = !isLastSegment ? nextSepIdx + 1 : targetNormalPath.length;
454
+ if (segmentName === '.') {
455
+ continue;
456
+ }
457
+ let segmentNode = parentNode.get(segmentName);
458
+ // In normal paths all indirections are at the prefix, so we are at the
459
+ // nth ancestor of the root iff the path so far is n '..' segments.
460
+ if (segmentName === '..' && ancestorOfRootIdx != null) {
461
+ ancestorOfRootIdx++;
462
+ }
463
+ else if (segmentNode != null) {
464
+ ancestorOfRootIdx = null;
465
+ }
466
+ if (segmentNode == null) {
467
+ if (opts.makeDirectories !== true && segmentName !== '..') {
468
+ if (!opts.skipFallback && this.#fallbackFilesystem != null) {
469
+ const parentEnd = isLastSegment
470
+ ? fromIdx - segmentName.length - 1
471
+ : fromIdx - segmentName.length - 2;
472
+ const parentCanonicalPath = parentEnd > 0 ? targetNormalPath.slice(0, parentEnd) : '';
473
+ segmentNode = this.#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath);
474
+ if (segmentNode != null) {
475
+ ancestorOfRootIdx = null;
476
+ }
477
+ }
478
+ if (segmentNode == null) {
479
+ return {
480
+ canonicalMissingPath: isLastSegment
481
+ ? targetNormalPath
482
+ : targetNormalPath.slice(0, fromIdx - 1),
483
+ exists: false,
484
+ missingSegmentName: segmentName,
485
+ };
486
+ }
487
+ }
488
+ if (segmentNode == null) {
489
+ segmentNode = new Map();
490
+ if (opts.makeDirectories === true) {
491
+ if (changeListener != null) {
492
+ const canonicalPath = isLastSegment
493
+ ? targetNormalPath
494
+ : targetNormalPath.slice(0, fromIdx - 1);
495
+ changeListener.directoryAdded(canonicalPath);
496
+ }
497
+ parentNode.set(segmentName, segmentNode);
498
+ }
499
+ else if (!opts.skipFallback &&
500
+ this.#fallbackFilesystem != null) {
501
+ parentNode.set(segmentName, segmentNode);
502
+ }
503
+ }
504
+ }
505
+ // We are done if...
506
+ if (
507
+ // ...at a directory node and the only subsequent character is `/`, or
508
+ (nextSepIdx === targetNormalPath.length - 1 && isDirectory(segmentNode)) ||
509
+ // there are no subsequent `/`, and this node is anything but a symlink
510
+ // we're required to resolve due to followLeaf.
511
+ (isLastSegment &&
512
+ (isDirectory(segmentNode) || isRegularFile(segmentNode) || opts.followLeaf === false))) {
513
+ return {
514
+ ancestorOfRootIdx,
515
+ canonicalPath: isLastSegment ? targetNormalPath : targetNormalPath.slice(0, -1), // remove trailing `/`
516
+ exists: true,
517
+ node: segmentNode,
518
+ parentNode,
519
+ };
520
+ }
521
+ // If the next node is a directory, go into it
522
+ if (isDirectory(segmentNode)) {
523
+ parentNode = segmentNode;
524
+ if (collectAncestors && isUnseen) {
525
+ const currentPath = isLastSegment
526
+ ? targetNormalPath
527
+ : targetNormalPath.slice(0, fromIdx - 1);
528
+ collectAncestors.push({
529
+ ancestorOfRootIdx,
530
+ node: segmentNode,
531
+ normalPath: currentPath,
532
+ segmentName,
533
+ });
534
+ }
535
+ }
536
+ else {
537
+ const currentPath = isLastSegment
538
+ ? targetNormalPath
539
+ : targetNormalPath.slice(0, fromIdx - 1);
540
+ if (isRegularFile(segmentNode)) {
541
+ // Regular file in a directory path
542
+ return {
543
+ canonicalMissingPath: currentPath,
544
+ exists: false,
545
+ missingSegmentName: segmentName,
546
+ };
547
+ }
548
+ // Symlink in a directory path
549
+ const normalSymlinkTarget = this.#resolveSymlinkTargetToNormalPath(segmentNode, currentPath);
550
+ if (normalSymlinkTarget == null) {
551
+ return {
552
+ canonicalMissingPath: currentPath,
553
+ exists: false,
554
+ missingSegmentName: segmentName,
555
+ };
556
+ }
557
+ if (opts.collectLinkPaths) {
558
+ opts.collectLinkPaths.add(this.#pathUtils.normalToAbsolute(currentPath));
559
+ }
560
+ const remainingTargetPath = isLastSegment ? '' : targetNormalPath.slice(fromIdx);
561
+ // Append any subsequent path segments to the symlink target, and reset
562
+ // with our new target.
563
+ const joinedResult = this.#pathUtils.joinNormalToRelative(normalSymlinkTarget, remainingTargetPath);
564
+ targetNormalPath = joinedResult.normalPath;
565
+ // Two special cases (covered by unit tests):
566
+ //
567
+ // If the symlink target is the root, the root should be a counted as
568
+ // an ancestor. We'd otherwise miss counting it because we normally
569
+ // push new ancestors only when entering a directory.
570
+ //
571
+ // If the symlink target is an ancestor of the root *and* joining it
572
+ // with the remaining path results in collapsing segments, e.g:
573
+ // '../..' + 'parentofroot/root/foo.js' = 'foo.js', then we must add
574
+ // parentofroot and root as ancestors.
575
+ if (collectAncestors &&
576
+ !isLastSegment &&
577
+ // No-op optimisation to bail out the common case of nothing to do.
578
+ ((ancestorOfRootIdx = this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget)) === 0 ||
579
+ joinedResult.collapsedSegments > 0)) {
580
+ let node = this.#rootNode;
581
+ let collapsedPath = '';
582
+ const reverseAncestors = [];
583
+ for (let i = 0; i <= joinedResult.collapsedSegments && isDirectory(node); i++) {
584
+ if (
585
+ // Add the root only if the target is the root or we have
586
+ // collapsed segments.
587
+ i > 0 ||
588
+ ancestorOfRootIdx === 0 ||
589
+ joinedResult.collapsedSegments > 0) {
590
+ reverseAncestors.push({
591
+ ancestorOfRootIdx: i,
592
+ node,
593
+ normalPath: collapsedPath,
594
+ segmentName: this.#pathUtils.getBasenameOfNthAncestor(i),
595
+ });
596
+ }
597
+ node = node.get('..') ?? new Map();
598
+ collapsedPath = collapsedPath === '' ? '..' : collapsedPath + path_1.default.sep + '..';
599
+ }
600
+ collectAncestors.push(...reverseAncestors.reverse());
601
+ }
602
+ // For the purpose of collecting ancestors: Ignore the traversal to
603
+ // the symlink target, and start collecting ancestors only
604
+ // from the target itself (ie, the basename of the normal target path)
605
+ // onwards.
606
+ unseenPathFromIdx = normalSymlinkTarget.lastIndexOf(path_1.default.sep) + 1;
607
+ if (seen == null) {
608
+ // Optimisation: set this lazily only when we've encountered a symlink
609
+ seen = new Set([requestedNormalPath]);
610
+ }
611
+ if (seen.has(targetNormalPath)) {
612
+ // TODO: Warn `Symlink cycle detected: ${[...seen, node].join(' -> ')}`
613
+ return {
614
+ canonicalMissingPath: targetNormalPath,
615
+ exists: false,
616
+ missingSegmentName: segmentName,
617
+ };
618
+ }
619
+ seen.add(targetNormalPath);
620
+ fromIdx = 0;
621
+ parentNode = this.#rootNode;
622
+ ancestorOfRootIdx = 0;
623
+ }
624
+ }
625
+ (0, invariant_1.default)(parentNode === this.#rootNode, 'Unexpectedly escaped traversal');
626
+ return {
627
+ ancestorOfRootIdx: 0,
628
+ canonicalPath: targetNormalPath,
629
+ exists: true,
630
+ node: this.#rootNode,
631
+ parentNode: null,
632
+ };
633
+ }
634
+ /**
635
+ * Given a start path (which need not exist), a subpath and type, and
636
+ * optionally a 'breakOnSegment', performs the following:
637
+ *
638
+ * X = mixedStartPath
639
+ * do
640
+ * if basename(X) === opts.breakOnSegment
641
+ * return null
642
+ * if X + subpath exists and has type opts.subpathType
643
+ * return {
644
+ * absolutePath: realpath(X + subpath)
645
+ * containerRelativePath: relative(mixedStartPath, X)
646
+ * }
647
+ * X = dirname(X)
648
+ * while X !== dirname(X)
649
+ *
650
+ * If opts.invalidatedBy is given, collects all absolute, real paths that if
651
+ * added or removed may invalidate this result.
652
+ *
653
+ * Useful for finding the closest package scope (subpath: package.json,
654
+ * type f, breakOnSegment: node_modules) or closest potential package root
655
+ * (subpath: node_modules/pkg, type: d) in Node.js resolution.
656
+ */
657
+ hierarchicalLookup(mixedStartPath, subpath, opts) {
658
+ const ancestorsOfInput = [];
659
+ const normalPath = this.#normalizePath(mixedStartPath);
660
+ const invalidatedBy = opts.invalidatedBy;
661
+ const closestLookup = this.#lookupByNormalPath(normalPath, {
662
+ collectAncestors: ancestorsOfInput,
663
+ collectLinkPaths: invalidatedBy,
664
+ });
665
+ if (closestLookup.exists && isDirectory(closestLookup.node)) {
666
+ const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(closestLookup.canonicalPath, subpath, opts.subpathType, invalidatedBy, null);
667
+ if (maybeAbsolutePathMatch != null) {
668
+ return {
669
+ absolutePath: maybeAbsolutePathMatch,
670
+ containerRelativePath: '',
671
+ };
672
+ }
673
+ }
674
+ else {
675
+ if (invalidatedBy && (!closestLookup.exists || !isDirectory(closestLookup.node))) {
676
+ invalidatedBy.add(this.#pathUtils.normalToAbsolute(closestLookup.exists ? closestLookup.canonicalPath : closestLookup.canonicalMissingPath));
677
+ }
678
+ if (opts.breakOnSegment != null &&
679
+ !closestLookup.exists &&
680
+ closestLookup.missingSegmentName === opts.breakOnSegment) {
681
+ return null;
682
+ }
683
+ }
684
+ // Let the "common root" be the nearest common ancestor of this.rootDir
685
+ // and the input path. We'll look for a match in two stages:
686
+ // 1. Every collected ancestor of the input path, from nearest to furthest,
687
+ // that is a descendent of the common root
688
+ // 2. The common root, and its ancestors.
689
+ let commonRoot = this.#rootNode;
690
+ let commonRootDepth = 0;
691
+ // Collected ancestors do not include the lookup result itself, so go one
692
+ // further if the input path is itself a root ancestor.
693
+ if (closestLookup.exists && closestLookup.ancestorOfRootIdx != null) {
694
+ commonRootDepth = closestLookup.ancestorOfRootIdx;
695
+ (0, invariant_1.default)(isDirectory(closestLookup.node), 'ancestors of the root must be directories');
696
+ commonRoot = closestLookup.node;
697
+ }
698
+ else {
699
+ // Establish the common root by counting the '..' segments at the start
700
+ // of the collected ancestors.
701
+ for (const ancestor of ancestorsOfInput) {
702
+ if (ancestor.ancestorOfRootIdx == null) {
703
+ break;
704
+ }
705
+ commonRootDepth = ancestor.ancestorOfRootIdx;
706
+ commonRoot = ancestor.node;
707
+ }
708
+ }
709
+ // Phase 1: Consider descendenants of the common root, from deepest to
710
+ // shallowest.
711
+ for (let candidateIdx = ancestorsOfInput.length - 1; candidateIdx >= commonRootDepth; --candidateIdx) {
712
+ const candidate = ancestorsOfInput[candidateIdx];
713
+ if (candidate.segmentName === opts.breakOnSegment) {
714
+ return null;
715
+ }
716
+ const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(candidate.normalPath, subpath, opts.subpathType, invalidatedBy, {
717
+ ancestorOfRootIdx: candidate.ancestorOfRootIdx,
718
+ node: candidate.node,
719
+ pathIdx: candidate.normalPath.length > 0 ? candidate.normalPath.length + 1 : 0,
720
+ });
721
+ if (maybeAbsolutePathMatch != null) {
722
+ // Determine the input path relative to the current candidate. Note
723
+ // that the candidate path will always be canonical (real), whereas the
724
+ // input may contain symlinks, so the candidate is not necessarily a
725
+ // prefix of the input. Use the fact that each remaining candidate
726
+ // corresponds to a leading segment of the input normal path, and
727
+ // discard the first candidateIdx + 1 segments of the input path.
728
+ //
729
+ // The next 5 lines are equivalent to (but faster than)
730
+ // normalPath.split('/').slice(candidateIdx + 1).join('/').
731
+ let prefixLength = commonRootDepth * 3; // Leading '../'
732
+ for (let i = commonRootDepth; i <= candidateIdx; i++) {
733
+ prefixLength = normalPath.indexOf(path_1.default.sep, prefixLength + 1);
734
+ }
735
+ const containerRelativePath = normalPath.slice(prefixLength + 1);
736
+ return {
737
+ absolutePath: maybeAbsolutePathMatch,
738
+ containerRelativePath,
739
+ };
740
+ }
741
+ }
742
+ // Phase 2: Consider the common root and its ancestors
743
+ // This will be '', '..', '../..', etc.
744
+ let candidateNormalPath = commonRootDepth > 0 ? normalPath.slice(0, 3 * commonRootDepth - 1) : '';
745
+ const remainingNormalPath = normalPath.slice(commonRootDepth * 3);
746
+ let nextNode = commonRoot;
747
+ let depthBelowCommonRoot = 0;
748
+ while (isDirectory(nextNode)) {
749
+ const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(candidateNormalPath, subpath, opts.subpathType, invalidatedBy, null);
750
+ if (maybeAbsolutePathMatch != null) {
751
+ const rootDirParts = this.#pathUtils.getParts();
752
+ const relativeParts = depthBelowCommonRoot > 0
753
+ ? rootDirParts.slice(-(depthBelowCommonRoot + commonRootDepth), commonRootDepth > 0 ? -commonRootDepth : undefined)
754
+ : [];
755
+ if (remainingNormalPath !== '') {
756
+ relativeParts.push(remainingNormalPath);
757
+ }
758
+ return {
759
+ absolutePath: maybeAbsolutePathMatch,
760
+ containerRelativePath: relativeParts.join(path_1.default.sep),
761
+ };
762
+ }
763
+ depthBelowCommonRoot++;
764
+ candidateNormalPath =
765
+ candidateNormalPath === '' ? '..' : candidateNormalPath + path_1.default.sep + '..';
766
+ nextNode = nextNode.get('..');
767
+ }
768
+ return null;
769
+ }
770
+ #checkCandidateHasSubpath(normalCandidatePath, subpath, subpathType, invalidatedBy, start) {
771
+ const lookupResult = this.#lookupByNormalPath(this.#pathUtils.joinNormalToRelative(normalCandidatePath, subpath).normalPath, {
772
+ collectLinkPaths: invalidatedBy,
773
+ });
774
+ if (lookupResult.exists &&
775
+ // Should be a Map iff subpathType is directory
776
+ isDirectory(lookupResult.node) === (subpathType === 'd')) {
777
+ return this.#pathUtils.normalToAbsolute(lookupResult.canonicalPath);
778
+ }
779
+ else if (invalidatedBy) {
780
+ invalidatedBy.add(this.#pathUtils.normalToAbsolute(lookupResult.exists ? lookupResult.canonicalPath : lookupResult.canonicalMissingPath));
781
+ }
782
+ return null;
783
+ }
784
+ *metadataIterator(opts) {
785
+ yield* this.#metadataIterator(this.#rootNode, opts);
786
+ }
787
+ *#metadataIterator(rootNode, opts, prefix = '') {
788
+ for (const [name, node] of rootNode) {
789
+ if (node == null) {
790
+ continue;
791
+ }
792
+ else if (!opts.includeNodeModules && isDirectory(node) && name === 'node_modules') {
793
+ continue;
794
+ }
795
+ const prefixedName = prefix === '' ? name : prefix + path_1.default.sep + name;
796
+ if (isDirectory(node)) {
797
+ yield* this.#metadataIterator(node, opts, prefixedName);
798
+ }
799
+ else if (isRegularFile(node) || opts.includeSymlinks) {
800
+ yield { baseName: name, canonicalPath: prefixedName, metadata: node };
801
+ }
802
+ }
803
+ }
804
+ #normalizePath(relativeOrAbsolutePath) {
805
+ return path_1.default.isAbsolute(relativeOrAbsolutePath)
806
+ ? this.#pathUtils.absoluteToNormal(relativeOrAbsolutePath)
807
+ : this.#pathUtils.relativeToNormal(relativeOrAbsolutePath);
808
+ }
809
+ *#directoryNodeIterator(node, parent, ancestorOfRootIdx) {
810
+ if (ancestorOfRootIdx != null && ancestorOfRootIdx > 0 && parent) {
811
+ yield [this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1), parent];
812
+ }
813
+ yield* node.entries();
814
+ }
815
+ /**
816
+ * Enumerate paths under a given node, including symlinks and through
817
+ * symlinks (if `follow` is enabled).
818
+ */
819
+ *#pathIterator(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx, opts, pathPrefix = '', followedLinks = new Set()) {
820
+ const pathSep = opts.alwaysYieldPosix ? '/' : path_1.default.sep;
821
+ const prefixWithSep = pathPrefix === '' ? pathPrefix : pathPrefix + pathSep;
822
+ // Optimization: We can attempt to eagerly populate directories we're visiting
823
+ // if they're missing and not accessing a parent ('..')
824
+ if (this.#fallbackFilesystem != null &&
825
+ iterationRootNode.size === 0 &&
826
+ pathPrefix !== '..' &&
827
+ !pathPrefix.endsWith(pathSep + '..')) {
828
+ const canonicalRoot = opts.canonicalPathOfRoot;
829
+ const rootCanonical = pathPrefix === '' ? canonicalRoot : canonicalRoot + path_1.default.sep + pathPrefix;
830
+ this.#populateDirFromFilesystem(iterationRootNode, rootCanonical, false);
831
+ }
832
+ for (const [name, node] of this.#directoryNodeIterator(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx)) {
833
+ if (node == null) {
834
+ continue;
835
+ }
836
+ else if (opts.subtreeOnly && name === '..') {
837
+ continue;
838
+ }
839
+ const nodePath = prefixWithSep + name;
840
+ if (!isDirectory(node)) {
841
+ if (isRegularFile(node)) {
842
+ // regular file
843
+ yield nodePath;
844
+ }
845
+ else {
846
+ // symlink
847
+ const nodePathWithSystemSeparators = pathSep === path_1.default.sep ? nodePath : nodePath.replaceAll(pathSep, path_1.default.sep);
848
+ // Although both paths are normal, the node path may begin '..' so we
849
+ // can't simply concatenate.
850
+ const normalPathOfSymlink = path_1.default.join(opts.canonicalPathOfRoot, nodePathWithSystemSeparators);
851
+ // We can't resolve the symlink directly here because we only have
852
+ // its normal path, and we need a canonical path for resolution
853
+ // (imagine our normal path contains a symlink 'bar' -> '.', and we
854
+ // are at /foo/bar/baz where baz -> '..' - that should resolve to
855
+ // /foo, not /foo/bar). We *can* use _lookupByNormalPath to walk to
856
+ // the canonical symlink, and then to its target.
857
+ const resolved = this.#lookupByNormalPath(normalPathOfSymlink, {
858
+ followLeaf: true,
859
+ });
860
+ if (!resolved.exists) {
861
+ // Symlink goes nowhere, nothing to report.
862
+ continue;
863
+ }
864
+ const target = resolved.node;
865
+ if (!isDirectory(target)) {
866
+ // Symlink points to a file, just yield the path of the symlink.
867
+ yield nodePath;
868
+ }
869
+ else if (opts.recursive && opts.follow && !followedLinks.has(node)) {
870
+ // Symlink points to a directory - iterate over its contents using
871
+ // the path where we found the symlink as a prefix.
872
+ yield* this.#pathIterator(target, resolved.parentNode, resolved.ancestorOfRootIdx, opts, nodePath, new Set([...followedLinks, node]));
873
+ }
874
+ }
875
+ }
876
+ else if (opts.recursive) {
877
+ // Optimization: We can attempt to eagerly popuplate directories we're visiting
878
+ // if they're missing and not accessing a parent ('..')
879
+ if (this.#fallbackFilesystem != null && node.size === 0 && name !== '..') {
880
+ const nodePathWithSystemSeparators = pathSep === path_1.default.sep ? nodePath : nodePath.replaceAll(pathSep, path_1.default.sep);
881
+ const canonicalPath = opts.canonicalPathOfRoot === ''
882
+ ? nodePathWithSystemSeparators
883
+ : opts.canonicalPathOfRoot + path_1.default.sep + nodePathWithSystemSeparators;
884
+ this.#populateDirFromFilesystem(node, canonicalPath, false);
885
+ }
886
+ yield* this.#pathIterator(node, iterationRootParentNode, ancestorOfRootIdx != null && ancestorOfRootIdx > 0 ? ancestorOfRootIdx - 1 : null, opts, nodePath, followedLinks);
887
+ }
888
+ }
889
+ }
890
+ #resolveSymlinkTargetToNormalPath(symlinkNode, canonicalPathOfSymlink) {
891
+ const symlinkTarget = symlinkNode[constants_1.default.SYMLINK];
892
+ if (symlinkTarget === 1) {
893
+ // Symlink target not yet resolved — read it lazily on first traversal
894
+ const absoluteSymlink = this.#pathUtils.normalToAbsolute(canonicalPathOfSymlink);
895
+ try {
896
+ const literalSymlinkTarget = fs_1.default.readlinkSync(absoluteSymlink);
897
+ const normalTarget = this.#pathUtils.resolveSymlinkToNormal(canonicalPathOfSymlink, literalSymlinkTarget);
898
+ symlinkNode[constants_1.default.SYMLINK] = (0, normalizePathSeparatorsToPosix_1.default)(normalTarget);
899
+ symlinkNode[constants_1.default.VISITED] = 1;
900
+ return normalTarget;
901
+ }
902
+ catch {
903
+ return null;
904
+ }
905
+ }
906
+ else if (symlinkTarget === 0 || symlinkTarget == null) {
907
+ // WARN: We shouldn't call this method on non-symlinks. Outside of tests
908
+ // this condition shouldn't trigger. It's fine not to resolve a symlink if
909
+ // it does trigger however
910
+ return null;
911
+ }
912
+ else {
913
+ (0, invariant_1.default)(typeof symlinkTarget === 'string', 'Expected symlink target to be populated.');
914
+ return (0, normalizePathSeparatorsToSystem_1.default)(symlinkTarget);
915
+ }
916
+ }
917
+ #getFileData(filePath, opts = { followLeaf: true }) {
918
+ const normalPath = this.#normalizePath(filePath);
919
+ const result = this.#lookupByNormalPath(normalPath, {
920
+ followLeaf: opts.followLeaf,
921
+ });
922
+ if (!result.exists || isDirectory(result.node)) {
923
+ return null;
924
+ }
925
+ return result.node;
926
+ }
927
+ #cloneTree(root, prefix) {
928
+ const clone = new Map();
929
+ for (const [name, node] of root) {
930
+ if (node == null) {
931
+ continue;
932
+ }
933
+ else if (isDirectory(node)) {
934
+ const childPath = prefix === '' ? name : prefix + path_1.default.sep + name;
935
+ if (this.#rootPattern == null || this.#rootPattern.test(childPath + path_1.default.sep)) {
936
+ clone.set(name, this.#cloneTree(node, childPath));
937
+ }
938
+ }
939
+ else {
940
+ clone.set(name, [...node]);
941
+ }
942
+ }
943
+ return clone;
944
+ }
945
+ #isOutsideFallbackBoundary(canonicalPath) {
946
+ const maxDepth = this.#fallbackBoundaryDepth;
947
+ return maxDepth != null && (0, RootPathUtils_1.getAncestorOfRootIdx)(canonicalPath) > maxDepth;
948
+ }
949
+ /**
950
+ * Synchronously populate a missing tree node by querying the injected
951
+ * fallback filesystem. The fallback returns tree-compatible nodes
952
+ * (FileMetadata tuples or directory Maps) that are inserted directly.
953
+ *
954
+ * Returns the newly created node, or null if the path doesn't exist on disk.
955
+ */
956
+ #populateFromFilesystem(parentNode, segmentName, parentCanonicalPath) {
957
+ const fallback = this.#fallbackFilesystem;
958
+ if (fallback == null) {
959
+ return null;
960
+ }
961
+ const childCanonicalPath = parentCanonicalPath === '' ? segmentName : parentCanonicalPath + path_1.default.sep + segmentName;
962
+ if (this.#rootPattern?.test(childCanonicalPath + path_1.default.sep) ||
963
+ this.#isOutsideFallbackBoundary(childCanonicalPath)) {
964
+ return null;
965
+ }
966
+ else if (parentCanonicalPath !== '' && (0, fallback_1.shouldFallbackCrawlDir)(parentCanonicalPath)) {
967
+ this.#populateDirFromFilesystem(parentNode, parentCanonicalPath, true);
968
+ return parentNode.get(segmentName) ?? null;
969
+ }
970
+ else if (parentNode.has(segmentName)) {
971
+ return parentNode.get(segmentName) ?? null;
972
+ }
973
+ else {
974
+ const parentAbsolute = this.#pathUtils.normalToAbsolute(parentCanonicalPath);
975
+ const absolutePath = parentAbsolute + path_1.default.sep + segmentName;
976
+ const node = fallback.lookup(childCanonicalPath, absolutePath, parentNode.get(segmentName));
977
+ parentNode.set(segmentName, node);
978
+ return node;
979
+ }
980
+ }
981
+ /**
982
+ * Populate an existing (potentially empty sentinel) directory node from
983
+ * the filesystem. Used by #pathIterator to fill lazy directories before
984
+ * iteration, and by #populateFromFilesystem for optimistic parent
985
+ * population.
986
+ */
987
+ #populateDirFromFilesystem(dirNode, canonicalPath, skipCheck) {
988
+ const fallback = this.#fallbackFilesystem;
989
+ if (fallback == null ||
990
+ (!skipCheck &&
991
+ (this.#rootPattern?.test(canonicalPath + path_1.default.sep) ||
992
+ this.#isOutsideFallbackBoundary(canonicalPath)))) {
993
+ return;
994
+ }
995
+ const absolutePath = this.#pathUtils.normalToAbsolute(canonicalPath);
996
+ const entries = fallback.readdir(canonicalPath, absolutePath, dirNode);
997
+ if (entries != null && entries !== dirNode) {
998
+ for (const [name, entry] of entries) {
999
+ if (!dirNode.has(name)) {
1000
+ dirNode.set(name, entry);
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ exports.default = TreeFS;