@expo/metro-file-map 56.0.0 → 56.0.2

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.
@@ -5,6 +5,13 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import type { BuildParameters, CacheData, CacheManager, CacheManagerFactoryOptions, CacheManagerWriteOptions } from '../types';
8
+ declare global {
9
+ namespace NodeJS {
10
+ interface Process {
11
+ isBun?: boolean;
12
+ }
13
+ }
14
+ }
8
15
  interface AutoSaveOptions {
9
16
  readonly debounceMs: number;
10
17
  }
@@ -17,7 +17,12 @@ const timers_1 = require("timers");
17
17
  const v8_1 = require("v8");
18
18
  const rootRelativeCacheKeys_1 = __importDefault(require("../lib/rootRelativeCacheKeys"));
19
19
  const debug = require('debug')('Metro:FileMapCache');
20
- const DEFAULT_PREFIX = 'metro-file-map';
20
+ let DEFAULT_PREFIX = 'metro-file-map';
21
+ if (process.isBun) {
22
+ // NOTE(@kitten): The v8 serialize/deserialize format isn't 100% compatible between
23
+ // Node and Bun and therefore we should fork the cache file
24
+ DEFAULT_PREFIX += '-bun';
25
+ }
21
26
  const DEFAULT_DIRECTORY = (0, os_1.tmpdir)();
22
27
  const DEFAULT_AUTO_SAVE_DEBOUNCE_MS = 5000;
23
28
  // NOTE(@kitten): We're incompatible with Metro, so need our own naming
@@ -68,7 +68,8 @@ function createFallbackFilesystem(opts) {
68
68
  }
69
69
  if (entry.isDirectory()) {
70
70
  // NOTE(@kitten): ".git" and ".hg" check replace the VCS_DIRECTORIES ignore pattern
71
- if (!result.has(name) && name !== '.git' && name !== '.hg') {
71
+ // NOTE(@kitten): `.cxx` is ephemeral and should always be safe to ignore
72
+ if (!result.has(name) && name !== '.git' && name !== '.hg' && name !== '.cxx') {
72
73
  const childDir = new Map();
73
74
  markDir(childDir, FallbackFlag.VISITED);
74
75
  result.set(name, childDir);
@@ -58,15 +58,21 @@ function find(roots, extensions, ignore, includeSymlinks, rootDir, console, prev
58
58
  fs.readdir(directory, { withFileTypes: true }, (err, entries) => {
59
59
  activeCalls--;
60
60
  if (err) {
61
- console.warn(`Error "${err.code ?? err.message}" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`);
61
+ // NOTE(@kitten): This isn't necessarily a problem and we can ignore this
62
+ /*
63
+ console.warn(
64
+ `Error "${(err as any).code ?? err.message}" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`
65
+ );
66
+ */
62
67
  }
63
68
  else {
64
69
  for (let idx = 0; idx < entries.length; idx++) {
65
70
  const entry = entries[idx];
66
71
  const name = entry.name;
67
72
  // NOTE(@kitten): This replaces the VCS_DIRECTORIES ignore pattern
73
+ // NOTE(@kitten): `.cxx` is ephemeral and should always be safe to ignore
68
74
  const isDirectory = entry.isDirectory();
69
- if (isDirectory && (name === '.git' || name === '.hg')) {
75
+ if (isDirectory && (name === '.git' || name === '.hg' || name === '.cxx')) {
70
76
  continue;
71
77
  }
72
78
  const file = directory + path.sep + name;
@@ -49,7 +49,7 @@ const path = __importStar(require("path"));
49
49
  const perf_hooks_1 = require("perf_hooks");
50
50
  const planQuery_1 = require("./planQuery");
51
51
  const RootPathUtils_1 = require("../../lib/RootPathUtils");
52
- const isVcsPath_1 = __importDefault(require("../../lib/isVcsPath"));
52
+ const isWatcherExcluded_1 = __importDefault(require("../../lib/isWatcherExcluded"));
53
53
  const normalizePathSeparatorsToPosix_1 = __importDefault(require("../../lib/normalizePathSeparatorsToPosix"));
54
54
  const normalizePathSeparatorsToSystem_1 = __importDefault(require("../../lib/normalizePathSeparatorsToSystem"));
55
55
  const WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS = 10000;
@@ -248,7 +248,7 @@ async function watchmanCrawl({ abortSignal, computeSha1, extensions, ignore, inc
248
248
  // Whether watchman can return exists: false in a fresh instance
249
249
  // response is unknown, but there's nothing we need to do in that case.
250
250
  }
251
- else if (!(0, isVcsPath_1.default)(fileData.name) && !ignore(filePath)) {
251
+ else if (!(0, isWatcherExcluded_1.default)(fileData.name) && !ignore(filePath)) {
252
252
  const { mtime_ms, size } = fileData;
253
253
  (0, invariant_1.default)(mtime_ms != null && size != null, 'missing file data in watchman response');
254
254
  const mtime = typeof mtime_ms === 'number' ? mtime_ms : mtime_ms.toNumber();
@@ -48,6 +48,15 @@ const UP_FRAGMENT_SEP = '..' + path_1.default.sep;
48
48
  const SEP_UP_FRAGMENT = path_1.default.sep + '..';
49
49
  const UP_FRAGMENT_SEP_LENGTH = UP_FRAGMENT_SEP.length;
50
50
  const CURRENT_FRAGMENT = '.' + path_1.default.sep;
51
+ const IS_WIN32 = path_1.default.sep === '\\';
52
+ const ROOT_BASE_IDX = IS_WIN32 ? 0 : 1;
53
+ function startsWithDriveLetter(str) {
54
+ if (!IS_WIN32 || str.charCodeAt(1) !== 58 /* ':' */) {
55
+ return false;
56
+ }
57
+ const c = str.charCodeAt(0);
58
+ return (c >= 65 && c <= 90) /* A-Z */ || (c >= 97 && c <= 122) /* a-z */;
59
+ }
51
60
  class RootPathUtils {
52
61
  #rootDir;
53
62
  #rootDirnames;
@@ -121,6 +130,13 @@ class RootPathUtils {
121
130
  if (right.length === 0) {
122
131
  return left;
123
132
  }
133
+ else if (IS_WIN32 && pos > this.#rootDepth * UP_FRAGMENT_SEP_LENGTH) {
134
+ // On a real file system, navigating to `..` at the top level (posix `/`
135
+ // or Windows drive) is a no-op, but we can't respect that on Windows
136
+ // because Metro uses e.g. `..\..\D:\foo` to represent cross-drive
137
+ // relative paths.
138
+ return right;
139
+ }
124
140
  // left may already end in a path separator only if it is a filesystem root,
125
141
  // '/' or 'X:\'.
126
142
  if (i === this.#rootDepth) {
@@ -135,8 +151,11 @@ class RootPathUtils {
135
151
  resolveSymlinkToNormal(symlinkNormalPath, readlinkResult) {
136
152
  let target = (0, normalizePathSeparatorsToSystem_1.default)(readlinkResult);
137
153
  // WARN: This only applies to Windows + Node 20 case, where the value is completely
138
- // unnormalized and a trailing slash may be returned
139
- if (target[target.length - 1] === path_1.default.sep) {
154
+ // unnormalized and a trailing slash may be returned. Skip the strip when the target
155
+ // is a filesystem root: POSIX '/' or Windows 'X:\'
156
+ const len = target.length;
157
+ const isFsRoot = len === 1 || (len === 3 && startsWithDriveLetter(target));
158
+ if (!isFsRoot && target[len - 1] === path_1.default.sep) {
140
159
  target = target.slice(0, -1);
141
160
  }
142
161
  if (path_1.default.isAbsolute(target)) {
@@ -176,7 +195,7 @@ class RootPathUtils {
176
195
  if (relativePath === '') {
177
196
  return { collapsedSegments: 0, normalPath };
178
197
  }
179
- const left = normalPath + path_1.default.sep;
198
+ const left = normalPath.endsWith(path_1.default.sep) ? normalPath : normalPath + path_1.default.sep;
180
199
  const rawPath = left + relativePath;
181
200
  if (normalPath === '..' || normalPath.endsWith(SEP_UP_FRAGMENT)) {
182
201
  const collapsed = this.#tryCollapseIndirectionsInSuffix(rawPath, 0, 0);
@@ -262,9 +281,10 @@ class RootPathUtils {
262
281
  collapsedSegments,
263
282
  };
264
283
  }
265
- // Cap the number of indirections at the total number of root segments.
266
- // File systems treat '..' at the root as '.'.
267
- if (totalUpIndirections < this.#rootParts.length - 1) {
284
+ // Cap the number of indirections at the total number of root parts.
285
+ // File systems treat '..' at the root as '.'. For Windows, cross-device
286
+ // paths need to survive this
287
+ if (totalUpIndirections < this.#rootParts.length - ROOT_BASE_IDX) {
268
288
  totalUpIndirections++;
269
289
  }
270
290
  if (nextIndirection !== pos + 1 || // Fallback when ./ later in the path, or leading
@@ -13,10 +13,10 @@ const fs_1 = __importDefault(require("fs"));
13
13
  const invariant_1 = __importDefault(require("invariant"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const constants_1 = __importDefault(require("../constants"));
16
+ const RootPathUtils_1 = require("./RootPathUtils");
16
17
  const normalizePathSeparatorsToPosix_1 = __importDefault(require("./normalizePathSeparatorsToPosix"));
17
18
  const normalizePathSeparatorsToSystem_1 = __importDefault(require("./normalizePathSeparatorsToSystem"));
18
19
  const fallback_1 = require("../crawlers/node/fallback");
19
- const RootPathUtils_1 = require("./RootPathUtils");
20
20
  function isDirectory(node) {
21
21
  return node instanceof Map;
22
22
  }
@@ -116,8 +116,11 @@ class TreeFS {
116
116
  return tfs;
117
117
  }
118
118
  getSize(mixedPath) {
119
- const fileMetadata = this.#getFileData(mixedPath);
120
- return (fileMetadata && fileMetadata[constants_1.default.SIZE]) ?? null;
119
+ const result = this.#lookup(this.#normalizePath(mixedPath));
120
+ if (!result.exists || isDirectory(result.node)) {
121
+ return null;
122
+ }
123
+ return result.node[constants_1.default.SIZE] ?? null;
121
124
  }
122
125
  getDifference(files, options) {
123
126
  const changedFiles = new Map(files);
@@ -127,9 +130,7 @@ class TreeFS {
127
130
  let rootNode = this.#rootNode;
128
131
  let prefix = '';
129
132
  if (subpath != null && subpath !== '') {
130
- const lookupResult = this.#lookupByNormalPath(subpath, {
131
- followLeaf: true,
132
- });
133
+ const lookupResult = this.#lookup(subpath);
133
134
  if (!lookupResult.exists || !isDirectory(lookupResult.node)) {
134
135
  // Directory doesn't exist, nothing to compare - all files are new
135
136
  return { changedFiles, removedFiles };
@@ -137,15 +138,12 @@ class TreeFS {
137
138
  rootNode = lookupResult.node;
138
139
  prefix = lookupResult.canonicalPath;
139
140
  }
140
- for (const { canonicalPath, metadata } of this.#metadataIterator(rootNode, {
141
- includeNodeModules: true,
142
- includeSymlinks: true,
143
- }, prefix)) {
141
+ this.#forEachMetadata(rootNode, { includeNodeModules: true, includeSymlinks: true }, prefix, (_baseName, canonicalPath, metadata) => {
144
142
  const newMetadata = files.get(canonicalPath);
145
143
  if (newMetadata) {
146
144
  if (isRegularFile(newMetadata) !== isRegularFile(metadata)) {
147
145
  // Types differ, file has changed
148
- continue;
146
+ return;
149
147
  }
150
148
  if (newMetadata[constants_1.default.MTIME] != null &&
151
149
  newMetadata[constants_1.default.MTIME] !== 0 &&
@@ -170,28 +168,29 @@ class TreeFS {
170
168
  else {
171
169
  removedFiles.add(canonicalPath);
172
170
  }
173
- }
171
+ });
174
172
  return {
175
173
  changedFiles,
176
174
  removedFiles,
177
175
  };
178
176
  }
179
177
  getMtimeByNormalPath(normalPath) {
180
- const result = this.#lookupByNormalPath(normalPath, {
181
- followLeaf: false,
182
- skipFallback: true,
183
- });
178
+ // skipFallback=true: this is a cache-validation lookup; consulting the
179
+ // fallback filesystem here would be expensive AND mutate the tree as a
180
+ // side effect via `#populateFromFilesystem`.
181
+ const result = this.#walkLookupNoFollow(this.#rootNode, 0, 0, normalPath, undefined, true);
184
182
  return result.exists && !isDirectory(result.node) ? result.node[constants_1.default.MTIME] : null;
185
183
  }
186
184
  getSha1(mixedPath) {
187
- const fileMetadata = this.#getFileData(mixedPath);
188
- return (fileMetadata && fileMetadata[constants_1.default.SHA1]) ?? null;
185
+ const result = this.#lookup(this.#normalizePath(mixedPath));
186
+ if (!result.exists || isDirectory(result.node)) {
187
+ return null;
188
+ }
189
+ return result.node[constants_1.default.SHA1] ?? null;
189
190
  }
190
191
  async getOrComputeSha1(mixedPath) {
191
192
  const normalPath = this.#normalizePath(mixedPath);
192
- const result = this.#lookupByNormalPath(normalPath, {
193
- followLeaf: true,
194
- });
193
+ const result = this.#lookup(normalPath);
195
194
  if (!result.exists || isDirectory(result.node)) {
196
195
  return null;
197
196
  }
@@ -230,16 +229,13 @@ class TreeFS {
230
229
  : { sha1 };
231
230
  }
232
231
  exists(mixedPath) {
233
- const result = this.#getFileData(mixedPath);
234
- return result != null;
232
+ const result = this.#lookup(this.#normalizePath(mixedPath));
233
+ return result.exists && !isDirectory(result.node);
235
234
  }
236
235
  lookup(mixedPath) {
237
236
  const normalPath = this.#normalizePath(mixedPath);
238
237
  const links = new Set();
239
- const result = this.#lookupByNormalPath(normalPath, {
240
- collectLinkPaths: links,
241
- followLeaf: true,
242
- });
238
+ const result = this.#lookup(normalPath, links);
243
239
  if (!result.exists) {
244
240
  const { canonicalMissingPath } = result;
245
241
  return {
@@ -257,19 +253,33 @@ class TreeFS {
257
253
  return { exists: true, links, realPath, type: 'f', metadata: node };
258
254
  }
259
255
  getAllFiles() {
260
- return Array.from(this.metadataIterator({
261
- includeNodeModules: true,
262
- includeSymlinks: false,
263
- }), ({ canonicalPath }) => this.#pathUtils.normalToAbsolute(canonicalPath));
256
+ const result = [];
257
+ this.#collectAllFilesInto(this.#rootNode, '', result);
258
+ return result;
259
+ }
260
+ // NOTE(@kitten): Specialize the `getAllFiles` collector, to avoid a megamorphic deopt in V8
261
+ #collectAllFilesInto(node, prefix, result) {
262
+ for (const [name, child] of node) {
263
+ if (child == null) {
264
+ continue;
265
+ }
266
+ const prefixedName = prefix === '' ? name : prefix + path_1.default.sep + name;
267
+ if (isDirectory(child)) {
268
+ this.#collectAllFilesInto(child, prefixedName, result);
269
+ }
270
+ else if (isRegularFile(child)) {
271
+ result.push(this.#pathUtils.normalToAbsolute(prefixedName));
272
+ }
273
+ }
264
274
  }
265
275
  linkStats(mixedPath) {
266
- const fileMetadata = this.#getFileData(mixedPath, { followLeaf: false });
267
- if (fileMetadata == null) {
276
+ const result = this.#lookupNoFollow(this.#normalizePath(mixedPath));
277
+ if (!result.exists || isDirectory(result.node)) {
268
278
  return null;
269
279
  }
270
- const fileType = isRegularFile(fileMetadata) ? 'f' : 'l';
280
+ const fileMetadata = result.node;
271
281
  return {
272
- fileType,
282
+ fileType: isRegularFile(fileMetadata) ? 'f' : 'l',
273
283
  modifiedTime: fileMetadata[constants_1.default.MTIME],
274
284
  size: fileMetadata[constants_1.default.SIZE],
275
285
  };
@@ -282,7 +292,7 @@ class TreeFS {
282
292
  *matchFiles(opts) {
283
293
  const { filter = null, filterCompareAbsolute = false, filterComparePosix = false, follow = false, recursive = true, rootDir = null, } = opts;
284
294
  const normalRoot = rootDir == null ? '' : this.#normalizePath(rootDir);
285
- const contextRootResult = this.#lookupByNormalPath(normalRoot);
295
+ const contextRootResult = this.#lookup(normalRoot);
286
296
  if (!contextRootResult.exists) {
287
297
  return;
288
298
  }
@@ -295,13 +305,14 @@ class TreeFS {
295
305
  const contextRootAbsolutePathForComparison = filterComparePosix && path_1.default.sep !== '/'
296
306
  ? contextRootAbsolutePath.replaceAll(path_1.default.sep, '/')
297
307
  : contextRootAbsolutePath;
298
- for (const relativePathForComparison of this.#pathIterator(contextRoot, contextRootParent, ancestorOfRootIdx, {
308
+ const matches = [];
309
+ this.#forEachPath(contextRoot, contextRootParent, ancestorOfRootIdx, {
299
310
  alwaysYieldPosix: filterComparePosix,
300
311
  canonicalPathOfRoot: rootRealPath,
301
312
  follow,
302
313
  recursive,
303
314
  subtreeOnly: rootDir != null,
304
- })) {
315
+ }, '', new Set(), (relativePathForComparison) => {
305
316
  if (filter == null ||
306
317
  filter.test(
307
318
  // NOTE(EvanBacon): Ensure files start with `./` for matching purposes
@@ -313,24 +324,44 @@ class TreeFS {
313
324
  const relativePath = filterComparePosix === true && path_1.default.sep !== '/'
314
325
  ? relativePathForComparison.replaceAll('/', path_1.default.sep)
315
326
  : relativePathForComparison;
316
- yield path_1.default.join(contextRootAbsolutePath, relativePath);
327
+ matches.push(path_1.default.join(contextRootAbsolutePath, relativePath));
317
328
  }
318
- }
329
+ });
330
+ yield* matches;
319
331
  }
320
332
  addOrModify(mixedPath, metadata, changeListener) {
321
333
  const normalPath = this.#normalizePath(mixedPath);
334
+ const dirname = path_1.default.dirname(normalPath);
335
+ const basename = path_1.default.basename(normalPath);
322
336
  // Walk the tree to find the *real* path of the parent node, creating
323
337
  // directories as we need.
324
- const parentDirNode = this.#lookupByNormalPath(path_1.default.dirname(normalPath), {
325
- changeListener,
326
- makeDirectories: true,
327
- });
338
+ const onSegment = changeListener
339
+ ? (_node, segmentNormalPath, _segmentName, _idx, isNewlyCreated) => {
340
+ if (isNewlyCreated) {
341
+ changeListener.directoryAdded(segmentNormalPath);
342
+ }
343
+ }
344
+ : undefined;
345
+ const parentDirNode = this.#walkAndMakeDirectories(this.#rootNode, 0, 0, dirname, true, onSegment);
328
346
  if (!parentDirNode.exists) {
329
347
  throw new Error(`TreeFS: Failed to make parent directory entry for ${mixedPath}`);
330
348
  }
331
- // Normalize the resulting path to account for the parent node being root.
332
- const canonicalPath = this.#normalizePath(parentDirNode.canonicalPath + path_1.default.sep + path_1.default.basename(normalPath));
333
- this.bulkAddOrModify(new Map([[canonicalPath, metadata]]), changeListener);
349
+ if (!isDirectory(parentDirNode.node)) {
350
+ throw new Error(`TreeFS: Could not add directory ${dirname}, adding ${mixedPath}. ` +
351
+ `${dirname} already exists in the file map as a file.`);
352
+ }
353
+ const canonicalPath = this.#normalizePath(parentDirNode.canonicalPath + path_1.default.sep + basename);
354
+ if (changeListener != null) {
355
+ const existingNode = parentDirNode.node.get(basename);
356
+ if (existingNode != null) {
357
+ (0, invariant_1.default)(!isDirectory(existingNode), 'Detected addition or modification of file %s, but it is tracked as a non-empty directory', canonicalPath);
358
+ changeListener.fileModified(canonicalPath, existingNode, metadata);
359
+ }
360
+ else {
361
+ changeListener.fileAdded(canonicalPath, metadata);
362
+ }
363
+ }
364
+ parentDirNode.node.set(basename, metadata);
334
365
  }
335
366
  bulkAddOrModify(addedOrModifiedFiles, changeListener) {
336
367
  // Optimisation: Bulk FileData are typically clustered by directory, so we
@@ -339,16 +370,19 @@ class TreeFS {
339
370
  // faster than caching all lookups in a Map, and 70% faster than no cache.
340
371
  let lastDir;
341
372
  let directoryNode;
373
+ const onSegment = changeListener
374
+ ? (_node, segmentNormalPath, _segmentName, _idx, isNewlyCreated) => {
375
+ if (isNewlyCreated) {
376
+ changeListener.directoryAdded(segmentNormalPath);
377
+ }
378
+ }
379
+ : undefined;
342
380
  for (const [normalPath, metadata] of addedOrModifiedFiles) {
343
381
  const lastSepIdx = normalPath.lastIndexOf(path_1.default.sep);
344
382
  const dirname = lastSepIdx === -1 ? '' : normalPath.slice(0, lastSepIdx);
345
383
  const basename = lastSepIdx === -1 ? normalPath : normalPath.slice(lastSepIdx + 1);
346
384
  if (directoryNode == null || dirname !== lastDir) {
347
- const lookup = this.#lookupByNormalPath(dirname, {
348
- changeListener,
349
- followLeaf: false,
350
- makeDirectories: true,
351
- });
385
+ const lookup = this.#walkAndMakeDirectories(this.#rootNode, 0, 0, dirname, false, onSegment);
352
386
  if (!lookup.exists) {
353
387
  // This should only be possible if the input is non-real and
354
388
  // lookup hits a broken symlink.
@@ -379,14 +413,17 @@ class TreeFS {
379
413
  }
380
414
  remove(mixedPath, changeListener) {
381
415
  const normalPath = this.#normalizePath(mixedPath);
382
- const result = this.#lookupByNormalPath(normalPath, { followLeaf: false });
416
+ this.#removeNormalPath(normalPath, changeListener);
417
+ }
418
+ #removeNormalPath(normalPath, changeListener) {
419
+ const result = this.#lookupNoFollow(normalPath);
383
420
  if (!result.exists) {
384
421
  return;
385
422
  }
386
423
  const { parentNode, canonicalPath, node } = result;
387
424
  if (isDirectory(node) && node.size > 0) {
388
425
  for (const basename of node.keys()) {
389
- this.remove(canonicalPath + path_1.default.sep + basename, changeListener);
426
+ this.#removeNormalPath(canonicalPath + path_1.default.sep + basename, changeListener);
390
427
  }
391
428
  // Removing the last file will delete this directory
392
429
  return;
@@ -405,9 +442,8 @@ class TreeFS {
405
442
  // NB: This isn't the most efficient algorithm - in the case of
406
443
  // removing the last file in a deep hierarchy it's O(depth^2), but
407
444
  // that's not expected to be a case common enough to justify
408
- // implementation complexity, or slowing down more common uses of
409
- // _lookupByNormalPath.
410
- this.remove(path_1.default.dirname(canonicalPath), changeListener);
445
+ // implementation complexity, or slowing down more common lookups.
446
+ this.#removeNormalPath(path_1.default.dirname(canonicalPath), changeListener);
411
447
  }
412
448
  }
413
449
  }
@@ -425,8 +461,15 @@ class TreeFS {
425
461
  *
426
462
  * Note that this code is extremely hot during resolution, being the most
427
463
  * expensive part of a file existence check. Benchmark any modifications!
464
+ *
465
+ * Each flag combination is implemented as its own specialised walker
466
+ * (`#walkLookup`, `#walkLookupNoFollow`, `#walkAndMakeDirectories`) so
467
+ * the hot inner loop has no per-iteration
468
+ * `opts.X` branches. Thin convenience wrappers below (`#lookup`,
469
+ * `#lookupNoFollow`, `#lookupFromNode`, `#walkAndStream`) cover the
470
+ * common call shapes.
428
471
  */
429
- #lookupByNormalPath(requestedNormalPath, opts = { followLeaf: true, makeDirectories: false }) {
472
+ #walkLookup(startNode, startPathIdx, startAncestorOfRootIdx, requestedNormalPath, onSegment, collectLinkPaths) {
430
473
  // We'll update the target if we hit a symlink.
431
474
  let targetNormalPath = requestedNormalPath;
432
475
  // Lazy-initialised set of seen target paths, to detect symlink cycles.
@@ -434,20 +477,11 @@ class TreeFS {
434
477
  // Set when a symlink is followed, to allow fallback population outside
435
478
  // the boundary for paths reachable transitively through symlinks.
436
479
  let followedSymlink = false;
437
- // Pointer to the first character of the current path segment in
438
- // targetNormalPath.
439
- let fromIdx = opts.start?.pathIdx ?? 0;
440
- // The parent of the current segment.
441
- let parentNode = opts.start?.node ?? this.#rootNode;
442
- // If a returned node is (an ancestor of) the root, this is the number of
443
- // levels below the root, i.e. '' is 0, '..' is 1, '../..' is 2, otherwise
444
- // null.
445
- let ancestorOfRootIdx = opts.start?.ancestorOfRootIdx ?? 0;
446
- const { collectAncestors, changeListener } = opts;
447
- // Used only when collecting ancestors, to avoid double-counting nodes and
480
+ let fromIdx = startPathIdx;
481
+ let parentNode = startNode;
482
+ let ancestorOfRootIdx = startAncestorOfRootIdx;
483
+ // Used only when streaming ancestors, to avoid double-yielding nodes and
448
484
  // paths when traversing a symlink takes us back to rootNode and out again.
449
- // This tracks the first character of the first segment not already
450
- // collected.
451
485
  let unseenPathFromIdx = 0;
452
486
  while (targetNormalPath.length > fromIdx) {
453
487
  const nextSepIdx = targetNormalPath.indexOf(path_1.default.sep, fromIdx);
@@ -461,24 +495,22 @@ class TreeFS {
461
495
  continue;
462
496
  }
463
497
  let segmentNode = parentNode.get(segmentName);
464
- // In normal paths all indirections are at the prefix, so we are at the
465
- // nth ancestor of the root iff the path so far is n '..' segments.
466
498
  if (segmentName === '..' && ancestorOfRootIdx != null) {
467
499
  ancestorOfRootIdx++;
468
500
  }
469
501
  else if (segmentNode != null) {
470
- ancestorOfRootIdx = null;
502
+ ancestorOfRootIdx = undefined;
471
503
  }
472
504
  if (segmentNode == null) {
473
- if (opts.makeDirectories !== true && segmentName !== '..') {
474
- if (!opts.skipFallback && this.#fallbackFilesystem != null) {
505
+ if (segmentName !== '..') {
506
+ if (this.#fallbackFilesystem != null) {
475
507
  const parentEnd = isLastSegment
476
508
  ? fromIdx - segmentName.length - 1
477
509
  : fromIdx - segmentName.length - 2;
478
510
  const parentCanonicalPath = parentEnd > 0 ? targetNormalPath.slice(0, parentEnd) : '';
479
511
  segmentNode = this.#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath, followedSymlink);
480
512
  if (segmentNode != null) {
481
- ancestorOfRootIdx = null;
513
+ ancestorOfRootIdx = undefined;
482
514
  }
483
515
  }
484
516
  if (segmentNode == null) {
@@ -493,16 +525,7 @@ class TreeFS {
493
525
  }
494
526
  if (segmentNode == null) {
495
527
  segmentNode = new Map();
496
- if (opts.makeDirectories === true) {
497
- if (changeListener != null) {
498
- const canonicalPath = isLastSegment
499
- ? targetNormalPath
500
- : targetNormalPath.slice(0, fromIdx - 1);
501
- changeListener.directoryAdded(canonicalPath);
502
- }
503
- parentNode.set(segmentName, segmentNode);
504
- }
505
- else if (!opts.skipFallback && this.#fallbackFilesystem != null) {
528
+ if (this.#fallbackFilesystem != null) {
506
529
  parentNode.set(segmentName, segmentNode);
507
530
  }
508
531
  }
@@ -511,13 +534,12 @@ class TreeFS {
511
534
  if (
512
535
  // ...at a directory node and the only subsequent character is `/`, or
513
536
  (nextSepIdx === targetNormalPath.length - 1 && isDirectory(segmentNode)) ||
514
- // there are no subsequent `/`, and this node is anything but a symlink
515
- // we're required to resolve due to followLeaf.
516
- (isLastSegment &&
517
- (isDirectory(segmentNode) || isRegularFile(segmentNode) || opts.followLeaf === false))) {
537
+ // ...there are no subsequent `/`, and this node is a directory or a
538
+ // regular file. (A leaf symlink falls through and is followed.)
539
+ (isLastSegment && (isDirectory(segmentNode) || isRegularFile(segmentNode)))) {
518
540
  return {
519
541
  ancestorOfRootIdx,
520
- canonicalPath: isLastSegment ? targetNormalPath : targetNormalPath.slice(0, -1), // remove trailing `/`
542
+ canonicalPath: isLastSegment ? targetNormalPath : targetNormalPath.slice(0, -1),
521
543
  exists: true,
522
544
  node: segmentNode,
523
545
  parentNode,
@@ -526,16 +548,11 @@ class TreeFS {
526
548
  // If the next node is a directory, go into it
527
549
  if (isDirectory(segmentNode)) {
528
550
  parentNode = segmentNode;
529
- if (collectAncestors && isUnseen) {
551
+ if (onSegment != null && isUnseen) {
530
552
  const currentPath = isLastSegment
531
553
  ? targetNormalPath
532
554
  : targetNormalPath.slice(0, fromIdx - 1);
533
- collectAncestors.push({
534
- ancestorOfRootIdx,
535
- node: segmentNode,
536
- normalPath: currentPath,
537
- segmentName,
538
- });
555
+ onSegment(segmentNode, currentPath, segmentName, ancestorOfRootIdx);
539
556
  }
540
557
  }
541
558
  else {
@@ -559,8 +576,8 @@ class TreeFS {
559
576
  missingSegmentName: segmentName,
560
577
  };
561
578
  }
562
- if (opts.collectLinkPaths) {
563
- opts.collectLinkPaths.add(this.#pathUtils.normalToAbsolute(currentPath));
579
+ if (collectLinkPaths != null) {
580
+ collectLinkPaths.add(this.#pathUtils.normalToAbsolute(currentPath));
564
581
  }
565
582
  const remainingTargetPath = isLastSegment ? '' : targetNormalPath.slice(fromIdx);
566
583
  // Append any subsequent path segments to the symlink target, and reset
@@ -569,18 +586,19 @@ class TreeFS {
569
586
  targetNormalPath = joinedResult.normalPath;
570
587
  // Two special cases (covered by unit tests):
571
588
  //
572
- // If the symlink target is the root, the root should be a counted as
573
- // an ancestor. We'd otherwise miss counting it because we normally
574
- // push new ancestors only when entering a directory.
589
+ // If the symlink target is the root, the root should be counted as an
590
+ // ancestor. We'd otherwise miss it because new ancestors are only
591
+ // streamed when entering a directory.
575
592
  //
576
593
  // If the symlink target is an ancestor of the root *and* joining it
577
594
  // with the remaining path results in collapsing segments, e.g:
578
- // '../..' + 'parentofroot/root/foo.js' = 'foo.js', then we must add
595
+ // '../..' + 'parentofroot/root/foo.js' = 'foo.js', then we must yield
579
596
  // parentofroot and root as ancestors.
580
- if (collectAncestors &&
597
+ if (onSegment != null &&
581
598
  !isLastSegment &&
582
599
  // No-op optimisation to bail out the common case of nothing to do.
583
- ((ancestorOfRootIdx = this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget)) === 0 ||
600
+ ((ancestorOfRootIdx =
601
+ this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget) ?? undefined) === 0 ||
584
602
  joinedResult.collapsedSegments > 0)) {
585
603
  let node = this.#rootNode;
586
604
  let collapsedPath = '';
@@ -602,12 +620,16 @@ class TreeFS {
602
620
  node = node.get('..') ?? new Map();
603
621
  collapsedPath = collapsedPath === '' ? '..' : collapsedPath + path_1.default.sep + '..';
604
622
  }
605
- collectAncestors.push(...reverseAncestors.reverse());
623
+ // Emit in shallowest-first order, matching today's
624
+ // collectAncestors.push(...reverseAncestors.reverse()).
625
+ for (let i = reverseAncestors.length - 1; i >= 0; i--) {
626
+ const a = reverseAncestors[i];
627
+ onSegment(a.node, a.normalPath, a.segmentName, a.ancestorOfRootIdx);
628
+ }
606
629
  }
607
- // For the purpose of collecting ancestors: Ignore the traversal to
608
- // the symlink target, and start collecting ancestors only
609
- // from the target itself (ie, the basename of the normal target path)
610
- // onwards.
630
+ // For the purpose of streaming ancestors: Ignore the traversal to the
631
+ // symlink target, and start yielding ancestors only from the target
632
+ // itself (i.e. the basename of the normal target path) onwards.
611
633
  unseenPathFromIdx = normalSymlinkTarget.lastIndexOf(path_1.default.sep) + 1;
612
634
  if (seen == null) {
613
635
  // Optimisation: set this lazily only when we've encountered a symlink
@@ -634,9 +656,289 @@ class TreeFS {
634
656
  canonicalPath: targetNormalPath,
635
657
  exists: true,
636
658
  node: this.#rootNode,
637
- parentNode: null,
659
+ parentNode: undefined,
638
660
  };
639
661
  }
662
+ /**
663
+ * Specialised body for lstat-style lookups: followLeaf=false,
664
+ * makeDirectories=false. A symlink at the leaf is returned as-is.
665
+ * `skipFallback=true` disables the fallback-FS consultation (used by
666
+ * `getMtimeByNormalPath`, which must not trigger fallback population as
667
+ * a side effect of validation).
668
+ */
669
+ #walkLookupNoFollow(startNode, startPathIdx, startAncestorOfRootIdx, requestedNormalPath, onSegment, skipFallback) {
670
+ let targetNormalPath = requestedNormalPath;
671
+ let seen;
672
+ let followedSymlink = false;
673
+ let fromIdx = startPathIdx;
674
+ let parentNode = startNode;
675
+ let ancestorOfRootIdx = startAncestorOfRootIdx;
676
+ let unseenPathFromIdx = 0;
677
+ while (targetNormalPath.length > fromIdx) {
678
+ const nextSepIdx = targetNormalPath.indexOf(path_1.default.sep, fromIdx);
679
+ const isLastSegment = nextSepIdx === -1;
680
+ const segmentName = isLastSegment
681
+ ? targetNormalPath.slice(fromIdx)
682
+ : targetNormalPath.slice(fromIdx, nextSepIdx);
683
+ const isUnseen = fromIdx >= unseenPathFromIdx;
684
+ fromIdx = !isLastSegment ? nextSepIdx + 1 : targetNormalPath.length;
685
+ if (segmentName === '.') {
686
+ continue;
687
+ }
688
+ let segmentNode = parentNode.get(segmentName);
689
+ if (segmentName === '..' && ancestorOfRootIdx != null) {
690
+ ancestorOfRootIdx++;
691
+ }
692
+ else if (segmentNode != null) {
693
+ ancestorOfRootIdx = undefined;
694
+ }
695
+ if (segmentNode == null) {
696
+ if (segmentName !== '..') {
697
+ if (!skipFallback && this.#fallbackFilesystem != null) {
698
+ const parentEnd = isLastSegment
699
+ ? fromIdx - segmentName.length - 1
700
+ : fromIdx - segmentName.length - 2;
701
+ const parentCanonicalPath = parentEnd > 0 ? targetNormalPath.slice(0, parentEnd) : '';
702
+ segmentNode = this.#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath, followedSymlink);
703
+ if (segmentNode != null) {
704
+ ancestorOfRootIdx = undefined;
705
+ }
706
+ }
707
+ if (segmentNode == null) {
708
+ return {
709
+ canonicalMissingPath: isLastSegment
710
+ ? targetNormalPath
711
+ : targetNormalPath.slice(0, fromIdx - 1),
712
+ exists: false,
713
+ missingSegmentName: segmentName,
714
+ };
715
+ }
716
+ }
717
+ if (segmentNode == null) {
718
+ segmentNode = new Map();
719
+ if (!skipFallback && this.#fallbackFilesystem != null) {
720
+ parentNode.set(segmentName, segmentNode);
721
+ }
722
+ }
723
+ }
724
+ // Done: at the last segment we return whatever we found (no leaf
725
+ // follow). Also done if the only remaining character is the trailing
726
+ // path separator and the node is a directory.
727
+ if (isLastSegment ||
728
+ (nextSepIdx === targetNormalPath.length - 1 && isDirectory(segmentNode))) {
729
+ return {
730
+ ancestorOfRootIdx,
731
+ canonicalPath: isLastSegment ? targetNormalPath : targetNormalPath.slice(0, -1),
732
+ exists: true,
733
+ node: segmentNode,
734
+ parentNode,
735
+ };
736
+ }
737
+ if (isDirectory(segmentNode)) {
738
+ parentNode = segmentNode;
739
+ if (onSegment != null && isUnseen) {
740
+ const currentPath = targetNormalPath.slice(0, fromIdx - 1);
741
+ onSegment(segmentNode, currentPath, segmentName, ancestorOfRootIdx);
742
+ }
743
+ }
744
+ else {
745
+ const currentPath = targetNormalPath.slice(0, fromIdx - 1);
746
+ if (isRegularFile(segmentNode)) {
747
+ return {
748
+ canonicalMissingPath: currentPath,
749
+ exists: false,
750
+ missingSegmentName: segmentName,
751
+ };
752
+ }
753
+ // Symlink in an interior position — still follow it (only the leaf
754
+ // is exempt from following under followLeaf=false).
755
+ const normalSymlinkTarget = this.#resolveSymlinkTargetToNormalPath(segmentNode, currentPath);
756
+ if (normalSymlinkTarget == null) {
757
+ return {
758
+ canonicalMissingPath: currentPath,
759
+ exists: false,
760
+ missingSegmentName: segmentName,
761
+ };
762
+ }
763
+ const remainingTargetPath = targetNormalPath.slice(fromIdx);
764
+ const joinedResult = this.#pathUtils.joinNormalToRelative(normalSymlinkTarget, remainingTargetPath);
765
+ targetNormalPath = joinedResult.normalPath;
766
+ if (onSegment != null &&
767
+ ((ancestorOfRootIdx =
768
+ this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget) ?? undefined) === 0 ||
769
+ joinedResult.collapsedSegments > 0)) {
770
+ let node = this.#rootNode;
771
+ let collapsedPath = '';
772
+ const reverseAncestors = [];
773
+ for (let i = 0; i <= joinedResult.collapsedSegments && isDirectory(node); i++) {
774
+ if (i > 0 || ancestorOfRootIdx === 0 || joinedResult.collapsedSegments > 0) {
775
+ reverseAncestors.push({
776
+ ancestorOfRootIdx: i,
777
+ node,
778
+ normalPath: collapsedPath,
779
+ segmentName: this.#pathUtils.getBasenameOfNthAncestor(i),
780
+ });
781
+ }
782
+ node = node.get('..') ?? new Map();
783
+ collapsedPath = collapsedPath === '' ? '..' : collapsedPath + path_1.default.sep + '..';
784
+ }
785
+ for (let i = reverseAncestors.length - 1; i >= 0; i--) {
786
+ const a = reverseAncestors[i];
787
+ onSegment(a.node, a.normalPath, a.segmentName, a.ancestorOfRootIdx);
788
+ }
789
+ }
790
+ unseenPathFromIdx = normalSymlinkTarget.lastIndexOf(path_1.default.sep) + 1;
791
+ if (seen == null) {
792
+ seen = new Set([requestedNormalPath]);
793
+ }
794
+ if (seen.has(targetNormalPath)) {
795
+ return {
796
+ canonicalMissingPath: targetNormalPath,
797
+ exists: false,
798
+ missingSegmentName: segmentName,
799
+ };
800
+ }
801
+ seen.add(targetNormalPath);
802
+ followedSymlink = true;
803
+ fromIdx = 0;
804
+ parentNode = this.#rootNode;
805
+ ancestorOfRootIdx = 0;
806
+ }
807
+ }
808
+ (0, invariant_1.default)(parentNode === this.#rootNode, 'Unexpectedly escaped traversal');
809
+ return {
810
+ ancestorOfRootIdx: 0,
811
+ canonicalPath: targetNormalPath,
812
+ exists: true,
813
+ node: this.#rootNode,
814
+ parentNode: undefined,
815
+ };
816
+ }
817
+ /**
818
+ * Specialised body that creates missing directory nodes as it walks.
819
+ * `followLeaf` is parameterised so callers that look up `dirname(...)`
820
+ * can choose lstat-style or stat-style at the leaf. `onSegment` fires
821
+ * for every directory encountered, with `isNewlyCreated` distinguishing
822
+ * dirs the walker just created from dirs that already existed.
823
+ */
824
+ #walkAndMakeDirectories(startNode, startPathIdx, startAncestorOfRootIdx, requestedNormalPath, followLeaf, onSegment) {
825
+ let targetNormalPath = requestedNormalPath;
826
+ let seen;
827
+ let fromIdx = startPathIdx;
828
+ let parentNode = startNode;
829
+ let ancestorOfRootIdx = startAncestorOfRootIdx;
830
+ let unseenPathFromIdx = 0;
831
+ while (targetNormalPath.length > fromIdx) {
832
+ const nextSepIdx = targetNormalPath.indexOf(path_1.default.sep, fromIdx);
833
+ const isLastSegment = nextSepIdx === -1;
834
+ const segmentName = isLastSegment
835
+ ? targetNormalPath.slice(fromIdx)
836
+ : targetNormalPath.slice(fromIdx, nextSepIdx);
837
+ const isUnseen = fromIdx >= unseenPathFromIdx;
838
+ fromIdx = !isLastSegment ? nextSepIdx + 1 : targetNormalPath.length;
839
+ if (segmentName === '.') {
840
+ continue;
841
+ }
842
+ let segmentNode = parentNode.get(segmentName);
843
+ let wasJustCreated = false;
844
+ if (segmentName === '..' && ancestorOfRootIdx != null) {
845
+ ancestorOfRootIdx++;
846
+ }
847
+ else if (segmentNode != null) {
848
+ ancestorOfRootIdx = undefined;
849
+ }
850
+ if (segmentNode == null) {
851
+ segmentNode = new Map();
852
+ parentNode.set(segmentName, segmentNode);
853
+ wasJustCreated = true;
854
+ }
855
+ if ((nextSepIdx === targetNormalPath.length - 1 && isDirectory(segmentNode)) ||
856
+ (isLastSegment && (isDirectory(segmentNode) || isRegularFile(segmentNode) || !followLeaf))) {
857
+ if (wasJustCreated && onSegment != null) {
858
+ const currentPath = isLastSegment
859
+ ? targetNormalPath
860
+ : targetNormalPath.slice(0, fromIdx - 1);
861
+ onSegment(segmentNode, currentPath, segmentName, ancestorOfRootIdx, true);
862
+ }
863
+ return {
864
+ ancestorOfRootIdx,
865
+ canonicalPath: isLastSegment ? targetNormalPath : targetNormalPath.slice(0, -1),
866
+ exists: true,
867
+ node: segmentNode,
868
+ parentNode,
869
+ };
870
+ }
871
+ if (isDirectory(segmentNode)) {
872
+ parentNode = segmentNode;
873
+ if (onSegment != null && isUnseen) {
874
+ const currentPath = isLastSegment
875
+ ? targetNormalPath
876
+ : targetNormalPath.slice(0, fromIdx - 1);
877
+ onSegment(segmentNode, currentPath, segmentName, ancestorOfRootIdx, wasJustCreated);
878
+ }
879
+ }
880
+ else {
881
+ const currentPath = isLastSegment
882
+ ? targetNormalPath
883
+ : targetNormalPath.slice(0, fromIdx - 1);
884
+ if (isRegularFile(segmentNode)) {
885
+ return {
886
+ canonicalMissingPath: currentPath,
887
+ exists: false,
888
+ missingSegmentName: segmentName,
889
+ };
890
+ }
891
+ // Interior symlink: follow it.
892
+ const normalSymlinkTarget = this.#resolveSymlinkTargetToNormalPath(segmentNode, currentPath);
893
+ if (normalSymlinkTarget == null) {
894
+ return {
895
+ canonicalMissingPath: currentPath,
896
+ exists: false,
897
+ missingSegmentName: segmentName,
898
+ };
899
+ }
900
+ const remainingTargetPath = isLastSegment ? '' : targetNormalPath.slice(fromIdx);
901
+ const joinedResult = this.#pathUtils.joinNormalToRelative(normalSymlinkTarget, remainingTargetPath);
902
+ targetNormalPath = joinedResult.normalPath;
903
+ unseenPathFromIdx = normalSymlinkTarget.lastIndexOf(path_1.default.sep) + 1;
904
+ if (seen == null) {
905
+ seen = new Set([requestedNormalPath]);
906
+ }
907
+ if (seen.has(targetNormalPath)) {
908
+ return {
909
+ canonicalMissingPath: targetNormalPath,
910
+ exists: false,
911
+ missingSegmentName: segmentName,
912
+ };
913
+ }
914
+ seen.add(targetNormalPath);
915
+ fromIdx = 0;
916
+ parentNode = this.#rootNode;
917
+ ancestorOfRootIdx = 0;
918
+ }
919
+ }
920
+ (0, invariant_1.default)(parentNode === this.#rootNode, 'Unexpectedly escaped traversal');
921
+ return {
922
+ ancestorOfRootIdx: 0,
923
+ canonicalPath: targetNormalPath,
924
+ exists: true,
925
+ node: this.#rootNode,
926
+ parentNode: undefined,
927
+ };
928
+ }
929
+ // Convenience wrappers for the common walker call shapes.
930
+ #lookup(normalPath, collectLinkPaths) {
931
+ return this.#walkLookup(this.#rootNode, 0, 0, normalPath, undefined, collectLinkPaths);
932
+ }
933
+ #lookupNoFollow(normalPath) {
934
+ return this.#walkLookupNoFollow(this.#rootNode, 0, 0, normalPath, undefined, false);
935
+ }
936
+ #lookupFromNode(startNode, startPathIdx, startAncestorOfRootIdx, target, collectLinkPaths) {
937
+ return this.#walkLookup(startNode, startPathIdx, startAncestorOfRootIdx, target, undefined, collectLinkPaths);
938
+ }
939
+ #walkAndStream(normalPath, onSegment, collectLinkPaths) {
940
+ return this.#walkLookup(this.#rootNode, 0, 0, normalPath, onSegment, collectLinkPaths);
941
+ }
640
942
  /**
641
943
  * Given a start path (which need not exist), a subpath and type, and
642
944
  * optionally a 'breakOnSegment', performs the following:
@@ -663,13 +965,22 @@ class TreeFS {
663
965
  hierarchicalLookup(mixedStartPath, subpath, opts) {
664
966
  const ancestorsOfInput = [];
665
967
  const normalPath = this.#normalizePath(mixedStartPath);
666
- const invalidatedBy = opts.invalidatedBy;
667
- const closestLookup = this.#lookupByNormalPath(normalPath, {
668
- collectAncestors: ancestorsOfInput,
669
- collectLinkPaths: invalidatedBy,
670
- });
968
+ const invalidatedBy = opts.invalidatedBy ?? undefined;
969
+ const onSegment = (node, normalPathOfDir, segmentName, ancestorOfRootIdx) => {
970
+ ancestorsOfInput.push({
971
+ ancestorOfRootIdx,
972
+ node,
973
+ normalPath: normalPathOfDir,
974
+ segmentName,
975
+ });
976
+ };
977
+ const closestLookup = this.#walkAndStream(normalPath, onSegment, invalidatedBy);
671
978
  if (closestLookup.exists && isDirectory(closestLookup.node)) {
672
- const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(closestLookup.canonicalPath, subpath, opts.subpathType, invalidatedBy, null);
979
+ const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(closestLookup.canonicalPath, subpath, opts.subpathType, invalidatedBy, {
980
+ ancestorOfRootIdx: closestLookup.ancestorOfRootIdx,
981
+ node: closestLookup.node,
982
+ pathIdx: closestLookup.canonicalPath.length > 0 ? closestLookup.canonicalPath.length + 1 : 0,
983
+ });
673
984
  if (maybeAbsolutePathMatch != null) {
674
985
  return {
675
986
  absolutePath: maybeAbsolutePathMatch,
@@ -712,7 +1023,7 @@ class TreeFS {
712
1023
  commonRoot = ancestor.node;
713
1024
  }
714
1025
  }
715
- // Phase 1: Consider descendenants of the common root, from deepest to
1026
+ // Phase 1: Consider descendants of the common root, from deepest to
716
1027
  // shallowest.
717
1028
  for (let candidateIdx = ancestorsOfInput.length - 1; candidateIdx >= commonRootDepth; --candidateIdx) {
718
1029
  const candidate = ancestorsOfInput[candidateIdx];
@@ -725,15 +1036,12 @@ class TreeFS {
725
1036
  pathIdx: candidate.normalPath.length > 0 ? candidate.normalPath.length + 1 : 0,
726
1037
  });
727
1038
  if (maybeAbsolutePathMatch != null) {
728
- // Determine the input path relative to the current candidate. Note
729
- // that the candidate path will always be canonical (real), whereas the
730
- // input may contain symlinks, so the candidate is not necessarily a
731
- // prefix of the input. Use the fact that each remaining candidate
732
- // corresponds to a leading segment of the input normal path, and
733
- // discard the first candidateIdx + 1 segments of the input path.
734
- //
735
- // The next 5 lines are equivalent to (but faster than)
736
- // normalPath.split('/').slice(candidateIdx + 1).join('/').
1039
+ // Determine the input path relative to the current candidate. The
1040
+ // candidate path is always canonical (real); the input may contain
1041
+ // symlinks, so the candidate is not necessarily a prefix of the input.
1042
+ // Use the fact that each remaining candidate corresponds to a leading
1043
+ // segment of the input normal path, and discard the first
1044
+ // candidateIdx + 1 segments of the input path.
737
1045
  let prefixLength = commonRootDepth * 3; // Leading '../'
738
1046
  for (let i = commonRootDepth; i <= candidateIdx; i++) {
739
1047
  prefixLength = normalPath.indexOf(path_1.default.sep, prefixLength + 1);
@@ -745,14 +1053,17 @@ class TreeFS {
745
1053
  };
746
1054
  }
747
1055
  }
748
- // Phase 2: Consider the common root and its ancestors
749
- // This will be '', '..', '../..', etc.
1056
+ // Phase 2: Consider the common root and its ancestors.
750
1057
  let candidateNormalPath = commonRootDepth > 0 ? normalPath.slice(0, 3 * commonRootDepth - 1) : '';
751
1058
  const remainingNormalPath = normalPath.slice(commonRootDepth * 3);
752
1059
  let nextNode = commonRoot;
753
1060
  let depthBelowCommonRoot = 0;
754
1061
  while (isDirectory(nextNode)) {
755
- const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(candidateNormalPath, subpath, opts.subpathType, invalidatedBy, null);
1062
+ const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(candidateNormalPath, subpath, opts.subpathType, invalidatedBy, {
1063
+ ancestorOfRootIdx: commonRootDepth + depthBelowCommonRoot,
1064
+ node: nextNode,
1065
+ pathIdx: candidateNormalPath.length > 0 ? candidateNormalPath.length + 1 : 0,
1066
+ });
756
1067
  if (maybeAbsolutePathMatch != null) {
757
1068
  const rootDirParts = this.#pathUtils.getParts();
758
1069
  const relativeParts = depthBelowCommonRoot > 0
@@ -774,9 +1085,51 @@ class TreeFS {
774
1085
  return null;
775
1086
  }
776
1087
  #checkCandidateHasSubpath(normalCandidatePath, subpath, subpathType, invalidatedBy, start) {
777
- const lookupResult = this.#lookupByNormalPath(this.#pathUtils.joinNormalToRelative(normalCandidatePath, subpath).normalPath, {
778
- collectLinkPaths: invalidatedBy,
779
- });
1088
+ // NOTE(@kitten): The most common call for package.json only needs a simple map
1089
+ // lookup, and can skip traversal entirely
1090
+ if (start != null &&
1091
+ subpath.length > 0 &&
1092
+ subpath !== '.' &&
1093
+ subpath !== '..' &&
1094
+ subpath.indexOf(path_1.default.sep) === -1) {
1095
+ const child = start.node.get(subpath);
1096
+ if (child == null) {
1097
+ if (this.#fallbackFilesystem != null) {
1098
+ // noop: fall through to slow-path
1099
+ }
1100
+ else {
1101
+ if (invalidatedBy) {
1102
+ invalidatedBy.add(this.#pathUtils.normalToAbsolute(normalCandidatePath === '' ? subpath : normalCandidatePath + path_1.default.sep + subpath));
1103
+ }
1104
+ return null;
1105
+ }
1106
+ }
1107
+ else {
1108
+ const childIsDirectory = isDirectory(child);
1109
+ if (!childIsDirectory && !isRegularFile(child)) {
1110
+ // noop: Handle symlinks in the slow path, since it needs state tracking
1111
+ }
1112
+ else {
1113
+ const absolutePath = this.#pathUtils.normalToAbsolute(normalCandidatePath === '' ? subpath : normalCandidatePath + path_1.default.sep + subpath);
1114
+ if (childIsDirectory === (subpathType === 'd')) {
1115
+ return absolutePath;
1116
+ }
1117
+ else {
1118
+ if (invalidatedBy)
1119
+ invalidatedBy.add(absolutePath);
1120
+ return null;
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ // NOTE(@kitten): We can forward `start` if there's no indirection in the candidate path
1126
+ const canForwardStart = start != null &&
1127
+ normalCandidatePath !== '..' &&
1128
+ !normalCandidatePath.endsWith(path_1.default.sep + '..');
1129
+ const target = this.#pathUtils.joinNormalToRelative(normalCandidatePath, subpath).normalPath;
1130
+ const lookupResult = canForwardStart
1131
+ ? this.#lookupFromNode(start.node, start.pathIdx, start.ancestorOfRootIdx, target, invalidatedBy)
1132
+ : this.#lookup(target, invalidatedBy);
780
1133
  if (lookupResult.exists &&
781
1134
  // Should be a Map iff subpathType is directory
782
1135
  isDirectory(lookupResult.node) === (subpathType === 'd')) {
@@ -787,10 +1140,14 @@ class TreeFS {
787
1140
  }
788
1141
  return null;
789
1142
  }
790
- metadataIterator(opts) {
791
- return this.#metadataIterator(this.#rootNode, opts);
1143
+ *metadataIterator(opts) {
1144
+ const buffer = [];
1145
+ this.#forEachMetadata(this.#rootNode, opts, '', (baseName, canonicalPath, metadata) => {
1146
+ buffer.push({ baseName, canonicalPath, metadata });
1147
+ });
1148
+ yield* buffer;
792
1149
  }
793
- *#metadataIterator(rootNode, opts, prefix = '') {
1150
+ #forEachMetadata(rootNode, opts, prefix, callback) {
794
1151
  for (const [name, node] of rootNode) {
795
1152
  if (node == null) {
796
1153
  continue;
@@ -800,10 +1157,10 @@ class TreeFS {
800
1157
  }
801
1158
  const prefixedName = prefix === '' ? name : prefix + path_1.default.sep + name;
802
1159
  if (isDirectory(node)) {
803
- yield* this.#metadataIterator(node, opts, prefixedName);
1160
+ this.#forEachMetadata(node, opts, prefixedName, callback);
804
1161
  }
805
1162
  else if (isRegularFile(node) || opts.includeSymlinks) {
806
- yield { baseName: name, canonicalPath: prefixedName, metadata: node };
1163
+ callback(name, prefixedName, node);
807
1164
  }
808
1165
  }
809
1166
  }
@@ -812,17 +1169,13 @@ class TreeFS {
812
1169
  ? this.#pathUtils.absoluteToNormal(relativeOrAbsolutePath)
813
1170
  : this.#pathUtils.relativeToNormal(relativeOrAbsolutePath);
814
1171
  }
815
- *#directoryNodeIterator(node, parent, ancestorOfRootIdx) {
816
- if (ancestorOfRootIdx != null && ancestorOfRootIdx > 0 && parent) {
817
- yield [this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1), parent];
818
- }
819
- yield* node.entries();
820
- }
821
1172
  /**
822
1173
  * Enumerate paths under a given node, including symlinks and through
823
- * symlinks (if `follow` is enabled).
1174
+ * symlinks (if `follow` is enabled). Invokes `callback` for each matching
1175
+ * path. Inlines what was previously `#directoryNodeIterator` (yielding the
1176
+ * parent entry first when `ancestorOfRootIdx > 0`).
824
1177
  */
825
- *#pathIterator(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx, opts, pathPrefix = '', followedLinks = new Set()) {
1178
+ #forEachPath(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx, opts, pathPrefix, followedLinks, callback) {
826
1179
  const pathSep = opts.alwaysYieldPosix ? '/' : path_1.default.sep;
827
1180
  const prefixWithSep = pathPrefix === '' ? pathPrefix : pathPrefix + pathSep;
828
1181
  // Optimization: We can attempt to eagerly populate directories we're visiting
@@ -835,18 +1188,18 @@ class TreeFS {
835
1188
  const rootCanonical = pathPrefix === '' ? canonicalRoot : canonicalRoot + path_1.default.sep + pathPrefix;
836
1189
  this.#populateDirFromFilesystem(iterationRootNode, rootCanonical, false, false);
837
1190
  }
838
- for (const [name, node] of this.#directoryNodeIterator(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx)) {
1191
+ const visitEntry = (name, node) => {
839
1192
  if (node == null) {
840
- continue;
1193
+ return;
841
1194
  }
842
1195
  else if (opts.subtreeOnly && name === '..') {
843
- continue;
1196
+ return;
844
1197
  }
845
1198
  const nodePath = prefixWithSep + name;
846
1199
  if (!isDirectory(node)) {
847
1200
  if (isRegularFile(node)) {
848
1201
  // regular file
849
- yield nodePath;
1202
+ callback(nodePath);
850
1203
  }
851
1204
  else {
852
1205
  // symlink
@@ -858,24 +1211,22 @@ class TreeFS {
858
1211
  // its normal path, and we need a canonical path for resolution
859
1212
  // (imagine our normal path contains a symlink 'bar' -> '.', and we
860
1213
  // are at /foo/bar/baz where baz -> '..' - that should resolve to
861
- // /foo, not /foo/bar). We *can* use _lookupByNormalPath to walk to
862
- // the canonical symlink, and then to its target.
863
- const resolved = this.#lookupByNormalPath(normalPathOfSymlink, {
864
- followLeaf: true,
865
- });
1214
+ // /foo, not /foo/bar). We *can* use #walkLookup to walk to the
1215
+ // canonical symlink, and then to its target.
1216
+ const resolved = this.#lookup(normalPathOfSymlink);
866
1217
  if (!resolved.exists) {
867
1218
  // Symlink goes nowhere, nothing to report.
868
- continue;
1219
+ return;
869
1220
  }
870
1221
  const target = resolved.node;
871
1222
  if (!isDirectory(target)) {
872
1223
  // Symlink points to a file, just yield the path of the symlink.
873
- yield nodePath;
1224
+ callback(nodePath);
874
1225
  }
875
1226
  else if (opts.recursive && opts.follow && !followedLinks.has(node)) {
876
1227
  // Symlink points to a directory - iterate over its contents using
877
1228
  // the path where we found the symlink as a prefix.
878
- yield* this.#pathIterator(target, resolved.parentNode, resolved.ancestorOfRootIdx, opts, nodePath, new Set([...followedLinks, node]));
1229
+ this.#forEachPath(target, resolved.parentNode, resolved.ancestorOfRootIdx, opts, nodePath, new Set([...followedLinks, node]), callback);
879
1230
  }
880
1231
  }
881
1232
  }
@@ -889,8 +1240,16 @@ class TreeFS {
889
1240
  : opts.canonicalPathOfRoot + path_1.default.sep + nodePathWithSystemSeparators;
890
1241
  this.#populateDirFromFilesystem(node, canonicalPath, false, false);
891
1242
  }
892
- yield* this.#pathIterator(node, iterationRootParentNode, ancestorOfRootIdx != null && ancestorOfRootIdx > 0 ? ancestorOfRootIdx - 1 : null, opts, nodePath, followedLinks);
1243
+ this.#forEachPath(node, iterationRootParentNode, ancestorOfRootIdx != null && ancestorOfRootIdx > 0 ? ancestorOfRootIdx - 1 : undefined, opts, nodePath, followedLinks, callback);
893
1244
  }
1245
+ };
1246
+ // Inlined #directoryNodeIterator: yield the parent entry first when we're
1247
+ // at a directory above the root, then iterate `node.entries()`.
1248
+ if (ancestorOfRootIdx != null && ancestorOfRootIdx > 0 && iterationRootParentNode) {
1249
+ visitEntry(this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1), iterationRootParentNode);
1250
+ }
1251
+ for (const [name, node] of iterationRootNode) {
1252
+ visitEntry(name, node);
894
1253
  }
895
1254
  }
896
1255
  #resolveSymlinkTargetToNormalPath(symlinkNode, canonicalPathOfSymlink) {
@@ -906,30 +1265,20 @@ class TreeFS {
906
1265
  return normalTarget;
907
1266
  }
908
1267
  catch {
909
- return null;
1268
+ return undefined;
910
1269
  }
911
1270
  }
912
1271
  else if (symlinkTarget === 0 || symlinkTarget == null) {
913
1272
  // WARN: We shouldn't call this method on non-symlinks. Outside of tests
914
1273
  // this condition shouldn't trigger. It's fine not to resolve a symlink if
915
1274
  // it does trigger however
916
- return null;
1275
+ return undefined;
917
1276
  }
918
1277
  else {
919
1278
  (0, invariant_1.default)(typeof symlinkTarget === 'string', 'Expected symlink target to be populated.');
920
1279
  return (0, normalizePathSeparatorsToSystem_1.default)(symlinkTarget);
921
1280
  }
922
1281
  }
923
- #getFileData(filePath, opts = { followLeaf: true }) {
924
- const normalPath = this.#normalizePath(filePath);
925
- const result = this.#lookupByNormalPath(normalPath, {
926
- followLeaf: opts.followLeaf,
927
- });
928
- if (!result.exists || isDirectory(result.node)) {
929
- return null;
930
- }
931
- return result.node;
932
- }
933
1282
  /**
934
1283
  * Return a filtered view of the tree containing only content under watched
935
1284
  * roots. Walk each root path from #rootNode, creating intermediate directory
@@ -1037,7 +1386,7 @@ class TreeFS {
1037
1386
  }
1038
1387
  /**
1039
1388
  * Populate an existing (potentially empty sentinel) directory node from
1040
- * the filesystem. Used by #pathIterator to fill lazy directories before
1389
+ * the filesystem. Used by #forEachPath to fill lazy directories before
1041
1390
  * iteration, and by #populateFromFilesystem for optimistic parent
1042
1391
  * population.
1043
1392
  */
@@ -0,0 +1 @@
1
+ export default function isWatcherExcluded(filePath: string): boolean;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = isWatcherExcluded;
4
+ const EXCLUDED_DIR_SEGMENT = /(?:^|[/\\])\.(?:git|hg|cxx)[/\\]/;
5
+ function isWatcherExcluded(filePath) {
6
+ return EXCLUDED_DIR_SEGMENT.test(filePath);
7
+ }
@@ -45,7 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.AbstractWatcher = void 0;
46
46
  const events_1 = __importDefault(require("events"));
47
47
  const path = __importStar(require("path"));
48
- const isVcsPath_1 = __importDefault(require("../lib/isVcsPath"));
48
+ const isWatcherExcluded_1 = __importDefault(require("../lib/isWatcherExcluded"));
49
49
  const common_1 = require("./common");
50
50
  class AbstractWatcher {
51
51
  root;
@@ -60,8 +60,8 @@ class AbstractWatcher {
60
60
  this.ignored = ignored;
61
61
  this.globs = globs;
62
62
  this.doIgnore = ignored
63
- ? (filePath) => (0, isVcsPath_1.default)(filePath) || (0, common_1.posixPathMatchesPattern)(ignored, filePath)
64
- : isVcsPath_1.default;
63
+ ? (filePath) => (0, isWatcherExcluded_1.default)(filePath) || (0, common_1.posixPathMatchesPattern)(ignored, filePath)
64
+ : isWatcherExcluded_1.default;
65
65
  this.root = path.resolve(dir);
66
66
  }
67
67
  onFileEvent(listener) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/metro-file-map",
3
- "version": "56.0.0",
3
+ "version": "56.0.2",
4
4
  "description": "A metro-file-map fork for Expo used with the Metro bundler",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "gitHead": "a30353e69ca0d72b9fac5830abc631feda1ba3ae",
45
+ "gitHead": "51c27fce31a5b3a877a4b05d832dabf4a99db5e1",
46
46
  "scripts": {
47
47
  "build": "expo-module tsc",
48
48
  "clean": "expo-module clean",
@@ -1 +0,0 @@
1
- export default function isVcsPath(filePath: string): boolean;
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = isVcsPath;
4
- const VCS_DIR_SEGMENT = /(?:^|[/\\])\.(?:git|hg)[/\\]/;
5
- function isVcsPath(filePath) {
6
- return VCS_DIR_SEGMENT.test(filePath);
7
- }