@@ -16,10 +16,10 @@ const { TIMEOUT_MAX } = require('internal/timers');
16
16
17
17
const EventEmitter = require ( 'events' ) ;
18
18
const { addAbortListener } = require ( 'internal/events/abort_listener' ) ;
19
- const { watch } = require ( 'fs' ) ;
19
+ const { watch, existsSync } = require ( 'fs' ) ;
20
20
const { fileURLToPath } = require ( 'internal/url' ) ;
21
21
const { resolve, dirname } = require ( 'path' ) ;
22
- const { setTimeout } = require ( 'timers' ) ;
22
+ const { setTimeout, clearTimeout , setInterval , clearInterval } = require ( 'timers' ) ;
23
23
24
24
const supportsRecursiveWatching = process . platform === 'win32' ||
25
25
process . platform === 'darwin' ;
@@ -31,18 +31,28 @@ class FilesWatcher extends EventEmitter {
31
31
#depencencyOwners = new SafeMap ( ) ;
32
32
#ownerDependencies = new SafeMap ( ) ;
33
33
#debounce;
34
+ #renameInterval;
35
+ #renameTimeout;
34
36
#mode;
35
37
#signal;
36
38
#passthroughIPC = false ;
37
39
#ipcHandlers = new SafeWeakMap ( ) ;
38
40
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 ) {
40
48
super ( { __proto__ : null , captureRejections : true } ) ;
41
49
42
50
validateNumber ( debounce , 'options.debounce' , 0 , TIMEOUT_MAX ) ;
43
51
validateOneOf ( mode , 'options.mode' , [ 'filter' , 'all' ] ) ;
44
52
this . #debounce = debounce ;
45
53
this . #mode = mode ;
54
+ this . #renameInterval = renameInterval ;
55
+ this . #renameTimeout = renameTimeout ;
46
56
this . #signal = signal ;
47
57
this . #passthroughIPC = Boolean ( process . send ) ;
48
58
@@ -79,7 +89,10 @@ class FilesWatcher extends EventEmitter {
79
89
watcher . handle . close ( ) ;
80
90
}
81
91
82
- #onChange( trigger ) {
92
+ #onChange( eventType , trigger , recursive ) {
93
+ if ( eventType === 'rename' && ! recursive ) {
94
+ return this . #rewatch( trigger ) ;
95
+ }
83
96
if ( this . #debouncing. has ( trigger ) ) {
84
97
return ;
85
98
}
@@ -94,6 +107,39 @@ class FilesWatcher extends EventEmitter {
94
107
} , this . #debounce) . unref ( ) ;
95
108
}
96
109
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
+
97
143
get watchedPaths ( ) {
98
144
return [ ...this . #watchers. keys ( ) ] ;
99
145
}
@@ -106,7 +152,7 @@ class FilesWatcher extends EventEmitter {
106
152
watcher . on ( 'change' , ( eventType , fileName ) => {
107
153
// `fileName` can be `null` if it cannot be determined. See
108
154
// 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 ) ;
110
156
} ) ;
111
157
this . #watchers. set ( path , { handle : watcher , recursive } ) ;
112
158
if ( recursive ) {
0 commit comments