@@ -64,6 +64,15 @@ namespace ts.server.typingsInstaller {
64
64
onRequestCompleted : RequestCompletedAction ;
65
65
}
66
66
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
+
67
76
type ProjectWatchers = Map < FileWatcher > & { isInvoked ?: boolean ; } ;
68
77
69
78
export abstract class TypingsInstaller {
@@ -73,6 +82,7 @@ namespace ts.server.typingsInstaller {
73
82
private readonly projectWatchers = createMap < ProjectWatchers > ( ) ;
74
83
private safeList : JsTyping . SafeList | undefined ;
75
84
readonly pendingRunRequests : PendingRequest [ ] = [ ] ;
85
+ private readonly toCanonicalFileName : GetCanonicalFileName ;
76
86
77
87
private installRunCount = 1 ;
78
88
private inFlightRequestCount = 0 ;
@@ -86,6 +96,7 @@ namespace ts.server.typingsInstaller {
86
96
private readonly typesMapLocation : Path ,
87
97
private readonly throttleLimit : number ,
88
98
protected readonly log = nullLog ) {
99
+ this . toCanonicalFileName = createGetCanonicalFileName ( installTypingHost . useCaseSensitiveFileNames ) ;
89
100
if ( this . log . isEnabled ( ) ) {
90
101
this . log . writeLine ( `Global cache location '${ globalCachePath } ', safe file path '${ safeListPath } ', types map path ${ typesMapLocation } ` ) ;
91
102
}
@@ -147,7 +158,7 @@ namespace ts.server.typingsInstaller {
147
158
}
148
159
149
160
// start watching files
150
- this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch ) ;
161
+ this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch , req . projectRootPath ) ;
151
162
152
163
// install typings
153
164
if ( discoverTypingsResult . newTypingNames . length ) {
@@ -367,51 +378,117 @@ namespace ts.server.typingsInstaller {
367
378
}
368
379
}
369
380
370
- private watchFiles ( projectName : string , files : string [ ] ) {
381
+ private watchFiles ( projectName : string , files : string [ ] , projectRootPath : Path ) {
371
382
if ( ! files . length ) {
372
383
// shut down existing watchers
373
384
this . closeWatchers ( projectName ) ;
374
385
return ;
375
386
}
376
387
377
388
let watchers = this . projectWatchers . get ( projectName ) ;
389
+ const toRemove = createMap < FileWatcher > ( ) ;
378
390
if ( ! watchers ) {
379
391
watchers = createMap ( ) ;
380
392
this . projectWatchers . set ( projectName , watchers ) ;
381
393
}
394
+ else {
395
+ copyEntries ( watchers , toRemove ) ;
396
+ }
382
397
383
- watchers . isInvoked = false ;
384
398
// handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
399
+ watchers . isInvoked = false ;
400
+
385
401
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 ;
413
464
}
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
+ } ) ;
415
492
}
416
493
417
494
private createSetTypings ( request : DiscoverTypings , typings : string [ ] ) : SetTypings {
0 commit comments