metro-file-map 0.80.8 → 0.80.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro-file-map",
3
- "version": "0.80.8",
3
+ "version": "0.80.10",
4
4
  "description": "[Experimental] - 🚇 File crawling, watching and mapping for Metro",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -16,6 +16,7 @@
16
16
  "anymatch": "^3.0.3",
17
17
  "debug": "^2.2.0",
18
18
  "fb-watchman": "^2.0.0",
19
+ "flow-enums-runtime": "^0.0.6",
19
20
  "graceful-fs": "^4.2.4",
20
21
  "invariant": "^2.2.4",
21
22
  "jest-worker": "^29.6.3",
package/src/Watcher.js CHANGED
@@ -86,6 +86,7 @@ class Watcher extends _events.default {
86
86
  const crawlerOptions = {
87
87
  abortSignal: options.abortSignal,
88
88
  computeSha1: options.computeSha1,
89
+ console: options.console,
89
90
  includeSymlinks: options.enableSymlinks,
90
91
  extensions: options.extensions,
91
92
  forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
@@ -103,6 +103,7 @@ export class Watcher extends EventEmitter {
103
103
  const crawlerOptions: CrawlerOptions = {
104
104
  abortSignal: options.abortSignal,
105
105
  computeSha1: options.computeSha1,
106
+ console: options.console,
106
107
  includeSymlinks: options.enableSymlinks,
107
108
  extensions: options.extensions,
108
109
  forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
@@ -52,7 +52,15 @@ function _interopRequireDefault(obj) {
52
52
  return obj && obj.__esModule ? obj : { default: obj };
53
53
  }
54
54
  const debug = require("debug")("Metro:NodeCrawler");
55
- function find(roots, extensions, ignore, includeSymlinks, rootDir, callback) {
55
+ function find(
56
+ roots,
57
+ extensions,
58
+ ignore,
59
+ includeSymlinks,
60
+ rootDir,
61
+ console,
62
+ callback
63
+ ) {
56
64
  const result = new Map();
57
65
  let activeCalls = 0;
58
66
  const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
@@ -66,43 +74,47 @@ function find(roots, extensions, ignore, includeSymlinks, rootDir, callback) {
66
74
  (err, entries) => {
67
75
  activeCalls--;
68
76
  if (err) {
69
- callback(result);
70
- return;
71
- }
72
- entries.forEach((entry) => {
73
- const file = path.join(directory, entry.name.toString());
74
- if (ignore(file)) {
75
- return;
76
- }
77
- if (entry.isSymbolicLink() && !includeSymlinks) {
78
- return;
79
- }
80
- if (entry.isDirectory()) {
81
- search(file);
82
- return;
83
- }
84
- activeCalls++;
85
- fs.lstat(file, (err, stat) => {
86
- activeCalls--;
87
- if (!err && stat) {
88
- const ext = path.extname(file).substr(1);
89
- if (stat.isSymbolicLink() || extensions.includes(ext)) {
90
- result.set(pathUtils.absoluteToNormal(file), [
91
- "",
92
- stat.mtime.getTime(),
93
- stat.size,
94
- 0,
95
- "",
96
- null,
97
- stat.isSymbolicLink() ? 1 : 0,
98
- ]);
99
- }
77
+ console.warn(
78
+ `Error "${
79
+ err.code ?? err.message
80
+ }" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`
81
+ );
82
+ } else {
83
+ entries.forEach((entry) => {
84
+ const file = path.join(directory, entry.name.toString());
85
+ if (ignore(file)) {
86
+ return;
87
+ }
88
+ if (entry.isSymbolicLink() && !includeSymlinks) {
89
+ return;
100
90
  }
101
- if (activeCalls === 0) {
102
- callback(result);
91
+ if (entry.isDirectory()) {
92
+ search(file);
93
+ return;
103
94
  }
95
+ activeCalls++;
96
+ fs.lstat(file, (err, stat) => {
97
+ activeCalls--;
98
+ if (!err && stat) {
99
+ const ext = path.extname(file).substr(1);
100
+ if (stat.isSymbolicLink() || extensions.includes(ext)) {
101
+ result.set(pathUtils.absoluteToNormal(file), [
102
+ "",
103
+ stat.mtime.getTime(),
104
+ stat.size,
105
+ 0,
106
+ "",
107
+ null,
108
+ stat.isSymbolicLink() ? 1 : 0,
109
+ ]);
110
+ }
111
+ }
112
+ if (activeCalls === 0) {
113
+ callback(result);
114
+ }
115
+ });
104
116
  });
105
- });
117
+ }
106
118
  if (activeCalls === 0) {
107
119
  callback(result);
108
120
  }
@@ -121,6 +133,7 @@ function findNative(
121
133
  ignore,
122
134
  includeSymlinks,
123
135
  rootDir,
136
+ console,
124
137
  callback
125
138
  ) {
126
139
  const extensionClause = extensions.length
@@ -175,6 +188,7 @@ function findNative(
175
188
  }
176
189
  module.exports = async function nodeCrawl(options) {
177
190
  const {
191
+ console,
178
192
  previousState,
179
193
  extensions,
180
194
  forceNodeFilesystemAPI,
@@ -204,9 +218,25 @@ module.exports = async function nodeCrawl(options) {
204
218
  resolve(difference);
205
219
  };
206
220
  if (useNativeFind) {
207
- findNative(roots, extensions, ignore, includeSymlinks, rootDir, callback);
221
+ findNative(
222
+ roots,
223
+ extensions,
224
+ ignore,
225
+ includeSymlinks,
226
+ rootDir,
227
+ console,
228
+ callback
229
+ );
208
230
  } else {
209
- find(roots, extensions, ignore, includeSymlinks, rootDir, callback);
231
+ find(
232
+ roots,
233
+ extensions,
234
+ ignore,
235
+ includeSymlinks,
236
+ rootDir,
237
+ console,
238
+ callback
239
+ );
210
240
  }
211
241
  });
212
242
  };
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type {
13
13
  CanonicalPath,
14
+ Console,
14
15
  CrawlerOptions,
15
16
  FileData,
16
17
  IgnoreMatcher,
@@ -33,6 +34,7 @@ function find(
33
34
  ignore: IgnoreMatcher,
34
35
  includeSymlinks: boolean,
35
36
  rootDir: string,
37
+ console: Console,
36
38
  callback: Callback,
37
39
  ): void {
38
40
  const result: FileData = new Map();
@@ -44,51 +46,52 @@ function find(
44
46
  fs.readdir(directory, {withFileTypes: true}, (err, entries) => {
45
47
  activeCalls--;
46
48
  if (err) {
47
- callback(result);
48
- return;
49
- }
50
-
51
- entries.forEach((entry: fs.Dirent) => {
52
- const file = path.join(directory, entry.name.toString());
53
-
54
- if (ignore(file)) {
55
- return;
56
- }
57
-
58
- if (entry.isSymbolicLink() && !includeSymlinks) {
59
- return;
60
- }
61
-
62
- if (entry.isDirectory()) {
63
- search(file);
64
- return;
65
- }
49
+ console.warn(
50
+ `Error "${err.code ?? err.message}" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`,
51
+ );
52
+ } else {
53
+ entries.forEach((entry: fs.Dirent) => {
54
+ const file = path.join(directory, entry.name.toString());
55
+
56
+ if (ignore(file)) {
57
+ return;
58
+ }
66
59
 
67
- activeCalls++;
60
+ if (entry.isSymbolicLink() && !includeSymlinks) {
61
+ return;
62
+ }
68
63
 
69
- fs.lstat(file, (err, stat) => {
70
- activeCalls--;
64
+ if (entry.isDirectory()) {
65
+ search(file);
66
+ return;
67
+ }
71
68
 
72
- if (!err && stat) {
73
- const ext = path.extname(file).substr(1);
74
- if (stat.isSymbolicLink() || extensions.includes(ext)) {
75
- result.set(pathUtils.absoluteToNormal(file), [
76
- '',
77
- stat.mtime.getTime(),
78
- stat.size,
79
- 0,
80
- '',
81
- null,
82
- stat.isSymbolicLink() ? 1 : 0,
83
- ]);
69
+ activeCalls++;
70
+
71
+ fs.lstat(file, (err, stat) => {
72
+ activeCalls--;
73
+
74
+ if (!err && stat) {
75
+ const ext = path.extname(file).substr(1);
76
+ if (stat.isSymbolicLink() || extensions.includes(ext)) {
77
+ result.set(pathUtils.absoluteToNormal(file), [
78
+ '',
79
+ stat.mtime.getTime(),
80
+ stat.size,
81
+ 0,
82
+ '',
83
+ null,
84
+ stat.isSymbolicLink() ? 1 : 0,
85
+ ]);
86
+ }
84
87
  }
85
- }
86
88
 
87
- if (activeCalls === 0) {
88
- callback(result);
89
- }
89
+ if (activeCalls === 0) {
90
+ callback(result);
91
+ }
92
+ });
90
93
  });
91
- });
94
+ }
92
95
 
93
96
  if (activeCalls === 0) {
94
97
  callback(result);
@@ -109,6 +112,7 @@ function findNative(
109
112
  ignore: IgnoreMatcher,
110
113
  includeSymlinks: boolean,
111
114
  rootDir: string,
115
+ console: Console,
112
116
  callback: Callback,
113
117
  ): void {
114
118
  // Examples:
@@ -172,6 +176,7 @@ module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{
172
176
  changedFiles: FileData,
173
177
  }> {
174
178
  const {
179
+ console,
175
180
  previousState,
176
181
  extensions,
177
182
  forceNodeFilesystemAPI,
@@ -194,7 +199,7 @@ module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{
194
199
  debug('Using system find: %s', useNativeFind);
195
200
 
196
201
  return new Promise((resolve, reject) => {
197
- const callback = (fileData: FileData) => {
202
+ const callback: Callback = fileData => {
198
203
  const difference = previousState.fileSystem.getDifference(fileData);
199
204
 
200
205
  perfLogger?.point('nodeCrawl_end');
@@ -209,9 +214,25 @@ module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{
209
214
  };
210
215
 
211
216
  if (useNativeFind) {
212
- findNative(roots, extensions, ignore, includeSymlinks, rootDir, callback);
217
+ findNative(
218
+ roots,
219
+ extensions,
220
+ ignore,
221
+ includeSymlinks,
222
+ rootDir,
223
+ console,
224
+ callback,
225
+ );
213
226
  } else {
214
- find(roots, extensions, ignore, includeSymlinks, rootDir, callback);
227
+ find(
228
+ roots,
229
+ extensions,
230
+ ignore,
231
+ includeSymlinks,
232
+ rootDir,
233
+ console,
234
+ callback,
235
+ );
215
236
  }
216
237
  });
217
238
  };
@@ -88,6 +88,7 @@ export type Console = typeof global.console;
88
88
  export type CrawlerOptions = {
89
89
  abortSignal: ?AbortSignal,
90
90
  computeSha1: boolean,
91
+ console: Console,
91
92
  extensions: $ReadOnlyArray<string>,
92
93
  forceNodeFilesystemAPI: boolean,
93
94
  ignore: IgnoreMatcher,
@@ -45,8 +45,9 @@ function _interopRequireWildcard(obj, nodeInterop) {
45
45
  }
46
46
  return newObj;
47
47
  }
48
- const UP_FRAGMENT = ".." + path.sep;
49
- const UP_FRAGMENT_LENGTH = UP_FRAGMENT.length;
48
+ const UP_FRAGMENT_SEP = ".." + path.sep;
49
+ const SEP_UP_FRAGMENT = path.sep + "..";
50
+ const UP_FRAGMENT_SEP_LENGTH = UP_FRAGMENT_SEP.length;
50
51
  const CURRENT_FRAGMENT = "." + path.sep;
51
52
  class RootPathUtils {
52
53
  #rootDir;
@@ -70,6 +71,9 @@ class RootPathUtils {
70
71
  this.#rootParts.pop();
71
72
  }
72
73
  }
74
+ getBasenameOfNthAncestor(n) {
75
+ return this.#rootParts[this.#rootParts.length - 1 - n];
76
+ }
73
77
  absoluteToNormal(absolutePath) {
74
78
  let endOfMatchingPrefix = 0;
75
79
  let lastMatchingPartIdx = 0;
@@ -100,11 +104,11 @@ class RootPathUtils {
100
104
  let i = 0;
101
105
  let pos = 0;
102
106
  while (
103
- normalPath.startsWith(UP_FRAGMENT, pos) ||
107
+ normalPath.startsWith(UP_FRAGMENT_SEP, pos) ||
104
108
  (normalPath.endsWith("..") && normalPath.length === 2 + pos)
105
109
  ) {
106
110
  left = this.#rootDirnames[i === this.#rootDepth ? this.#rootDepth : ++i];
107
- pos += UP_FRAGMENT_LENGTH;
111
+ pos += UP_FRAGMENT_SEP_LENGTH;
108
112
  }
109
113
  const right = pos === 0 ? normalPath : normalPath.slice(pos);
110
114
  if (right.length === 0) {
@@ -121,13 +125,25 @@ class RootPathUtils {
121
125
  path.relative(this.#rootDir, path.join(this.#rootDir, relativePath))
122
126
  );
123
127
  }
128
+ joinNormalToRelative(normalPath, relativePath) {
129
+ if (normalPath === "") {
130
+ return relativePath;
131
+ }
132
+ if (relativePath === "") {
133
+ return normalPath;
134
+ }
135
+ if (normalPath === ".." || normalPath.endsWith(SEP_UP_FRAGMENT)) {
136
+ return this.relativeToNormal(normalPath + path.sep + relativePath);
137
+ }
138
+ return normalPath + path.sep + relativePath;
139
+ }
124
140
  #tryCollapseIndirectionsInSuffix(
125
141
  fullPath,
126
142
  startOfRelativePart,
127
143
  implicitUpIndirections
128
144
  ) {
129
145
  let totalUpIndirections = implicitUpIndirections;
130
- for (let pos = startOfRelativePart; ; pos += UP_FRAGMENT_LENGTH) {
146
+ for (let pos = startOfRelativePart; ; pos += UP_FRAGMENT_SEP_LENGTH) {
131
147
  const nextIndirection = fullPath.indexOf(CURRENT_FRAGMENT, pos);
132
148
  if (nextIndirection === -1) {
133
149
  while (totalUpIndirections > 0) {
@@ -149,12 +165,12 @@ class RootPathUtils {
149
165
  right === "" ||
150
166
  (right === ".." && totalUpIndirections >= this.#rootParts.length - 1)
151
167
  ) {
152
- return UP_FRAGMENT.repeat(totalUpIndirections).slice(0, -1);
168
+ return UP_FRAGMENT_SEP.repeat(totalUpIndirections).slice(0, -1);
153
169
  }
154
170
  if (totalUpIndirections === 0) {
155
171
  return right;
156
172
  }
157
- return UP_FRAGMENT.repeat(totalUpIndirections) + right;
173
+ return UP_FRAGMENT_SEP.repeat(totalUpIndirections) + right;
158
174
  }
159
175
  if (totalUpIndirections < this.#rootParts.length - 1) {
160
176
  totalUpIndirections++;
@@ -34,8 +34,9 @@ import * as path from 'path';
34
34
  * back to `node:path` equivalents in those cases.
35
35
  */
36
36
 
37
- const UP_FRAGMENT = '..' + path.sep;
38
- const UP_FRAGMENT_LENGTH = UP_FRAGMENT.length;
37
+ const UP_FRAGMENT_SEP = '..' + path.sep;
38
+ const SEP_UP_FRAGMENT = path.sep + '..';
39
+ const UP_FRAGMENT_SEP_LENGTH = UP_FRAGMENT_SEP.length;
39
40
  const CURRENT_FRAGMENT = '.' + path.sep;
40
41
 
41
42
  export class RootPathUtils {
@@ -66,6 +67,10 @@ export class RootPathUtils {
66
67
  }
67
68
  }
68
69
 
70
+ getBasenameOfNthAncestor(n: number): string {
71
+ return this.#rootParts[this.#rootParts.length - 1 - n];
72
+ }
73
+
69
74
  // absolutePath may be any well-formed absolute path.
70
75
  absoluteToNormal(absolutePath: string): string {
71
76
  let endOfMatchingPrefix = 0;
@@ -114,11 +119,11 @@ export class RootPathUtils {
114
119
  let i = 0;
115
120
  let pos = 0;
116
121
  while (
117
- normalPath.startsWith(UP_FRAGMENT, pos) ||
122
+ normalPath.startsWith(UP_FRAGMENT_SEP, pos) ||
118
123
  (normalPath.endsWith('..') && normalPath.length === 2 + pos)
119
124
  ) {
120
125
  left = this.#rootDirnames[i === this.#rootDepth ? this.#rootDepth : ++i];
121
- pos += UP_FRAGMENT_LENGTH;
126
+ pos += UP_FRAGMENT_SEP_LENGTH;
122
127
  }
123
128
  const right = pos === 0 ? normalPath : normalPath.slice(pos);
124
129
  if (right.length === 0) {
@@ -139,6 +144,22 @@ export class RootPathUtils {
139
144
  );
140
145
  }
141
146
 
147
+ // Takes a normal and relative path, and joins them efficiently into a normal
148
+ // path, including collapsing trailing '..' in the first part with leading
149
+ // project root segments in the relative part.
150
+ joinNormalToRelative(normalPath: string, relativePath: string): string {
151
+ if (normalPath === '') {
152
+ return relativePath;
153
+ }
154
+ if (relativePath === '') {
155
+ return normalPath;
156
+ }
157
+ if (normalPath === '..' || normalPath.endsWith(SEP_UP_FRAGMENT)) {
158
+ return this.relativeToNormal(normalPath + path.sep + relativePath);
159
+ }
160
+ return normalPath + path.sep + relativePath;
161
+ }
162
+
142
163
  // Internal: Tries to collapse sequences like `../root/foo` for root
143
164
  // `/project/root` down to the normal 'foo'.
144
165
  #tryCollapseIndirectionsInSuffix(
@@ -151,7 +172,7 @@ export class RootPathUtils {
151
172
  // unmatched suffix e.g /project/[../../foo], but bail out to Node's
152
173
  // path.relative if we find a possible indirection after any later segment,
153
174
  // or on any "./" that isn't a "../".
154
- for (let pos = startOfRelativePart; ; pos += UP_FRAGMENT_LENGTH) {
175
+ for (let pos = startOfRelativePart; ; pos += UP_FRAGMENT_SEP_LENGTH) {
155
176
  const nextIndirection = fullPath.indexOf(CURRENT_FRAGMENT, pos);
156
177
  if (nextIndirection === -1) {
157
178
  // If we have any indirections, they may "collapse" if a subsequent
@@ -185,13 +206,13 @@ export class RootPathUtils {
185
206
  ) {
186
207
  // If we have no right side (or an indirection that would take us
187
208
  // below the root), just ensure we don't include a trailing separtor.
188
- return UP_FRAGMENT.repeat(totalUpIndirections).slice(0, -1);
209
+ return UP_FRAGMENT_SEP.repeat(totalUpIndirections).slice(0, -1);
189
210
  }
190
211
  // Optimisation for the common case, saves a concatenation.
191
212
  if (totalUpIndirections === 0) {
192
213
  return right;
193
214
  }
194
- return UP_FRAGMENT.repeat(totalUpIndirections) + right;
215
+ return UP_FRAGMENT_SEP.repeat(totalUpIndirections) + right;
195
216
  }
196
217
 
197
218
  // Cap the number of indirections at the total number of root segments.
package/src/lib/TreeFS.js CHANGED
@@ -105,22 +105,20 @@ class TreeFS {
105
105
  }
106
106
  lookup(mixedPath) {
107
107
  const normalPath = this._normalizePath(mixedPath);
108
+ const links = new Set();
108
109
  const result = this._lookupByNormalPath(normalPath, {
110
+ collectLinkPaths: links,
109
111
  followLeaf: true,
110
112
  });
111
113
  if (!result.exists) {
112
- const { canonicalMissingPath, canonicalLinkPaths } = result;
114
+ const { canonicalMissingPath } = result;
113
115
  return {
114
116
  exists: false,
115
- links: new Set(
116
- canonicalLinkPaths.map((canonicalPath) =>
117
- this.#pathUtils.normalToAbsolute(canonicalPath)
118
- )
119
- ),
117
+ links,
120
118
  missing: this.#pathUtils.normalToAbsolute(canonicalMissingPath),
121
119
  };
122
120
  }
123
- const { canonicalPath, canonicalLinkPaths, node } = result;
121
+ const { canonicalPath, node } = result;
124
122
  const type =
125
123
  node instanceof Map
126
124
  ? "d"
@@ -135,11 +133,7 @@ class TreeFS {
135
133
  );
136
134
  return {
137
135
  exists: true,
138
- links: new Set(
139
- canonicalLinkPaths.map((canonicalPath) =>
140
- this.#pathUtils.normalToAbsolute(canonicalPath)
141
- )
142
- ),
136
+ links,
143
137
  realPath: this.#pathUtils.normalToAbsolute(canonicalPath),
144
138
  type,
145
139
  };
@@ -180,8 +174,12 @@ class TreeFS {
180
174
  if (!contextRootResult.exists) {
181
175
  return;
182
176
  }
183
- const { canonicalPath: rootRealPath, node: contextRoot } =
184
- contextRootResult;
177
+ const {
178
+ ancestorOfRootIdx,
179
+ canonicalPath: rootRealPath,
180
+ node: contextRoot,
181
+ parentNode: contextRootParent,
182
+ } = contextRootResult;
185
183
  if (!(contextRoot instanceof Map)) {
186
184
  return;
187
185
  }
@@ -194,13 +192,18 @@ class TreeFS {
194
192
  filterComparePosix && _path.default.sep !== "/"
195
193
  ? contextRootAbsolutePath.replaceAll(_path.default.sep, "/")
196
194
  : contextRootAbsolutePath;
197
- for (const relativePathForComparison of this._pathIterator(contextRoot, {
198
- alwaysYieldPosix: filterComparePosix,
199
- canonicalPathOfRoot: rootRealPath,
200
- follow,
201
- recursive,
202
- subtreeOnly: rootDir != null,
203
- })) {
195
+ for (const relativePathForComparison of this._pathIterator(
196
+ contextRoot,
197
+ contextRootParent,
198
+ ancestorOfRootIdx,
199
+ {
200
+ alwaysYieldPosix: filterComparePosix,
201
+ canonicalPathOfRoot: rootRealPath,
202
+ follow,
203
+ recursive,
204
+ subtreeOnly: rootDir != null,
205
+ }
206
+ )) {
204
207
  if (
205
208
  filter == null ||
206
209
  filter.test(
@@ -295,10 +298,10 @@ class TreeFS {
295
298
  }
296
299
  ) {
297
300
  let targetNormalPath = requestedNormalPath;
298
- const canonicalLinkPaths = [];
299
301
  let seen;
300
302
  let fromIdx = 0;
301
303
  let parentNode = this.#rootNode;
304
+ let ancestorOfRootIdx = null;
302
305
  while (targetNormalPath.length > fromIdx) {
303
306
  const nextSepIdx = targetNormalPath.indexOf(_path.default.sep, fromIdx);
304
307
  const isLastSegment = nextSepIdx === -1;
@@ -310,10 +313,15 @@ class TreeFS {
310
313
  continue;
311
314
  }
312
315
  let segmentNode = parentNode.get(segmentName);
316
+ if (segmentName === "..") {
317
+ ancestorOfRootIdx =
318
+ ancestorOfRootIdx == null ? 1 : ancestorOfRootIdx + 1;
319
+ } else if (segmentNode != null) {
320
+ ancestorOfRootIdx = null;
321
+ }
313
322
  if (segmentNode == null) {
314
323
  if (opts.makeDirectories !== true && segmentName !== "..") {
315
324
  return {
316
- canonicalLinkPaths,
317
325
  canonicalMissingPath: isLastSegment
318
326
  ? targetNormalPath
319
327
  : targetNormalPath.slice(0, fromIdx - 1),
@@ -332,7 +340,7 @@ class TreeFS {
332
340
  opts.followLeaf === false)
333
341
  ) {
334
342
  return {
335
- canonicalLinkPaths,
343
+ ancestorOfRootIdx,
336
344
  canonicalPath: targetNormalPath,
337
345
  exists: true,
338
346
  node: segmentNode,
@@ -347,7 +355,6 @@ class TreeFS {
347
355
  : targetNormalPath.slice(0, fromIdx - 1);
348
356
  if (segmentNode[_constants.default.SYMLINK] === 0) {
349
357
  return {
350
- canonicalLinkPaths,
351
358
  canonicalMissingPath: currentPath,
352
359
  exists: false,
353
360
  };
@@ -356,18 +363,22 @@ class TreeFS {
356
363
  segmentNode,
357
364
  currentPath
358
365
  );
359
- canonicalLinkPaths.push(currentPath);
366
+ if (opts.collectLinkPaths) {
367
+ opts.collectLinkPaths.add(
368
+ this.#pathUtils.normalToAbsolute(currentPath)
369
+ );
370
+ }
360
371
  targetNormalPath = isLastSegment
361
372
  ? normalSymlinkTarget
362
- : normalSymlinkTarget +
363
- _path.default.sep +
364
- targetNormalPath.slice(fromIdx);
373
+ : this.#pathUtils.joinNormalToRelative(
374
+ normalSymlinkTarget,
375
+ targetNormalPath.slice(fromIdx)
376
+ );
365
377
  if (seen == null) {
366
378
  seen = new Set([requestedNormalPath]);
367
379
  }
368
380
  if (seen.has(targetNormalPath)) {
369
381
  return {
370
- canonicalLinkPaths,
371
382
  canonicalMissingPath: targetNormalPath,
372
383
  exists: false,
373
384
  };
@@ -382,7 +393,7 @@ class TreeFS {
382
393
  "Unexpectedly escaped traversal"
383
394
  );
384
395
  return {
385
- canonicalLinkPaths,
396
+ ancestorOfRootIdx: null,
386
397
  canonicalPath: targetNormalPath,
387
398
  exists: true,
388
399
  node: this.#rootNode,
@@ -422,10 +433,30 @@ class TreeFS {
422
433
  ? this.#pathUtils.absoluteToNormal(relativeOrAbsolutePath)
423
434
  : this.#pathUtils.relativeToNormal(relativeOrAbsolutePath);
424
435
  }
425
- *_pathIterator(rootNode, opts, pathPrefix = "", followedLinks = new Set()) {
436
+ *#directoryNodeIterator(node, parent, ancestorOfRootIdx) {
437
+ if (ancestorOfRootIdx != null && parent) {
438
+ yield [
439
+ this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1),
440
+ parent,
441
+ ];
442
+ }
443
+ yield* node.entries();
444
+ }
445
+ *_pathIterator(
446
+ iterationRootNode,
447
+ iterationRootParentNode,
448
+ ancestorOfRootIdx,
449
+ opts,
450
+ pathPrefix = "",
451
+ followedLinks = new Set()
452
+ ) {
426
453
  const pathSep = opts.alwaysYieldPosix ? "/" : _path.default.sep;
427
454
  const prefixWithSep = pathPrefix === "" ? pathPrefix : pathPrefix + pathSep;
428
- for (const [name, node] of rootNode ?? this.#rootNode) {
455
+ for (const [name, node] of this.#directoryNodeIterator(
456
+ iterationRootNode,
457
+ iterationRootParentNode,
458
+ ancestorOfRootIdx
459
+ )) {
429
460
  if (opts.subtreeOnly && name === "..") {
430
461
  continue;
431
462
  }
@@ -458,6 +489,8 @@ class TreeFS {
458
489
  ) {
459
490
  yield* this._pathIterator(
460
491
  target,
492
+ resolved.parentNode,
493
+ resolved.ancestorOfRootIdx,
461
494
  opts,
462
495
  nodePath,
463
496
  new Set([...followedLinks, node])
@@ -465,7 +498,16 @@ class TreeFS {
465
498
  }
466
499
  }
467
500
  } else if (opts.recursive) {
468
- yield* this._pathIterator(node, opts, nodePath, followedLinks);
501
+ yield* this._pathIterator(
502
+ node,
503
+ iterationRootParentNode,
504
+ ancestorOfRootIdx != null && ancestorOfRootIdx > 1
505
+ ? ancestorOfRootIdx - 1
506
+ : null,
507
+ opts,
508
+ nodePath,
509
+ followedLinks
510
+ );
469
511
  }
470
512
  }
471
513
  }
@@ -142,20 +142,20 @@ export default class TreeFS implements MutableFileSystem {
142
142
 
143
143
  lookup(mixedPath: Path): LookupResult {
144
144
  const normalPath = this._normalizePath(mixedPath);
145
- const result = this._lookupByNormalPath(normalPath, {followLeaf: true});
145
+ const links = new Set<string>();
146
+ const result = this._lookupByNormalPath(normalPath, {
147
+ collectLinkPaths: links,
148
+ followLeaf: true,
149
+ });
146
150
  if (!result.exists) {
147
- const {canonicalMissingPath, canonicalLinkPaths} = result;
151
+ const {canonicalMissingPath} = result;
148
152
  return {
149
153
  exists: false,
150
- links: new Set(
151
- canonicalLinkPaths.map(canonicalPath =>
152
- this.#pathUtils.normalToAbsolute(canonicalPath),
153
- ),
154
- ),
154
+ links,
155
155
  missing: this.#pathUtils.normalToAbsolute(canonicalMissingPath),
156
156
  };
157
157
  }
158
- const {canonicalPath, canonicalLinkPaths, node} = result;
158
+ const {canonicalPath, node} = result;
159
159
  const type = node instanceof Map ? 'd' : node[H.SYMLINK] === 0 ? 'f' : 'l';
160
160
  invariant(
161
161
  type !== 'l',
@@ -165,11 +165,7 @@ export default class TreeFS implements MutableFileSystem {
165
165
  );
166
166
  return {
167
167
  exists: true,
168
- links: new Set(
169
- canonicalLinkPaths.map(canonicalPath =>
170
- this.#pathUtils.normalToAbsolute(canonicalPath),
171
- ),
172
- ),
168
+ links,
173
169
  realPath: this.#pathUtils.normalToAbsolute(canonicalPath),
174
170
  type,
175
171
  };
@@ -229,7 +225,12 @@ export default class TreeFS implements MutableFileSystem {
229
225
  if (!contextRootResult.exists) {
230
226
  return;
231
227
  }
232
- const {canonicalPath: rootRealPath, node: contextRoot} = contextRootResult;
228
+ const {
229
+ ancestorOfRootIdx,
230
+ canonicalPath: rootRealPath,
231
+ node: contextRoot,
232
+ parentNode: contextRootParent,
233
+ } = contextRootResult;
233
234
  if (!(contextRoot instanceof Map)) {
234
235
  return;
235
236
  }
@@ -245,13 +246,18 @@ export default class TreeFS implements MutableFileSystem {
245
246
  ? contextRootAbsolutePath.replaceAll(path.sep, '/')
246
247
  : contextRootAbsolutePath;
247
248
 
248
- for (const relativePathForComparison of this._pathIterator(contextRoot, {
249
- alwaysYieldPosix: filterComparePosix,
250
- canonicalPathOfRoot: rootRealPath,
251
- follow,
252
- recursive,
253
- subtreeOnly: rootDir != null,
254
- })) {
249
+ for (const relativePathForComparison of this._pathIterator(
250
+ contextRoot,
251
+ contextRootParent,
252
+ ancestorOfRootIdx,
253
+ {
254
+ alwaysYieldPosix: filterComparePosix,
255
+ canonicalPathOfRoot: rootRealPath,
256
+ follow,
257
+ recursive,
258
+ subtreeOnly: rootDir != null,
259
+ },
260
+ )) {
255
261
  if (
256
262
  filter == null ||
257
263
  filter.test(
@@ -357,6 +363,9 @@ export default class TreeFS implements MutableFileSystem {
357
363
  _lookupByNormalPath(
358
364
  requestedNormalPath: string,
359
365
  opts: {
366
+ // Mutable Set into which absolute real paths of traversed symlinks will
367
+ // be added. Omit for performance if not needed.
368
+ collectLinkPaths?: ?Set<string>,
360
369
  // Like lstat vs stat, whether to follow a symlink at the basename of
361
370
  // the given path, or return the details of the symlink itself.
362
371
  followLeaf?: boolean,
@@ -364,28 +373,25 @@ export default class TreeFS implements MutableFileSystem {
364
373
  } = {followLeaf: true, makeDirectories: false},
365
374
  ):
366
375
  | {
367
- canonicalLinkPaths: Array<string>,
376
+ ancestorOfRootIdx: ?number,
368
377
  canonicalPath: string,
369
378
  exists: true,
370
379
  node: MixedNode,
371
380
  parentNode: DirectoryNode,
372
381
  }
373
382
  | {
374
- canonicalLinkPaths: Array<string>,
383
+ ancestorOfRootIdx: ?number,
375
384
  canonicalPath: string,
376
385
  exists: true,
377
386
  node: DirectoryNode,
378
387
  parentNode: null,
379
388
  }
380
389
  | {
381
- canonicalLinkPaths: Array<string>,
382
390
  canonicalMissingPath: string,
383
391
  exists: false,
384
392
  } {
385
393
  // We'll update the target if we hit a symlink.
386
394
  let targetNormalPath = requestedNormalPath;
387
- // Set of traversed symlink paths to return.
388
- const canonicalLinkPaths: Array<string> = [];
389
395
  // Lazy-initialised set of seen target paths, to detect symlink cycles.
390
396
  let seen: ?Set<string>;
391
397
  // Pointer to the first character of the current path segment in
@@ -393,6 +399,9 @@ export default class TreeFS implements MutableFileSystem {
393
399
  let fromIdx = 0;
394
400
  // The parent of the current segment
395
401
  let parentNode = this.#rootNode;
402
+ // If a returned node is a strict ancestor of the root, this is the number
403
+ // of levels below the root, i.e. '..' is 1, '../..' is 2, otherwise null.
404
+ let ancestorOfRootIdx: ?number = null;
396
405
 
397
406
  while (targetNormalPath.length > fromIdx) {
398
407
  const nextSepIdx = targetNormalPath.indexOf(path.sep, fromIdx);
@@ -408,10 +417,18 @@ export default class TreeFS implements MutableFileSystem {
408
417
 
409
418
  let segmentNode = parentNode.get(segmentName);
410
419
 
420
+ // In normal paths all indirections are at the prefix, so we are at the
421
+ // nth ancestor of the root iff the path so far is n '..' segments.
422
+ if (segmentName === '..') {
423
+ ancestorOfRootIdx =
424
+ ancestorOfRootIdx == null ? 1 : ancestorOfRootIdx + 1;
425
+ } else if (segmentNode != null) {
426
+ ancestorOfRootIdx = null;
427
+ }
428
+
411
429
  if (segmentNode == null) {
412
430
  if (opts.makeDirectories !== true && segmentName !== '..') {
413
431
  return {
414
- canonicalLinkPaths,
415
432
  canonicalMissingPath: isLastSegment
416
433
  ? targetNormalPath
417
434
  : targetNormalPath.slice(0, fromIdx - 1),
@@ -433,7 +450,7 @@ export default class TreeFS implements MutableFileSystem {
433
450
  opts.followLeaf === false)
434
451
  ) {
435
452
  return {
436
- canonicalLinkPaths,
453
+ ancestorOfRootIdx,
437
454
  canonicalPath: targetNormalPath,
438
455
  exists: true,
439
456
  node: segmentNode,
@@ -452,7 +469,6 @@ export default class TreeFS implements MutableFileSystem {
452
469
  if (segmentNode[H.SYMLINK] === 0) {
453
470
  // Regular file in a directory path
454
471
  return {
455
- canonicalLinkPaths,
456
472
  canonicalMissingPath: currentPath,
457
473
  exists: false,
458
474
  };
@@ -463,13 +479,21 @@ export default class TreeFS implements MutableFileSystem {
463
479
  segmentNode,
464
480
  currentPath,
465
481
  );
466
- canonicalLinkPaths.push(currentPath);
482
+ if (opts.collectLinkPaths) {
483
+ opts.collectLinkPaths.add(
484
+ this.#pathUtils.normalToAbsolute(currentPath),
485
+ );
486
+ }
467
487
 
468
488
  // Append any subsequent path segments to the symlink target, and reset
469
489
  // with our new target.
470
490
  targetNormalPath = isLastSegment
471
491
  ? normalSymlinkTarget
472
- : normalSymlinkTarget + path.sep + targetNormalPath.slice(fromIdx);
492
+ : this.#pathUtils.joinNormalToRelative(
493
+ normalSymlinkTarget,
494
+ targetNormalPath.slice(fromIdx),
495
+ );
496
+
473
497
  if (seen == null) {
474
498
  // Optimisation: set this lazily only when we've encountered a symlink
475
499
  seen = new Set([requestedNormalPath]);
@@ -477,7 +501,6 @@ export default class TreeFS implements MutableFileSystem {
477
501
  if (seen.has(targetNormalPath)) {
478
502
  // TODO: Warn `Symlink cycle detected: ${[...seen, node].join(' -> ')}`
479
503
  return {
480
- canonicalLinkPaths,
481
504
  canonicalMissingPath: targetNormalPath,
482
505
  exists: false,
483
506
  };
@@ -489,7 +512,7 @@ export default class TreeFS implements MutableFileSystem {
489
512
  }
490
513
  invariant(parentNode === this.#rootNode, 'Unexpectedly escaped traversal');
491
514
  return {
492
- canonicalLinkPaths,
515
+ ancestorOfRootIdx: null,
493
516
  canonicalPath: targetNormalPath,
494
517
  exists: true,
495
518
  node: this.#rootNode,
@@ -540,12 +563,28 @@ export default class TreeFS implements MutableFileSystem {
540
563
  : this.#pathUtils.relativeToNormal(relativeOrAbsolutePath);
541
564
  }
542
565
 
566
+ *#directoryNodeIterator(
567
+ node: DirectoryNode,
568
+ parent: ?DirectoryNode,
569
+ ancestorOfRootIdx: ?number,
570
+ ): Iterator<[string, MixedNode]> {
571
+ if (ancestorOfRootIdx != null && parent) {
572
+ yield [
573
+ this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1),
574
+ parent,
575
+ ];
576
+ }
577
+ yield* node.entries();
578
+ }
579
+
543
580
  /**
544
581
  * Enumerate paths under a given node, including symlinks and through
545
582
  * symlinks (if `follow` is enabled).
546
583
  */
547
584
  *_pathIterator(
548
- rootNode: DirectoryNode,
585
+ iterationRootNode: DirectoryNode,
586
+ iterationRootParentNode: ?DirectoryNode,
587
+ ancestorOfRootIdx: ?number,
549
588
  opts: $ReadOnly<{
550
589
  alwaysYieldPosix: boolean,
551
590
  canonicalPathOfRoot: string,
@@ -558,7 +597,11 @@ export default class TreeFS implements MutableFileSystem {
558
597
  ): Iterable<Path> {
559
598
  const pathSep = opts.alwaysYieldPosix ? '/' : path.sep;
560
599
  const prefixWithSep = pathPrefix === '' ? pathPrefix : pathPrefix + pathSep;
561
- for (const [name, node] of rootNode ?? this.#rootNode) {
600
+ for (const [name, node] of this.#directoryNodeIterator(
601
+ iterationRootNode,
602
+ iterationRootParentNode,
603
+ ancestorOfRootIdx,
604
+ )) {
562
605
  if (opts.subtreeOnly && name === '..') {
563
606
  continue;
564
607
  }
@@ -608,6 +651,8 @@ export default class TreeFS implements MutableFileSystem {
608
651
  // the path where we found the symlink as a prefix.
609
652
  yield* this._pathIterator(
610
653
  target,
654
+ resolved.parentNode,
655
+ resolved.ancestorOfRootIdx,
611
656
  opts,
612
657
  nodePath,
613
658
  new Set([...followedLinks, node]),
@@ -615,7 +660,16 @@ export default class TreeFS implements MutableFileSystem {
615
660
  }
616
661
  }
617
662
  } else if (opts.recursive) {
618
- yield* this._pathIterator(node, opts, nodePath, followedLinks);
663
+ yield* this._pathIterator(
664
+ node,
665
+ iterationRootParentNode,
666
+ ancestorOfRootIdx != null && ancestorOfRootIdx > 1
667
+ ? ancestorOfRootIdx - 1
668
+ : null,
669
+ opts,
670
+ nodePath,
671
+ followedLinks,
672
+ );
619
673
  }
620
674
  }
621
675
  }
@@ -7,7 +7,7 @@ exports.default = void 0;
7
7
  class RecrawlWarning {
8
8
  static RECRAWL_WARNINGS = [];
9
9
  static REGEXP =
10
- /Recrawled this watch (\d+) times, most recently because:\n([^:]+)/;
10
+ /Recrawled this watch (\d+) times?, most recently because:\n([^:]+)/;
11
11
  constructor(root, count) {
12
12
  this.root = root;
13
13
  this.count = count;
@@ -19,7 +19,7 @@
19
19
  export default class RecrawlWarning {
20
20
  static RECRAWL_WARNINGS: Array<RecrawlWarning> = [];
21
21
  static REGEXP: RegExp =
22
- /Recrawled this watch (\d+) times, most recently because:\n([^:]+)/;
22
+ /Recrawled this watch (\d+) times?, most recently because:\n([^:]+)/;
23
23
 
24
24
  root: string;
25
25
  count: number;