@@ -8,7 +8,8 @@ namespace ts {
88 write ( s : string ) : void ;
99 readFile ( path : string , encoding ?: string ) : string ;
1010 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 ;
1213 resolvePath ( path : string ) : string ;
1314 fileExists ( path : string ) : boolean ;
1415 directoryExists ( path : string ) : boolean ;
@@ -20,6 +21,12 @@ namespace ts {
2021 exit ( exitCode ?: number ) : void ;
2122 }
2223
24+ interface WatchedFile {
25+ fileName : string ;
26+ callback : ( fileName : string , removed ?: boolean ) => void ;
27+ mtime : Date ;
28+ }
29+
2330 export interface FileWatcher {
2431 close ( ) : void ;
2532 }
@@ -192,6 +199,103 @@ namespace ts {
192199 const _path = require ( "path" ) ;
193200 const _os = require ( "os" ) ;
194201
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+
195299 const platform : string = _os . platform ( ) ;
196300 // win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive
197301 const useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin" ;
@@ -284,25 +388,36 @@ namespace ts {
284388 readFile,
285389 writeFile,
286390 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+ }
289399
400+ let watchedFile = watchedFileSet . addFile ( fileName , callback ) ;
290401 return {
291- close ( ) { _fs . unwatchFile ( fileName , fileChanged ) ; }
402+ close : ( ) => watchedFileSet . removeFile ( watchedFile )
292403 } ;
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+ } ;
302419 }
303-
304- callback ( fileName , /* removed */ false ) ;
305- }
420+ ) ;
306421 } ,
307422 resolvePath : function ( path : string ) : string {
308423 return _path . resolve ( path ) ;
0 commit comments