@@ -64,6 +64,15 @@ namespace ts.server.typingsInstaller {
6464 onRequestCompleted : RequestCompletedAction ;
6565 }
6666
67+ function isPackageOrBowerJson ( fileName : string ) {
68+ const base = getBaseFileName ( fileName ) ;
69+ return base === "package.json" || base === "bower.json" ;
70+ }
71+
72+ function isInNodeModulesOrBowerComponents ( f : string ) {
73+ return stringContains ( f , "/node_modules/" ) || stringContains ( f , "/bower_components/" ) ;
74+ }
75+
6776 type ProjectWatchers = Map < FileWatcher > & { isInvoked ?: boolean ; } ;
6877
6978 export abstract class TypingsInstaller {
@@ -73,6 +82,7 @@ namespace ts.server.typingsInstaller {
7382 private readonly projectWatchers = createMap < ProjectWatchers > ( ) ;
7483 private safeList : JsTyping . SafeList | undefined ;
7584 readonly pendingRunRequests : PendingRequest [ ] = [ ] ;
85+ private readonly toCanonicalFileName : GetCanonicalFileName ;
7686
7787 private installRunCount = 1 ;
7888 private inFlightRequestCount = 0 ;
@@ -86,6 +96,7 @@ namespace ts.server.typingsInstaller {
8696 private readonly typesMapLocation : Path ,
8797 private readonly throttleLimit : number ,
8898 protected readonly log = nullLog ) {
99+ this . toCanonicalFileName = createGetCanonicalFileName ( installTypingHost . useCaseSensitiveFileNames ) ;
89100 if ( this . log . isEnabled ( ) ) {
90101 this . log . writeLine ( `Global cache location '${ globalCachePath } ', safe file path '${ safeListPath } ', types map path ${ typesMapLocation } ` ) ;
91102 }
@@ -147,7 +158,7 @@ namespace ts.server.typingsInstaller {
147158 }
148159
149160 // start watching files
150- this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch ) ;
161+ this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch , req . projectRootPath ) ;
151162
152163 // install typings
153164 if ( discoverTypingsResult . newTypingNames . length ) {
@@ -367,51 +378,117 @@ namespace ts.server.typingsInstaller {
367378 }
368379 }
369380
370- private watchFiles ( projectName : string , files : string [ ] ) {
381+ private watchFiles ( projectName : string , files : string [ ] , projectRootPath : Path ) {
371382 if ( ! files . length ) {
372383 // shut down existing watchers
373384 this . closeWatchers ( projectName ) ;
374385 return ;
375386 }
376387
377388 let watchers = this . projectWatchers . get ( projectName ) ;
389+ const toRemove = createMap < FileWatcher > ( ) ;
378390 if ( ! watchers ) {
379391 watchers = createMap ( ) ;
380392 this . projectWatchers . set ( projectName , watchers ) ;
381393 }
394+ else {
395+ copyEntries ( watchers , toRemove ) ;
396+ }
382397
383- watchers . isInvoked = false ;
384398 // handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
399+ watchers . isInvoked = false ;
400+
385401 const isLoggingEnabled = this . log . isEnabled ( ) ;
386- mutateMap (
387- watchers ,
388- arrayToSet ( files ) ,
389- {
390- // Watch the missing files
391- createNewValue : file => {
392- if ( isLoggingEnabled ) {
393- this . log . writeLine ( `FileWatcher:: Added:: WatchInfo: ${ file } ` ) ;
394- }
395- const watcher = this . installTypingHost . watchFile ( file , ( f , eventKind ) => {
396- if ( isLoggingEnabled ) {
397- this . log . writeLine ( `FileWatcher:: Triggered with ${ f } eventKind: ${ FileWatcherEventKind [ eventKind ] } :: WatchInfo: ${ file } :: handler is already invoked '${ watchers . isInvoked } '` ) ;
398- }
399- if ( ! watchers . isInvoked ) {
400- watchers . isInvoked = true ;
401- this . sendResponse ( { projectName, kind : ActionInvalidate } ) ;
402- }
403- } , /*pollingInterval*/ 2000 ) ;
404- return isLoggingEnabled ? {
405- close : ( ) => {
406- this . log . writeLine ( `FileWatcher:: Closed:: WatchInfo: ${ file } ` ) ;
407- }
408- } : watcher ;
409- } ,
410- // Files that are no longer missing (e.g. because they are no longer required)
411- // should no longer be watched.
412- onDeleteValue : closeFileWatcher
402+ const createProjectWatcher = ( path : string , createWatch : ( path : string ) => FileWatcher ) => {
403+ toRemove . delete ( path ) ;
404+ if ( watchers . has ( path ) ) {
405+ return ;
406+ }
407+
408+ watchers . set ( path , createWatch ( path ) ) ;
409+ } ;
410+ const createProjectFileWatcher = ( file : string ) : FileWatcher => {
411+ if ( isLoggingEnabled ) {
412+ this . log . writeLine ( `FileWatcher:: Added:: WatchInfo: ${ file } ` ) ;
413+ }
414+ const watcher = this . installTypingHost . watchFile ( file , ( f , eventKind ) => {
415+ if ( isLoggingEnabled ) {
416+ this . log . writeLine ( `FileWatcher:: Triggered with ${ f } eventKind: ${ FileWatcherEventKind [ eventKind ] } :: WatchInfo: ${ file } :: handler is already invoked '${ watchers . isInvoked } '` ) ;
417+ }
418+ if ( ! watchers . isInvoked ) {
419+ watchers . isInvoked = true ;
420+ this . sendResponse ( { projectName, kind : ActionInvalidate } ) ;
421+ }
422+ } , /*pollingInterval*/ 2000 ) ;
423+
424+ return isLoggingEnabled ? {
425+ close : ( ) => {
426+ this . log . writeLine ( `FileWatcher:: Closed:: WatchInfo: ${ file } ` ) ;
427+ watcher . close ( ) ;
428+ }
429+ } : watcher ;
430+ } ;
431+ const createProjectDirectoryWatcher = ( dir : string ) : FileWatcher => {
432+ if ( isLoggingEnabled ) {
433+ this . log . writeLine ( `DirectoryWatcher:: Added:: WatchInfo: ${ dir } recursive` ) ;
434+ }
435+ const watcher = this . installTypingHost . watchDirectory ( dir , f => {
436+ if ( isLoggingEnabled ) {
437+ this . log . writeLine ( `DirectoryWatcher:: Triggered with ${ f } :: WatchInfo: ${ dir } recursive :: handler is already invoked '${ watchers . isInvoked } '` ) ;
438+ }
439+ if ( watchers . isInvoked ) {
440+ return ;
441+ }
442+ f = this . toCanonicalFileName ( f ) ;
443+ if ( isPackageOrBowerJson ( f ) && f !== this . toCanonicalFileName ( combinePaths ( this . globalCachePath , "package.json" ) ) ) {
444+ watchers . isInvoked = true ;
445+ this . sendResponse ( { projectName, kind : ActionInvalidate } ) ;
446+ }
447+ } , /*recursive*/ true ) ;
448+
449+ return isLoggingEnabled ? {
450+ close : ( ) => {
451+ this . log . writeLine ( `DirectoryWatcher:: Closed:: WatchInfo: ${ dir } recursive` ) ;
452+ watcher . close ( ) ;
453+ }
454+ } : watcher ;
455+ } ;
456+
457+ // Create watches from list of files
458+ for ( const file of files ) {
459+ const filePath = this . toCanonicalFileName ( file ) ;
460+ if ( isPackageOrBowerJson ( filePath ) ) {
461+ // package.json or bower.json exists, watch the file to detect changes and update typings
462+ createProjectWatcher ( filePath , createProjectFileWatcher ) ;
463+ continue ;
413464 }
414- ) ;
465+
466+ // path in projectRoot, watch project root
467+ if ( containsPath ( projectRootPath , filePath , projectRootPath , ! this . installTypingHost . useCaseSensitiveFileNames ) ) {
468+ createProjectWatcher ( projectRootPath , createProjectDirectoryWatcher ) ;
469+ continue ;
470+ }
471+
472+ // path in global cache, watch global cache
473+ if ( containsPath ( this . globalCachePath , filePath , projectRootPath , ! this . installTypingHost . useCaseSensitiveFileNames ) ) {
474+ createProjectWatcher ( this . globalCachePath , createProjectDirectoryWatcher ) ;
475+ continue ;
476+ }
477+
478+ // Get path without node_modules and bower_components
479+ let pathToWatch = getDirectoryPath ( filePath ) ;
480+ while ( isInNodeModulesOrBowerComponents ( pathToWatch ) ) {
481+ pathToWatch = getDirectoryPath ( pathToWatch ) ;
482+ }
483+
484+ createProjectWatcher ( pathToWatch , createProjectDirectoryWatcher ) ;
485+ }
486+
487+ // Remove unused watches
488+ toRemove . forEach ( ( watch , path ) => {
489+ watch . close ( ) ;
490+ watchers . delete ( path ) ;
491+ } ) ;
415492 }
416493
417494 private createSetTypings ( request : DiscoverTypings , typings : string [ ] ) : SetTypings {
0 commit comments