metro-file-map 0.83.4 → 0.83.6

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 (70) hide show
  1. package/package.json +3 -2
  2. package/src/Watcher.d.ts +74 -0
  3. package/src/Watcher.js +68 -48
  4. package/src/Watcher.js.flow +84 -51
  5. package/src/cache/DiskCacheManager.d.ts +49 -0
  6. package/src/cache/DiskCacheManager.js +1 -5
  7. package/src/constants.d.ts +22 -0
  8. package/src/crawlers/node/hasNativeFindSupport.d.ts +19 -0
  9. package/src/crawlers/node/index.d.ts +21 -0
  10. package/src/crawlers/node/index.js +6 -10
  11. package/src/crawlers/node/index.js.flow +8 -6
  12. package/src/crawlers/watchman/index.d.ts +23 -0
  13. package/src/crawlers/watchman/index.js +2 -9
  14. package/src/crawlers/watchman/index.js.flow +2 -6
  15. package/src/flow-types.d.ts +460 -0
  16. package/src/flow-types.js.flow +89 -29
  17. package/src/index.d.ts +182 -0
  18. package/src/index.js +148 -132
  19. package/src/index.js.flow +200 -155
  20. package/src/lib/FileProcessor.d.ts +60 -0
  21. package/src/lib/FileProcessor.js +1 -5
  22. package/src/lib/FileSystemChangeAggregator.d.ts +40 -0
  23. package/src/lib/FileSystemChangeAggregator.js +89 -0
  24. package/src/lib/FileSystemChangeAggregator.js.flow +143 -0
  25. package/src/lib/RootPathUtils.d.ts +30 -0
  26. package/src/lib/RootPathUtils.js +2 -9
  27. package/src/lib/TreeFS.d.ts +174 -0
  28. package/src/lib/TreeFS.js +68 -21
  29. package/src/lib/TreeFS.js.flow +89 -16
  30. package/src/lib/checkWatchmanCapabilities.d.ts +20 -0
  31. package/src/lib/normalizePathSeparatorsToPosix.d.ts +20 -0
  32. package/src/lib/normalizePathSeparatorsToPosix.js +1 -4
  33. package/src/lib/normalizePathSeparatorsToSystem.d.ts +20 -0
  34. package/src/lib/normalizePathSeparatorsToSystem.js +1 -4
  35. package/src/lib/rootRelativeCacheKeys.d.ts +24 -0
  36. package/src/lib/rootRelativeCacheKeys.js +1 -5
  37. package/src/lib/sorting.d.ts +23 -0
  38. package/src/plugins/DependencyPlugin.d.ts +52 -0
  39. package/src/plugins/DependencyPlugin.js +1 -3
  40. package/src/plugins/DependencyPlugin.js.flow +1 -16
  41. package/src/plugins/HastePlugin.d.ts +69 -0
  42. package/src/plugins/HastePlugin.js +12 -16
  43. package/src/plugins/HastePlugin.js.flow +12 -12
  44. package/src/plugins/MockPlugin.d.ts +48 -0
  45. package/src/plugins/MockPlugin.js +18 -25
  46. package/src/plugins/MockPlugin.js.flow +18 -22
  47. package/src/plugins/dependencies/dependencyExtractor.d.ts +1 -1
  48. package/src/plugins/haste/DuplicateHasteCandidatesError.d.ts +31 -0
  49. package/src/plugins/haste/DuplicateHasteCandidatesError.js +1 -5
  50. package/src/plugins/haste/HasteConflictsError.d.ts +23 -0
  51. package/src/plugins/haste/HasteConflictsError.js +1 -5
  52. package/src/plugins/haste/computeConflicts.d.ts +34 -0
  53. package/src/plugins/haste/computeConflicts.js +1 -5
  54. package/src/plugins/haste/getPlatformExtension.d.ts +21 -0
  55. package/src/plugins/mocks/getMockName.d.ts +20 -0
  56. package/src/plugins/mocks/getMockName.js +1 -4
  57. package/src/watchers/AbstractWatcher.d.ts +41 -0
  58. package/src/watchers/AbstractWatcher.js +2 -9
  59. package/src/watchers/FallbackWatcher.d.ts +28 -0
  60. package/src/watchers/FallbackWatcher.js +21 -12
  61. package/src/watchers/FallbackWatcher.js.flow +28 -5
  62. package/src/watchers/NativeWatcher.d.ts +55 -0
  63. package/src/watchers/NativeWatcher.js +28 -9
  64. package/src/watchers/NativeWatcher.js.flow +33 -6
  65. package/src/watchers/RecrawlWarning.d.ts +32 -0
  66. package/src/watchers/WatchmanWatcher.d.ts +34 -0
  67. package/src/watchers/WatchmanWatcher.js +2 -9
  68. package/src/watchers/common.d.ts +70 -0
  69. package/src/watchers/common.js +7 -6
  70. 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,43 +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
-
671
- await Promise.all([
672
- plugins.map(({plugin, dataIdx}) => {
673
- const mapFn: (
674
- [CanonicalPath, FileMetadata],
675
- ) => [CanonicalPath, unknown] =
676
- dataIdx != null
677
- ? ([relativePath, fileData]) => [relativePath, fileData[dataIdx]]
678
- : ([relativePath, fileData]) => [relativePath, null];
679
- return plugin.bulkUpdate({
680
- addedOrModified: mapIterator(changedFiles.entries(), mapFn),
681
- removed: mapIterator(removed.values(), mapFn),
682
- });
683
- }),
684
- ]);
671
+ this.#plugins.forEach(({plugin, dataIdx}) => {
672
+ plugin.onChanged(
673
+ changeAggregator.getMappedView(
674
+ dataIdx != null ? metadata => metadata[dataIdx] : () => null,
675
+ ),
676
+ );
677
+ });
685
678
  this.#startupPerfLogger?.point('applyFileDelta_updatePlugins_end');
686
679
  this.#startupPerfLogger?.point('applyFileDelta_end');
680
+
681
+ return changeAggregator;
687
682
  }
688
683
 
689
684
  /**
@@ -693,8 +688,7 @@ export default class FileMap extends EventEmitter {
693
688
  fileSystem: FileSystem,
694
689
  clocks: WatchmanClocks,
695
690
  plugins: ReadonlyArray<IndexedPlugin>,
696
- changed: FileData,
697
- removed: Set<CanonicalPath>,
691
+ changedSinceCacheRead: boolean,
698
692
  ) {
699
693
  this.#startupPerfLogger?.point('persist_start');
700
694
  await this.#cacheManager.write(
@@ -709,7 +703,7 @@ export default class FileMap extends EventEmitter {
709
703
  ),
710
704
  }),
711
705
  {
712
- changedSinceCacheRead: changed.size + removed.size > 0,
706
+ changedSinceCacheRead,
713
707
  eventSource: {
714
708
  onChange: cb => {
715
709
  // Inform the cache about changes to internal state, including:
@@ -748,20 +742,68 @@ export default class FileMap extends EventEmitter {
748
742
  const hasWatchedExtension = (filePath: string) =>
749
743
  this.#options.extensions.some(ext => filePath.endsWith(ext));
750
744
 
751
- let changeQueue: Promise<null | void> = Promise.resolve();
752
745
  let nextEmit: ?{
753
- eventsQueue: EventsQueue,
746
+ events: Array<InternalEnqueuedEvent>,
754
747
  firstEventTimestamp: number,
755
748
  firstEnqueuedTimestamp: number,
756
749
  } = null;
757
750
 
758
751
  const emitChange = () => {
759
- if (nextEmit == null || nextEmit.eventsQueue.length === 0) {
752
+ if (nextEmit == null) {
760
753
  // Nothing to emit
761
754
  return;
762
755
  }
763
- const {eventsQueue, firstEventTimestamp, firstEnqueuedTimestamp} =
764
- 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
+
765
807
  const hmrPerfLogger = this.#options.perfLoggerFactory?.('HMR', {
766
808
  key: this.#getNextChangeID(),
767
809
  });
@@ -771,21 +813,24 @@ export default class FileMap extends EventEmitter {
771
813
  timestamp: firstEnqueuedTimestamp,
772
814
  });
773
815
  hmrPerfLogger.point('waitingForChangeInterval_end');
774
- hmrPerfLogger.annotate({
775
- int: {eventsQueueLength: eventsQueue.length},
776
- });
816
+ hmrPerfLogger.annotate({int: {changeSize}});
777
817
  hmrPerfLogger.point('fileChange_start');
778
818
  }
779
819
  const changeEvent: ChangeEvent = {
780
- eventsQueue,
820
+ changes: changesWithMetadata,
781
821
  logger: hmrPerfLogger,
822
+ rootDir: this.#options.rootDir,
782
823
  };
783
824
  this.emit('change', changeEvent);
784
825
  nextEmit = null;
785
826
  };
786
827
 
828
+ let changeQueue: Promise<null | void> = Promise.resolve();
829
+
787
830
  const onChange = (change: WatcherBackendChangeEvent) => {
831
+ // Recrawl events bypass normal filtering - they trigger a full subdirectory scan
788
832
  if (
833
+ change.event !== 'recrawl' &&
789
834
  change.metadata &&
790
835
  // Ignore all directory events
791
836
  (change.metadata.type === 'd' ||
@@ -811,73 +856,38 @@ export default class FileMap extends EventEmitter {
811
856
 
812
857
  const relativeFilePath =
813
858
  this.#pathUtils.absoluteToNormal(absoluteFilePath);
814
- const linkStats = fileSystem.linkStats(relativeFilePath);
815
-
816
- // The file has been accessed, not modified. If the modified time is
817
- // null, then it is assumed that the watcher does not have capabilities
818
- // to detect modified time, and change processing proceeds.
819
- if (
820
- change.event === 'touch' &&
821
- linkStats != null &&
822
- change.metadata.modifiedTime != null &&
823
- linkStats.modifiedTime === change.metadata.modifiedTime
824
- ) {
825
- return;
826
- }
827
-
828
- // Emitted events, unlike memoryless backend events, specify 'add' or
829
- // 'change' instead of 'touch'.
830
- const eventTypeToEmit =
831
- change.event === 'touch'
832
- ? linkStats == null
833
- ? 'add'
834
- : 'change'
835
- : 'delete';
836
859
 
837
860
  const onChangeStartTime = performance.timeOrigin + performance.now();
838
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
+
839
871
  changeQueue = changeQueue
840
872
  .then(async () => {
841
873
  // If we get duplicate events for the same file, ignore them.
842
874
  if (
843
875
  nextEmit != null &&
844
- nextEmit.eventsQueue.find(
876
+ nextEmit.events.find(
845
877
  event =>
846
- event.type === eventTypeToEmit &&
847
- event.filePath === absoluteFilePath &&
878
+ event.type === change.event &&
879
+ event.relativeFilePath === relativeFilePath &&
848
880
  ((!event.metadata && !change.metadata) ||
849
881
  (event.metadata &&
850
882
  change.metadata &&
851
- event.metadata.modifiedTime != null &&
883
+ event.metadata[H.MTIME] != null &&
852
884
  change.metadata.modifiedTime != null &&
853
- event.metadata.modifiedTime ===
854
- change.metadata.modifiedTime)),
885
+ event.metadata[H.MTIME] === change.metadata.modifiedTime)),
855
886
  )
856
887
  ) {
857
888
  return null;
858
889
  }
859
890
 
860
- const linkStats = fileSystem.linkStats(relativeFilePath);
861
-
862
- const enqueueEvent = (metadata: ChangeEventMetadata) => {
863
- const event = {
864
- filePath: absoluteFilePath,
865
- metadata,
866
- type: eventTypeToEmit,
867
- };
868
- if (nextEmit == null) {
869
- nextEmit = {
870
- eventsQueue: [event],
871
- firstEnqueuedTimestamp:
872
- performance.timeOrigin + performance.now(),
873
- firstEventTimestamp: onChangeStartTime,
874
- };
875
- } else {
876
- nextEmit.eventsQueue.push(event);
877
- }
878
- return null;
879
- };
880
-
881
891
  // If the file was added or modified,
882
892
  // parse it and update the file map.
883
893
  if (change.event === 'touch') {
@@ -907,17 +917,12 @@ export default class FileMap extends EventEmitter {
907
917
  },
908
918
  );
909
919
  }
910
- fileSystem.addOrModify(relativeFilePath, fileMetadata);
911
- this.#updateClock(clocks, change.clock);
912
- plugins.forEach(({plugin, dataIdx}) =>
913
- dataIdx != null
914
- ? plugin.onNewOrModifiedFile(
915
- relativeFilePath,
916
- fileMetadata[dataIdx],
917
- )
918
- : plugin.onNewOrModifiedFile(relativeFilePath),
919
- );
920
- enqueueEvent(change.metadata);
920
+ enqueueEvent({
921
+ clock: change.clock,
922
+ relativeFilePath,
923
+ metadata: fileMetadata,
924
+ type: change.event,
925
+ });
921
926
  } catch (e) {
922
927
  if (!['ENOENT', 'EACCESS'].includes(e.code)) {
923
928
  throw e;
@@ -930,26 +935,68 @@ export default class FileMap extends EventEmitter {
930
935
  // point.
931
936
  }
932
937
  } else if (change.event === 'delete') {
933
- if (linkStats == null) {
934
- // Don't emit deletion events for files we weren't retaining.
935
- // 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
+ ) {
936
965
  return null;
937
966
  }
938
- // We've already checked linkStats != null above, so the file
939
- // exists in the file map and remove should always return metadata.
940
- const metadata = nullthrows(fileSystem.remove(relativeFilePath));
941
- this.#updateClock(clocks, change.clock);
942
- plugins.forEach(({plugin, dataIdx}) =>
943
- dataIdx != null
944
- ? plugin.onRemovedFile(relativeFilePath, metadata[dataIdx])
945
- : 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,
946
973
  );
947
974
 
948
- enqueueEvent({
949
- modifiedTime: null,
950
- size: null,
951
- 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,
952
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);
953
1000
  } else {
954
1001
  throw new Error(
955
1002
  `metro-file-map: Unrecognized event type from watcher: ${change.event}`,
@@ -1060,11 +1107,9 @@ export default class FileMap extends EventEmitter {
1060
1107
  }
1061
1108
 
1062
1109
  // TODO: Replace with it.map() from Node 22+
1063
- const mapIterator: <T, S>(Iterator<T>, (T) => S) => Iterable<S> = (it, fn) =>
1064
- 'map' in it
1065
- ? it.map(fn)
1066
- : (function* mapped() {
1067
- for (const item of it) {
1068
- yield fn(item);
1069
- }
1070
- })();
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,60 @@
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<<2ea213f753eef5de14cb8a27f68b9fa2>>
10
+ *
11
+ * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js
12
+ * Original file: packages/metro-file-map/src/lib/FileProcessor.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
+ FileMapPluginWorker,
20
+ FileMetadata,
21
+ PerfLogger,
22
+ } from '../flow-types';
23
+
24
+ type ProcessFileRequest = Readonly<{
25
+ /**
26
+ * Populate metadata[H.SHA1] with the SHA1 of the file's contents.
27
+ */
28
+ computeSha1: boolean;
29
+ /**
30
+ * Only if processing has already required reading the file's contents, return
31
+ * the contents as a Buffer - null otherwise. Not supported for batches.
32
+ */
33
+ maybeReturnContent: boolean;
34
+ }>;
35
+ interface MaybeCodedError extends Error {
36
+ code?: string;
37
+ }
38
+ export declare class FileProcessor {
39
+ constructor(
40
+ opts: Readonly<{
41
+ maxFilesPerWorker?: null | undefined | number;
42
+ maxWorkers: number;
43
+ pluginWorkers: null | undefined | ReadonlyArray<FileMapPluginWorker>;
44
+ perfLogger: null | undefined | PerfLogger;
45
+ rootDir: string;
46
+ }>,
47
+ );
48
+ processBatch(
49
+ files: ReadonlyArray<[string, FileMetadata]>,
50
+ req: ProcessFileRequest,
51
+ ): Promise<{
52
+ errors: Array<{normalFilePath: string; error: MaybeCodedError}>;
53
+ }>;
54
+ processRegularFile(
55
+ normalPath: string,
56
+ fileMetadata: FileMetadata,
57
+ req: ProcessFileRequest,
58
+ ): null | undefined | {content: null | undefined | Buffer};
59
+ end(): Promise<void>;
60
+ }
@@ -10,11 +10,7 @@ var _RootPathUtils = require("./RootPathUtils");
10
10
  var _jestWorker = require("jest-worker");
11
11
  var _path = require("path");
12
12
  function _interopRequireDefault(e) {
13
- return e && e.__esModule
14
- ? e
15
- : {
16
- default: e,
17
- };
13
+ return e && e.__esModule ? e : { default: e };
18
14
  }
19
15
  const debug = require("debug")("Metro:FileMap");
20
16
  const NODE_MODULES_SEP = "node_modules" + _path.sep;