Skip to content

Commit 4863ada

Browse files
committed
Track missing files
1. Expose missing files from the `Program`. 2. In `tsc --watch` and `tsserver`, add file watchers to missing files. 3. When missing files are created, schedule compilation (tsc) or refresh the containing projects (tsserver).
1 parent 587309d commit 4863ada

File tree

7 files changed

+102
-5
lines changed

7 files changed

+102
-5
lines changed

src/compiler/program.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,8 @@ namespace ts {
513513
}
514514
}
515515

516+
const missingFilePaths = filesByName.getKeys().filter(p => !filesByName.get(p));
517+
516518
// unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks
517519
moduleResolutionCache = undefined;
518520

@@ -524,6 +526,7 @@ namespace ts {
524526
getSourceFile,
525527
getSourceFileByPath,
526528
getSourceFiles: () => files,
529+
getMissingFilePaths: () => missingFilePaths,
527530
getCompilerOptions: () => options,
528531
getSyntacticDiagnostics,
529532
getOptionsDiagnostics,
@@ -862,11 +865,31 @@ namespace ts {
862865
return oldProgram.structureIsReused;
863866
}
864867

868+
// If a file has ceased to be missing, then we need to discard some of the old
869+
// structure in order to pick it up.
870+
// Caution: if the file has created and then deleted between since it was discovered to
871+
// be missing, then the corresponding file watcher will have been closed and no new one
872+
// will be created until we encounter a change that prevents complete structure reuse.
873+
// During this interval, creation of the file will go unnoticed. We expect this to be
874+
// both rare and low-impact.
875+
if (oldProgram.getMissingFilePaths) {
876+
const missingFilePaths: Path[] = oldProgram.getMissingFilePaths() || emptyArray;
877+
for (const missingFilePath of missingFilePaths) {
878+
if (host.fileExists(missingFilePath)) {
879+
return oldProgram.structureIsReused = StructureIsReused.SafeModules;
880+
}
881+
}
882+
}
883+
865884
// update fileName -> file mapping
866885
for (let i = 0; i < newSourceFiles.length; i++) {
867886
filesByName.set(filePaths[i], newSourceFiles[i]);
868887
}
869888

889+
for (const p of oldProgram.getMissingFilePaths()) {
890+
filesByName.set(p, undefined);
891+
}
892+
870893
files = newSourceFiles;
871894
fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics();
872895

src/compiler/sys.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,20 @@ namespace ts {
340340
}
341341

342342
function fileChanged(curr: any, prev: any) {
343-
if (+curr.mtime <= +prev.mtime) {
343+
const isCurrZero = +curr.mtime === 0;
344+
const isPrevZero = +prev.mtime === 0;
345+
const added = !isCurrZero && isPrevZero;
346+
const deleted = isCurrZero && !isPrevZero;
347+
348+
// This value is consistent with poll() in createPollingWatchedFileSet()
349+
// and depended upon by the file watchers created in Project.updateGraphWorker.
350+
const removed = deleted ? true : (added ? false : undefined);
351+
352+
if (!added && !deleted && +curr.mtime <= +prev.mtime) {
344353
return;
345354
}
346355

347-
callback(fileName);
356+
callback(fileName, removed);
348357
}
349358
},
350359
watchDirectory: (directoryName, callback, recursive) => {

src/compiler/tsc.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,19 @@ namespace ts {
285285

286286
setCachedProgram(compileResult.program);
287287
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
288+
289+
if (compileResult.program.getMissingFilePaths) {
290+
const missingPaths = compileResult.program.getMissingFilePaths() || [];
291+
missingPaths.forEach((path: Path): void => {
292+
const fileWatcher = sys.watchFile(path, (_fileName: string, removed?: boolean) => {
293+
// removed = deleted ? true : (added ? false : undefined)
294+
if (removed === false) {
295+
fileWatcher.close();
296+
startTimerForRecompilation();
297+
}
298+
});
299+
});
300+
}
288301
}
289302

290303
function cachedFileExists(fileName: string): boolean {

src/compiler/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2426,6 +2426,12 @@ namespace ts {
24262426
*/
24272427
getSourceFiles(): SourceFile[];
24282428

2429+
/**
2430+
* Get a list of file names that were passed to 'createProgram' or referenced in a
2431+
* program source file but could not be located.
2432+
*/
2433+
getMissingFilePaths?(): Path[];
2434+
24292435
/**
24302436
* Emits the JavaScript and declaration files. If targetSourceFile is not specified, then
24312437
* the JavaScript and declaration files will be produced for all the files in this program.

src/server/lsHost.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,11 @@ namespace ts.server {
210210
return this.host.resolvePath(path);
211211
}
212212

213-
fileExists(path: string): boolean {
214-
return this.host.fileExists(path);
213+
fileExists(file: string): boolean {
214+
// As an optimization, don't hit the disks for files we already know don't exist
215+
// (because we're watching for their creation).
216+
const path = toPath(file, this.host.getCurrentDirectory(), this.getCanonicalFileName);
217+
return !this.project.isWatchedMissingFile(path) && this.host.fileExists(file);
215218
}
216219

217220
readFile(fileName: string): string {

src/server/project.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ namespace ts.server {
107107
private rootFilesMap: Map<ScriptInfo> = createMap<ScriptInfo>();
108108
private program: ts.Program;
109109
private externalFiles: SortedReadonlyArray<string>;
110+
private missingFilesMap: FileMap<FileWatcher> = createFileMap<FileWatcher>();
110111

111112
private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap();
112113
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
@@ -606,6 +607,39 @@ namespace ts.server {
606607
}
607608
}
608609

610+
if (hasChanges && this.program.getMissingFilePaths) {
611+
const missingFilePaths = this.program.getMissingFilePaths() || emptyArray;
612+
const missingFilePathsSet = createMap<true>();
613+
missingFilePaths.forEach(p => missingFilePathsSet.set(p, true));
614+
615+
// Files that are no longer missing (e.g. because they are no longer required)
616+
// should no longer be watched.
617+
this.missingFilesMap.getKeys().forEach(p => {
618+
if (!missingFilePathsSet.has(p)) {
619+
this.missingFilesMap.get(p).close();
620+
this.missingFilesMap.remove(p);
621+
}
622+
});
623+
624+
// Missing files that are not yet watched should be added to the map.
625+
missingFilePaths.forEach(p => {
626+
if (!this.missingFilesMap.contains(p)) {
627+
const fileWatcher = ts.sys.watchFile(p, (_filename: string, removed?: boolean) => {
628+
// removed = deleted ? true : (added ? false : undefined)
629+
if (removed === false && this.missingFilesMap.contains(p)) {
630+
fileWatcher.close();
631+
this.missingFilesMap.remove(p);
632+
633+
// When a missing file is created, we should update the graph.
634+
this.markAsDirty();
635+
this.updateGraph();
636+
}
637+
});
638+
this.missingFilesMap.set(p, fileWatcher);
639+
}
640+
});
641+
}
642+
609643
const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray<string>;
610644
this.externalFiles = this.getExternalFiles();
611645
enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles,
@@ -626,6 +660,10 @@ namespace ts.server {
626660
return hasChanges;
627661
}
628662

663+
isWatchedMissingFile(path: Path) {
664+
return this.missingFilesMap.contains(path);
665+
}
666+
629667
getScriptInfoLSHost(fileName: string) {
630668
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false);
631669
if (scriptInfo) {

src/server/server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,9 @@ namespace ts.server {
522522
return;
523523
}
524524

525+
// removed = deleted ? true : (added ? false : undefined)
526+
// This value is consistent with sys.watchFile()
527+
// and depended upon by the file watchers created in performCompilation() in tsc's executeCommandLine().
525528
fs.stat(watchedFile.fileName, (err: any, stats: any) => {
526529
if (err) {
527530
watchedFile.callback(watchedFile.fileName);
@@ -560,7 +563,9 @@ namespace ts.server {
560563
const file: WatchedFile = {
561564
fileName,
562565
callback,
563-
mtime: getModifiedTime(fileName)
566+
mtime: sys.fileExists(fileName)
567+
? getModifiedTime(fileName)
568+
: new Date(0) // Any subsequent modification will occur after this time
564569
};
565570

566571
watchedFiles.push(file);

0 commit comments

Comments
 (0)