Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release-2.7] Handle file versioning little better in tsc --watch mode #21731

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 66 additions & 59 deletions src/compiler/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ namespace ts {
}
}

const initialVersion = 1;

/**
* Creates the watch from the host for root files and compiler options
*/
Expand All @@ -429,19 +431,25 @@ namespace ts {
*/
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfConfigFile<T>): WatchOfConfigFile<T>;
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfFilesAndCompilerOptions<T> & WatchCompilerHostOfConfigFile<T>): WatchOfFilesAndCompilerOptions<T> | WatchOfConfigFile<T> {
interface HostFileInfo {
interface FilePresentOnHost {
version: number;
sourceFile: SourceFile;
fileWatcher: FileWatcher;
}
type FileMissingOnHost = number;
interface FilePresenceUnknownOnHost {
version: number;
}
type FileMayBePresentOnHost = FilePresentOnHost | FilePresenceUnknownOnHost;
type HostFileInfo = FilePresentOnHost | FileMissingOnHost | FilePresenceUnknownOnHost;

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

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

const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
let newLine = updateNewLine();

writeLog(`Current directory: ${currentDirectory} CaseSensitiveFileNames: ${useCaseSensitiveFileNames}`);
if (configFileName) {
watchFile(host, configFileName, scheduleProgramReload, writeLog);
}

const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
let newLine = updateNewLine();

const compilerHost: CompilerHost & ResolutionCacheHost = {
// Members for CompilerHost
getSourceFile: (fileName, languageVersion, onError?, shouldCreateNewSourceFile?) => getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile),
Expand Down Expand Up @@ -573,6 +582,12 @@ namespace ts {
}

// Compile the program
if (loggingEnabled) {
writeLog(`CreatingProgramWith::`);
writeLog(` roots: ${JSON.stringify(rootFileNames)}`);
writeLog(` options: ${JSON.stringify(compilerOptions)}`);
}

const needsUpdateInTypeRootWatch = hasChangedCompilerOptions || !program;
hasChangedCompilerOptions = false;
resolutionCache.startCachingPerDirectoryResolution();
Expand Down Expand Up @@ -622,11 +637,20 @@ namespace ts {
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
}

function isFileMissingOnHost(hostSourceFile: HostFileInfo): hostSourceFile is FileMissingOnHost {
return typeof hostSourceFile === "number";
}

function isFilePresentOnHost(hostSourceFile: FileMayBePresentOnHost): hostSourceFile is FilePresentOnHost {
return !!(hostSourceFile as FilePresentOnHost).sourceFile;
}

function fileExists(fileName: string) {
const path = toPath(fileName);
const hostSourceFileInfo = sourceFilesCache.get(path);
if (hostSourceFileInfo !== undefined) {
return !isString(hostSourceFileInfo);
// If file is missing on host from cache, we can definitely say file doesnt exist
// otherwise we need to ensure from the disk
if (isFileMissingOnHost(sourceFilesCache.get(path))) {
return true;
}

return directoryStructureHost.fileExists(fileName);
Expand All @@ -635,39 +659,42 @@ namespace ts {
function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
const hostSourceFile = sourceFilesCache.get(path);
// No source file on the host
if (isString(hostSourceFile)) {
if (isFileMissingOnHost(hostSourceFile)) {
return undefined;
}

// Create new source file if requested or the versions dont match
if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) {
if (!hostSourceFile || shouldCreateNewSourceFile || !isFilePresentOnHost(hostSourceFile) || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) {
const sourceFile = getNewSourceFile();
if (hostSourceFile) {
if (shouldCreateNewSourceFile) {
hostSourceFile.version++;
}

if (sourceFile) {
hostSourceFile.sourceFile = sourceFile;
// Set the source file and create file watcher now that file was present on the disk
(hostSourceFile as FilePresentOnHost).sourceFile = sourceFile;
sourceFile.version = hostSourceFile.version.toString();
if (!hostSourceFile.fileWatcher) {
hostSourceFile.fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
if (!(hostSourceFile as FilePresentOnHost).fileWatcher) {
(hostSourceFile as FilePresentOnHost).fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
}
}
else {
// There is no source file on host any more, close the watch, missing file paths will track it
hostSourceFile.fileWatcher.close();
sourceFilesCache.set(path, hostSourceFile.version.toString());
if (isFilePresentOnHost(hostSourceFile)) {
hostSourceFile.fileWatcher.close();
}
sourceFilesCache.set(path, hostSourceFile.version);
}
}
else {
let fileWatcher: FileWatcher;
if (sourceFile) {
sourceFile.version = "1";
fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
sourceFilesCache.set(path, { sourceFile, version: 1, fileWatcher });
sourceFile.version = initialVersion.toString();
const fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog);
sourceFilesCache.set(path, { sourceFile, version: initialVersion, fileWatcher });
}
else {
sourceFilesCache.set(path, "0");
sourceFilesCache.set(path, initialVersion);
}
}
return sourceFile;
Expand All @@ -692,20 +719,22 @@ namespace ts {
}
}

function removeSourceFile(path: Path) {
function nextSourceFileVersion(path: Path) {
const hostSourceFile = sourceFilesCache.get(path);
if (hostSourceFile !== undefined) {
if (!isString(hostSourceFile)) {
hostSourceFile.fileWatcher.close();
resolutionCache.invalidateResolutionOfFile(path);
if (isFileMissingOnHost(hostSourceFile)) {
// The next version, lets set it as presence unknown file
sourceFilesCache.set(path, { version: Number(hostSourceFile) + 1 });
}
else {
hostSourceFile.version++;
}
sourceFilesCache.delete(path);
}
}

function getSourceVersion(path: Path): string {
const hostSourceFile = sourceFilesCache.get(path);
return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString();
return !hostSourceFile || isFileMissingOnHost(hostSourceFile) ? undefined : hostSourceFile.version.toString();
}

function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) {
Expand All @@ -716,10 +745,10 @@ namespace ts {
// there was version update and new source file was created.
if (hostSourceFileInfo) {
// record the missing file paths so they can be removed later if watchers arent tracking them
if (isString(hostSourceFileInfo)) {
if (isFileMissingOnHost(hostSourceFileInfo)) {
(missingFilePathsRequestedForRelease || (missingFilePathsRequestedForRelease = [])).push(oldSourceFile.path);
}
else if (hostSourceFileInfo.sourceFile === oldSourceFile) {
else if ((hostSourceFileInfo as FilePresentOnHost).sourceFile === oldSourceFile) {
sourceFilesCache.delete(oldSourceFile.path);
resolutionCache.removeResolutionsOfFile(oldSourceFile.path);
}
Expand Down Expand Up @@ -803,27 +832,12 @@ namespace ts {

function onSourceFileChange(fileName: string, eventKind: FileWatcherEventKind, path: Path) {
updateCachedSystemWithFile(fileName, path, eventKind);
const hostSourceFile = sourceFilesCache.get(path);
if (hostSourceFile) {
// Update the cache
if (eventKind === FileWatcherEventKind.Deleted) {
resolutionCache.invalidateResolutionOfFile(path);
if (!isString(hostSourceFile)) {
hostSourceFile.fileWatcher.close();
sourceFilesCache.set(path, (++hostSourceFile.version).toString());
}
}
else {
// Deleted file created
if (isString(hostSourceFile)) {
sourceFilesCache.delete(path);
}
else {
// file changed - just update the version
hostSourceFile.version++;
}
}

// Update the source file cache
if (eventKind === FileWatcherEventKind.Deleted && sourceFilesCache.get(path)) {
resolutionCache.invalidateResolutionOfFile(path);
}
nextSourceFileVersion(path);

// Update the program
scheduleProgramUpdate();
Expand Down Expand Up @@ -851,7 +865,7 @@ namespace ts {
missingFilesMap.delete(missingFilePath);

// Delete the entry in the source files cache so that new source file is created
removeSourceFile(missingFilePath);
nextSourceFileVersion(missingFilePath);

// When a missing file is created, we should update the graph.
scheduleProgramUpdate();
Expand Down Expand Up @@ -880,17 +894,10 @@ namespace ts {
const fileOrDirectoryPath = toPath(fileOrDirectory);

// Since the file existance changed, update the sourceFiles cache
const result = cachedDirectoryStructureHost && cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);

// Instead of deleting the file, mark it as changed instead
// Many times node calls add/remove/file when watching directories recursively
const hostSourceFile = sourceFilesCache.get(fileOrDirectoryPath);
if (hostSourceFile && !isString(hostSourceFile) && (result ? result.fileExists : directoryStructureHost.fileExists(fileOrDirectory))) {
hostSourceFile.version++;
}
else {
removeSourceFile(fileOrDirectoryPath);
if (cachedDirectoryStructureHost) {
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
}
nextSourceFileVersion(fileOrDirectoryPath);

// If the the added or created file or directory is not supported file name, ignore the file
// But when watched directory is added/removed, we need to reload the file list
Expand Down
32 changes: 32 additions & 0 deletions src/harness/unittests/tscWatchMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1719,6 +1719,38 @@ namespace ts.tscWatch {
return [files[0]];
}
});

it("file is deleted and created as part of change", () => {
const projectLocation = "/home/username/project";
const file: FileOrFolder = {
path: `${projectLocation}/app/file.ts`,
content: "var a = 10;"
};
const fileJs = `${projectLocation}/app/file.js`;
const configFile: FileOrFolder = {
path: `${projectLocation}/tsconfig.json`,
content: JSON.stringify({
include: [
"app/**/*.ts"
]
})
};
const files = [file, configFile, libFile];
const host = createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true });
createWatchOfConfigFile("tsconfig.json", host);
verifyProgram();

file.content += "\nvar b = 10;";

host.reloadFS(files, { invokeFileDeleteCreateAsPartInsteadOfChange: true });
host.runQueuedTimeoutCallbacks();
verifyProgram();

function verifyProgram() {
assert.isTrue(host.fileExists(fileJs));
assert.equal(host.readFile(fileJs), file.content + "\n");
}
});
});

describe("tsc-watch module resolution caching", () => {
Expand Down
18 changes: 14 additions & 4 deletions src/harness/virtualFileSystemWithWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,12 @@ interface Array<T> {}`
}

export interface ReloadWatchInvokeOptions {
/** Invokes the directory watcher for the parent instead of the file changed */
invokeDirectoryWatcherInsteadOfFileChanged: boolean;
/** When new file is created, do not invoke watches for it */
ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
/** Invoke the file delete, followed by create instead of file changed */
invokeFileDeleteCreateAsPartInsteadOfChange: boolean;
}

export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost {
Expand Down Expand Up @@ -315,12 +319,18 @@ interface Array<T> {}`
if (isString(fileOrDirectory.content)) {
// Update file
if (currentEntry.content !== fileOrDirectory.content) {
currentEntry.content = fileOrDirectory.content;
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath);
if (options && options.invokeFileDeleteCreateAsPartInsteadOfChange) {
this.removeFileOrFolder(currentEntry, returnFalse);
this.ensureFileOrFolder(fileOrDirectory);
}
else {
this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed);
currentEntry.content = fileOrDirectory.content;
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath);
}
else {
this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed);
}
}
}
}
Expand Down