@@ -291,7 +291,8 @@ namespace ts.server {
291291 ClosedScriptInfo = "Closed Script info" ,
292292 ConfigFileForInferredRoot = "Config file for the inferred project root" ,
293293 FailedLookupLocation = "Directory of Failed lookup locations in module resolution" ,
294- TypeRoots = "Type root directory"
294+ TypeRoots = "Type root directory" ,
295+ NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them" ,
295296 }
296297
297298 const enum ConfigFileWatcherStatus {
@@ -353,10 +354,18 @@ namespace ts.server {
353354 return ! ! ( infoOrFileName as ScriptInfo ) . containingProjects ;
354355 }
355356
357+ interface ScriptInfoInNodeModulesWatcher extends FileWatcher {
358+ refCount : number ;
359+ }
360+
356361 function getDetailWatchInfo ( watchType : WatchType , project : Project | undefined ) {
357362 return `Project: ${ project ? project . getProjectName ( ) : "" } WatchType: ${ watchType } ` ;
358363 }
359364
365+ function isScriptInfoWatchedFromNodeModules ( info : ScriptInfo ) {
366+ return ! info . isScriptOpen ( ) && info . mTime !== undefined ;
367+ }
368+
360369 /*@internal */
361370 export function updateProjectIfDirty ( project : Project ) {
362371 return project . dirty && project . updateGraph ( ) ;
@@ -380,6 +389,7 @@ namespace ts.server {
380389 * Container of all known scripts
381390 */
382391 private readonly filenameToScriptInfo = createMap < ScriptInfo > ( ) ;
392+ private readonly scriptInfoInNodeModulesWatchers = createMap < ScriptInfoInNodeModulesWatcher > ( ) ;
383393 /**
384394 * Contains all the deleted script info's version information so that
385395 * it does not reset when creating script info again
@@ -1923,18 +1933,97 @@ namespace ts.server {
19231933 if ( ! info . isDynamicOrHasMixedContent ( ) &&
19241934 ( ! this . globalCacheLocationDirectoryPath ||
19251935 ! startsWith ( info . path , this . globalCacheLocationDirectoryPath ) ) ) {
1926- const { fileName } = info ;
1927- info . fileWatcher = this . watchFactory . watchFilePath (
1928- this . host ,
1929- fileName ,
1930- ( fileName , eventKind , path ) => this . onSourceFileChanged ( fileName , eventKind , path ) ,
1931- PollingInterval . Medium ,
1932- info . path ,
1933- WatchType . ClosedScriptInfo
1934- ) ;
1936+ const indexOfNodeModules = info . path . indexOf ( "/node_modules/" ) ;
1937+ if ( ! this . host . getModifiedTime || indexOfNodeModules === - 1 ) {
1938+ info . fileWatcher = this . watchFactory . watchFilePath (
1939+ this . host ,
1940+ info . fileName ,
1941+ ( fileName , eventKind , path ) => this . onSourceFileChanged ( fileName , eventKind , path ) ,
1942+ PollingInterval . Medium ,
1943+ info . path ,
1944+ WatchType . ClosedScriptInfo
1945+ ) ;
1946+ }
1947+ else {
1948+ info . mTime = this . getModifiedTime ( info ) ;
1949+ info . fileWatcher = this . watchClosedScriptInfoInNodeModules ( info . path . substr ( 0 , indexOfNodeModules ) as Path ) ;
1950+ }
1951+ }
1952+ }
1953+
1954+ private watchClosedScriptInfoInNodeModules ( dir : Path ) : ScriptInfoInNodeModulesWatcher {
1955+ // Watch only directory
1956+ const existing = this . scriptInfoInNodeModulesWatchers . get ( dir ) ;
1957+ if ( existing ) {
1958+ existing . refCount ++ ;
1959+ return existing ;
1960+ }
1961+
1962+ const watchDir = dir + "/node_modules" as Path ;
1963+ const watcher = this . watchFactory . watchDirectory (
1964+ this . host ,
1965+ watchDir ,
1966+ ( fileOrDirectory ) => {
1967+ const fileOrDirectoryPath = this . toPath ( fileOrDirectory ) ;
1968+ // Has extension
1969+ Debug . assert ( result . refCount > 0 ) ;
1970+ if ( watchDir === fileOrDirectoryPath ) {
1971+ this . refreshScriptInfosInDirectory ( watchDir ) ;
1972+ }
1973+ else {
1974+ const info = this . getScriptInfoForPath ( fileOrDirectoryPath ) ;
1975+ if ( info ) {
1976+ if ( isScriptInfoWatchedFromNodeModules ( info ) ) {
1977+ this . refreshScriptInfo ( info ) ;
1978+ }
1979+ }
1980+ // Folder
1981+ else if ( ! hasExtension ( fileOrDirectoryPath ) ) {
1982+ this . refreshScriptInfosInDirectory ( fileOrDirectoryPath ) ;
1983+ }
1984+ }
1985+ } ,
1986+ WatchDirectoryFlags . Recursive ,
1987+ WatchType . NodeModulesForClosedScriptInfo
1988+ ) ;
1989+ const result : ScriptInfoInNodeModulesWatcher = {
1990+ close : ( ) => {
1991+ if ( result . refCount === 1 ) {
1992+ watcher . close ( ) ;
1993+ this . scriptInfoInNodeModulesWatchers . delete ( dir ) ;
1994+ }
1995+ else {
1996+ result . refCount -- ;
1997+ }
1998+ } ,
1999+ refCount : 1
2000+ } ;
2001+ this . scriptInfoInNodeModulesWatchers . set ( dir , result ) ;
2002+ return result ;
2003+ }
2004+
2005+ private getModifiedTime ( info : ScriptInfo ) {
2006+ return ( this . host . getModifiedTime ! ( info . path ) || missingFileModifiedTime ) . getTime ( ) ;
2007+ }
2008+
2009+ private refreshScriptInfo ( info : ScriptInfo ) {
2010+ const mTime = this . getModifiedTime ( info ) ;
2011+ if ( mTime !== info . mTime ) {
2012+ const eventKind = getFileWatcherEventKind ( info . mTime ! , mTime ) ;
2013+ info . mTime = mTime ;
2014+ this . onSourceFileChanged ( info . fileName , eventKind , info . path ) ;
19352015 }
19362016 }
19372017
2018+ private refreshScriptInfosInDirectory ( dir : Path ) {
2019+ dir = dir + directorySeparator as Path ;
2020+ this . filenameToScriptInfo . forEach ( info => {
2021+ if ( isScriptInfoWatchedFromNodeModules ( info ) && startsWith ( info . path , dir ) ) {
2022+ this . refreshScriptInfo ( info ) ;
2023+ }
2024+ } ) ;
2025+ }
2026+
19382027 private stopWatchingScriptInfo ( info : ScriptInfo ) {
19392028 if ( info . fileWatcher ) {
19402029 info . fileWatcher . close ( ) ;
0 commit comments