@@ -64,6 +64,20 @@ 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 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
+
67
81
type ProjectWatchers = Map < FileWatcher > & { isInvoked ?: boolean ; } ;
68
82
69
83
export abstract class TypingsInstaller {
@@ -73,6 +87,8 @@ namespace ts.server.typingsInstaller {
73
87
private readonly projectWatchers = createMap < ProjectWatchers > ( ) ;
74
88
private safeList : JsTyping . SafeList | undefined ;
75
89
readonly pendingRunRequests : PendingRequest [ ] = [ ] ;
90
+ private readonly toCanonicalFileName : GetCanonicalFileName ;
91
+ private readonly globalCacheCanonicalPackageJsonPath : string ;
76
92
77
93
private installRunCount = 1 ;
78
94
private inFlightRequestCount = 0 ;
@@ -86,6 +102,8 @@ namespace ts.server.typingsInstaller {
86
102
private readonly typesMapLocation : Path ,
87
103
private readonly throttleLimit : number ,
88
104
protected readonly log = nullLog ) {
105
+ this . toCanonicalFileName = createGetCanonicalFileName ( installTypingHost . useCaseSensitiveFileNames ) ;
106
+ this . globalCacheCanonicalPackageJsonPath = combinePaths ( this . toCanonicalFileName ( globalCachePath ) , "package.json" ) ;
89
107
if ( this . log . isEnabled ( ) ) {
90
108
this . log . writeLine ( `Global cache location '${ globalCachePath } ', safe file path '${ safeListPath } ', types map path ${ typesMapLocation } ` ) ;
91
109
}
@@ -147,7 +165,7 @@ namespace ts.server.typingsInstaller {
147
165
}
148
166
149
167
// start watching files
150
- this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch ) ;
168
+ this . watchFiles ( req . projectName , discoverTypingsResult . filesToWatch , req . projectRootPath ) ;
151
169
152
170
// install typings
153
171
if ( discoverTypingsResult . newTypingNames . length ) {
@@ -367,51 +385,112 @@ namespace ts.server.typingsInstaller {
367
385
}
368
386
}
369
387
370
- private watchFiles ( projectName : string , files : string [ ] ) {
388
+ private watchFiles ( projectName : string , files : string [ ] , projectRootPath : Path ) {
371
389
if ( ! files . length ) {
372
390
// shut down existing watchers
373
391
this . closeWatchers ( projectName ) ;
374
392
return ;
375
393
}
376
394
377
395
let watchers = this . projectWatchers . get ( projectName ) ;
396
+ const toRemove = createMap < FileWatcher > ( ) ;
378
397
if ( ! watchers ) {
379
398
watchers = createMap ( ) ;
380
399
this . projectWatchers . set ( projectName , watchers ) ;
381
400
}
401
+ else {
402
+ copyEntries ( watchers , toRemove ) ;
403
+ }
382
404
383
- watchers . isInvoked = false ;
384
405
// handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
406
+ watchers . isInvoked = false ;
407
+
385
408
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 ;
413
483
}
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
+ } ) ;
415
494
}
416
495
417
496
private createSetTypings ( request : DiscoverTypings , typings : string [ ] ) : SetTypings {
0 commit comments