diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 688400fc6d5c2..1438bebc78c8d 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -317,18 +317,22 @@ namespace ts { const newTime = modifiedTime.getTime(); if (oldTime !== newTime) { watchedFile.mtime = modifiedTime; - const eventKind = oldTime === 0 - ? FileWatcherEventKind.Created - : newTime === 0 - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; - watchedFile.callback(watchedFile.fileName, eventKind); + watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime)); return true; } return false; } + /*@internal*/ + export function getFileWatcherEventKind(oldTime: number, newTime: number) { + return oldTime === 0 + ? FileWatcherEventKind.Created + : newTime === 0 + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; + } + /*@internal*/ export interface RecursiveDirectoryWatcherHost { watchDirectory: HostWatchDirectory; diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index bfa999a3790d5..c1ad196149eba 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -3028,7 +3028,7 @@ namespace ts.projectSystem { const project = projectService.configuredProjects.get(configFile.path); assert.isDefined(project); checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]); - checkWatchedFiles(host, [libFile.path, module1.path, module2.path, configFile.path]); + checkWatchedFiles(host, [libFile.path, configFile.path]); checkWatchedDirectories(host, [], /*recursive*/ false); const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); watchedRecursiveDirectories.push(`${root}/a/b/src`, `${root}/a/b/node_modules`); @@ -6632,6 +6632,7 @@ namespace ts.projectSystem { projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)); // we would now not have failed lookup in the parent of appFolder since lodash is available recursiveWatchedDirectories.length = 1; + recursiveWatchedDirectories.push(`${root}/a/b/node_modules`); // npm installation complete, timeout after reload fs timeoutAfterReloadFs = true; verifyAfterPartialOrCompleteNpmInstall(2); @@ -6654,7 +6655,7 @@ namespace ts.projectSystem { const projectFilePaths = map(projectFiles, f => f.path); checkProjectActualFiles(project, projectFilePaths); - const filesWatched = filter(projectFilePaths, p => p !== app.path); + const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); checkWatchedFiles(host, filesWatched); checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); checkWatchedDirectories(host, [], /*recursive*/ false); @@ -7689,10 +7690,23 @@ namespace ts.projectSystem { } function verifyWatchesWithConfigFile(host: TestServerHost, files: FileOrFolder[], openFile: FileOrFolder) { - checkWatchedFiles(host, mapDefined(files, f => f === openFile ? undefined : f.path)); + const nodeModulesDirs = createMap(); + checkWatchedFiles(host, mapDefined(files, f => { + if (f === openFile) { + return undefined; + } + + const indexOfNodeModules = f.path.indexOf("/node_modules/"); + if (indexOfNodeModules === -1) { + return f.path; + } + + nodeModulesDirs.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true); + return undefined; + })); checkWatchedDirectories(host, [], /*recursive*/ false); const configDirectory = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configDirectory, `${configDirectory}/${nodeModulesAtTypes}`], /*recursive*/ true); + checkWatchedDirectories(host, [configDirectory, `${configDirectory}/${nodeModulesAtTypes}`].concat(arrayFrom(nodeModulesDirs.keys())), /*recursive*/ true); } describe("from files in same folder", () => { @@ -7892,7 +7906,7 @@ namespace ts.projectSystem { verifyTrace(resolutionTrace, expectedTrace); const currentDirectory = getDirectoryPath(file1.path); - const watchedFiles = mapDefined(files, f => f === file1 ? undefined : f.path); + const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path); forEachAncestorDirectory(currentDirectory, d => { watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json")); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ef82e6de30e03..4d6320534a0ae 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -251,7 +251,8 @@ namespace ts.server { ClosedScriptInfo = "Closed Script info", ConfigFileForInferredRoot = "Config file for the inferred project root", FailedLookupLocation = "Directory of Failed lookup locations in module resolution", - TypeRoots = "Type root directory" + TypeRoots = "Type root directory", + ScriptInfoInNodeModules = "node_modules for closed script infos", } const enum ConfigFileWatcherStatus { @@ -306,6 +307,10 @@ namespace ts.server { syntaxOnly?: boolean; } + interface ScriptInfoInNodeModulesWatcher extends FileWatcher { + refCount: number; + } + function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) { return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; } @@ -325,6 +330,7 @@ namespace ts.server { * Container of all known scripts */ private readonly filenameToScriptInfo = createMap(); + private readonly scriptInfoInNodeModulesWatchers = createMap (); /** * Map to the real path of the infos */ @@ -1742,16 +1748,86 @@ namespace ts.server { if (!info.isDynamicOrHasMixedContent() && (!this.globalCacheLocationDirectoryPath || !startsWith(info.path, this.globalCacheLocationDirectoryPath))) { - const { fileName } = info; - info.fileWatcher = this.watchFactory.watchFilePath( - this.host, - fileName, - (fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path), - PollingInterval.Medium, - info.path, - WatchType.ClosedScriptInfo - ); + + const indexOfNodeModules = info.path.indexOf("/node_modules/"); + if (indexOfNodeModules === -1) { + info.fileWatcher = this.watchFactory.watchFilePath( + this.host, + info.fileName, + (fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path), + PollingInterval.Medium, + info.path, + WatchType.ClosedScriptInfo + ); + } + else { + info.mTime = this.getModifiedTime(info); + info.fileWatcher = this.watchClosedScriptInfoInNodeModules(info.path.substr(0, indexOfNodeModules) as Path); + } + } + } + + + private watchClosedScriptInfoInNodeModules(dir: Path): ScriptInfoInNodeModulesWatcher { + // Watch only directory + const existing = this.scriptInfoInNodeModulesWatchers.get(dir); + if (existing) { + existing.refCount++; + return existing; } + + const watchDir = dir + "/node_modules" as Path; + let watcher = this.watchFactory.watchDirectory( + this.host, + watchDir, + (fileOrDirectory) => { + const fileOrDirectoryPath = this.toPath(fileOrDirectory); + if (fileOrDirectoryPath.indexOf("/.staging") || fileOrDirectoryPath.indexOf("/.bin")) return; + // Has extension + Debug.assert(result.refCount > 0); + if (watchDir === fileOrDirectoryPath) { + this.refreshScriptInfosInDirectory(watchDir); + } + else { + this.refreshScriptInfosInDirectory(hasExtension(fileOrDirectoryPath) ? getDirectoryPath(fileOrDirectoryPath) : fileOrDirectoryPath); + } + }, + WatchDirectoryFlags.Recursive, + WatchType.ScriptInfoInNodeModules + ); + const result: ScriptInfoInNodeModulesWatcher = { + close: () => { + if (result.refCount === 1) { + watcher.close(); + watcher = undefined; + this.scriptInfoInNodeModulesWatchers.delete(dir); + } + else { + result.refCount--; + } + }, + refCount: 1 + }; + this.scriptInfoInNodeModulesWatchers.set(dir, result); + return result; + } + + private getModifiedTime(info: ScriptInfo) { + return (this.host.getModifiedTime(info.path) || new Date()).getTime(); + } + + private refreshScriptInfosInDirectory(dir: Path) { + dir = dir + directorySeparator as Path; + this.filenameToScriptInfo.forEach(info => { + if (startsWith(info.path, dir)) { + const mTime = this.getModifiedTime(info); + if (mTime !== info.mTime) { + const eventKind = getFileWatcherEventKind(info.mTime, mTime); + info.mTime = mTime; + this.onSourceFileChanged(info.fileName, eventKind, info.path); + } + } + }); } private stopWatchingScriptInfo(info: ScriptInfo) { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index efae3a5faa68d..e3cd74a182bb4 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -221,6 +221,9 @@ namespace ts.server { /** Set to real path if path is different from info.path */ private realpath: Path | undefined; + /*@internal*/ + mTime: number; + constructor( private readonly host: ServerHost, readonly fileName: NormalizedPath, diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 60b5d7ffd9371..9547e0f5f58ac 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7968,6 +7968,7 @@ declare namespace ts.server { * Container of all known scripts */ private readonly filenameToScriptInfo; + private readonly scriptInfoInNodeModulesWatchers; /** * maps external project file name to list of config files that were the part of this project */ @@ -8127,6 +8128,9 @@ declare namespace ts.server { private createInferredProject; getScriptInfo(uncheckedFileName: string): ScriptInfo; private watchClosedScriptInfo; + private watchClosedScriptInfoInNodeModules; + private getModifiedTime; + private refreshScriptInfosInDirectory; private stopWatchingScriptInfo; getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean;