mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Merge pull request #21693 from Microsoft/fileChangeThroughDeleteAndCreate
Handle file versioning little better in tsc --watch mode
This commit is contained in:
+62
-60
@@ -420,6 +420,8 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
const initialVersion = 1;
|
||||
|
||||
/**
|
||||
* Creates the watch from the host for root files and compiler options
|
||||
*/
|
||||
@@ -429,11 +431,17 @@ 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
|
||||
@@ -441,7 +449,7 @@ namespace ts {
|
||||
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
|
||||
@@ -480,14 +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),
|
||||
@@ -575,7 +583,9 @@ namespace ts {
|
||||
|
||||
// Compile the program
|
||||
if (loggingEnabled) {
|
||||
writeLog(`CreatingProgramWith::\n roots: ${JSON.stringify(rootFileNames)}\n options: ${JSON.stringify(compilerOptions)}`);
|
||||
writeLog(`CreatingProgramWith::`);
|
||||
writeLog(` roots: ${JSON.stringify(rootFileNames)}`);
|
||||
writeLog(` options: ${JSON.stringify(compilerOptions)}`);
|
||||
}
|
||||
|
||||
const needsUpdateInTypeRootWatch = hasChangedCompilerOptions || !program;
|
||||
@@ -627,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);
|
||||
@@ -640,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;
|
||||
@@ -697,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) {
|
||||
@@ -721,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);
|
||||
}
|
||||
@@ -808,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();
|
||||
@@ -856,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();
|
||||
@@ -885,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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user