11/// <reference path="core.ts"/>
22
33namespace ts {
4- export type CallbackForWatchedFile = ( path : string , removed ?: boolean ) => void ;
5- export type CallbackForWatchedDirectory = ( path : string ) => void ;
4+ export type FileWatcherCallback = ( path : string , removed ?: boolean ) => void ;
5+ export type DirWatcherCallback = ( path : string ) => void ;
66
77 export interface System {
88 args : string [ ] ;
@@ -11,8 +11,8 @@ namespace ts {
1111 write ( s : string ) : void ;
1212 readFile ( path : string , encoding ?: string ) : string ;
1313 writeFile ( path : string , data : string , writeByteOrderMark ?: boolean ) : void ;
14- watchFile ?( path : string , callback : CallbackForWatchedFile ) : FileWatcher ;
15- watchDirectory ?( path : string , callback : CallbackForWatchedDirectory , recursive ?: boolean ) : FileWatcher ;
14+ watchFile ?( path : string , callback : FileWatcherCallback ) : FileWatcher ;
15+ watchDirectory ?( path : string , callback : DirWatcherCallback , recursive ?: boolean ) : FileWatcher ;
1616 resolvePath ( path : string ) : string ;
1717 fileExists ( path : string ) : boolean ;
1818 directoryExists ( path : string ) : boolean ;
@@ -26,13 +26,17 @@ namespace ts {
2626
2727 interface WatchedFile {
2828 fileName : string ;
29- callback : CallbackForWatchedFile ;
29+ callback : FileWatcherCallback ;
3030 mtime ?: Date ;
3131 }
3232
3333 export interface FileWatcher {
3434 close ( ) : void ;
3535 }
36+
37+ export interface DirWatcher extends FileWatcher {
38+ referenceCount : number ;
39+ }
3640
3741 declare var require : any ;
3842 declare var module : any ;
@@ -65,8 +69,8 @@ namespace ts {
6569 readFile ( path : string ) : string ;
6670 writeFile ( path : string , contents : string ) : void ;
6771 readDirectory ( path : string , extension ?: string , exclude ?: string [ ] ) : string [ ] ;
68- watchFile ?( path : string , callback : CallbackForWatchedFile ) : FileWatcher ;
69- watchDirectory ?( path : string , callback : CallbackForWatchedDirectory , recursive ?: boolean ) : FileWatcher ;
72+ watchFile ?( path : string , callback : FileWatcherCallback ) : FileWatcher ;
73+ watchDirectory ?( path : string , callback : DirWatcherCallback , recursive ?: boolean ) : FileWatcher ;
7074 } ;
7175
7276 export var sys : System = ( function ( ) {
@@ -274,7 +278,7 @@ namespace ts {
274278 } , interval ) ;
275279 }
276280
277- function addFile ( fileName : string , callback : CallbackForWatchedFile ) : WatchedFile {
281+ function addFile ( fileName : string , callback : FileWatcherCallback ) : WatchedFile {
278282 const file : WatchedFile = {
279283 fileName,
280284 callback,
@@ -301,53 +305,124 @@ namespace ts {
301305 } ;
302306 }
303307
304-
305-
306308 function createWatchedFileSet ( ) {
307- const watchedDirectories = createFileMap < FileWatcher > ( ) ;
308- const watchedFiles = createFileMap < CallbackForWatchedFile > ( ) ;
309+ const dirWatchers = createFileMap < DirWatcher > ( ) ;
310+ const recursiveDirWatchers = createFileMap < DirWatcher > ( ) ;
311+ const fileWatcherCallbacks = createFileMap < FileWatcherCallback > ( ) ;
312+ const dirWatcherCallbacks = createFileMap < DirWatcherCallback > ( ) ;
313+
309314 const currentDirectory = process . cwd ( ) ;
315+ return { addFile, removeFile, addDir } ;
316+
317+ function addDir ( dirName : string , callback : DirWatcherCallback , recursive ?: boolean ) {
318+ const dirPath = toPath ( dirName , currentDirectory , getCanonicalPath ) ;
319+ dirWatcherCallbacks . set ( dirPath , callback ) ;
320+ const { watcher, isRecursive } = addDirWatcher ( dirPath , recursive ) ;
321+ return {
322+ close : ( ) => reduceDirWatcherRefCount ( watcher , dirPath , isRecursive )
323+ }
324+ }
325+
326+ function reduceDirWatcherRefCount ( watcher : DirWatcher , dirPath : Path , isRecursive : boolean ) {
327+ watcher . referenceCount -= 1 ;
328+ if ( watcher . referenceCount <= 0 ) {
329+ watcher . close ( ) ;
330+ if ( isRecursive ) {
331+ recursiveDirWatchers . remove ( dirPath ) ;
332+ } else {
333+ dirWatchers . remove ( dirPath ) ;
334+ }
335+ }
336+ }
337+
338+ function addDirWatcher ( dirPath : Path , recursive ?: boolean ) : { watcher : DirWatcher , isRecursive : boolean } {
339+ let watchers : FileMap < DirWatcher > ;
340+ let options : { persistent : boolean , recursive ?: boolean } = { persistent : true } ;
341+
342+ // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
343+ // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
344+ if ( isNode4OrLater ( ) && recursive === true ) {
345+ if ( recursiveDirWatchers . contains ( dirPath ) ) {
346+ const watcher = recursiveDirWatchers . get ( dirPath ) ;
347+ watcher . referenceCount += 1 ;
348+ return { watcher, isRecursive : true } ;
349+ }
350+ watchers = recursiveDirWatchers ;
351+ options . recursive = true ;
352+ } else {
353+ if ( dirWatchers . contains ( dirPath ) ) {
354+ const watcher = dirWatchers . get ( dirPath ) ;
355+ watcher . referenceCount += 1 ;
356+ return { watcher, isRecursive : false } ;
357+ }
358+ watchers = dirWatchers ;
359+ }
310360
311- return { addFile, removeFile } ;
361+ const watcher : DirWatcher = _fs . watch ( dirPath , options , ( eventName : string , relativeFileName : string ) => fileEventHandler ( eventName , relativeFileName , dirPath ) ) ;
362+ watcher . referenceCount = 1 ;
363+ watchers . set ( dirPath , watcher ) ;
364+ return { watcher, isRecursive : false } ;
365+ }
312366
313- function addFile ( fileName : string , callback : CallbackForWatchedFile ) : WatchedFile {
314- const path = toPath ( fileName , currentDirectory , getCanonicalPath ) ;
315- const parentDirPath = getDirectoryPath ( path ) ;
367+ function findDirWatcherForFile ( filePath : Path ) : { watcher : DirWatcher , watcherPath : Path , isRecursive : boolean } {
368+ let watcher : DirWatcher ;
369+ let watcherPath : Path ;
370+ let isRecursive = false ;
371+ recursiveDirWatchers . forEachValue ( dirPath => {
372+ if ( filePath . indexOf ( dirPath ) === 0 ) {
373+ watcherPath = dirPath ;
374+ watcher = recursiveDirWatchers . get ( dirPath ) ;
375+ isRecursive = true ;
376+ return ;
377+ }
378+ } ) ;
379+ if ( ! watcher ) {
380+ const parentDirPath = getDirectoryPath ( filePath ) ;
381+ if ( dirWatchers . contains ( parentDirPath ) ) {
382+ watcherPath = parentDirPath ;
383+ watcher = dirWatchers . get ( parentDirPath ) ;
384+ }
385+ }
386+ return { watcher, watcherPath, isRecursive } ;
387+ }
316388
317- if ( ! watchedDirectories . contains ( parentDirPath ) ) {
318- watchedDirectories . set ( parentDirPath , _fs . watch (
319- parentDirPath ,
320- ( eventName : string , relativeFileName : string ) => fileEventHandler ( eventName , relativeFileName , parentDirPath )
321- ) ) ;
389+ function addFile ( fileName : string , callback : FileWatcherCallback ) : WatchedFile {
390+ const filePath = toPath ( fileName , currentDirectory , getCanonicalPath ) ;
391+ const { watcher } = findDirWatcherForFile ( filePath ) ;
392+ if ( ! watcher ) {
393+ addDirWatcher ( getDirectoryPath ( filePath ) ) ;
394+ } else {
395+ watcher . referenceCount += 1 ;
322396 }
323- watchedFiles . set ( path , callback ) ;
397+ fileWatcherCallbacks . set ( filePath , callback ) ;
324398 return { fileName, callback } ;
325399 }
326400
327401 function removeFile ( file : WatchedFile ) {
328- const path = toPath ( file . fileName , currentDirectory , getCanonicalPath ) ;
329- watchedFiles . remove ( path ) ;
330-
331- const parentDirPath = getDirectoryPath ( path ) ;
332- if ( watchedDirectories . contains ( parentDirPath ) ) {
333- let hasWatchedChildren = false ;
334- watchedFiles . forEachValue ( ( key , _ ) => {
335- if ( ts . getDirectoryPath ( key ) === parentDirPath ) {
336- hasWatchedChildren = true ;
337- }
338- } ) ;
339- if ( ! hasWatchedChildren ) {
340- watchedDirectories . get ( parentDirPath ) . close ( ) ;
341- watchedDirectories . remove ( parentDirPath ) ;
342- }
402+ const filePath = toPath ( file . fileName , currentDirectory , getCanonicalPath ) ;
403+ fileWatcherCallbacks . remove ( filePath ) ;
404+
405+ const { watcher, watcherPath, isRecursive } = findDirWatcherForFile ( filePath ) ;
406+ if ( watcher ) {
407+ reduceDirWatcherRefCount ( watcher , watcherPath , isRecursive ) ;
343408 }
344409 }
345410
346- function fileEventHandler ( eventName : string , fileName : string , basePath : string ) {
347- const path = ts . toPath ( fileName , basePath , getCanonicalPath ) ;
348- if ( watchedFiles . contains ( path ) ) {
349- const callback = watchedFiles . get ( path ) ;
350- callback ( fileName ) ;
411+ /**
412+ * @param watcherPath is the path from which the watcher is triggered.
413+ */
414+ function fileEventHandler ( eventName : string , relativefileName : string , baseDirPath : Path ) {
415+ // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined"
416+ const filePath = relativefileName === undefined ? undefined : toPath ( relativefileName , baseDirPath , getCanonicalPath ) ;
417+ // Directory callbacks are not set for file content changes, they are more often used for
418+ // adding/removing/renaming files, which corresponds to the "rename" event
419+ if ( eventName === "rename" && dirWatcherCallbacks . contains ( baseDirPath ) ) {
420+ const dirCallback = dirWatcherCallbacks . get ( baseDirPath ) ;
421+ dirCallback ( filePath ) ;
422+ }
423+ if ( fileWatcherCallbacks . contains ( filePath ) ) {
424+ const fileCallback = fileWatcherCallbacks . get ( filePath ) ;
425+ fileCallback ( filePath ) ;
351426 }
352427 }
353428 }
@@ -477,22 +552,7 @@ namespace ts {
477552 } ;
478553 } ,
479554 watchDirectory : ( path , callback , recursive ) => {
480- // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
481- // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
482- const options = isNode4OrLater ( ) ? { persistent : true } : { persistent : true , recursive : ! ! recursive } ;
483- return _fs . watch (
484- path ,
485- options ,
486- ( eventName : string , relativeFileName : string ) => {
487- // In watchDirectory we only care about adding and removing files (when event name is
488- // "rename"); changes made within files are handled by corresponding fileWatchers (when
489- // event name is "change")
490- if ( eventName === "rename" ) {
491- // When deleting a file, the passed baseFileName is null
492- callback ( ! relativeFileName ? relativeFileName : normalizePath ( ts . combinePaths ( path , relativeFileName ) ) ) ;
493- } ;
494- }
495- ) ;
555+ return watchedFileSet . addDir ( path , callback , recursive ) ;
496556 } ,
497557 resolvePath : function ( path : string ) : string {
498558 return _path . resolve ( path ) ;
0 commit comments