diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 1d66eeaf34a..b292ff3965a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -513,6 +513,8 @@ namespace ts { } } + const missingFilePaths = filesByName.getKeys().filter(p => !filesByName.get(p)); + // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; @@ -524,6 +526,7 @@ namespace ts { getSourceFile, getSourceFileByPath, getSourceFiles: () => files, + getMissingFilePaths: () => missingFilePaths, getCompilerOptions: () => options, getSyntacticDiagnostics, getOptionsDiagnostics, @@ -862,11 +865,31 @@ namespace ts { return oldProgram.structureIsReused; } + // If a file has ceased to be missing, then we need to discard some of the old + // structure in order to pick it up. + // Caution: if the file has created and then deleted between since it was discovered to + // be missing, then the corresponding file watcher will have been closed and no new one + // will be created until we encounter a change that prevents complete structure reuse. + // During this interval, creation of the file will go unnoticed. We expect this to be + // both rare and low-impact. + if (oldProgram.getMissingFilePaths) { + const missingFilePaths: Path[] = oldProgram.getMissingFilePaths() || emptyArray; + for (const missingFilePath of missingFilePaths) { + if (host.fileExists(missingFilePath)) { + return oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + } + } + // update fileName -> file mapping for (let i = 0; i < newSourceFiles.length; i++) { filesByName.set(filePaths[i], newSourceFiles[i]); } + for (const p of oldProgram.getMissingFilePaths()) { + filesByName.set(p, undefined); + } + files = newSourceFiles; fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index cd14f3b2801..8448c9b3615 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -340,11 +340,20 @@ namespace ts { } function fileChanged(curr: any, prev: any) { - if (+curr.mtime <= +prev.mtime) { + const isCurrZero = +curr.mtime === 0; + const isPrevZero = +prev.mtime === 0; + const added = !isCurrZero && isPrevZero; + const deleted = isCurrZero && !isPrevZero; + + // This value is consistent with poll() in createPollingWatchedFileSet() + // and depended upon by the file watchers created in Project.updateGraphWorker. + const removed = deleted ? true : (added ? false : undefined); + + if (!added && !deleted && +curr.mtime <= +prev.mtime) { return; } - callback(fileName); + callback(fileName, removed); } }, watchDirectory: (directoryName, callback, recursive) => { diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 3b2422bfe08..8264282cba9 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -285,6 +285,19 @@ namespace ts { setCachedProgram(compileResult.program); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + + if (compileResult.program.getMissingFilePaths) { + const missingPaths = compileResult.program.getMissingFilePaths() || []; + missingPaths.forEach((path: Path): void => { + const fileWatcher = sys.watchFile(path, (_fileName: string, removed?: boolean) => { + // removed = deleted ? true : (added ? false : undefined) + if (removed === false) { + fileWatcher.close(); + startTimerForRecompilation(); + } + }); + }); + } } function cachedFileExists(fileName: string): boolean { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5c959d3736e..99d62091ed5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2426,6 +2426,12 @@ namespace ts { */ getSourceFiles(): SourceFile[]; + /** + * Get a list of file names that were passed to 'createProgram' or referenced in a + * program source file but could not be located. + */ + getMissingFilePaths?(): Path[]; + /** * Emits the JavaScript and declaration files. If targetSourceFile is not specified, then * the JavaScript and declaration files will be produced for all the files in this program. diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index b8f9807139f..1cccaf979cf 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -210,8 +210,11 @@ namespace ts.server { return this.host.resolvePath(path); } - fileExists(path: string): boolean { - return this.host.fileExists(path); + fileExists(file: string): boolean { + // As an optimization, don't hit the disks for files we already know don't exist + // (because we're watching for their creation). + const path = toPath(file, this.host.getCurrentDirectory(), this.getCanonicalFileName); + return !this.project.isWatchedMissingFile(path) && this.host.fileExists(file); } readFile(fileName: string): string { diff --git a/src/server/project.ts b/src/server/project.ts index d4544af5e59..d1b4a627fac 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -107,6 +107,7 @@ namespace ts.server { private rootFilesMap: Map = createMap(); private program: ts.Program; private externalFiles: SortedReadonlyArray; + private missingFilesMap: FileMap = createFileMap(); private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap(); private lastCachedUnresolvedImportsList: SortedReadonlyArray; @@ -606,6 +607,39 @@ namespace ts.server { } } + if (hasChanges && this.program.getMissingFilePaths) { + const missingFilePaths = this.program.getMissingFilePaths() || emptyArray; + const missingFilePathsSet = createMap(); + missingFilePaths.forEach(p => missingFilePathsSet.set(p, true)); + + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + this.missingFilesMap.getKeys().forEach(p => { + if (!missingFilePathsSet.has(p)) { + this.missingFilesMap.get(p).close(); + this.missingFilesMap.remove(p); + } + }); + + // Missing files that are not yet watched should be added to the map. + missingFilePaths.forEach(p => { + if (!this.missingFilesMap.contains(p)) { + const fileWatcher = ts.sys.watchFile(p, (_filename: string, removed?: boolean) => { + // removed = deleted ? true : (added ? false : undefined) + if (removed === false && this.missingFilesMap.contains(p)) { + fileWatcher.close(); + this.missingFilesMap.remove(p); + + // When a missing file is created, we should update the graph. + this.markAsDirty(); + this.updateGraph(); + } + }); + this.missingFilesMap.set(p, fileWatcher); + } + }); + } + const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; this.externalFiles = this.getExternalFiles(); enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, @@ -626,6 +660,10 @@ namespace ts.server { return hasChanges; } + isWatchedMissingFile(path: Path) { + return this.missingFilesMap.contains(path); + } + getScriptInfoLSHost(fileName: string) { const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); if (scriptInfo) { diff --git a/src/server/server.ts b/src/server/server.ts index 5f2b7054755..9874969064e 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -522,6 +522,9 @@ namespace ts.server { return; } + // removed = deleted ? true : (added ? false : undefined) + // This value is consistent with sys.watchFile() + // and depended upon by the file watchers created in performCompilation() in tsc's executeCommandLine(). fs.stat(watchedFile.fileName, (err: any, stats: any) => { if (err) { watchedFile.callback(watchedFile.fileName); @@ -560,7 +563,9 @@ namespace ts.server { const file: WatchedFile = { fileName, callback, - mtime: getModifiedTime(fileName) + mtime: sys.fileExists(fileName) + ? getModifiedTime(fileName) + : new Date(0) // Any subsequent modification will occur after this time }; watchedFiles.push(file);