diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 0b6b676f7f6..0946948f43d 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -361,9 +361,13 @@ namespace ts { return { dir: rootDir, dirPath: rootPath }; } - let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())); - let dirPath = getDirectoryPath(failedLookupLocationPath); + return getDirectoryToWatchFromFailedLookupLocationDirectory( + getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), + getDirectoryPath(failedLookupLocationPath) + ); + } + function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: Path) { // If directory path contains node module, get the most parent node_modules directory for watching while (stringContains(dirPath, "/node_modules/")) { dir = getDirectoryPath(dir); @@ -621,7 +625,19 @@ namespace ts { clearMap(typeRootsWatches, closeFileWatcher); } - function createTypeRootsWatch(_typeRootPath: string, typeRoot: string): FileWatcher { + function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: Path): Path | undefined { + if (allFilesHaveInvalidatedResolution) { + return undefined; + } + + if (isInDirectoryPath(rootPath, typeRootPath)) { + return rootPath; + } + const { dirPath, ignore } = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath); + return !ignore && directoryWatchesOfFailedLookups.has(dirPath) && dirPath; + } + + function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher { // Create new watch and recursive info return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => { const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); @@ -634,6 +650,13 @@ namespace ts { // We could potentially store more data here about whether it was/would be really be used or not // and with that determine to trigger compilation but for now this is enough resolutionHost.onChangedAutomaticTypeDirectiveNames(); + + // Since directory watchers invoked are flaky, the failed lookup location events might not be triggered + // So handle to failed lookup locations here as well to ensure we are invalidating resolutions + const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath); + if (dirPath && invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { + resolutionHost.onInvalidatedResolution(); + } }, WatchDirectoryFlags.Recursive); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index b32bd8d4113..f4e9087643e 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -6294,6 +6294,44 @@ namespace ts.projectSystem { verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); }); }); + + it("when node_modules dont receive event for the @types file addition", () => { + const projectLocation = "/user/username/folder/myproject"; + const app: FileOrFolder = { + path: `${projectLocation}/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: FileOrFolder = { + path: `${projectLocation}/tsconfig.json`, + content: "" + }; + + const files = [app, tsconfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(app.path); + + const project = service.configuredProjects.get(tsconfig.path); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + + const debugTypesFile: FileOrFolder = { + path: `${projectLocation}/node_modules/@types/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + // Do not invoke recursive directory watcher for anything other than node_module/@types + const invoker = host.invokeWatchedDirectoriesRecursiveCallback; + host.invokeWatchedDirectoriesRecursiveCallback = (fullPath, relativePath) => { + if (fullPath.endsWith("@types")) { + invoker.call(host, fullPath, relativePath); + } + }; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); + }); }); describe("tsserverProjectSystem ProjectsChangedInBackground", () => { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index c206d219e2a..cd5f5cf1b18 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -547,8 +547,8 @@ interface Array {}` // Invoke directory and recursive directory watcher for the folder // Here we arent invoking recursive directory watchers for the base folders // since that is something we would want to do for both file as well as folder we are deleting - invokeWatcherCallbacks(this.watchedDirectories.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath)); - invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath)); + this.invokeWatchedDirectoriesCallback(fileOrDirectory.fullPath, relativePath); + this.invokeWatchedDirectoriesRecursiveCallback(fileOrDirectory.fullPath, relativePath); } if (basePath !== fileOrDirectory.path) { @@ -561,9 +561,17 @@ interface Array {}` } } - private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) { - const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); - invokeWatcherCallbacks(callbacks, ({ cb }) => cb(fileFullPath, eventKind)); + // For overriding the methods + invokeWatchedDirectoriesCallback(folderFullPath: string, relativePath: string) { + invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); + } + + invokeWatchedDirectoriesRecursiveCallback(folderFullPath: string, relativePath: string) { + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); + } + + invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, useFileNameInCallback?: boolean) { + invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb, fileName }) => cb(useFileNameInCallback ? fileName : fileFullPath, eventKind)); } private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { @@ -576,8 +584,8 @@ interface Array {}` private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); // Folder is changed when the directory watcher is invoked - invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(folderFullPath)), ({ cb, fileName }) => cb(fileName, FileWatcherEventKind.Changed)); - invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); + this.invokeFileWatcher(folderFullPath, FileWatcherEventKind.Changed, /*useFileNameInCallback*/ true); + this.invokeWatchedDirectoriesCallback(folderFullPath, relativePath); this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); } @@ -590,7 +598,7 @@ interface Array {}` */ private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(fullPath, fileName); - invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => this.directoryCallback(cb, relativePath)); + this.invokeWatchedDirectoriesRecursiveCallback(fullPath, relativePath); const basePath = getDirectoryPath(fullPath); if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { this.invokeRecursiveDirectoryWatcher(basePath, fileName);