metro-file-map 0.84.2 → 0.84.3

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 (37) 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 +2 -13
  22. package/src/plugins/DependencyPlugin.js +1 -3
  23. package/src/plugins/DependencyPlugin.js.flow +1 -16
  24. package/src/plugins/HastePlugin.d.ts +3 -11
  25. package/src/plugins/HastePlugin.js +11 -11
  26. package/src/plugins/HastePlugin.js.flow +12 -12
  27. package/src/plugins/MockPlugin.d.ts +3 -5
  28. package/src/plugins/MockPlugin.js +17 -20
  29. package/src/plugins/MockPlugin.js.flow +18 -22
  30. package/src/watchers/FallbackWatcher.js +19 -3
  31. package/src/watchers/FallbackWatcher.js.flow +28 -5
  32. package/src/watchers/NativeWatcher.d.ts +2 -2
  33. package/src/watchers/NativeWatcher.js +27 -5
  34. package/src/watchers/NativeWatcher.js.flow +33 -6
  35. package/src/watchers/common.d.ts +3 -1
  36. package/src/watchers/common.js +6 -1
  37. package/src/watchers/common.js.flow +1 -0
package/src/index.js.flow CHANGED
@@ -17,12 +17,13 @@ import type {
17
17
  CacheManagerFactory,
18
18
  CacheManagerFactoryOptions,
19
19
  CanonicalPath,
20
+ ChangedFileMetadata,
20
21
  ChangeEvent,
21
22
  ChangeEventClock,
22
23
  ChangeEventMetadata,
23
24
  Console,
24
25
  CrawlerOptions,
25
- EventsQueue,
26
+ CrawlResult,
26
27
  FileData,
27
28
  FileMapPlugin,
28
29
  FileMapPluginWorker,
@@ -31,6 +32,7 @@ import type {
31
32
  HasteMapData,
32
33
  HasteMapItem,
33
34
  HType,
35
+ InputFileMapPlugin,
34
36
  MutableFileSystem,
35
37
  Path,
36
38
  PerfLogger,
@@ -44,6 +46,7 @@ import {DiskCacheManager} from './cache/DiskCacheManager';
44
46
  import H from './constants';
45
47
  import checkWatchmanCapabilities from './lib/checkWatchmanCapabilities';
46
48
  import {FileProcessor} from './lib/FileProcessor';
49
+ import {FileSystemChangeAggregator} from './lib/FileSystemChangeAggregator';
47
50
  import normalizePathSeparatorsToPosix from './lib/normalizePathSeparatorsToPosix';
48
51
  import normalizePathSeparatorsToSystem from './lib/normalizePathSeparatorsToSystem';
49
52
  import {RootPathUtils} from './lib/RootPathUtils';
@@ -52,7 +55,6 @@ import {Watcher} from './Watcher';
52
55
  import EventEmitter from 'events';
53
56
  import {promises as fsPromises} from 'fs';
54
57
  import invariant from 'invariant';
55
- import nullthrows from 'nullthrows';
56
58
  import * as path from 'path';
57
59
  import {performance} from 'perf_hooks';
58
60
 
@@ -69,6 +71,7 @@ export type {
69
71
  FileSystem,
70
72
  HasteMapData,
71
73
  HasteMapItem,
74
+ InputFileMapPlugin,
72
75
  };
73
76
 
74
77
  export type InputOptions = Readonly<{
@@ -77,7 +80,7 @@ export type InputOptions = Readonly<{
77
80
  extensions: ReadonlyArray<string>,
78
81
  forceNodeFilesystemAPI?: ?boolean,
79
82
  ignorePattern?: ?RegExp,
80
- plugins?: ReadonlyArray<AnyFileMapPlugin>,
83
+ plugins?: ReadonlyArray<InputFileMapPlugin>,
81
84
  retainAllFiles: boolean,
82
85
  rootDir: string,
83
86
  roots: ReadonlyArray<string>,
@@ -111,12 +114,24 @@ type InternalOptions = Readonly<{
111
114
  watchmanDeferStates: ReadonlyArray<string>,
112
115
  }>;
113
116
 
114
- // $FlowFixMe[unclear-type] Plugin types cannot be known statically
115
- type AnyFileMapPlugin = FileMapPlugin<any, any>;
116
117
  type IndexedPlugin = Readonly<{
117
- plugin: AnyFileMapPlugin,
118
+ // $FlowFixMe[unclear-type] Plugin types cannot be known statically
119
+ plugin: FileMapPlugin<any, any>,
118
120
  dataIdx: ?number,
119
121
  }>;
122
+ type InternalEnqueuedEvent = Readonly<
123
+ | {
124
+ clock: ?ChangeEventClock,
125
+ relativeFilePath: string,
126
+ metadata: FileMetadata,
127
+ type: 'touch',
128
+ }
129
+ | {
130
+ clock: ?ChangeEventClock,
131
+ relativeFilePath: string,
132
+ type: 'delete',
133
+ },
134
+ >;
120
135
 
121
136
  export {DiskCacheManager} from './cache/DiskCacheManager';
122
137
  export {default as DependencyPlugin} from './plugins/DependencyPlugin';
@@ -421,7 +436,7 @@ export default class FileMap extends EventEmitter {
421
436
  };
422
437
  },
423
438
  fileIterator: opts =>
424
- mapIterator(
439
+ mapIterable(
425
440
  fileSystem.metadataIterator(opts),
426
441
  ({baseName, canonicalPath, metadata}) => ({
427
442
  baseName,
@@ -437,7 +452,13 @@ export default class FileMap extends EventEmitter {
437
452
  ]);
438
453
 
439
454
  // Update `fileSystem` and plugins based on the file delta.
440
- await this.#applyFileDelta(fileSystem, plugins, fileDelta);
455
+ const actualChanges = await this.#applyFileDelta(
456
+ fileSystem,
457
+ plugins,
458
+ fileDelta,
459
+ );
460
+
461
+ const changeSize = actualChanges.getSize();
441
462
 
442
463
  // Validate plugins before persisting them.
443
464
  plugins.forEach(({plugin}) => plugin.assertValid());
@@ -447,14 +468,9 @@ export default class FileMap extends EventEmitter {
447
468
  fileSystem,
448
469
  watchmanClocks,
449
470
  plugins,
450
- fileDelta.changedFiles,
451
- fileDelta.removedFiles,
452
- );
453
- debug(
454
- 'Finished mapping files (%d changes, %d removed).',
455
- fileDelta.changedFiles.size,
456
- fileDelta.removedFiles.size,
471
+ changeSize > 0,
457
472
  );
473
+ debug('Finished mapping files (%d changes).', changeSize);
458
474
 
459
475
  await this.#watch(fileSystem, watchmanClocks, plugins);
460
476
  return {fileSystem};
@@ -492,11 +508,7 @@ export default class FileMap extends EventEmitter {
492
508
  */
493
509
  async #buildFileDelta(
494
510
  previousState: CrawlerOptions['previousState'],
495
- ): Promise<{
496
- removedFiles: Set<CanonicalPath>,
497
- changedFiles: FileData,
498
- clocks?: WatchmanClocks,
499
- }> {
511
+ ): Promise<CrawlResult> {
500
512
  this.#startupPerfLogger?.point('buildFileDelta_start');
501
513
 
502
514
  const {
@@ -540,10 +552,9 @@ export default class FileMap extends EventEmitter {
540
552
 
541
553
  watcher.on('status', status => this.emit('status', status));
542
554
 
543
- return watcher.crawl().then(result => {
544
- this.#startupPerfLogger?.point('buildFileDelta_end');
545
- return result;
546
- });
555
+ const result = await watcher.crawl();
556
+ this.#startupPerfLogger?.point('buildFileDelta_end');
557
+ return result;
547
558
  }
548
559
 
549
560
  #maybeReadLink(normalPath: Path, fileMetadata: FileMetadata): ?Promise<void> {
@@ -568,25 +579,20 @@ export default class FileMap extends EventEmitter {
568
579
  removedFiles: ReadonlySet<CanonicalPath>,
569
580
  clocks?: WatchmanClocks,
570
581
  }>,
571
- ): Promise<void> {
582
+ ): Promise<FileSystemChangeAggregator> {
572
583
  this.#startupPerfLogger?.point('applyFileDelta_start');
573
584
  const {changedFiles, removedFiles} = delta;
574
585
  this.#startupPerfLogger?.point('applyFileDelta_preprocess_start');
575
- const missingFiles: Set<string> = new Set();
576
-
577
586
  // Remove files first so that we don't mistake moved modules
578
587
  // modules as duplicates.
579
588
  this.#startupPerfLogger?.point('applyFileDelta_remove_start');
580
- const removed: Array<[string, FileMetadata]> = [];
589
+ const changeAggregator = new FileSystemChangeAggregator();
581
590
  for (const relativeFilePath of removedFiles) {
582
- const metadata = fileSystem.remove(relativeFilePath);
583
- if (metadata) {
584
- removed.push([relativeFilePath, metadata]);
585
- }
591
+ fileSystem.remove(relativeFilePath, changeAggregator);
586
592
  }
587
593
  this.#startupPerfLogger?.point('applyFileDelta_remove_end');
588
594
 
589
- const readLinkPromises = [];
595
+ const readLinkPromises: Array<Promise<void>> = [];
590
596
  const readLinkErrors: Array<{
591
597
  normalFilePath: string,
592
598
  error: Error & {code?: string},
@@ -606,9 +612,9 @@ export default class FileMap extends EventEmitter {
606
612
  const maybeReadLink = this.#maybeReadLink(normalFilePath, fileData);
607
613
  if (maybeReadLink) {
608
614
  readLinkPromises.push(
609
- maybeReadLink.catch(error =>
610
- readLinkErrors.push({normalFilePath, error}),
611
- ),
615
+ maybeReadLink.catch(error => {
616
+ readLinkErrors.push({normalFilePath, error});
617
+ }),
612
618
  );
613
619
  }
614
620
  }
@@ -647,38 +653,32 @@ export default class FileMap extends EventEmitter {
647
653
  /* $FlowFixMe[incompatible-type] Error exposed after improved typing of
648
654
  * Array.{includes,indexOf,lastIndexOf} */
649
655
  if (['ENOENT', 'EACCESS'].includes(error.code)) {
650
- missingFiles.add(normalFilePath);
656
+ delta.changedFiles.delete(normalFilePath);
657
+ fileSystem.remove(normalFilePath, changeAggregator);
651
658
  } else {
652
659
  // Anything else is fatal.
653
660
  throw error;
654
661
  }
655
662
  }
656
- for (const relativeFilePath of missingFiles) {
657
- changedFiles.delete(relativeFilePath);
658
- const metadata = fileSystem.remove(relativeFilePath);
659
- if (metadata) {
660
- removed.push([relativeFilePath, metadata]);
661
- }
662
- }
663
+
663
664
  this.#startupPerfLogger?.point('applyFileDelta_missing_end');
664
665
 
665
666
  this.#startupPerfLogger?.point('applyFileDelta_add_start');
666
- fileSystem.bulkAddOrModify(changedFiles);
667
+ fileSystem.bulkAddOrModify(changedFiles, changeAggregator);
667
668
  this.#startupPerfLogger?.point('applyFileDelta_add_end');
668
669
 
669
670
  this.#startupPerfLogger?.point('applyFileDelta_updatePlugins_start');
670
- plugins.forEach(({plugin, dataIdx}) => {
671
- const mapFn: ([CanonicalPath, FileMetadata]) => [CanonicalPath, unknown] =
672
- dataIdx != null
673
- ? ([relativePath, fileData]) => [relativePath, fileData[dataIdx]]
674
- : ([relativePath, fileData]) => [relativePath, null];
675
- plugin.bulkUpdate({
676
- addedOrModified: mapIterator(changedFiles.entries(), mapFn),
677
- removed: mapIterator(removed.values(), mapFn),
678
- });
671
+ this.#plugins.forEach(({plugin, dataIdx}) => {
672
+ plugin.onChanged(
673
+ changeAggregator.getMappedView(
674
+ dataIdx != null ? metadata => metadata[dataIdx] : () => null,
675
+ ),
676
+ );
679
677
  });
680
678
  this.#startupPerfLogger?.point('applyFileDelta_updatePlugins_end');
681
679
  this.#startupPerfLogger?.point('applyFileDelta_end');
680
+
681
+ return changeAggregator;
682
682
  }
683
683
 
684
684
  /**
@@ -688,8 +688,7 @@ export default class FileMap extends EventEmitter {
688
688
  fileSystem: FileSystem,
689
689
  clocks: WatchmanClocks,
690
690
  plugins: ReadonlyArray<IndexedPlugin>,
691
- changed: FileData,
692
- removed: Set<CanonicalPath>,
691
+ changedSinceCacheRead: boolean,
693
692
  ) {
694
693
  this.#startupPerfLogger?.point('persist_start');
695
694
  await this.#cacheManager.write(
@@ -704,7 +703,7 @@ export default class FileMap extends EventEmitter {
704
703
  ),
705
704
  }),
706
705
  {
707
- changedSinceCacheRead: changed.size + removed.size > 0,
706
+ changedSinceCacheRead,
708
707
  eventSource: {
709
708
  onChange: cb => {
710
709
  // Inform the cache about changes to internal state, including:
@@ -743,20 +742,68 @@ export default class FileMap extends EventEmitter {
743
742
  const hasWatchedExtension = (filePath: string) =>
744
743
  this.#options.extensions.some(ext => filePath.endsWith(ext));
745
744
 
746
- let changeQueue: Promise<null | void> = Promise.resolve();
747
745
  let nextEmit: ?{
748
- eventsQueue: EventsQueue,
746
+ events: Array<InternalEnqueuedEvent>,
749
747
  firstEventTimestamp: number,
750
748
  firstEnqueuedTimestamp: number,
751
749
  } = null;
752
750
 
753
751
  const emitChange = () => {
754
- if (nextEmit == null || nextEmit.eventsQueue.length === 0) {
752
+ if (nextEmit == null) {
755
753
  // Nothing to emit
756
754
  return;
757
755
  }
758
- const {eventsQueue, firstEventTimestamp, firstEnqueuedTimestamp} =
759
- nextEmit;
756
+ const {events, firstEventTimestamp, firstEnqueuedTimestamp} = nextEmit;
757
+
758
+ const changeAggregator = new FileSystemChangeAggregator();
759
+
760
+ // Process a sequence of events. Note that preserving ordering is
761
+ // important here - a file may be both removed and added in the same
762
+ // batch.
763
+ // `changeAggregator` flattens this over time into the net change from
764
+ // this sequence.
765
+ for (const event of events) {
766
+ const {relativeFilePath, clock} = event;
767
+ if (event.type === 'delete') {
768
+ fileSystem.remove(relativeFilePath, changeAggregator);
769
+ } else {
770
+ fileSystem.addOrModify(
771
+ relativeFilePath,
772
+ event.metadata,
773
+ changeAggregator,
774
+ );
775
+ }
776
+ this.#updateClock(clocks, clock);
777
+ }
778
+
779
+ const changeSize = changeAggregator.getSize();
780
+
781
+ if (changeSize === 0) {
782
+ // We had events, but they've exactly cancelled each other out, reset
783
+ // so that timers are correct for the next change.
784
+ nextEmit = null;
785
+ return;
786
+ }
787
+
788
+ const _netChange = changeAggregator.getView();
789
+ this.#plugins.forEach(({plugin, dataIdx}) => {
790
+ plugin.onChanged(
791
+ changeAggregator.getMappedView(
792
+ dataIdx != null ? metadata => metadata[dataIdx] : () => null,
793
+ ),
794
+ );
795
+ });
796
+
797
+ const toPublicMetadata = (
798
+ metadata: Readonly<FileMetadata>,
799
+ ): ChangedFileMetadata => ({
800
+ isSymlink: metadata[H.SYMLINK] !== 0,
801
+ modifiedTime: metadata[H.MTIME] ?? null,
802
+ });
803
+
804
+ const changesWithMetadata =
805
+ changeAggregator.getMappedView(toPublicMetadata);
806
+
760
807
  const hmrPerfLogger = this.#options.perfLoggerFactory?.('HMR', {
761
808
  key: this.#getNextChangeID(),
762
809
  });
@@ -766,21 +813,24 @@ export default class FileMap extends EventEmitter {
766
813
  timestamp: firstEnqueuedTimestamp,
767
814
  });
768
815
  hmrPerfLogger.point('waitingForChangeInterval_end');
769
- hmrPerfLogger.annotate({
770
- int: {eventsQueueLength: eventsQueue.length},
771
- });
816
+ hmrPerfLogger.annotate({int: {changeSize}});
772
817
  hmrPerfLogger.point('fileChange_start');
773
818
  }
774
819
  const changeEvent: ChangeEvent = {
775
- eventsQueue,
820
+ changes: changesWithMetadata,
776
821
  logger: hmrPerfLogger,
822
+ rootDir: this.#options.rootDir,
777
823
  };
778
824
  this.emit('change', changeEvent);
779
825
  nextEmit = null;
780
826
  };
781
827
 
828
+ let changeQueue: Promise<null | void> = Promise.resolve();
829
+
782
830
  const onChange = (change: WatcherBackendChangeEvent) => {
831
+ // Recrawl events bypass normal filtering - they trigger a full subdirectory scan
783
832
  if (
833
+ change.event !== 'recrawl' &&
784
834
  change.metadata &&
785
835
  // Ignore all directory events
786
836
  (change.metadata.type === 'd' ||
@@ -806,73 +856,38 @@ export default class FileMap extends EventEmitter {
806
856
 
807
857
  const relativeFilePath =
808
858
  this.#pathUtils.absoluteToNormal(absoluteFilePath);
809
- const linkStats = fileSystem.linkStats(relativeFilePath);
810
-
811
- // The file has been accessed, not modified. If the modified time is
812
- // null, then it is assumed that the watcher does not have capabilities
813
- // to detect modified time, and change processing proceeds.
814
- if (
815
- change.event === 'touch' &&
816
- linkStats != null &&
817
- change.metadata.modifiedTime != null &&
818
- linkStats.modifiedTime === change.metadata.modifiedTime
819
- ) {
820
- return;
821
- }
822
-
823
- // Emitted events, unlike memoryless backend events, specify 'add' or
824
- // 'change' instead of 'touch'.
825
- const eventTypeToEmit =
826
- change.event === 'touch'
827
- ? linkStats == null
828
- ? 'add'
829
- : 'change'
830
- : 'delete';
831
859
 
832
860
  const onChangeStartTime = performance.timeOrigin + performance.now();
833
861
 
862
+ const enqueueEvent = (event: InternalEnqueuedEvent) => {
863
+ nextEmit ??= {
864
+ events: [],
865
+ firstEnqueuedTimestamp: performance.timeOrigin + performance.now(),
866
+ firstEventTimestamp: onChangeStartTime,
867
+ };
868
+ nextEmit.events.push(event);
869
+ };
870
+
834
871
  changeQueue = changeQueue
835
872
  .then(async () => {
836
873
  // If we get duplicate events for the same file, ignore them.
837
874
  if (
838
875
  nextEmit != null &&
839
- nextEmit.eventsQueue.find(
876
+ nextEmit.events.find(
840
877
  event =>
841
- event.type === eventTypeToEmit &&
842
- event.filePath === absoluteFilePath &&
878
+ event.type === change.event &&
879
+ event.relativeFilePath === relativeFilePath &&
843
880
  ((!event.metadata && !change.metadata) ||
844
881
  (event.metadata &&
845
882
  change.metadata &&
846
- event.metadata.modifiedTime != null &&
883
+ event.metadata[H.MTIME] != null &&
847
884
  change.metadata.modifiedTime != null &&
848
- event.metadata.modifiedTime ===
849
- change.metadata.modifiedTime)),
885
+ event.metadata[H.MTIME] === change.metadata.modifiedTime)),
850
886
  )
851
887
  ) {
852
888
  return null;
853
889
  }
854
890
 
855
- const linkStats = fileSystem.linkStats(relativeFilePath);
856
-
857
- const enqueueEvent = (metadata: ChangeEventMetadata) => {
858
- const event = {
859
- filePath: absoluteFilePath,
860
- metadata,
861
- type: eventTypeToEmit,
862
- };
863
- if (nextEmit == null) {
864
- nextEmit = {
865
- eventsQueue: [event],
866
- firstEnqueuedTimestamp:
867
- performance.timeOrigin + performance.now(),
868
- firstEventTimestamp: onChangeStartTime,
869
- };
870
- } else {
871
- nextEmit.eventsQueue.push(event);
872
- }
873
- return null;
874
- };
875
-
876
891
  // If the file was added or modified,
877
892
  // parse it and update the file map.
878
893
  if (change.event === 'touch') {
@@ -902,17 +917,12 @@ export default class FileMap extends EventEmitter {
902
917
  },
903
918
  );
904
919
  }
905
- fileSystem.addOrModify(relativeFilePath, fileMetadata);
906
- this.#updateClock(clocks, change.clock);
907
- plugins.forEach(({plugin, dataIdx}) =>
908
- dataIdx != null
909
- ? plugin.onNewOrModifiedFile(
910
- relativeFilePath,
911
- fileMetadata[dataIdx],
912
- )
913
- : plugin.onNewOrModifiedFile(relativeFilePath),
914
- );
915
- enqueueEvent(change.metadata);
920
+ enqueueEvent({
921
+ clock: change.clock,
922
+ relativeFilePath,
923
+ metadata: fileMetadata,
924
+ type: change.event,
925
+ });
916
926
  } catch (e) {
917
927
  if (!['ENOENT', 'EACCESS'].includes(e.code)) {
918
928
  throw e;
@@ -925,26 +935,68 @@ export default class FileMap extends EventEmitter {
925
935
  // point.
926
936
  }
927
937
  } else if (change.event === 'delete') {
928
- if (linkStats == null) {
929
- // Don't emit deletion events for files we weren't retaining.
930
- // This is expected for deletion of an ignored file.
938
+ enqueueEvent({
939
+ clock: change.clock,
940
+ relativeFilePath,
941
+ type: 'delete',
942
+ });
943
+ } else if (change.event === 'recrawl') {
944
+ // Recrawl event: flush pending changes and re-crawl the directory
945
+ emitChange();
946
+
947
+ // The relativePath is relative to the watcher root (change.root),
948
+ // but we need a path relative to rootDir for the recrawl.
949
+ const absoluteDirPath = path.join(
950
+ change.root,
951
+ normalizePathSeparatorsToSystem(change.relativePath),
952
+ );
953
+ const subpath = this.#pathUtils.absoluteToNormal(absoluteDirPath);
954
+
955
+ // Crawl the specific subdirectory
956
+ const watcher = this.#watcher;
957
+ invariant(watcher != null, 'Watcher must be initialized');
958
+ const crawlResult = await watcher.recrawl(subpath, fileSystem);
959
+
960
+ // Skip if no changes
961
+ if (
962
+ crawlResult.changedFiles.size === 0 &&
963
+ crawlResult.removedFiles.size === 0
964
+ ) {
931
965
  return null;
932
966
  }
933
- // We've already checked linkStats != null above, so the file
934
- // exists in the file map and remove should always return metadata.
935
- const metadata = nullthrows(fileSystem.remove(relativeFilePath));
936
- this.#updateClock(clocks, change.clock);
937
- plugins.forEach(({plugin, dataIdx}) =>
938
- dataIdx != null
939
- ? plugin.onRemovedFile(relativeFilePath, metadata[dataIdx])
940
- : plugin.onRemovedFile(relativeFilePath),
967
+
968
+ // Reuse the same batch processing logic as build()
969
+ const recrawlChangeAggregator = await this.#applyFileDelta(
970
+ fileSystem,
971
+ this.#plugins,
972
+ crawlResult,
941
973
  );
942
974
 
943
- enqueueEvent({
944
- modifiedTime: null,
945
- size: null,
946
- type: linkStats.fileType,
975
+ // Update clock if provided
976
+ this.#updateClock(clocks, change.clock);
977
+
978
+ // Skip emit if no changes after processing
979
+ if (recrawlChangeAggregator.getSize() === 0) {
980
+ return null;
981
+ }
982
+
983
+ // Emit changes directly
984
+ const toPublicMetadata = (
985
+ metadata: Readonly<FileMetadata>,
986
+ ): ChangedFileMetadata => ({
987
+ isSymlink: metadata[H.SYMLINK] !== 0,
988
+ modifiedTime: metadata[H.MTIME] ?? null,
947
989
  });
990
+
991
+ const changesWithMetadata =
992
+ recrawlChangeAggregator.getMappedView(toPublicMetadata);
993
+
994
+ const changeEvent: ChangeEvent = {
995
+ changes: changesWithMetadata,
996
+ logger: null,
997
+ rootDir: this.#options.rootDir,
998
+ };
999
+ this.emit('change', changeEvent);
948
1000
  } else {
949
1001
  throw new Error(
950
1002
  `metro-file-map: Unrecognized event type from watcher: ${change.event}`,
@@ -1055,11 +1107,9 @@ export default class FileMap extends EventEmitter {
1055
1107
  }
1056
1108
 
1057
1109
  // TODO: Replace with it.map() from Node 22+
1058
- const mapIterator: <T, S>(Iterator<T>, (T) => S) => Iterable<S> = (it, fn) =>
1059
- 'map' in it
1060
- ? it.map(fn)
1061
- : (function* mapped() {
1062
- for (const item of it) {
1063
- yield fn(item);
1064
- }
1065
- })();
1110
+ const mapIterable: <T, S>(Iterable<T>, (T) => S) => Iterator<S> = (it, fn) =>
1111
+ (function* mapped() {
1112
+ for (const item of it) {
1113
+ yield fn(item);
1114
+ }
1115
+ })();
@@ -0,0 +1,40 @@
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
+ * @noformat
8
+ * @oncall react_native
9
+ * @generated SignedSource<<5feda1b197530a9a5fdbc57200633ac5>>
10
+ *
11
+ * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
12
+ * Original file: packages/metro-file-map/src/lib/FileSystemChangeAggregator.js
13
+ * To regenerate, run:
14
+ * js1 build metro-ts-defs (internal) OR
15
+ * yarn run build-ts-defs (OSS)
16
+ */
17
+
18
+ import type {
19
+ CanonicalPath,
20
+ FileMetadata,
21
+ FileSystemListener,
22
+ ReadonlyFileSystemChanges,
23
+ } from '../flow-types';
24
+
25
+ export declare class FileSystemChangeAggregator implements FileSystemListener {
26
+ directoryAdded(canonicalPath: CanonicalPath): void;
27
+ directoryRemoved(canonicalPath: CanonicalPath): void;
28
+ fileAdded(canonicalPath: CanonicalPath, data: FileMetadata): void;
29
+ fileModified(
30
+ canonicalPath: CanonicalPath,
31
+ oldData: FileMetadata,
32
+ newData: FileMetadata,
33
+ ): void;
34
+ fileRemoved(canonicalPath: CanonicalPath, data: FileMetadata): void;
35
+ getSize(): number;
36
+ getView(): ReadonlyFileSystemChanges<FileMetadata>;
37
+ getMappedView<T>(
38
+ metadataMapFn: (metadata: FileMetadata) => T,
39
+ ): ReadonlyFileSystemChanges<T>;
40
+ }