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).
This commit is contained in:
Andrew Casey
2017-06-20 15:54:43 -07:00
parent 587309d029
commit 4863ada22c
7 changed files with 102 additions and 5 deletions

View File

@@ -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();

View File

@@ -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) => {

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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 {

View File

@@ -107,6 +107,7 @@ namespace ts.server {
private rootFilesMap: Map<ScriptInfo> = createMap<ScriptInfo>();
private program: ts.Program;
private externalFiles: SortedReadonlyArray<string>;
private missingFilesMap: FileMap<FileWatcher> = createFileMap<FileWatcher>();
private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap();
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
@@ -606,6 +607,39 @@ namespace ts.server {
}
}
if (hasChanges && this.program.getMissingFilePaths) {
const missingFilePaths = this.program.getMissingFilePaths() || emptyArray;
const missingFilePathsSet = createMap<true>();
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<string>;
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) {

View File

@@ -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);