@@ -16,10 +16,10 @@ const { TIMEOUT_MAX } = require('internal/timers');
1616
1717const EventEmitter = require ( 'events' ) ;
1818const { addAbortListener } = require ( 'internal/events/abort_listener' ) ;
19- const { watch } = require ( 'fs' ) ;
19+ const { watch, existsSync } = require ( 'fs' ) ;
2020const { fileURLToPath } = require ( 'internal/url' ) ;
2121const { resolve, dirname } = require ( 'path' ) ;
22- const { setTimeout } = require ( 'timers' ) ;
22+ const { setTimeout, clearTimeout , setInterval , clearInterval } = require ( 'timers' ) ;
2323
2424const supportsRecursiveWatching = process . platform === 'win32' ||
2525 process . platform === 'darwin' ;
@@ -31,18 +31,28 @@ class FilesWatcher extends EventEmitter {
3131 #depencencyOwners = new SafeMap ( ) ;
3232 #ownerDependencies = new SafeMap ( ) ;
3333 #debounce;
34+ #renameInterval;
35+ #renameTimeout;
3436 #mode;
3537 #signal;
3638 #passthroughIPC = false ;
3739 #ipcHandlers = new SafeWeakMap ( ) ;
3840
39- constructor ( { debounce = 200 , mode = 'filter' , signal } = kEmptyObject ) {
41+ constructor ( {
42+ debounce = 200 ,
43+ mode = 'filter' ,
44+ renameInterval = 1000 ,
45+ renameTimeout = 60_000 ,
46+ signal,
47+ } = kEmptyObject ) {
4048 super ( { __proto__ : null , captureRejections : true } ) ;
4149
4250 validateNumber ( debounce , 'options.debounce' , 0 , TIMEOUT_MAX ) ;
4351 validateOneOf ( mode , 'options.mode' , [ 'filter' , 'all' ] ) ;
4452 this . #debounce = debounce ;
4553 this . #mode = mode ;
54+ this . #renameInterval = renameInterval ;
55+ this . #renameTimeout = renameTimeout ;
4656 this . #signal = signal ;
4757 this . #passthroughIPC = Boolean ( process . send ) ;
4858
@@ -79,7 +89,10 @@ class FilesWatcher extends EventEmitter {
7989 watcher . handle . close ( ) ;
8090 }
8191
82- #onChange( trigger ) {
92+ #onChange( eventType , trigger , recursive ) {
93+ if ( eventType === 'rename' && ! recursive ) {
94+ return this . #rewatch( trigger ) ;
95+ }
8396 if ( this . #debouncing. has ( trigger ) ) {
8497 return ;
8598 }
@@ -94,6 +107,39 @@ class FilesWatcher extends EventEmitter {
94107 } , this . #debounce) . unref ( ) ;
95108 }
96109
110+ // When a file is removed, wait for it to be re-added.
111+ // Often this re-add is immediate - some editors (e.g., gedit) and some docker mount modes do this.
112+ #rewatch( path ) {
113+ if ( this . #isPathWatched( path ) ) {
114+ this . #unwatch( this . #watchers. get ( path ) ) ;
115+ this . #watchers. delete ( path ) ;
116+ if ( existsSync ( path ) ) {
117+ this . watchPath ( path , false ) ;
118+ // This might be redundant. If the file was re-added due to a save event, we will probably see change -> rename.
119+ // However, in certain situations it's entirely possible for the content to have changed after the rename
120+ // In these situations we'd miss the change after the rename event
121+ this . #onChange( 'change' , path , false ) ;
122+ return ;
123+ }
124+ let timeout ;
125+
126+ // Wait for the file to exist - check every `renameInterval` ms
127+ const interval = setInterval ( async ( ) => {
128+ if ( existsSync ( path ) ) {
129+ clearInterval ( interval ) ;
130+ clearTimeout ( timeout ) ;
131+ this . watchPath ( path , false ) ;
132+ this . #onChange( 'change' , path , false ) ;
133+ }
134+ } , this . #renameInterval) . unref ( ) ;
135+
136+ // Don't wait forever - after `renameTimeout` ms, stop trying
137+ timeout = setTimeout ( ( ) => {
138+ clearInterval ( interval ) ;
139+ } , this . #renameTimeout) . unref ( ) ;
140+ }
141+ }
142+
97143 get watchedPaths ( ) {
98144 return [ ...this . #watchers. keys ( ) ] ;
99145 }
@@ -106,7 +152,7 @@ class FilesWatcher extends EventEmitter {
106152 watcher . on ( 'change' , ( eventType , fileName ) => {
107153 // `fileName` can be `null` if it cannot be determined. See
108154 // https://github.com/nodejs/node/pull/49891#issuecomment-1744673430.
109- this . #onChange( recursive ? resolve ( path , fileName ?? '' ) : path ) ;
155+ this . #onChange( eventType , recursive ? resolve ( path , fileName ) : path , recursive ) ;
110156 } ) ;
111157 this . #watchers. set ( path , { handle : watcher , recursive } ) ;
112158 if ( recursive ) {
0 commit comments