diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index f2d20ab19a2..6ca318f7183 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -9,6 +9,7 @@ namespace ts { resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[]; resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfDeletedFile(filePath: Path): void; + invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string): void; clear(): void; } @@ -33,8 +34,7 @@ namespace ts { export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, - clearProgramAndScheduleUpdate: () => void, - watchForFailedLookupLocation: (fileName: string, callback: FileWatcherCallback) => FileWatcher, + watchForFailedLookupLocation: (failedLookupLocation: string, containingFile: string, name: string) => FileWatcher, log: (s: string) => void, resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache { @@ -54,6 +54,7 @@ namespace ts { resolveModuleNames, resolveTypeReferenceDirectives, invalidateResolutionOfDeletedFile, + invalidateResolutionOfChangedFailedLookupLocation, clear }; @@ -185,11 +186,8 @@ namespace ts { log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name} refCount: ${failedLookupLocationWatcher.refCount}`); } else { - const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, (__fileName, eventKind) => { - log(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); - // There is some kind of change in the failed lookup location, update the program - clearProgramAndScheduleUpdate(); - }); + log(`Watcher: FailedLookupLocations: Status: new watch: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); + const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, containingFile, name); failedLookupLocationsWatches.set(failedLookupLocationPath, { fileWatcher, refCount: 1 }); } } @@ -260,7 +258,7 @@ namespace ts { }); } else if (value) { - value.forEach((resolution) => { + value.forEach((resolution, __name) => { if (resolution && !resolution.isInvalidated) { const result = getResult(resolution); if (result) { @@ -274,9 +272,31 @@ namespace ts { }); } + function invalidateResolutionCacheOfChangedFailedLookupLocation( + failedLookupLocation: string, + cache: Map>) { + cache.forEach((value, _containingFilePath) => { + if (value) { + value.forEach((resolution, __name) => { + if (resolution && !resolution.isInvalidated && contains(resolution.failedLookupLocations, failedLookupLocation)) { + // TODO: mark the file as needing re-evaluation of module resolution instead of using it blindly. + // Note: Right now this invalidation path is not used at all as it doesnt matter as we are anyways clearing the program, + // which means all the resolutions will be discarded. + resolution.isInvalidated = true; + } + }); + } + }); + } + function invalidateResolutionOfDeletedFile(filePath: Path) { invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); } + + function invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string) { + invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocation, resolvedModuleNames); + invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocation, resolvedTypeReferenceDirectives); + } } } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 6b16474e268..c668d4a1510 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -255,6 +255,7 @@ namespace ts { let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // Cache that stores the source file and version info + let missingFilePathsRequestedForRelease: Path[]; // These paths are held temparirly so that we can remove the entry from source file cache if the file is not tracked by missing files watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -274,8 +275,7 @@ namespace ts { const resolutionCache = createResolutionCache( fileName => toPath(fileName), () => compilerOptions, - () => clearExistingProgramAndScheduleProgramUpdate(), - (fileName, callback) => system.watchFile(fileName, callback), + watchFailedLookupLocation, s => writeLog(s) ); @@ -310,6 +310,19 @@ namespace ts { // Update watches missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); + if (missingFilePathsRequestedForRelease) { + // These are the paths that program creater told us as not in use any more but were missing on the disk. + // We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO, + // if there is already watcher for it (for missing files) + // At that point our watches were updated, hence now we know that these paths are not tracked and need to be removed + // so that at later time we have correct result of their presence + for (const missingFilePath of missingFilePathsRequestedForRelease) { + if (!missingFilesMap.has(missingFilePath)) { + sourceFilesCache.delete(missingFilePath); + } + } + missingFilePathsRequestedForRelease = undefined; + } afterCompile(host, program, builder); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); @@ -446,8 +459,14 @@ namespace ts { // remove the cached entry. // Note we arent deleting entry if file became missing in new program or // there was version update and new source file was created. - if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { - sourceFilesCache.delete(oldSourceFile.path); + if (hostSourceFileInfo) { + // record the missing file paths so they can be removed later if watchers arent tracking them + if (isString(hostSourceFileInfo)) { + (missingFilePathsRequestedForRelease || (missingFilePathsRequestedForRelease = [])).push(oldSourceFile.path); + } + else if (hostSourceFileInfo.sourceFile === oldSourceFile) { + sourceFilesCache.delete(oldSourceFile.path); + } } } @@ -471,11 +490,6 @@ namespace ts { scheduleProgramUpdate(); } - function clearExistingProgramAndScheduleProgramUpdate() { - program = undefined; - scheduleProgramUpdate(); - } - function updateProgram() { timerToUpdateProgram = undefined; reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); @@ -547,6 +561,24 @@ namespace ts { } } + function watchFailedLookupLocation(failedLookupLocation: string, containingFile: string, name: string) { + return host.watchFile(failedLookupLocation, (fileName, eventKind) => onFailedLookupLocationChange(fileName, eventKind, failedLookupLocation, containingFile, name)); + } + + function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, containingFile: string, name: string) { + writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`); + const path = toPath(failedLookupLocation); + updateCachedSystem(failedLookupLocation, path); + + // TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file + // refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions. + // For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again. + + // resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); + program = undefined; + scheduleProgramUpdate(); + } + function watchMissingFilePath(missingFilePath: Path) { return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index b7c71f28191..39776e379c9 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -335,7 +335,8 @@ namespace ts.projectSystem { checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + const moduleLookupLocations = ["/a/b/c/module.ts", "/a/b/c/module.tsx"]; + checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path, ...moduleLookupLocations)); }); it("can handle tsconfig file name with difference casing", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d617bdb2b9e..bf255c7defa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -247,7 +247,8 @@ namespace ts.server { WildCardDirectories = "Wild card directory", TypeRoot = "Type root of the project", ClosedScriptInfo = "Closed Script info", - ConfigFileForInferredRoot = "Config file for the inferred project root" + ConfigFileForInferredRoot = "Config file for the inferred project root", + FailedLookupLocation = "Failed lookup locations in module resolution" } /* @internal */ diff --git a/src/server/project.ts b/src/server/project.ts index c2fab978931..e600c3796e6 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -217,8 +217,7 @@ namespace ts.server { this.resolutionCache = createResolutionCache( fileName => this.projectService.toPath(fileName), () => this.compilerOptions, - () => this.markAsDirty(), - (fileName, callback) => host.watchFile(fileName, callback), + (failedLookupLocation, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, containingFile, name), s => this.projectService.logger.info(s), (primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host, this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName()) @@ -235,6 +234,23 @@ namespace ts.server { this.markAsDirty(); } + private watchFailedLookupLocation(failedLookupLocation: string, containingFile: string, name: string) { + // There is some kind of change in the failed lookup location, update the program + return this.projectService.addFileWatcher(WatchType.FailedLookupLocation, this, failedLookupLocation, (__fileName, eventKind) => { + this.projectService.logger.info(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); + if (this.projectKind === ProjectKind.Configured) { + (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(failedLookupLocation)); + } + this.updateTypes(); + // TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file + // refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions. + // For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again. + // this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); + // this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); + }); + } + private setInternalCompilerOptionsForEmittingJsFiles() { if (this.projectKind === ProjectKind.Inferred || this.projectKind === ProjectKind.External) { this.compilerOptions.noEmitForJsFiles = true;