Skip to content

Commit 3b1a3f8

Browse files
authored
Merge pull request #21693 from Microsoft/fileChangeThroughDeleteAndCreate
Handle file versioning little better in tsc --watch mode
2 parents 017f30e + 82aa1fb commit 3b1a3f8

File tree

3 files changed

+108
-64
lines changed

3 files changed

+108
-64
lines changed

src/compiler/watch.ts

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ namespace ts {
420420
}
421421
}
422422

423+
const initialVersion = 1;
424+
423425
/**
424426
* Creates the watch from the host for root files and compiler options
425427
*/
@@ -429,19 +431,25 @@ namespace ts {
429431
*/
430432
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfConfigFile<T>): WatchOfConfigFile<T>;
431433
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfFilesAndCompilerOptions<T> & WatchCompilerHostOfConfigFile<T>): WatchOfFilesAndCompilerOptions<T> | WatchOfConfigFile<T> {
432-
interface HostFileInfo {
434+
interface FilePresentOnHost {
433435
version: number;
434436
sourceFile: SourceFile;
435437
fileWatcher: FileWatcher;
436438
}
439+
type FileMissingOnHost = number;
440+
interface FilePresenceUnknownOnHost {
441+
version: number;
442+
}
443+
type FileMayBePresentOnHost = FilePresentOnHost | FilePresenceUnknownOnHost;
444+
type HostFileInfo = FilePresentOnHost | FileMissingOnHost | FilePresenceUnknownOnHost;
437445

438446
let builderProgram: T;
439447
let reloadLevel: ConfigFileProgramReloadLevel; // level to indicate if the program needs to be reloaded from config file/just filenames etc
440448
let missingFilesMap: Map<FileWatcher>; // Map of file watchers for the missing files
441449
let watchedWildcardDirectories: Map<WildcardDirectoryWatcher>; // map of watchers for the wild card directories in the config file
442450
let timerToUpdateProgram: any; // timer callback to recompile the program
443451

444-
const sourceFilesCache = createMap<HostFileInfo | string>(); // Cache that stores the source file and version info
452+
const sourceFilesCache = createMap<HostFileInfo>(); // Cache that stores the source file and version info
445453
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
446454
let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations
447455
let hasChangedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed
@@ -480,14 +488,14 @@ namespace ts {
480488
const watchFilePath = compilerOptions.extendedDiagnostics ? ts.addFilePathWatcherWithLogging : ts.addFilePathWatcher;
481489
const watchDirectoryWorker = compilerOptions.extendedDiagnostics ? ts.addDirectoryWatcherWithLogging : ts.addDirectoryWatcher;
482490

491+
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
492+
let newLine = updateNewLine();
493+
483494
writeLog(`Current directory: ${currentDirectory} CaseSensitiveFileNames: ${useCaseSensitiveFileNames}`);
484495
if (configFileName) {
485496
watchFile(host, configFileName, scheduleProgramReload, writeLog);
486497
}
487498

488-
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
489-
let newLine = updateNewLine();
490-
491499
const compilerHost: CompilerHost & ResolutionCacheHost = {
492500
// Members for CompilerHost
493501
getSourceFile: (fileName, languageVersion, onError?, shouldCreateNewSourceFile?) => getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile),
@@ -575,7 +583,9 @@ namespace ts {
575583

576584
// Compile the program
577585
if (loggingEnabled) {
578-
writeLog(`CreatingProgramWith::\n roots: ${JSON.stringify(rootFileNames)}\n options: ${JSON.stringify(compilerOptions)}`);
586+
writeLog(`CreatingProgramWith::`);
587+
writeLog(` roots: ${JSON.stringify(rootFileNames)}`);
588+
writeLog(` options: ${JSON.stringify(compilerOptions)}`);
579589
}
580590

581591
const needsUpdateInTypeRootWatch = hasChangedCompilerOptions || !program;
@@ -627,11 +637,20 @@ namespace ts {
627637
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
628638
}
629639

640+
function isFileMissingOnHost(hostSourceFile: HostFileInfo): hostSourceFile is FileMissingOnHost {
641+
return typeof hostSourceFile === "number";
642+
}
643+
644+
function isFilePresentOnHost(hostSourceFile: FileMayBePresentOnHost): hostSourceFile is FilePresentOnHost {
645+
return !!(hostSourceFile as FilePresentOnHost).sourceFile;
646+
}
647+
630648
function fileExists(fileName: string) {
631649
const path = toPath(fileName);
632-
const hostSourceFileInfo = sourceFilesCache.get(path);
633-
if (hostSourceFileInfo !== undefined) {
634-
return !isString(hostSourceFileInfo);
650+
// If file is missing on host from cache, we can definitely say file doesnt exist
651+
// otherwise we need to ensure from the disk
652+
if (isFileMissingOnHost(sourceFilesCache.get(path))) {
653+
return true;
635654
}
636655

637656
return directoryStructureHost.fileExists(fileName);
@@ -640,39 +659,42 @@ namespace ts {
640659
function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
641660
const hostSourceFile = sourceFilesCache.get(path);
642661
// No source file on the host
643-
if (isString(hostSourceFile)) {
662+
if (isFileMissingOnHost(hostSourceFile)) {
644663
return undefined;
645664
}
646665

647666
// Create new source file if requested or the versions dont match
648-
if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) {
667+
if (!hostSourceFile || shouldCreateNewSourceFile || !isFilePresentOnHost(hostSourceFile) || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) {
649668
const sourceFile = getNewSourceFile();
650669
if (hostSourceFile) {
651670
if (shouldCreateNewSourceFile) {
652671
hostSourceFile.version++;
653672
}
673+
654674
if (sourceFile) {
655-
hostSourceFile.sourceFile = sourceFile;
675+
// Set the source file and create file watcher now that file was present on the disk
676+
(hostSourceFile as FilePresentOnHost).sourceFile = sourceFile;
656677
sourceFile.version = hostSourceFile.version.toString();
657-
if (!hostSourceFile.fileWatcher) {
658-
hostSourceFile.fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
678+
if (!(hostSourceFile as FilePresentOnHost).fileWatcher) {
679+
(hostSourceFile as FilePresentOnHost).fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
659680
}
660681
}
661682
else {
662683
// There is no source file on host any more, close the watch, missing file paths will track it
663-
hostSourceFile.fileWatcher.close();
664-
sourceFilesCache.set(path, hostSourceFile.version.toString());
684+
if (isFilePresentOnHost(hostSourceFile)) {
685+
hostSourceFile.fileWatcher.close();
686+
}
687+
sourceFilesCache.set(path, hostSourceFile.version);
665688
}
666689
}
667690
else {
668-
let fileWatcher: FileWatcher;
669691
if (sourceFile) {
670-
sourceFile.version = "1";
671-
fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
672-
sourceFilesCache.set(path, { sourceFile, version: 1, fileWatcher });
692+
sourceFile.version = initialVersion.toString();
693+
const fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
694+
sourceFilesCache.set(path, { sourceFile, version: initialVersion, fileWatcher });
673695
}
674696
else {
675-
sourceFilesCache.set(path, "0");
697+
sourceFilesCache.set(path, initialVersion);
676698
}
677699
}
678700
return sourceFile;
@@ -697,20 +719,22 @@ namespace ts {
697719
}
698720
}
699721

700-
function removeSourceFile(path: Path) {
722+
function nextSourceFileVersion(path: Path) {
701723
const hostSourceFile = sourceFilesCache.get(path);
702724
if (hostSourceFile !== undefined) {
703-
if (!isString(hostSourceFile)) {
704-
hostSourceFile.fileWatcher.close();
705-
resolutionCache.invalidateResolutionOfFile(path);
725+
if (isFileMissingOnHost(hostSourceFile)) {
726+
// The next version, lets set it as presence unknown file
727+
sourceFilesCache.set(path, { version: Number(hostSourceFile) + 1 });
728+
}
729+
else {
730+
hostSourceFile.version++;
706731
}
707-
sourceFilesCache.delete(path);
708732
}
709733
}
710734

711735
function getSourceVersion(path: Path): string {
712736
const hostSourceFile = sourceFilesCache.get(path);
713-
return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString();
737+
return !hostSourceFile || isFileMissingOnHost(hostSourceFile) ? undefined : hostSourceFile.version.toString();
714738
}
715739

716740
function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) {
@@ -721,10 +745,10 @@ namespace ts {
721745
// there was version update and new source file was created.
722746
if (hostSourceFileInfo) {
723747
// record the missing file paths so they can be removed later if watchers arent tracking them
724-
if (isString(hostSourceFileInfo)) {
748+
if (isFileMissingOnHost(hostSourceFileInfo)) {
725749
(missingFilePathsRequestedForRelease || (missingFilePathsRequestedForRelease = [])).push(oldSourceFile.path);
726750
}
727-
else if (hostSourceFileInfo.sourceFile === oldSourceFile) {
751+
else if ((hostSourceFileInfo as FilePresentOnHost).sourceFile === oldSourceFile) {
728752
sourceFilesCache.delete(oldSourceFile.path);
729753
resolutionCache.removeResolutionsOfFile(oldSourceFile.path);
730754
}
@@ -808,27 +832,12 @@ namespace ts {
808832

809833
function onSourceFileChange(fileName: string, eventKind: FileWatcherEventKind, path: Path) {
810834
updateCachedSystemWithFile(fileName, path, eventKind);
811-
const hostSourceFile = sourceFilesCache.get(path);
812-
if (hostSourceFile) {
813-
// Update the cache
814-
if (eventKind === FileWatcherEventKind.Deleted) {
815-
resolutionCache.invalidateResolutionOfFile(path);
816-
if (!isString(hostSourceFile)) {
817-
hostSourceFile.fileWatcher.close();
818-
sourceFilesCache.set(path, (++hostSourceFile.version).toString());
819-
}
820-
}
821-
else {
822-
// Deleted file created
823-
if (isString(hostSourceFile)) {
824-
sourceFilesCache.delete(path);
825-
}
826-
else {
827-
// file changed - just update the version
828-
hostSourceFile.version++;
829-
}
830-
}
835+
836+
// Update the source file cache
837+
if (eventKind === FileWatcherEventKind.Deleted && sourceFilesCache.get(path)) {
838+
resolutionCache.invalidateResolutionOfFile(path);
831839
}
840+
nextSourceFileVersion(path);
832841

833842
// Update the program
834843
scheduleProgramUpdate();
@@ -856,7 +865,7 @@ namespace ts {
856865
missingFilesMap.delete(missingFilePath);
857866

858867
// Delete the entry in the source files cache so that new source file is created
859-
removeSourceFile(missingFilePath);
868+
nextSourceFileVersion(missingFilePath);
860869

861870
// When a missing file is created, we should update the graph.
862871
scheduleProgramUpdate();
@@ -885,17 +894,10 @@ namespace ts {
885894
const fileOrDirectoryPath = toPath(fileOrDirectory);
886895

887896
// Since the file existance changed, update the sourceFiles cache
888-
const result = cachedDirectoryStructureHost && cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
889-
890-
// Instead of deleting the file, mark it as changed instead
891-
// Many times node calls add/remove/file when watching directories recursively
892-
const hostSourceFile = sourceFilesCache.get(fileOrDirectoryPath);
893-
if (hostSourceFile && !isString(hostSourceFile) && (result ? result.fileExists : directoryStructureHost.fileExists(fileOrDirectory))) {
894-
hostSourceFile.version++;
895-
}
896-
else {
897-
removeSourceFile(fileOrDirectoryPath);
897+
if (cachedDirectoryStructureHost) {
898+
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
898899
}
900+
nextSourceFileVersion(fileOrDirectoryPath);
899901

900902
// If the the added or created file or directory is not supported file name, ignore the file
901903
// But when watched directory is added/removed, we need to reload the file list

src/harness/unittests/tscWatchMode.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,38 @@ namespace ts.tscWatch {
17191719
return [files[0]];
17201720
}
17211721
});
1722+
1723+
it("file is deleted and created as part of change", () => {
1724+
const projectLocation = "/home/username/project";
1725+
const file: FileOrFolder = {
1726+
path: `${projectLocation}/app/file.ts`,
1727+
content: "var a = 10;"
1728+
};
1729+
const fileJs = `${projectLocation}/app/file.js`;
1730+
const configFile: FileOrFolder = {
1731+
path: `${projectLocation}/tsconfig.json`,
1732+
content: JSON.stringify({
1733+
include: [
1734+
"app/**/*.ts"
1735+
]
1736+
})
1737+
};
1738+
const files = [file, configFile, libFile];
1739+
const host = createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true });
1740+
createWatchOfConfigFile("tsconfig.json", host);
1741+
verifyProgram();
1742+
1743+
file.content += "\nvar b = 10;";
1744+
1745+
host.reloadFS(files, { invokeFileDeleteCreateAsPartInsteadOfChange: true });
1746+
host.runQueuedTimeoutCallbacks();
1747+
verifyProgram();
1748+
1749+
function verifyProgram() {
1750+
assert.isTrue(host.fileExists(fileJs));
1751+
assert.equal(host.readFile(fileJs), file.content + "\n");
1752+
}
1753+
});
17221754
});
17231755

17241756
describe("tsc-watch module resolution caching", () => {

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,12 @@ interface Array<T> {}`
246246
}
247247

248248
export interface ReloadWatchInvokeOptions {
249+
/** Invokes the directory watcher for the parent instead of the file changed */
249250
invokeDirectoryWatcherInsteadOfFileChanged: boolean;
251+
/** When new file is created, do not invoke watches for it */
250252
ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
253+
/** Invoke the file delete, followed by create instead of file changed */
254+
invokeFileDeleteCreateAsPartInsteadOfChange: boolean;
251255
}
252256

253257
export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost {
@@ -315,12 +319,18 @@ interface Array<T> {}`
315319
if (isString(fileOrDirectory.content)) {
316320
// Update file
317321
if (currentEntry.content !== fileOrDirectory.content) {
318-
currentEntry.content = fileOrDirectory.content;
319-
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
320-
this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath);
322+
if (options && options.invokeFileDeleteCreateAsPartInsteadOfChange) {
323+
this.removeFileOrFolder(currentEntry, returnFalse);
324+
this.ensureFileOrFolder(fileOrDirectory);
321325
}
322326
else {
323-
this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed);
327+
currentEntry.content = fileOrDirectory.content;
328+
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
329+
this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath);
330+
}
331+
else {
332+
this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed);
333+
}
324334
}
325335
}
326336
}

0 commit comments

Comments
 (0)