@@ -64,6 +64,20 @@ 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 getDirectoryExcludingNodeModulesOrBowerComponents ( f : string ) {
73+ const indexOfNodeModules = f . indexOf ( "/node_modules/" ) ;
74+ const indexOfBowerComponents = f . indexOf ( "/bower_components/" ) ;
75+ const subStrLength = indexOfNodeModules === - 1 || indexOfBowerComponents === - 1 ?
76+ Math . max ( indexOfNodeModules , indexOfBowerComponents ) :
77+ Math . min ( indexOfNodeModules , indexOfBowerComponents ) ;
78+ return subStrLength === - 1 ? f : f . substr ( 0 , subStrLength ) ;
79+ }
80+
6781 type ProjectWatchers = Map < FileWatcher > & { isInvoked ?: boolean ; } ;
6882
6983 export abstract class TypingsInstaller {
@@ -73,6 +87,8 @@ namespace ts.server.typingsInstaller {
7387 private readonly projectWatchers = createMap < ProjectWatchers > ( ) ;
7488 private safeList : JsTyping . SafeList | undefined ;
7589 readonly pendingRunRequests : PendingRequest [ ] = [ ] ;
90+ private readonly toCanonicalFileName : GetCanonicalFileName ;
91+ private readonly globalCacheCanonicalPackageJsonPath : string ;
7692
7793 private installRunCount = 1 ;
7894 private inFlightRequestCount = 0 ;
@@ -86,6 +102,8 @@ namespace ts.server.typingsInstaller {
86102 private readonly typesMapLocation : Path ,
87103 private readonly throttleLimit : number ,
88104 protected readonly log = nullLog ) {
105+ this . toCanonicalFileName = createGetCanonicalFileName ( installTypingHost . useCaseSensitiveFileNames ) ;
106+ this . globalCacheCanonicalPackageJsonPath = combinePaths ( this . toCanonicalFileName ( globalCachePath ) , "package.json" ) ;
89107 if ( this . log . isEnabled ( ) ) {
90108 this . log . writeLine ( `Global cache location '${ globalCachePath } ', safe file path '${ safeListPath } ', types map path ${ typesMapLocation } ` ) ;
91109 }
@@ -147,7 +165,7 @@ namespace ts.server.typingsInstaller {
147165 }
148166
149167 // start watching files
150- this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch ) ;
168+ this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch , req . projectRootPath ) ;
151169
152170 // install typings
153171 if ( discoverTypingsResult . newTypingNames . length ) {
@@ -367,51 +385,112 @@ namespace ts.server.typingsInstaller {
367385 }
368386 }
369387
370- private watchFiles ( projectName : string , files : string [ ] ) {
388+ private watchFiles ( projectName : string , files : string [ ] , projectRootPath : Path ) {
371389 if ( ! files . length ) {
372390 // shut down existing watchers
373391 this . closeWatchers ( projectName ) ;
374392 return ;
375393 }
376394
377395 let watchers = this . projectWatchers . get ( projectName ) ;
396+ const toRemove = createMap < FileWatcher > ( ) ;
378397 if ( ! watchers ) {
379398 watchers = createMap ( ) ;
380399 this . projectWatchers . set ( projectName , watchers ) ;
381400 }
401+ else {
402+ copyEntries ( watchers , toRemove ) ;
403+ }
382404
383- watchers . isInvoked = false ;
384405 // handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
406+ watchers . isInvoked = false ;
407+
385408 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
409+ const createProjectWatcher = ( path : string , createWatch : ( path : string ) => FileWatcher ) => {
410+ toRemove . delete ( path ) ;
411+ if ( watchers . has ( path ) ) {
412+ return ;
413+ }
414+
415+ watchers . set ( path , createWatch ( path ) ) ;
416+ } ;
417+ const createProjectFileWatcher = ( file : string ) : FileWatcher => {
418+ if ( isLoggingEnabled ) {
419+ this . log . writeLine ( `FileWatcher:: Added:: WatchInfo: ${ file } ` ) ;
420+ }
421+ const watcher = this . installTypingHost . watchFile ( file , ( f , eventKind ) => {
422+ if ( isLoggingEnabled ) {
423+ this . log . writeLine ( `FileWatcher:: Triggered with ${ f } eventKind: ${ FileWatcherEventKind [ eventKind ] } :: WatchInfo: ${ file } :: handler is already invoked '${ watchers . isInvoked } '` ) ;
424+ }
425+ if ( ! watchers . isInvoked ) {
426+ watchers . isInvoked = true ;
427+ this . sendResponse ( { projectName, kind : ActionInvalidate } ) ;
428+ }
429+ } , /*pollingInterval*/ 2000 ) ;
430+
431+ return isLoggingEnabled ? {
432+ close : ( ) => {
433+ this . log . writeLine ( `FileWatcher:: Closed:: WatchInfo: ${ file } ` ) ;
434+ watcher . close ( ) ;
435+ }
436+ } : watcher ;
437+ } ;
438+ const createProjectDirectoryWatcher = ( dir : string ) : FileWatcher => {
439+ if ( isLoggingEnabled ) {
440+ this . log . writeLine ( `DirectoryWatcher:: Added:: WatchInfo: ${ dir } recursive` ) ;
441+ }
442+ const watcher = this . installTypingHost . watchDirectory ( dir , f => {
443+ if ( isLoggingEnabled ) {
444+ this . log . writeLine ( `DirectoryWatcher:: Triggered with ${ f } :: WatchInfo: ${ dir } recursive :: handler is already invoked '${ watchers . isInvoked } '` ) ;
445+ }
446+ if ( watchers . isInvoked ) {
447+ return ;
448+ }
449+ f = this . toCanonicalFileName ( f ) ;
450+ if ( f !== this . globalCacheCanonicalPackageJsonPath && isPackageOrBowerJson ( f ) ) {
451+ watchers . isInvoked = true ;
452+ this . sendResponse ( { projectName, kind : ActionInvalidate } ) ;
453+ }
454+ } , /*recursive*/ true ) ;
455+
456+ return isLoggingEnabled ? {
457+ close : ( ) => {
458+ this . log . writeLine ( `DirectoryWatcher:: Closed:: WatchInfo: ${ dir } recursive` ) ;
459+ watcher . close ( ) ;
460+ }
461+ } : watcher ;
462+ } ;
463+
464+ // Create watches from list of files
465+ for ( const file of files ) {
466+ const filePath = this . toCanonicalFileName ( file ) ;
467+ if ( isPackageOrBowerJson ( filePath ) ) {
468+ // package.json or bower.json exists, watch the file to detect changes and update typings
469+ createProjectWatcher ( filePath , createProjectFileWatcher ) ;
470+ continue ;
471+ }
472+
473+ // path in projectRoot, watch project root
474+ if ( containsPath ( projectRootPath , filePath , projectRootPath , ! this . installTypingHost . useCaseSensitiveFileNames ) ) {
475+ createProjectWatcher ( projectRootPath , createProjectDirectoryWatcher ) ;
476+ continue ;
477+ }
478+
479+ // path in global cache, watch global cache
480+ if ( containsPath ( this . globalCachePath , filePath , projectRootPath , ! this . installTypingHost . useCaseSensitiveFileNames ) ) {
481+ createProjectWatcher ( this . globalCachePath , createProjectDirectoryWatcher ) ;
482+ continue ;
413483 }
414- ) ;
484+
485+ // Get path without node_modules and bower_components
486+ createProjectWatcher ( getDirectoryExcludingNodeModulesOrBowerComponents ( getDirectoryPath ( filePath ) ) , createProjectDirectoryWatcher ) ;
487+ }
488+
489+ // Remove unused watches
490+ toRemove . forEach ( ( watch , path ) => {
491+ watch . close ( ) ;
492+ watchers . delete ( path ) ;
493+ } ) ;
415494 }
416495
417496 private createSetTypings ( request : DiscoverTypings , typings : string [ ] ) : SetTypings {
0 commit comments