metro-file-map 0.84.2 → 0.84.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/Watcher.d.ts +6 -9
  3. package/src/Watcher.js +66 -39
  4. package/src/Watcher.js.flow +84 -51
  5. package/src/crawlers/node/index.d.ts +3 -5
  6. package/src/crawlers/node/index.js +4 -1
  7. package/src/crawlers/node/index.js.flow +8 -6
  8. package/src/crawlers/watchman/index.d.ts +5 -12
  9. package/src/crawlers/watchman/index.js.flow +2 -6
  10. package/src/flow-types.d.ts +81 -32
  11. package/src/flow-types.js.flow +89 -29
  12. package/src/index.d.ts +4 -4
  13. package/src/index.js +145 -120
  14. package/src/index.js.flow +199 -149
  15. package/src/lib/FileSystemChangeAggregator.d.ts +40 -0
  16. package/src/lib/FileSystemChangeAggregator.js +89 -0
  17. package/src/lib/FileSystemChangeAggregator.js.flow +143 -0
  18. package/src/lib/TreeFS.d.ts +16 -8
  19. package/src/lib/TreeFS.js +67 -16
  20. package/src/lib/TreeFS.js.flow +89 -16
  21. package/src/plugins/DependencyPlugin.d.ts +5 -36
  22. package/src/plugins/DependencyPlugin.js +26 -48
  23. package/src/plugins/DependencyPlugin.js.flow +22 -100
  24. package/src/plugins/FileDataPlugin.d.ts +55 -0
  25. package/src/plugins/FileDataPlugin.js +41 -0
  26. package/src/plugins/FileDataPlugin.js.flow +76 -0
  27. package/src/plugins/HastePlugin.d.ts +3 -11
  28. package/src/plugins/HastePlugin.js +11 -11
  29. package/src/plugins/HastePlugin.js.flow +12 -12
  30. package/src/plugins/MockPlugin.d.ts +3 -5
  31. package/src/plugins/MockPlugin.js +17 -20
  32. package/src/plugins/MockPlugin.js.flow +18 -22
  33. package/src/watchers/FallbackWatcher.js +19 -3
  34. package/src/watchers/FallbackWatcher.js.flow +28 -5
  35. package/src/watchers/NativeWatcher.d.ts +2 -2
  36. package/src/watchers/NativeWatcher.js +27 -5
  37. package/src/watchers/NativeWatcher.js.flow +33 -6
  38. package/src/watchers/common.d.ts +3 -1
  39. package/src/watchers/common.js +6 -1
  40. package/src/watchers/common.js.flow +1 -0
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true,
5
+ });
6
+ exports.FileSystemChangeAggregator = void 0;
7
+ class FileSystemChangeAggregator {
8
+ #addedDirectories = new Set();
9
+ #removedDirectories = new Set();
10
+ #addedFiles = new Map();
11
+ #modifiedFiles = new Map();
12
+ #removedFiles = new Map();
13
+ #initialMetadata = new Map();
14
+ directoryAdded(canonicalPath) {
15
+ if (!this.#removedDirectories.delete(canonicalPath)) {
16
+ this.#addedDirectories.add(canonicalPath);
17
+ }
18
+ }
19
+ directoryRemoved(canonicalPath) {
20
+ if (!this.#addedDirectories.delete(canonicalPath)) {
21
+ this.#removedDirectories.add(canonicalPath);
22
+ }
23
+ }
24
+ fileAdded(canonicalPath, data) {
25
+ if (this.#removedFiles.delete(canonicalPath)) {
26
+ this.#modifiedFiles.set(canonicalPath, data);
27
+ } else {
28
+ this.#addedFiles.set(canonicalPath, data);
29
+ }
30
+ }
31
+ fileModified(canonicalPath, oldData, newData) {
32
+ if (this.#addedFiles.has(canonicalPath)) {
33
+ this.#addedFiles.set(canonicalPath, newData);
34
+ } else {
35
+ if (!this.#initialMetadata.has(canonicalPath)) {
36
+ this.#initialMetadata.set(canonicalPath, oldData);
37
+ }
38
+ this.#modifiedFiles.set(canonicalPath, newData);
39
+ }
40
+ }
41
+ fileRemoved(canonicalPath, data) {
42
+ if (!this.#addedFiles.delete(canonicalPath)) {
43
+ let initialData = this.#initialMetadata.get(canonicalPath);
44
+ if (!initialData) {
45
+ initialData = data;
46
+ this.#initialMetadata.set(canonicalPath, initialData);
47
+ }
48
+ this.#modifiedFiles.delete(canonicalPath);
49
+ this.#removedFiles.set(canonicalPath, initialData);
50
+ }
51
+ }
52
+ getSize() {
53
+ return (
54
+ this.#addedDirectories.size +
55
+ this.#removedDirectories.size +
56
+ this.#addedFiles.size +
57
+ this.#modifiedFiles.size +
58
+ this.#removedFiles.size
59
+ );
60
+ }
61
+ getView() {
62
+ return {
63
+ addedDirectories: this.#addedDirectories,
64
+ removedDirectories: this.#removedDirectories,
65
+ addedFiles: this.#addedFiles,
66
+ modifiedFiles: this.#modifiedFiles,
67
+ removedFiles: this.#removedFiles,
68
+ };
69
+ }
70
+ getMappedView(metadataMapFn) {
71
+ return {
72
+ addedDirectories: this.#addedDirectories,
73
+ removedDirectories: this.#removedDirectories,
74
+ addedFiles: mapIterable(this.#addedFiles, metadataMapFn),
75
+ modifiedFiles: mapIterable(this.#modifiedFiles, metadataMapFn),
76
+ removedFiles: mapIterable(this.#removedFiles, metadataMapFn),
77
+ };
78
+ }
79
+ }
80
+ exports.FileSystemChangeAggregator = FileSystemChangeAggregator;
81
+ function mapIterable(map, metadataMapFn) {
82
+ return {
83
+ *[Symbol.iterator]() {
84
+ for (const [path, metadata] of map) {
85
+ yield [path, metadataMapFn(metadata)];
86
+ }
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ * @oncall react_native
10
+ */
11
+
12
+ import type {
13
+ CanonicalPath,
14
+ FileMetadata,
15
+ FileSystemListener,
16
+ ReadonlyFileSystemChanges,
17
+ } from '../flow-types';
18
+
19
+ export class FileSystemChangeAggregator implements FileSystemListener {
20
+ // Mutually exclusive with removedDirectories
21
+ +#addedDirectories: Set<CanonicalPath> = new Set();
22
+ // Mutually exclusive with addedDirectories
23
+ +#removedDirectories: Set<CanonicalPath> = new Set();
24
+
25
+ // Mutually exclusive with modified and removed files
26
+ +#addedFiles: Map<CanonicalPath, FileMetadata> = new Map();
27
+ // Mutually exclusive with added and removed files
28
+ +#modifiedFiles: Map<CanonicalPath, FileMetadata> = new Map();
29
+ // Mutually exclusive with added and modified files
30
+ +#removedFiles: Map<CanonicalPath, FileMetadata> = new Map();
31
+
32
+ // Removed files must be paired with the file's metadata the last time it was
33
+ // observable by consumers - ie, immediately *before* this batch. To report
34
+ // this accurately with minimal overhead, we'll note the current metadata of
35
+ // a file the first time it is modified or removed within a batch. If it is
36
+ // re-added, modified and removed again, we still have the initial metadata.
37
+ // This is particularly important if, say, a regular file is replaced by a
38
+ // symlink, or vice-versa.
39
+ +#initialMetadata: Map<CanonicalPath, FileMetadata> = new Map();
40
+
41
+ directoryAdded(canonicalPath: CanonicalPath): void {
42
+ // Only add to newDirectories if this directory wasn't previously removed
43
+ // (i.e., it's truly new). If it was removed and re-added, the net effect
44
+ // is no directory change.
45
+ if (!this.#removedDirectories.delete(canonicalPath)) {
46
+ this.#addedDirectories.add(canonicalPath);
47
+ }
48
+ }
49
+
50
+ directoryRemoved(canonicalPath: CanonicalPath): void {
51
+ if (!this.#addedDirectories.delete(canonicalPath)) {
52
+ this.#removedDirectories.add(canonicalPath);
53
+ }
54
+ }
55
+
56
+ fileAdded(canonicalPath: CanonicalPath, data: FileMetadata): void {
57
+ if (this.#removedFiles.delete(canonicalPath)) {
58
+ // File was removed then re-added in the same batch - treat as modification
59
+ this.#modifiedFiles.set(canonicalPath, data);
60
+ } else {
61
+ // New file
62
+ this.#addedFiles.set(canonicalPath, data);
63
+ }
64
+ }
65
+
66
+ fileModified(
67
+ canonicalPath: CanonicalPath,
68
+ oldData: FileMetadata,
69
+ newData: FileMetadata,
70
+ ): void {
71
+ if (this.#addedFiles.has(canonicalPath)) {
72
+ // File did not exist before this batch. Further modification only
73
+ // updates metadata
74
+ this.#addedFiles.set(canonicalPath, newData);
75
+ } else {
76
+ if (!this.#initialMetadata.has(canonicalPath)) {
77
+ this.#initialMetadata.set(canonicalPath, oldData);
78
+ }
79
+ this.#modifiedFiles.set(canonicalPath, newData);
80
+ }
81
+ }
82
+
83
+ fileRemoved(canonicalPath: CanonicalPath, data: FileMetadata): void {
84
+ // Check if this file was added in the same batch
85
+ if (!this.#addedFiles.delete(canonicalPath)) {
86
+ let initialData = this.#initialMetadata.get(canonicalPath);
87
+ if (!initialData) {
88
+ initialData = data;
89
+ this.#initialMetadata.set(canonicalPath, initialData);
90
+ }
91
+
92
+ // File was not added in this batch, so add to removed with last metadata
93
+ this.#modifiedFiles.delete(canonicalPath);
94
+ this.#removedFiles.set(canonicalPath, initialData);
95
+ }
96
+ // else: File was added then removed in the same batch - no net change
97
+ }
98
+
99
+ getSize(): number {
100
+ return (
101
+ this.#addedDirectories.size +
102
+ this.#removedDirectories.size +
103
+ this.#addedFiles.size +
104
+ this.#modifiedFiles.size +
105
+ this.#removedFiles.size
106
+ );
107
+ }
108
+
109
+ getView(): ReadonlyFileSystemChanges<FileMetadata> {
110
+ return {
111
+ addedDirectories: this.#addedDirectories,
112
+ removedDirectories: this.#removedDirectories,
113
+ addedFiles: this.#addedFiles,
114
+ modifiedFiles: this.#modifiedFiles,
115
+ removedFiles: this.#removedFiles,
116
+ };
117
+ }
118
+
119
+ getMappedView<T>(
120
+ metadataMapFn: (metadata: FileMetadata) => T,
121
+ ): ReadonlyFileSystemChanges<T> {
122
+ return {
123
+ addedDirectories: this.#addedDirectories,
124
+ removedDirectories: this.#removedDirectories,
125
+ addedFiles: mapIterable(this.#addedFiles, metadataMapFn),
126
+ modifiedFiles: mapIterable(this.#modifiedFiles, metadataMapFn),
127
+ removedFiles: mapIterable(this.#removedFiles, metadataMapFn),
128
+ };
129
+ }
130
+ }
131
+
132
+ function mapIterable<T>(
133
+ map: Map<CanonicalPath, FileMetadata>,
134
+ metadataMapFn: (metadata: FileMetadata) => T,
135
+ ): Iterable<Readonly<[CanonicalPath, T]>> {
136
+ return {
137
+ *[Symbol.iterator](): Iterator<Readonly<[CanonicalPath, T]>> {
138
+ for (const [path, metadata] of map) {
139
+ yield [path, metadataMapFn(metadata)];
140
+ }
141
+ },
142
+ };
143
+ }
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
7
  * @noformat
8
- * @generated SignedSource<<1f36861cea798d8cc2a5dc61293ecb1b>>
8
+ * @generated SignedSource<<65a3c4140d459a56b8c949e52b32ea1b>>
9
9
  *
10
10
  * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
11
11
  * Original file: packages/metro-file-map/src/lib/TreeFS.js
@@ -19,6 +19,7 @@ import type {
19
19
  FileData,
20
20
  FileMetadata,
21
21
  FileStats,
22
+ FileSystemListener,
22
23
  LookupResult,
23
24
  MutableFileSystem,
24
25
  Path,
@@ -104,10 +105,10 @@ declare class TreeFS implements MutableFileSystem {
104
105
  getSerializableSnapshot(): CacheData['fileSystemData'];
105
106
  static fromDeserializedSnapshot(args: DeserializedSnapshotInput): TreeFS;
106
107
  getSize(mixedPath: Path): null | undefined | number;
107
- getDifference(files: FileData): {
108
- changedFiles: FileData;
109
- removedFiles: Set<string>;
110
- };
108
+ getDifference(
109
+ files: FileData,
110
+ options?: Readonly<{subpath?: string}>,
111
+ ): {changedFiles: FileData; removedFiles: Set<string>};
111
112
  getSha1(mixedPath: Path): null | undefined | string;
112
113
  getOrComputeSha1(
113
114
  mixedPath: Path,
@@ -122,9 +123,16 @@ declare class TreeFS implements MutableFileSystem {
122
123
  * for example: `a/b.js` -> `./a/b.js`
123
124
  */
124
125
  matchFiles(opts: MatchFilesOptions): Iterable<Path>;
125
- addOrModify(mixedPath: Path, metadata: FileMetadata): void;
126
- bulkAddOrModify(addedOrModifiedFiles: FileData): void;
127
- remove(mixedPath: Path): null | undefined | FileMetadata;
126
+ addOrModify(
127
+ mixedPath: Path,
128
+ metadata: FileMetadata,
129
+ changeListener?: FileSystemListener,
130
+ ): void;
131
+ bulkAddOrModify(
132
+ addedOrModifiedFiles: FileData,
133
+ changeListener?: FileSystemListener,
134
+ ): void;
135
+ remove(mixedPath: Path, changeListener?: FileSystemListener): void;
128
136
  /**
129
137
  * Given a start path (which need not exist), a subpath and type, and
130
138
  * optionally a 'breakOnSegment', performs the following:
package/src/lib/TreeFS.js CHANGED
@@ -48,13 +48,33 @@ class TreeFS {
48
48
  const fileMetadata = this.#getFileData(mixedPath);
49
49
  return (fileMetadata && fileMetadata[_constants.default.SIZE]) ?? null;
50
50
  }
51
- getDifference(files) {
51
+ getDifference(files, options) {
52
52
  const changedFiles = new Map(files);
53
53
  const removedFiles = new Set();
54
- for (const { canonicalPath, metadata } of this.metadataIterator({
55
- includeNodeModules: true,
56
- includeSymlinks: true,
57
- })) {
54
+ const subpath = options?.subpath;
55
+ let rootNode = this.#rootNode;
56
+ let prefix = "";
57
+ if (subpath != null && subpath !== "") {
58
+ const lookupResult = this.#lookupByNormalPath(subpath, {
59
+ followLeaf: true,
60
+ });
61
+ if (!lookupResult.exists || !isDirectory(lookupResult.node)) {
62
+ return {
63
+ changedFiles,
64
+ removedFiles,
65
+ };
66
+ }
67
+ rootNode = lookupResult.node;
68
+ prefix = lookupResult.canonicalPath;
69
+ }
70
+ for (const { canonicalPath, metadata } of this.#metadataIterator(
71
+ rootNode,
72
+ {
73
+ includeNodeModules: true,
74
+ includeSymlinks: true,
75
+ },
76
+ prefix,
77
+ )) {
58
78
  const newMetadata = files.get(canonicalPath);
59
79
  if (newMetadata) {
60
80
  if (isRegularFile(newMetadata) !== isRegularFile(metadata)) {
@@ -253,11 +273,12 @@ class TreeFS {
253
273
  }
254
274
  }
255
275
  }
256
- addOrModify(mixedPath, metadata) {
276
+ addOrModify(mixedPath, metadata, changeListener) {
257
277
  const normalPath = this.#normalizePath(mixedPath);
258
278
  const parentDirNode = this.#lookupByNormalPath(
259
279
  _path.default.dirname(normalPath),
260
280
  {
281
+ changeListener,
261
282
  makeDirectories: true,
262
283
  },
263
284
  );
@@ -271,9 +292,9 @@ class TreeFS {
271
292
  _path.default.sep +
272
293
  _path.default.basename(normalPath),
273
294
  );
274
- this.bulkAddOrModify(new Map([[canonicalPath, metadata]]));
295
+ this.bulkAddOrModify(new Map([[canonicalPath, metadata]]), changeListener);
275
296
  }
276
- bulkAddOrModify(addedOrModifiedFiles) {
297
+ bulkAddOrModify(addedOrModifiedFiles, changeListener) {
277
298
  let lastDir;
278
299
  let directoryNode;
279
300
  for (const [normalPath, metadata] of addedOrModifiedFiles) {
@@ -283,6 +304,7 @@ class TreeFS {
283
304
  lastSepIdx === -1 ? normalPath : normalPath.slice(lastSepIdx + 1);
284
305
  if (directoryNode == null || dirname !== lastDir) {
285
306
  const lookup = this.#lookupByNormalPath(dirname, {
307
+ changeListener,
286
308
  followLeaf: false,
287
309
  makeDirectories: true,
288
310
  });
@@ -301,30 +323,53 @@ class TreeFS {
301
323
  lastDir = dirname;
302
324
  directoryNode = lookup.node;
303
325
  }
326
+ if (changeListener != null) {
327
+ const existingNode = directoryNode.get(basename);
328
+ if (existingNode != null) {
329
+ (0, _invariant.default)(
330
+ !isDirectory(existingNode),
331
+ "Detected addition or modification of file %s, but it is tracked as a non-empty directory",
332
+ normalPath,
333
+ );
334
+ changeListener.fileModified(normalPath, existingNode, metadata);
335
+ } else {
336
+ changeListener.fileAdded(normalPath, metadata);
337
+ }
338
+ }
304
339
  directoryNode.set(basename, metadata);
305
340
  }
306
341
  }
307
- remove(mixedPath) {
342
+ remove(mixedPath, changeListener) {
308
343
  const normalPath = this.#normalizePath(mixedPath);
309
344
  const result = this.#lookupByNormalPath(normalPath, {
310
345
  followLeaf: false,
311
346
  });
312
347
  if (!result.exists) {
313
- return null;
348
+ return;
314
349
  }
315
350
  const { parentNode, canonicalPath, node } = result;
316
351
  if (isDirectory(node) && node.size > 0) {
317
- throw new Error(
318
- `TreeFS: remove called on a non-empty directory: ${mixedPath}`,
319
- );
352
+ for (const basename of node.keys()) {
353
+ this.remove(
354
+ canonicalPath + _path.default.sep + basename,
355
+ changeListener,
356
+ );
357
+ }
358
+ return;
320
359
  }
321
360
  if (parentNode != null) {
361
+ if (changeListener != null) {
362
+ if (isDirectory(node)) {
363
+ changeListener.directoryRemoved(canonicalPath);
364
+ } else {
365
+ changeListener.fileRemoved(canonicalPath, node);
366
+ }
367
+ }
322
368
  parentNode.delete(_path.default.basename(canonicalPath));
323
369
  if (parentNode.size === 0 && parentNode !== this.#rootNode) {
324
- this.remove(_path.default.dirname(canonicalPath));
370
+ this.remove(_path.default.dirname(canonicalPath), changeListener);
325
371
  }
326
372
  }
327
- return isDirectory(node) ? null : node;
328
373
  }
329
374
  #lookupByNormalPath(
330
375
  requestedNormalPath,
@@ -338,7 +383,7 @@ class TreeFS {
338
383
  let fromIdx = opts.start?.pathIdx ?? 0;
339
384
  let parentNode = opts.start?.node ?? this.#rootNode;
340
385
  let ancestorOfRootIdx = opts.start?.ancestorOfRootIdx ?? 0;
341
- const collectAncestors = opts.collectAncestors;
386
+ const { collectAncestors, changeListener } = opts;
342
387
  let unseenPathFromIdx = 0;
343
388
  while (targetNormalPath.length > fromIdx) {
344
389
  const nextSepIdx = targetNormalPath.indexOf(_path.default.sep, fromIdx);
@@ -369,6 +414,12 @@ class TreeFS {
369
414
  }
370
415
  segmentNode = new Map();
371
416
  if (opts.makeDirectories === true) {
417
+ if (changeListener != null) {
418
+ const canonicalPath = isLastSegment
419
+ ? targetNormalPath
420
+ : targetNormalPath.slice(0, fromIdx - 1);
421
+ changeListener.directoryAdded(canonicalPath);
422
+ }
372
423
  parentNode.set(segmentName, segmentNode);
373
424
  }
374
425
  }
@@ -13,6 +13,7 @@ import type {
13
13
  FileData,
14
14
  FileMetadata,
15
15
  FileStats,
16
+ FileSystemListener,
16
17
  LookupResult,
17
18
  MutableFileSystem,
18
19
  Path,
@@ -156,16 +157,45 @@ export default class TreeFS implements MutableFileSystem {
156
157
  return (fileMetadata && fileMetadata[H.SIZE]) ?? null;
157
158
  }
158
159
 
159
- getDifference(files: FileData): {
160
+ getDifference(
161
+ files: FileData,
162
+ options?: Readonly<{
163
+ // Only consider files under this normal subdirectory when computing
164
+ // removedFiles. If not provided, all files in the file system are
165
+ // considered.
166
+ subpath?: string,
167
+ }>,
168
+ ): {
160
169
  changedFiles: FileData,
161
170
  removedFiles: Set<string>,
162
171
  } {
163
172
  const changedFiles: FileData = new Map(files);
164
173
  const removedFiles: Set<string> = new Set();
165
- for (const {canonicalPath, metadata} of this.metadataIterator({
166
- includeNodeModules: true,
167
- includeSymlinks: true,
168
- })) {
174
+ const subpath = options?.subpath;
175
+
176
+ // If a subpath is specified, start iteration from that node
177
+ let rootNode: DirectoryNode = this.#rootNode;
178
+ let prefix: string = '';
179
+ if (subpath != null && subpath !== '') {
180
+ const lookupResult = this.#lookupByNormalPath(subpath, {
181
+ followLeaf: true,
182
+ });
183
+ if (!lookupResult.exists || !isDirectory(lookupResult.node)) {
184
+ // Directory doesn't exist, nothing to compare - all files are new
185
+ return {changedFiles, removedFiles};
186
+ }
187
+ rootNode = lookupResult.node;
188
+ prefix = lookupResult.canonicalPath;
189
+ }
190
+
191
+ for (const {canonicalPath, metadata} of this.#metadataIterator(
192
+ rootNode,
193
+ {
194
+ includeNodeModules: true,
195
+ includeSymlinks: true,
196
+ },
197
+ prefix,
198
+ )) {
169
199
  const newMetadata = files.get(canonicalPath);
170
200
  if (newMetadata) {
171
201
  if (isRegularFile(newMetadata) !== isRegularFile(metadata)) {
@@ -378,11 +408,16 @@ export default class TreeFS implements MutableFileSystem {
378
408
  }
379
409
  }
380
410
 
381
- addOrModify(mixedPath: Path, metadata: FileMetadata): void {
411
+ addOrModify(
412
+ mixedPath: Path,
413
+ metadata: FileMetadata,
414
+ changeListener?: FileSystemListener,
415
+ ): void {
382
416
  const normalPath = this.#normalizePath(mixedPath);
383
417
  // Walk the tree to find the *real* path of the parent node, creating
384
418
  // directories as we need.
385
419
  const parentDirNode = this.#lookupByNormalPath(path.dirname(normalPath), {
420
+ changeListener,
386
421
  makeDirectories: true,
387
422
  });
388
423
  if (!parentDirNode.exists) {
@@ -394,10 +429,13 @@ export default class TreeFS implements MutableFileSystem {
394
429
  const canonicalPath = this.#normalizePath(
395
430
  parentDirNode.canonicalPath + path.sep + path.basename(normalPath),
396
431
  );
397
- this.bulkAddOrModify(new Map([[canonicalPath, metadata]]));
432
+ this.bulkAddOrModify(new Map([[canonicalPath, metadata]]), changeListener);
398
433
  }
399
434
 
400
- bulkAddOrModify(addedOrModifiedFiles: FileData): void {
435
+ bulkAddOrModify(
436
+ addedOrModifiedFiles: FileData,
437
+ changeListener?: FileSystemListener,
438
+ ): void {
401
439
  // Optimisation: Bulk FileData are typically clustered by directory, so we
402
440
  // optimise for that case by remembering the last directory we looked up.
403
441
  // Experiments with large result sets show this to be significantly (~30%)
@@ -413,6 +451,7 @@ export default class TreeFS implements MutableFileSystem {
413
451
 
414
452
  if (directoryNode == null || dirname !== lastDir) {
415
453
  const lookup = this.#lookupByNormalPath(dirname, {
454
+ changeListener,
416
455
  followLeaf: false,
417
456
  makeDirectories: true,
418
457
  });
@@ -433,24 +472,48 @@ export default class TreeFS implements MutableFileSystem {
433
472
  lastDir = dirname;
434
473
  directoryNode = lookup.node;
435
474
  }
475
+ if (changeListener != null) {
476
+ const existingNode = directoryNode.get(basename);
477
+ if (existingNode != null) {
478
+ invariant(
479
+ !isDirectory(existingNode),
480
+ 'Detected addition or modification of file %s, but it is tracked as a non-empty directory',
481
+ normalPath,
482
+ );
483
+ // File already exists - this is a modification
484
+ changeListener.fileModified(normalPath, existingNode, metadata);
485
+ } else {
486
+ // New file
487
+ changeListener.fileAdded(normalPath, metadata);
488
+ }
489
+ }
436
490
  directoryNode.set(basename, metadata);
437
491
  }
438
492
  }
439
493
 
440
- remove(mixedPath: Path): ?FileMetadata {
494
+ remove(mixedPath: Path, changeListener?: FileSystemListener): void {
441
495
  const normalPath = this.#normalizePath(mixedPath);
442
496
  const result = this.#lookupByNormalPath(normalPath, {followLeaf: false});
443
497
  if (!result.exists) {
444
- return null;
498
+ return;
445
499
  }
446
500
  const {parentNode, canonicalPath, node} = result;
447
501
 
448
502
  if (isDirectory(node) && node.size > 0) {
449
- throw new Error(
450
- `TreeFS: remove called on a non-empty directory: ${mixedPath}`,
451
- );
503
+ for (const basename of node.keys()) {
504
+ this.remove(canonicalPath + path.sep + basename, changeListener);
505
+ }
506
+ // Removing the last file will delete this directory
507
+ return;
452
508
  }
453
509
  if (parentNode != null) {
510
+ if (changeListener != null) {
511
+ if (isDirectory(node)) {
512
+ changeListener.directoryRemoved(canonicalPath);
513
+ } else {
514
+ changeListener.fileRemoved(canonicalPath, node);
515
+ }
516
+ }
454
517
  parentNode.delete(path.basename(canonicalPath));
455
518
  if (parentNode.size === 0 && parentNode !== this.#rootNode) {
456
519
  // NB: This isn't the most efficient algorithm - in the case of
@@ -458,10 +521,9 @@ export default class TreeFS implements MutableFileSystem {
458
521
  // that's not expected to be a case common enough to justify
459
522
  // implementation complexity, or slowing down more common uses of
460
523
  // _lookupByNormalPath.
461
- this.remove(path.dirname(canonicalPath));
524
+ this.remove(path.dirname(canonicalPath), changeListener);
462
525
  }
463
526
  }
464
- return isDirectory(node) ? null : node;
465
527
  }
466
528
 
467
529
  /**
@@ -492,6 +554,10 @@ export default class TreeFS implements MutableFileSystem {
492
554
  // be added. Omit for performance if not needed.
493
555
  collectLinkPaths?: ?Set<string>,
494
556
 
557
+ // Low-level callbacks called on mutations of TreeFS data.
558
+ // Omit for performance if not needed.
559
+ changeListener?: FileSystemListener,
560
+
495
561
  // Like lstat vs stat, whether to follow a symlink at the basename of
496
562
  // the given path, or return the details of the symlink itself.
497
563
  followLeaf?: boolean,
@@ -541,7 +607,8 @@ export default class TreeFS implements MutableFileSystem {
541
607
  // null.
542
608
  let ancestorOfRootIdx: ?number = opts.start?.ancestorOfRootIdx ?? 0;
543
609
 
544
- const collectAncestors = opts.collectAncestors;
610
+ const {collectAncestors, changeListener} = opts;
611
+
545
612
  // Used only when collecting ancestors, to avoid double-counting nodes and
546
613
  // paths when traversing a symlink takes us back to rootNode and out again.
547
614
  // This tracks the first character of the first segment not already
@@ -583,6 +650,12 @@ export default class TreeFS implements MutableFileSystem {
583
650
  }
584
651
  segmentNode = new Map();
585
652
  if (opts.makeDirectories === true) {
653
+ if (changeListener != null) {
654
+ const canonicalPath = isLastSegment
655
+ ? targetNormalPath
656
+ : targetNormalPath.slice(0, fromIdx - 1);
657
+ changeListener.directoryAdded(canonicalPath);
658
+ }
586
659
  parentNode.set(segmentName, segmentNode);
587
660
  }
588
661
  }