@@ -8,7 +8,8 @@ namespace ts {
8
8
write ( s : string ) : void ;
9
9
readFile ( path : string , encoding ?: string ) : string ;
10
10
writeFile ( path : string , data : string , writeByteOrderMark ?: boolean ) : void ;
11
- watchFile ?( path : string , callback : ( path : string , removed : boolean ) => void ) : FileWatcher ;
11
+ watchFile ?( path : string , callback : ( path : string , removed ?: boolean ) => void ) : FileWatcher ;
12
+ watchDirectory ?( path : string , callback : ( path : string ) => void , recursive ?: boolean ) : FileWatcher ;
12
13
resolvePath ( path : string ) : string ;
13
14
fileExists ( path : string ) : boolean ;
14
15
directoryExists ( path : string ) : boolean ;
@@ -20,6 +21,12 @@ namespace ts {
20
21
exit ( exitCode ?: number ) : void ;
21
22
}
22
23
24
+ interface WatchedFile {
25
+ fileName : string ;
26
+ callback : ( fileName : string , removed ?: boolean ) => void ;
27
+ mtime : Date ;
28
+ }
29
+
23
30
export interface FileWatcher {
24
31
close ( ) : void ;
25
32
}
@@ -192,6 +199,103 @@ namespace ts {
192
199
const _path = require ( "path" ) ;
193
200
const _os = require ( "os" ) ;
194
201
202
+ // average async stat takes about 30 microseconds
203
+ // set chunk size to do 30 files in < 1 millisecond
204
+ function createWatchedFileSet ( interval = 2500 , chunkSize = 30 ) {
205
+ let watchedFiles : WatchedFile [ ] = [ ] ;
206
+ let nextFileToCheck = 0 ;
207
+ let watchTimer : any ;
208
+
209
+ function getModifiedTime ( fileName : string ) : Date {
210
+ return _fs . statSync ( fileName ) . mtime ;
211
+ }
212
+
213
+ function poll ( checkedIndex : number ) {
214
+ let watchedFile = watchedFiles [ checkedIndex ] ;
215
+ if ( ! watchedFile ) {
216
+ return ;
217
+ }
218
+
219
+ _fs . stat ( watchedFile . fileName , ( err : any , stats : any ) => {
220
+ if ( err ) {
221
+ watchedFile . callback ( watchedFile . fileName ) ;
222
+ }
223
+ else if ( watchedFile . mtime . getTime ( ) !== stats . mtime . getTime ( ) ) {
224
+ watchedFile . mtime = getModifiedTime ( watchedFile . fileName ) ;
225
+ watchedFile . callback ( watchedFile . fileName , watchedFile . mtime . getTime ( ) === 0 ) ;
226
+ }
227
+ } ) ;
228
+ }
229
+
230
+ // this implementation uses polling and
231
+ // stat due to inconsistencies of fs.watch
232
+ // and efficiency of stat on modern filesystems
233
+ function startWatchTimer ( ) {
234
+ watchTimer = setInterval ( ( ) => {
235
+ let count = 0 ;
236
+ let nextToCheck = nextFileToCheck ;
237
+ let firstCheck = - 1 ;
238
+ while ( ( count < chunkSize ) && ( nextToCheck !== firstCheck ) ) {
239
+ poll ( nextToCheck ) ;
240
+ if ( firstCheck < 0 ) {
241
+ firstCheck = nextToCheck ;
242
+ }
243
+ nextToCheck ++ ;
244
+ if ( nextToCheck === watchedFiles . length ) {
245
+ nextToCheck = 0 ;
246
+ }
247
+ count ++ ;
248
+ }
249
+ nextFileToCheck = nextToCheck ;
250
+ } , interval ) ;
251
+ }
252
+
253
+ function addFile ( fileName : string , callback : ( fileName : string , removed ?: boolean ) => void ) : WatchedFile {
254
+ let file : WatchedFile = {
255
+ fileName,
256
+ callback,
257
+ mtime : getModifiedTime ( fileName )
258
+ } ;
259
+
260
+ watchedFiles . push ( file ) ;
261
+ if ( watchedFiles . length === 1 ) {
262
+ startWatchTimer ( ) ;
263
+ }
264
+ return file ;
265
+ }
266
+
267
+ function removeFile ( file : WatchedFile ) {
268
+ watchedFiles = copyListRemovingItem ( file , watchedFiles ) ;
269
+ }
270
+
271
+ return {
272
+ getModifiedTime : getModifiedTime ,
273
+ poll : poll ,
274
+ startWatchTimer : startWatchTimer ,
275
+ addFile : addFile ,
276
+ removeFile : removeFile
277
+ } ;
278
+ }
279
+
280
+ // REVIEW: for now this implementation uses polling.
281
+ // The advantage of polling is that it works reliably
282
+ // on all os and with network mounted files.
283
+ // For 90 referenced files, the average time to detect
284
+ // changes is 2*msInterval (by default 5 seconds).
285
+ // The overhead of this is .04 percent (1/2500) with
286
+ // average pause of < 1 millisecond (and max
287
+ // pause less than 1.5 milliseconds); question is
288
+ // do we anticipate reference sets in the 100s and
289
+ // do we care about waiting 10-20 seconds to detect
290
+ // changes for large reference sets? If so, do we want
291
+ // to increase the chunk size or decrease the interval
292
+ // time dynamically to match the large reference set?
293
+ let watchedFileSet = createWatchedFileSet ( ) ;
294
+
295
+ function isNode4OrLater ( ) : Boolean {
296
+ return parseInt ( process . version . charAt ( 1 ) ) >= 4 ;
297
+ }
298
+
195
299
const platform : string = _os . platform ( ) ;
196
300
// win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive
197
301
const useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin" ;
@@ -284,25 +388,36 @@ namespace ts {
284
388
readFile,
285
389
writeFile,
286
390
watchFile : ( fileName , callback ) => {
287
- // watchFile polls a file every 250ms, picking up file notifications.
288
- _fs . watchFile ( fileName , { persistent : true , interval : 250 } , fileChanged ) ;
391
+ // Node 4.0 stablized the `fs.watch` function on Windows which avoids polling
392
+ // and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649
393
+ // and https://github.com/Microsoft/TypeScript/issues/4643), therefore
394
+ // if the current node.js version is newer than 4, use `fs.watch` instead.
395
+ if ( isNode4OrLater ( ) ) {
396
+ // Note: in node the callback of fs.watch is given only the relative file name as a parameter
397
+ return _fs . watch ( fileName , ( eventName : string , relativeFileName : string ) => callback ( fileName ) ) ;
398
+ }
289
399
400
+ let watchedFile = watchedFileSet . addFile ( fileName , callback ) ;
290
401
return {
291
- close ( ) { _fs . unwatchFile ( fileName , fileChanged ) ; }
402
+ close : ( ) => watchedFileSet . removeFile ( watchedFile )
292
403
} ;
293
-
294
- function fileChanged ( curr : any , prev : any ) {
295
- // mtime.getTime() equals 0 if file was removed
296
- if ( curr . mtime . getTime ( ) === 0 ) {
297
- callback ( fileName , /* removed */ true ) ;
298
- return ;
299
- }
300
- if ( + curr . mtime <= + prev . mtime ) {
301
- return ;
404
+ } ,
405
+ watchDirectory : ( path , callback , recursive ) => {
406
+ // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
407
+ // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
408
+ return _fs . watch (
409
+ path ,
410
+ { persisten : true , recursive : ! ! recursive } ,
411
+ ( eventName : string , relativeFileName : string ) => {
412
+ // In watchDirectory we only care about adding and removing files (when event name is
413
+ // "rename"); changes made within files are handled by corresponding fileWatchers (when
414
+ // event name is "change")
415
+ if ( eventName === "rename" ) {
416
+ // When deleting a file, the passed baseFileName is null
417
+ callback ( ! relativeFileName ? relativeFileName : normalizePath ( ts . combinePaths ( path , relativeFileName ) ) ) ;
418
+ } ;
302
419
}
303
-
304
- callback ( fileName , /* removed */ false ) ;
305
- }
420
+ ) ;
306
421
} ,
307
422
resolvePath : function ( path : string ) : string {
308
423
return _path . resolve ( path ) ;
0 commit comments