From ef5935b52ca1b4e14e03710b052d84b1b24d27de Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 24 Jul 2017 16:57:49 -0700 Subject: [PATCH] Initial refactoring so that watch from tsc follows the tsserver projects --- src/compiler/commandLineParser.ts | 2 +- src/compiler/core.ts | 164 +++++++ src/compiler/program.ts | 126 +++++- src/compiler/tsc.ts | 705 ++++++++++++++++++++++-------- src/compiler/types.ts | 6 +- src/compiler/utilities.ts | 93 ++++ src/server/editorServices.ts | 2 +- src/server/lsHost.ts | 111 +---- src/server/project.ts | 94 ++-- src/server/utilities.ts | 73 ---- src/services/services.ts | 92 +--- src/services/utilities.ts | 20 - 12 files changed, 970 insertions(+), 518 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 5a12f3d0ce4..e3348892844 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1979,7 +1979,7 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray): ExpandResult { + export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { basePath = normalizePath(basePath); const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2bec904437e..cbccdcbed02 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2559,4 +2559,168 @@ namespace ts { export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) { return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs; } + + export interface HostForCaching { + useCaseSensitiveFileNames: boolean; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + } + + export interface CachedHost { + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + addOrDeleteFileOrFolder(fileOrFolder: string): void; + clearCache(): void; + } + + export function createCachedHost(host: HostForCaching): CachedHost { + const cachedReadDirectoryResult = createMap(); + const getCurrentDirectory = memoize(() => host.getCurrentDirectory()); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + return { + writeFile, + fileExists, + directoryExists, + createDirectory, + getCurrentDirectory, + getDirectories, + readDirectory, + addOrDeleteFileOrFolder, + clearCache + }; + + function toPath(fileName: string) { + return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); + } + + function getFileSystemEntries(rootDir: string) { + const path = toPath(rootDir); + const cachedResult = cachedReadDirectoryResult.get(path); + if (cachedResult) { + return cachedResult; + } + + const resultFromHost: FileSystemEntries = { + files: host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], + directories: host.getDirectories(rootDir) || [] + }; + + cachedReadDirectoryResult.set(path, resultFromHost); + return resultFromHost; + } + + function canWorkWithCacheForDir(rootDir: string) { + // Some of the hosts might not be able to handle read directory or getDirectories + const path = toPath(rootDir); + if (cachedReadDirectoryResult.get(path)) { + return true; + } + try { + return getFileSystemEntries(rootDir); + } + catch (_e) { + return false; + } + } + + function fileNameEqual(name1: string, name2: string) { + return getCanonicalFileName(name1) === getCanonicalFileName(name2); + } + + function hasEntry(entries: ReadonlyArray, name: string) { + return some(entries, file => fileNameEqual(file, name)); + } + + function updateFileSystemEntry(entries: ReadonlyArray, baseName: string, isValid: boolean) { + if (hasEntry(entries, baseName)) { + if (!isValid) { + return filter(entries, entry => !fileNameEqual(entry, baseName)); + } + } + else if (isValid) { + return entries.concat(baseName); + } + return entries; + } + + function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { + const path = toPath(fileName); + const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseFileName = getBaseFileName(normalizePath(fileName)); + if (result) { + result.files = updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true); + } + return host.writeFile(fileName, data, writeByteOrderMark); + } + + function fileExists(fileName: string): boolean { + const path = toPath(fileName); + const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseName = getBaseFileName(normalizePath(fileName)); + return (result && hasEntry(result.files, baseName)) || host.fileExists(fileName); + } + + function directoryExists(dirPath: string): boolean { + const path = toPath(dirPath); + return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath); + } + + function createDirectory(dirPath: string) { + const path = toPath(dirPath); + const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseFileName = getBaseFileName(path); + if (result) { + result.directories = updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true); + } + host.createDirectory(dirPath); + } + + function getDirectories(rootDir: string): string[] { + if (canWorkWithCacheForDir(rootDir)) { + return getFileSystemEntries(rootDir).directories.slice(); + } + return host.getDirectories(rootDir); + } + function readDirectory(rootDir: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, includes?: ReadonlyArray, depth?: number): string[] { + if (canWorkWithCacheForDir(rootDir)) { + return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, path => getFileSystemEntries(path)); + } + return host.readDirectory(rootDir, extensions, excludes, includes, depth); + } + + function addOrDeleteFileOrFolder(fileOrFolder: string) { + const path = toPath(fileOrFolder); + const existingResult = cachedReadDirectoryResult.get(path); + if (existingResult) { + if (!host.directoryExists(fileOrFolder)) { + cachedReadDirectoryResult.delete(path); + } + } + else { + // Was this earlier file + const parentResult = cachedReadDirectoryResult.get(getDirectoryPath(path)); + if (parentResult) { + const baseName = getBaseFileName(fileOrFolder); + if (parentResult) { + parentResult.files = updateFileSystemEntry(parentResult.files, baseName, host.fileExists(path)); + parentResult.directories = updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(path)); + } + } + } + } + + function clearCache() { + cachedReadDirectoryResult.clear(); + } + } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 8bb7427fa2f..0c319b18053 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -386,6 +386,114 @@ namespace ts { allDiagnostics?: Diagnostic[]; } + export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, getSourceVersion: (path: Path) => string): boolean { + // If we haven't create a program yet, then it is not up-to-date + if (!program) { + return false; + } + + // If number of files in the program do not match, it is not up-to-date + if (program.getRootFileNames().length !== rootFileNames.length) { + return false; + } + + const fileNames = concatenate(rootFileNames, map(program.getSourceFiles(), sourceFile => sourceFile.fileName)); + // If any file is not up-to-date, then the whole program is not up-to-date + for (const fileName of fileNames) { + if (!sourceFileUpToDate(program.getSourceFile(fileName))) { + return false; + } + } + + const currentOptions = program.getCompilerOptions(); + // If the compilation settings do no match, then the program is not up-to-date + if (!compareDataObjects(currentOptions, newOptions)) { + return false; + } + + // If everything matches but the text of config file is changed, + // error locations can change for program options, so update the program + if (currentOptions.configFile && newOptions.configFile) { + return currentOptions.configFile.text === newOptions.configFile.text; + } + + return true; + + function sourceFileUpToDate(sourceFile: SourceFile): boolean { + if (!sourceFile) { + return false; + } + return sourceFile.version === getSourceVersion(sourceFile.path); + } + } + + function shouldProgramCreateNewSourceFiles(program: Program, newOptions: CompilerOptions) { + // If any of these options change, we cant reuse old source file even if version match + const oldOptions = program && program.getCompilerOptions(); + return oldOptions && + (oldOptions.target !== newOptions.target || + oldOptions.module !== newOptions.module || + oldOptions.moduleResolution !== newOptions.moduleResolution || + oldOptions.noResolve !== newOptions.noResolve || + oldOptions.jsx !== newOptions.jsx || + oldOptions.allowJs !== newOptions.allowJs || + oldOptions.disableSizeLimit !== newOptions.disableSizeLimit || + oldOptions.baseUrl !== newOptions.baseUrl || + !equalOwnProperties(oldOptions.paths, newOptions.paths)); + } + + /** + * Updates the existing missing file watches with the new set of missing files after new program is created + * @param program + * @param existingMap + * @param createMissingFileWatch + * @param closeExistingFileWatcher + */ + export function updateMissingFilePathsWatch(program: Program, existingMap: Map, + createMissingFileWatch: (missingFilePath: Path) => FileWatcher, + closeExistingFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void) { + + const missingFilePaths = program.getMissingFilePaths(); + const newMissingFilePathMap = arrayToSet(missingFilePaths); + // Update the missing file paths watcher + return mutateExistingMapWithNewSet( + existingMap, newMissingFilePathMap, + // Watch the missing files + createMissingFileWatch, + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + closeExistingFileWatcher + ); + } + + export type WildCardDirectoryWatchers = { watcher: FileWatcher, recursive: boolean }; + + export function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, + watchDirectory: (directory: string, recursive: boolean) => FileWatcher, + closeDirectoryWatcher: (directory: string, watcher: FileWatcher, recursive: boolean, recursiveChanged: boolean) => void) { + return mutateExistingMap( + existingWatchedForWildcards, wildcardDirectories, + // Create new watch and recursive info + (directory, flag) => { + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return { + watcher: watchDirectory(directory, recursive), + recursive + }; + }, + // Close existing watch thats not needed any more + (directory, { watcher, recursive }) => closeDirectoryWatcher(directory, watcher, recursive, /*recursiveChanged*/ false), + // Watcher is same if the recursive flags match + ({ recursive: existingRecursive }, flag) => { + // If the recursive dont match, it needs update + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return existingRecursive !== recursive; + }, + // Close existing watch that doesnt match in recursive flag + (directory, { watcher, recursive }) => closeDirectoryWatcher(directory, watcher, recursive, /*recursiveChanged*/ true) + ); + } + /** * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' * that represent a compilation unit. @@ -478,6 +586,7 @@ namespace ts { // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); if (structuralIsReused !== StructureIsReused.Completely) { forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false)); @@ -519,6 +628,17 @@ namespace ts { // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; + // Release any files we have acquired in the old program but are + // not part of the new program. + if (oldProgram && host.onReleaseOldSourceFile) { + const oldSourceFiles = oldProgram.getSourceFiles(); + for (const oldSourceFile of oldSourceFiles) { + if (!getSourceFile(oldSourceFile.path) || shouldCreateNewSourceFile) { + host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions()); + } + } + } + // unconditionally set oldProgram to undefined to prevent it from being captured in closure oldProgram = undefined; @@ -783,8 +903,8 @@ namespace ts { for (const oldSourceFile of oldProgram.getSourceFiles()) { const newSourceFile = host.getSourceFileByPath - ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target) - : host.getSourceFile(oldSourceFile.fileName, options.target); + ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target, /*onError*/ undefined, shouldCreateNewSourceFile) + : host.getSourceFile(oldSourceFile.fileName, options.target, /*onError*/ undefined, shouldCreateNewSourceFile); if (!newSourceFile) { return oldProgram.structureIsReused = StructureIsReused.Not; @@ -1593,7 +1713,7 @@ namespace ts { else { fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage)); } - }); + }, shouldCreateNewSourceFile); filesByName.set(path, file); if (file) { diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index db25afe45b6..cf793919a4c 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -98,22 +98,9 @@ namespace ts { export function executeCommandLine(args: string[]): void { const commandLine = parseCommandLine(args); - let configFileName: string; // Configuration file name (if any) - let cachedConfigFileText: string; // Cached configuration file text, used for reparsing (if any) - let configFileWatcher: FileWatcher; // Configuration file watcher - let directoryWatcher: FileWatcher; // Directory watcher to monitor source file addition/removal - let cachedProgram: Program; // Program cached from last compilation - let rootFileNames: string[]; // Root fileNames for compilation - let compilerOptions: CompilerOptions; // Compiler options for compilation - let compilerHost: CompilerHost; // Compiler host - let hostGetSourceFile: typeof compilerHost.getSourceFile; // getSourceFile method from default host - let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation - let timerHandleForDirectoryChanges: any; // Handle for 0.25s wait timer to trigger directory change handler - // This map stores and reuses results of fileExists check that happen inside 'createProgram' - // This allows to save time in module resolution heavy scenarios when existence of the same file might be checked multiple times. - let cachedExistingFiles: Map; - let hostFileExists: typeof compilerHost.fileExists; + // Configuration file name (if any) + let configFileName: string; if (commandLine.options.locale) { if (!isJSONSupported()) { @@ -126,7 +113,7 @@ namespace ts { // If there are any errors due to command line parsing and/or // setting up localization, report them and quit. if (commandLine.errors.length > 0) { - reportDiagnostics(commandLine.errors, compilerHost); + reportDiagnostics(commandLine.errors, /*host*/ undefined); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } @@ -183,232 +170,570 @@ namespace ts { return sys.exit(ExitStatus.Success); } - if (isWatchSet(commandLine.options)) { - if (!sys.watchFile) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - if (configFileName) { - configFileWatcher = sys.watchFile(configFileName, configFileChanged); - } - if (sys.watchDirectory && configFileName) { - const directory = ts.getDirectoryPath(configFileName); - directoryWatcher = sys.watchDirectory( - // When the configFileName is just "tsconfig.json", the watched directory should be - // the current directory; if there is a given "project" parameter, then the configFileName - // is an absolute file name. - directory === "" ? "." : directory, - watchedDirectoryChanged, /*recursive*/ true); - } - } - - performCompilation(); - - function parseConfigFile(): ParsedCommandLine { - if (!cachedConfigFileText) { - try { - cachedConfigFileText = sys.readFile(configFileName); - } - catch (e) { - const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - reportWatchDiagnostic(error); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; - } - } - if (!cachedConfigFileText) { - const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - reportDiagnostics([error], /* compilerHost */ undefined); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; - } - - const result = parseJsonText(configFileName, cachedConfigFileText); - reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); - - const cwd = sys.getCurrentDirectory(); - const configParseResult = parseJsonSourceFileConfigFileContent(result, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); - reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); - + if (configFileName) { + const configParseResult = parseConfigFile(configFileName, commandLine, sys); + const { fileNames, options } = configParseResult; if (isWatchSet(configParseResult.options)) { - if (!sys.watchFile) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - if (!directoryWatcher && sys.watchDirectory && configFileName) { - const directory = ts.getDirectoryPath(configFileName); - directoryWatcher = sys.watchDirectory( - // When the configFileName is just "tsconfig.json", the watched directory should be - // the current directory; if there is a given "project" parameter, then the configFileName - // is an absolute file name. - directory === "" ? "." : directory, - watchedDirectoryChanged, /*recursive*/ true); - } + reportWatchModeWithoutSysSupport(); + createWatchMode(commandLine, configFileName, fileNames, options, configParseResult.configFileSpecs, configParseResult.wildcardDirectories); } - return configParseResult; + else { + performCompilation(fileNames, options); + } + } + else if (isWatchSet(commandLine.options)) { + reportWatchModeWithoutSysSupport(); + createWatchMode(commandLine); + } + else { + performCompilation(commandLine.fileNames, commandLine.options); } - // Invoked to perform initial compilation or re-compilation in watch mode - function performCompilation() { - - if (!cachedProgram) { - if (configFileName) { - const configParseResult = parseConfigFile(); - rootFileNames = configParseResult.fileNames; - compilerOptions = configParseResult.options; - } - else { - rootFileNames = commandLine.fileNames; - compilerOptions = commandLine.options; - } - compilerHost = createCompilerHost(compilerOptions); - hostGetSourceFile = compilerHost.getSourceFile; - compilerHost.getSourceFile = getSourceFile; - - hostFileExists = compilerHost.fileExists; - compilerHost.fileExists = cachedFileExists; + function reportWatchModeWithoutSysSupport() { + if (!sys.watchFile || !sys.watchDirectory) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } + } + function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { if (compilerOptions.pretty) { reportDiagnosticWorker = reportDiagnosticWithColorAndContext; } - // reset the cache of existing files - cachedExistingFiles = createMap(); - + const compilerHost = createCompilerHost(compilerOptions); const compileResult = compile(rootFileNames, compilerOptions, compilerHost); + return sys.exit(compileResult.exitStatus); + } + } - if (!isWatchSet(compilerOptions)) { - return sys.exit(compileResult.exitStatus); + interface HostFileInfo { + version: number; + sourceFile: SourceFile; + fileWatcher: FileWatcher; + } + + function createWatchMode(commandLine: ParsedCommandLine, configFileName?: string, configFileRootFiles?: string[], configFileOptions?: CompilerOptions, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike) { + let program: Program; + let needsReload: boolean; + let missingFilesMap: Map; + let configFileWatcher: FileWatcher; + let watchedWildCardDirectories: Map; + let timerToUpdateProgram: any; + + let compilerOptions: CompilerOptions; + let rootFileNames: string[]; + + const sourceFilesCache = createMap(); + + let host: System; + if (configFileName) { + rootFileNames = configFileRootFiles; + compilerOptions = configFileOptions; + host = createCachedSystem(sys); + configFileWatcher = sys.watchFile(configFileName, onConfigFileChanged); + } + else { + rootFileNames = commandLine.fileNames; + compilerOptions = commandLine.options; + host = sys; + } + const currentDirectory = host.getCurrentDirectory(); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + + if (compilerOptions.pretty) { + reportDiagnosticWorker = reportDiagnosticWithColorAndContext; + } + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); + + function synchronizeProgram() { + writeLog(`Synchronizing program`); + + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion)) { + return; } - setCachedProgram(compileResult.program); + // Create the compiler host + const compilerHost = createWatchedCompilerHost(compilerOptions); + program = compile(rootFileNames, compilerOptions, compilerHost, program).program; + + // Update watches + missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); - - const missingPaths = compileResult.program.getMissingFilePaths(); - missingPaths.forEach(path => { - const fileWatcher = sys.watchFile(path, (_fileName, eventKind) => { - if (eventKind === FileWatcherEventKind.Created) { - fileWatcher.close(); - startTimerForRecompilation(); - } - }); - }); } - function cachedFileExists(fileName: string): boolean { - let fileExists = cachedExistingFiles.get(fileName); - if (fileExists === undefined) { - cachedExistingFiles.set(fileName, fileExists = hostFileExists(fileName)); + function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { + const existingDirectories = createMap(); + function directoryExists(directoryPath: string): boolean { + if (existingDirectories.has(directoryPath)) { + return true; + } + if (host.directoryExists(directoryPath)) { + existingDirectories.set(directoryPath, true); + return true; + } + return false; } - return fileExists; - } - function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void) { - // Return existing SourceFile object if one is available - if (cachedProgram) { - const sourceFile = cachedProgram.getSourceFile(fileName); - // A modified source file has no watcher and should not be reused - if (sourceFile && sourceFile.fileWatcher) { - return sourceFile; + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); } } - // Use default host function - const sourceFile = hostGetSourceFile(fileName, languageVersion, onError); - if (sourceFile && isWatchSet(compilerOptions) && sys.watchFile) { - // Attach a file watcher - sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName, eventKind) => sourceFileChanged(sourceFile, eventKind)); - } - return sourceFile; - } - // Change cached program to the given program - function setCachedProgram(program: Program) { - if (cachedProgram) { - const newSourceFiles = program ? program.getSourceFiles() : undefined; - forEach(cachedProgram.getSourceFiles(), sourceFile => { - if (!(newSourceFiles && contains(newSourceFiles, sourceFile))) { - if (sourceFile.fileWatcher) { - sourceFile.fileWatcher.close(); - sourceFile.fileWatcher = undefined; - } + type OutputFingerprint = { + hash: string; + byteOrderMark: boolean; + mtime: Date; + }; + let outputFingerprints: Map; + + function writeFileIfUpdated(fileName: string, data: string, writeByteOrderMark: boolean): void { + if (!outputFingerprints) { + outputFingerprints = createMap(); + } + + const hash = host.createHash(data); + const mtimeBefore = host.getModifiedTime(fileName); + + if (mtimeBefore) { + const fingerprint = outputFingerprints.get(fileName); + // If output has not been changed, and the file has no external modification + if (fingerprint && + fingerprint.byteOrderMark === writeByteOrderMark && + fingerprint.hash === hash && + fingerprint.mtime.getTime() === mtimeBefore.getTime()) { + return; } + } + + host.writeFile(fileName, data, writeByteOrderMark); + + const mtimeAfter = host.getModifiedTime(fileName); + + outputFingerprints.set(fileName, { + hash, + byteOrderMark: writeByteOrderMark, + mtime: mtimeAfter }); } - cachedProgram = program; - } - // If a source file changes, mark it as unwatched and start the recompilation timer - function sourceFileChanged(sourceFile: SourceFile, eventKind: FileWatcherEventKind) { - sourceFile.fileWatcher.close(); - sourceFile.fileWatcher = undefined; - if (eventKind === FileWatcherEventKind.Deleted) { - unorderedRemoveItem(rootFileNames, sourceFile.fileName); - } - startTimerForRecompilation(); - } + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void) { + try { + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - // If the configuration file changes, forget cached program and start the recompilation timer - function configFileChanged() { - setCachedProgram(undefined); - cachedConfigFileText = undefined; - startTimerForRecompilation(); - } + //if (isWatchSet(options) && sys.createHash && sys.getModifiedTime) { + writeFileIfUpdated(fileName, data, writeByteOrderMark); + //} + //else { + //host.writeFile(fileName, data, writeByteOrderMark); + //} - function watchedDirectoryChanged(fileName: string) { - if (fileName && !ts.isSupportedSourceFileName(fileName, compilerOptions)) { - return; + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + if (onError) { + onError(e.message); + } + } } - startTimerForHandlingDirectoryChanges(); + const newLine = getNewLineCharacter(options); + const realpath = host.realpath && ((path: string) => host.realpath(path)); + + return { + getSourceFile: getVersionedSourceFile, + getSourceFileByPath: getVersionedSourceFileByPath, + getDefaultLibLocation, + getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), + writeFile, + getCurrentDirectory: memoize(() => host.getCurrentDirectory()), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, + getCanonicalFileName, + getNewLine: () => newLine, + fileExists, + readFile: fileName => host.readFile(fileName), + trace: (s: string) => host.write(s + newLine), + directoryExists: directoryName => host.directoryExists(directoryName), + getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", + getDirectories: (path: string) => host.getDirectories(path), + realpath, + onReleaseOldSourceFile + }; + + // TODO: cache module resolution + //if (host.resolveModuleNames) { + // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + //} + //if (host.resolveTypeReferenceDirectives) { + // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { + // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); + // }; + //} } - function startTimerForHandlingDirectoryChanges() { - if (!sys.setTimeout || !sys.clearTimeout) { - return; + function fileExists(fileName: string) { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const hostSourceFileInfo = sourceFilesCache.get(path); + if (hostSourceFileInfo !== undefined) { + return !isString(hostSourceFileInfo); } - if (timerHandleForDirectoryChanges) { - sys.clearTimeout(timerHandleForDirectoryChanges); - } - timerHandleForDirectoryChanges = sys.setTimeout(directoryChangeHandler, 250); + return host.fileExists(fileName); } - function directoryChangeHandler() { - const parsedCommandLine = parseConfigFile(); - const newFileNames = ts.map(parsedCommandLine.fileNames, compilerHost.getCanonicalFileName); - const canonicalRootFileNames = ts.map(rootFileNames, compilerHost.getCanonicalFileName); + function getDefaultLibLocation(): string { + return getDirectoryPath(normalizePath(host.getExecutingFilePath())); + } - // We check if the project file list has changed. If so, we just throw away the old program and start fresh. - if (!arrayIsEqualTo(newFileNames && newFileNames.sort(), canonicalRootFileNames && canonicalRootFileNames.sort())) { - setCachedProgram(undefined); - startTimerForRecompilation(); + function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + } + + 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)) { + return undefined; + } + + // Create new source file if requested or the versions dont match + if (!hostSourceFile) { + const sourceFile = getSourceFile(fileName, languageVersion, onError); + if (sourceFile) { + sourceFile.version = "0"; + const fileWatcher = watchSourceFileForChanges(sourceFile.path); + sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + } + else { + sourceFilesCache.set(path, "0"); + } + return sourceFile; + } + else if (shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { + if (shouldCreateNewSourceFile) { + hostSourceFile.version++; + } + const newSourceFile = getSourceFile(fileName, languageVersion, onError); + if (newSourceFile) { + newSourceFile.version = hostSourceFile.version.toString(); + hostSourceFile.sourceFile = newSourceFile; + } + else { + // File doesnt exist any more + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, hostSourceFile.version.toString()); + } + + return newSourceFile; + } + + return hostSourceFile.sourceFile; + + function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile { + let text: string; + try { + performance.mark("beforeIORead"); + text = host.readFile(fileName, compilerOptions.charset); + performance.mark("afterIORead"); + performance.measure("I/O Read", "beforeIORead", "afterIORead"); + } + catch (e) { + if (onError) { + onError(e.message); + } + text = ""; + } + + return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; + } + } + + function removeSourceFile(path: Path) { + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile !== undefined) { + if (!isString(hostSourceFile)) { + hostSourceFile.fileWatcher.close(); + } + sourceFilesCache.delete(path); + } + } + + function getSourceVersion(path: Path): string { + const hostSourceFile = sourceFilesCache.get(path); + return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString(); + } + + function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) { + const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path); + // If this is the source file thats in the cache and new program doesnt need it, + // remove the cached entry. + // Note we arent deleting entry if file became missing in new program or + // there was version update and new source file was created. + if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { + sourceFilesCache.delete(oldSourceFile.path); } } // Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch // operations (such as saving all modified files in an editor) a chance to complete before we kick // off a new compilation. - function startTimerForRecompilation() { + function scheduleProgramUpdate() { if (!sys.setTimeout || !sys.clearTimeout) { return; } - if (timerHandleForRecompilation) { - sys.clearTimeout(timerHandleForRecompilation); + if (timerToUpdateProgram) { + sys.clearTimeout(timerToUpdateProgram); } - timerHandleForRecompilation = sys.setTimeout(recompile, 250); + timerToUpdateProgram = sys.setTimeout(updateProgram, 250); } - function recompile() { - timerHandleForRecompilation = undefined; + function scheduleProgramReload() { + Debug.assert(!!configFileName); + needsReload = true; + scheduleProgramUpdate(); + } + + function updateProgram() { + timerToUpdateProgram = undefined; reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - performCompilation(); + + if (needsReload) { + reloadConfigFile(); + } + else { + synchronizeProgram(); + } + } + + function reloadConfigFile() { + writeLog(`Reloading config file: ${configFileName}`); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); + + needsReload = false; + + const cachedHost = host as CachedSystem; + cachedHost.clearCache(); + const configParseResult = parseConfigFile(configFileName, commandLine, cachedHost); + rootFileNames = configParseResult.fileNames; + compilerOptions = configParseResult.options; + configFileSpecs = configParseResult.configFileSpecs; + configFileWildCardDirectories = configParseResult.wildcardDirectories; + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); + } + + function watchSourceFileForChanges(path: Path) { + return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind)); + } + + function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { + writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile) { + // Update the cache + if (eventKind === FileWatcherEventKind.Deleted) { + 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 program + scheduleProgramUpdate(); + } + + function watchMissingFilePath(missingFilePath: Path) { + return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); + } + + function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) { + fileWatcher.close(); + } + + function onMissingFileChange(filename: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { + writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${filename}`); + if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { + closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); + missingFilesMap.delete(missingFilePath); + + if (configFileName) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); + } + + // Delete the entry in the source files cache so that new source file is created + removeSourceFile(missingFilePath); + + // When a missing file is created, we should update the graph. + scheduleProgramUpdate(); + } + } + + function watchConfigFileWildCardDirectories() { + const wildcards = createMapFromTemplate(configFileWildCardDirectories); + watchedWildCardDirectories = updateWatchingWildcardDirectories( + watchedWildCardDirectories, wildcards, + watchWildCardDirectory, stopWatchingWildCardDirectory + ); + } + + function watchWildCardDirectory(directory: string, recursive: boolean) { + return host.watchDirectory(directory, fileName => + onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileName, directory)), + recursive); + } + + function stopWatchingWildCardDirectory(_directory: string, fileWatcher: FileWatcher, _recursive: boolean, _recursiveChanged: boolean) { + fileWatcher.close(); + } + + function onFileAddOrRemoveInWatchedDirectory(fileName: string) { + Debug.assert(!!configFileName); + (host as CachedSystem).addOrDeleteFileOrFolder(fileName); + + // Since the file existance changed, update the sourceFiles cache + removeSourceFile(toPath(fileName, currentDirectory, getCanonicalFileName)); + + // If a change was made inside "folder/file", node will trigger the callback twice: + // one with the fileName being "folder/file", and the other one with "folder". + // We don't respond to the second one. + if (fileName && !isSupportedSourceFileName(fileName, compilerOptions)) { + writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileName}`); + return; + } + + writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileName}`); + + // Reload is pending, do the reload + if (!needsReload) { + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtensions*/ []); + if (!configFileSpecs.filesSpecs) { + reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], /*host*/ undefined); + } + rootFileNames = result.fileNames; + + // Schedule Update the program + scheduleProgramUpdate(); + } + } + + function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) { + writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + scheduleProgramReload(); + } + + function writeLog(s: string) { + const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; + if (hasDiagnostics) { + host.write(s); + } } } - function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost) { + interface CachedSystem extends System { + addOrDeleteFileOrFolder(fileOrFolder: string): void; + clearCache(): void; + } + + function createCachedSystem(host: System): CachedSystem { + const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined; + const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined; + const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined; + const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined; + const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined; + const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined; + const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined; + const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined; + const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; + const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; + + const cachedHost = createCachedHost(host); + return { + args: host.args, + newLine: host.newLine, + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, + write: s => host.write(s), + readFile: (path, encoding?) => host.readFile(path, encoding), + getFileSize, + writeFile: (fileName, data, writeByteOrderMark?) => cachedHost.writeFile(fileName, data, writeByteOrderMark), + watchFile, + watchDirectory, + resolvePath: path => host.resolvePath(path), + fileExists: fileName => cachedHost.fileExists(fileName), + directoryExists: dir => cachedHost.directoryExists(dir), + createDirectory: dir => cachedHost.createDirectory(dir), + getExecutingFilePath: () => host.getExecutingFilePath(), + getCurrentDirectory: () => cachedHost.getCurrentDirectory(), + getDirectories: dir => cachedHost.getDirectories(dir), + readDirectory: (path, extensions, excludes, includes, depth) => cachedHost.readDirectory(path, extensions, excludes, includes, depth), + getModifiedTime, + createHash, + getMemoryUsage, + exit: exitCode => host.exit(exitCode), + realpath, + getEnvironmentVariable: name => host.getEnvironmentVariable(name), + tryEnableSourceMapsForHost, + debugMode: host.debugMode, + setTimeout, + clearTimeout, + addOrDeleteFileOrFolder: fileOrFolder => cachedHost.addOrDeleteFileOrFolder(fileOrFolder), + clearCache: () => cachedHost.clearCache() + }; + } + + function parseConfigFile(configFileName: string, commandLine: ParsedCommandLine, host: System): ParsedCommandLine { + let configFileText: string; + try { + configFileText = host.readFile(configFileName); + } + catch (e) { + const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); + reportWatchDiagnostic(error); + host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + if (!configFileText) { + const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); + reportDiagnostics([error], /* compilerHost */ undefined); + host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + + const result = parseJsonText(configFileName, configFileText); + reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); + + const cwd = host.getCurrentDirectory(); + const configParseResult = parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); + reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); + + return configParseResult; + } + + function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, oldProgram?: Program) { const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; let statistics: Statistic[]; if (hasDiagnostics) { @@ -416,7 +741,7 @@ namespace ts { statistics = []; } - const program = createProgram(fileNames, compilerOptions, compilerHost); + const program = createProgram(fileNames, compilerOptions, compilerHost, oldProgram); const exitStatus = compileProgram(); if (compilerOptions.listFiles) { @@ -481,6 +806,8 @@ namespace ts { } } + // TODO: in watch mode to emit only affected files + // Otherwise, emit and report any errors we ran into. const emitOutput = program.emit(); diagnostics = diagnostics.concat(emitOutput.diagnostics); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8929b19bfe3..45f5a16a80c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2326,6 +2326,7 @@ namespace ts { /* @internal */ patternAmbientModules?: PatternAmbientModule[]; /* @internal */ ambientModuleNames: ReadonlyArray; /* @internal */ checkJsDirective: CheckJsDirective | undefined; + /* @internal */ version: string; } export interface Bundle extends Node { @@ -3952,8 +3953,8 @@ namespace ts { } export interface CompilerHost extends ModuleResolutionHost { - getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile; - getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile; + getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile; + getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile; getCancellationToken?(): CancellationToken; getDefaultLibFileName(options: CompilerOptions): string; getDefaultLibLocation?(): string; @@ -3977,6 +3978,7 @@ namespace ts { */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; getEnvironmentVariable?(name: string): string; + onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void; } /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index facb0bdc2ee..08816382ecf 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3611,6 +3611,99 @@ namespace ts { export function getCombinedLocalAndExportSymbolFlags(symbol: Symbol): SymbolFlags { return symbol.exportSymbol ? symbol.exportSymbol.flags | symbol.flags : symbol.flags; } + + export function compareDataObjects(dst: any, src: any): boolean { + if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) { + return false; + } + + for (const e in dst) { + if (typeof dst[e] === "object") { + if (!compareDataObjects(dst[e], src[e])) { + return false; + } + } + else if (typeof dst[e] !== "function") { + if (dst[e] !== src[e]) { + return false; + } + } + } + return true; + } + + export function cleanExistingMap( + existingMap: Map, + onDeleteExistingValue: (key: string, existingValue: T) => void) { + if (existingMap) { + // Remove all + existingMap.forEach((existingValue, key) => { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + }); + } + } + + export function mutateExistingMapWithNewSet( + existingMap: Map, newMap: Map, + createNewValue: (key: string) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void + ): Map { + return mutateExistingMap( + existingMap, newMap, + /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), + onDeleteExistingValue, + ); + } + + export function mutateExistingMap( + existingMap: Map, newMap: Map, + createNewValue: (key: string, valueInNewMap: U) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void, + isSameValue?: (existingValue: T, valueInNewMap: U) => boolean, + OnDeleteExistingMismatchValue?: (key: string, existingValue: T) => void, + onSameExistingValue?: (existingValue: T, valueInNewMap: U) => void + ): Map { + // If there are new values update them + if (newMap) { + if (existingMap) { + // Needs update + existingMap.forEach((existingValue, key) => { + const valueInNewMap = newMap.get(key); + // Existing value - remove it + if (valueInNewMap === undefined) { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + } + // different value - remove it + else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) { + existingMap.delete(key); + OnDeleteExistingMismatchValue(key, existingValue); + } + else if (onSameExistingValue) { + onSameExistingValue(existingValue, valueInNewMap); + } + }); + } + else { + // Create new + existingMap = createMap(); + } + + // Add new values that are not already present + newMap.forEach((valueInNewMap, key) => { + if (!existingMap.has(key)) { + // New values + existingMap.set(key, createNewValue(key, valueInNewMap)); + } + }); + + return existingMap; + } + + cleanExistingMap(existingMap, onDeleteExistingValue); + return undefined; + } } namespace ts { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0b5cb8c119a..d617bdb2b9e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1350,7 +1350,7 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); + const cachedServerHost = new CachedServerHost(this.host); const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); this.logger.info(`Opened configuration file ${configFileName}`); return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedServerHost, clientFileName); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index fb1cd80b660..692547e4915 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -8,13 +8,12 @@ namespace ts.server { newLine: string; useCaseSensitiveFileNames: boolean; + private readonly cachedHost: CachedHost; + readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; - private cachedReadDirectoryResult = createMap(); - private readonly currentDirectory: string; - - constructor(private readonly host: ServerHost, private getCanonicalFileName: (fileName: string) => string) { + constructor(private readonly host: ServerHost) { this.args = host.args; this.newLine = host.newLine; this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames; @@ -24,41 +23,7 @@ namespace ts.server { if (this.host.realpath) { this.realpath = path => this.host.realpath(path); } - this.currentDirectory = this.host.getCurrentDirectory(); - } - - private toPath(fileName: string) { - return toPath(fileName, this.currentDirectory, this.getCanonicalFileName); - } - - private getFileSystemEntries(rootDir: string) { - const path = this.toPath(rootDir); - const cachedResult = this.cachedReadDirectoryResult.get(path); - if (cachedResult) { - return cachedResult; - } - - const resultFromHost: FileSystemEntries = { - files: this.host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], - directories: this.host.getDirectories(rootDir) || [] - }; - - this.cachedReadDirectoryResult.set(path, resultFromHost); - return resultFromHost; - } - - private canWorkWithCacheForDir(rootDir: string) { - // Some of the hosts might not be able to handle read directory or getDirectories - const path = this.toPath(rootDir); - if (this.cachedReadDirectoryResult.get(path)) { - return true; - } - try { - return this.getFileSystemEntries(rootDir); - } - catch (_e) { - return false; - } + this.cachedHost = createCachedHost(host); } write(s: string) { @@ -66,13 +31,7 @@ namespace ts.server { } writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) { - const path = this.toPath(fileName); - const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); - const baseFileName = getBaseFileName(toNormalizedPath(fileName)); - if (result) { - result.files = this.updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true); - } - return this.host.writeFile(fileName, data, writeByteOrderMark); + this.cachedHost.writeFile(fileName, data, writeByteOrderMark); } resolvePath(path: string) { @@ -88,7 +47,7 @@ namespace ts.server { } getCurrentDirectory() { - return this.currentDirectory; + return this.cachedHost.getCurrentDirectory(); } exit(exitCode?: number) { @@ -101,78 +60,32 @@ namespace ts.server { } getDirectories(rootDir: string) { - if (this.canWorkWithCacheForDir(rootDir)) { - return this.getFileSystemEntries(rootDir).directories.slice(); - } - return this.host.getDirectories(rootDir); + return this.cachedHost.getDirectories(rootDir); } readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { - if (this.canWorkWithCacheForDir(rootDir)) { - return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, path => this.getFileSystemEntries(path)); - } - return this.host.readDirectory(rootDir, extensions, excludes, includes, depth); + return this.cachedHost.readDirectory(rootDir, extensions, excludes, includes, depth); } fileExists(fileName: string): boolean { - const path = this.toPath(fileName); - const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); - const baseName = getBaseFileName(toNormalizedPath(fileName)); - return (result && this.hasEntry(result.files, baseName)) || this.host.fileExists(fileName); + return this.cachedHost.fileExists(fileName); } directoryExists(dirPath: string) { - const path = this.toPath(dirPath); - return this.cachedReadDirectoryResult.has(path) || this.host.directoryExists(dirPath); + return this.cachedHost.directoryExists(dirPath); } readFile(path: string, encoding?: string): string { return this.host.readFile(path, encoding); } - private fileNameEqual(name1: string, name2: string) { - return this.getCanonicalFileName(name1) === this.getCanonicalFileName(name2); - } - - private hasEntry(entries: ReadonlyArray, name: string) { - return some(entries, file => this.fileNameEqual(file, name)); - } - - private updateFileSystemEntry(entries: ReadonlyArray, baseName: string, isValid: boolean) { - if (this.hasEntry(entries, baseName)) { - if (!isValid) { - return filter(entries, entry => !this.fileNameEqual(entry, baseName)); - } - } - else if (isValid) { - return entries.concat(baseName); - } - return entries; - } addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath) { - const path = this.toPath(fileOrFolder); - const existingResult = this.cachedReadDirectoryResult.get(path); - if (existingResult) { - if (!this.host.directoryExists(fileOrFolder)) { - this.cachedReadDirectoryResult.delete(path); - } - } - else { - // Was this earlier file - const parentResult = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); - if (parentResult) { - const baseName = getBaseFileName(fileOrFolder); - if (parentResult) { - parentResult.files = this.updateFileSystemEntry(parentResult.files, baseName, this.host.fileExists(path)); - parentResult.directories = this.updateFileSystemEntry(parentResult.directories, baseName, this.host.directoryExists(path)); - } - } - } + return this.cachedHost.addOrDeleteFileOrFolder(fileOrFolder); } clearCache() { - this.cachedReadDirectoryResult = createMap(); + return this.cachedHost.clearCache(); } setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) { diff --git a/src/server/project.ts b/src/server/project.ts index 483dfe841a7..d7c062178c4 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -639,38 +639,13 @@ namespace ts.server { } } - const missingFilePaths = this.program.getMissingFilePaths(); - const newMissingFilePathMap = arrayToSet(missingFilePaths); // Update the missing file paths watcher - this.missingFilesMap = mutateExistingMapWithNewSet( - this.missingFilesMap, newMissingFilePathMap, + this.missingFilesMap = updateMissingFilePathsWatch(this.program, this.missingFilesMap, // Watch the missing files - missingFilePath => { - const fileWatcher = this.projectService.addFileWatcher( - WatchType.MissingFilePath, this, missingFilePath, - (filename, eventKind) => { - if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { - this.missingFilesMap.delete(missingFilePath); - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); - - if (this.projectKind === ProjectKind.Configured) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); - } - - // When a missing file is created, we should update the graph. - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); - } - } - ); - return fileWatcher; - }, + missingFilePath => this.addMissingFileWatcher(missingFilePath), // Files that are no longer missing (e.g. because they are no longer required) // should no longer be watched. - (missingFilePath, fileWatcher) => { - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded); - } + (missingFilePath, fileWatcher) => this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded) ); } @@ -694,6 +669,32 @@ namespace ts.server { return hasChanges; } + private addMissingFileWatcher(missingFilePath: Path) { + const fileWatcher = this.projectService.addFileWatcher( + WatchType.MissingFilePath, this, missingFilePath, + (filename, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { + this.missingFilesMap.delete(missingFilePath); + this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); + + if (this.projectKind === ProjectKind.Configured) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + } + + // When a missing file is created, we should update the graph. + this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); + } + } + ); + return fileWatcher; + } + + private closeMissingFileWatcher(missingFilePath: Path, fileWatcher: FileWatcher, reason: WatcherCloseReason) { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, reason); + } + isWatchedMissingFile(path: Path) { return this.missingFilesMap && this.missingFilesMap.has(path); } @@ -1135,33 +1136,18 @@ namespace ts.server { } watchWildcards(wildcardDirectories: Map) { - this.directoriesWatchedForWildcards = mutateExistingMap( - this.directoriesWatchedForWildcards, wildcardDirectories, - // Create new watch and recursive info - (directory, flag) => { - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return { - watcher: this.projectService.addDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), - recursive - ), - recursive - }; - }, - // Close existing watch thats not needed any more - (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.NotNeeded + this.directoriesWatchedForWildcards = updateWatchingWildcardDirectories(this.directoriesWatchedForWildcards, + wildcardDirectories, + // Create new directory watcher + (directory, recursive) => this.projectService.addDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + recursive ), - // Watcher is same if the recursive flags match - ({ recursive: existingRecursive }, flag) => { - // If the recursive dont match, it needs update - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return existingRecursive !== recursive; - }, - // Close existing watch that doesnt match in recursive flag - (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.RecursiveChanged + // Close directory watcher + (directory, watcher, recursive, recursiveChanged) => this.projectService.closeDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, watcher, recursive, + recursiveChanged ? WatcherCloseReason.RecursiveChanged : WatcherCloseReason.NotNeeded ) ); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 36b769182e5..85bba441e51 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -292,77 +292,4 @@ namespace ts.server { deleted(oldItems[oldIndex++]); } } - - export function cleanExistingMap( - existingMap: Map, - onDeleteExistingValue: (key: string, existingValue: T) => void) { - if (existingMap) { - // Remove all - existingMap.forEach((existingValue, key) => { - existingMap.delete(key); - onDeleteExistingValue(key, existingValue); - }); - } - } - - export function mutateExistingMapWithNewSet( - existingMap: Map, newMap: Map, - createNewValue: (key: string) => T, - onDeleteExistingValue: (key: string, existingValue: T) => void - ): Map { - return mutateExistingMap( - existingMap, newMap, - /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), - onDeleteExistingValue, - ); - } - - export function mutateExistingMap( - existingMap: Map, newMap: Map, - createNewValue: (key: string, valueInNewMap: U) => T, - onDeleteExistingValue: (key: string, existingValue: T) => void, - isSameValue?: (existingValue: T, valueInNewMap: U) => boolean, - OnDeleteExistingMismatchValue?: (key: string, existingValue: T) => void, - onSameExistingValue?: (existingValue: T, valueInNewMap: U) => void - ): Map { - // If there are new values update them - if (newMap) { - if (existingMap) { - // Needs update - existingMap.forEach((existingValue, key) => { - const valueInNewMap = newMap.get(key); - // Existing value - remove it - if (valueInNewMap === undefined) { - existingMap.delete(key); - onDeleteExistingValue(key, existingValue); - } - // different value - remove it - else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) { - existingMap.delete(key); - OnDeleteExistingMismatchValue(key, existingValue); - } - else if (onSameExistingValue) { - onSameExistingValue(existingValue, valueInNewMap); - } - }); - } - else { - // Create new - existingMap = createMap(); - } - - // Add new values that are not already present - newMap.forEach((valueInNewMap, key) => { - if (!existingMap.has(key)) { - // New values - existingMap.set(key, createNewValue(key, valueInNewMap)); - } - }); - - return existingMap; - } - - cleanExistingMap(existingMap, onDeleteExistingValue); - return undefined; - } } diff --git a/src/services/services.ts b/src/services/services.ts index fb87c0065d5..e22d6041159 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1114,9 +1114,9 @@ namespace ts { // Get a fresh cache of the host information let hostCache = new HostCache(host, getCanonicalFileName); - + const rootFileNames = hostCache.getRootFileNames(); // If the program is already up-to-date, we can reuse it - if (programUpToDate()) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (path) => hostCache.getVersion(path))) { return; } @@ -1126,18 +1126,7 @@ namespace ts { // the program points to old source files that have been invalidated because of // incremental parsing. - const oldSettings = program && program.getCompilerOptions(); const newSettings = hostCache.compilationSettings(); - const shouldCreateNewSourceFiles = oldSettings && - (oldSettings.target !== newSettings.target || - oldSettings.module !== newSettings.module || - oldSettings.moduleResolution !== newSettings.moduleResolution || - oldSettings.noResolve !== newSettings.noResolve || - oldSettings.jsx !== newSettings.jsx || - oldSettings.allowJs !== newSettings.allowJs || - oldSettings.disableSizeLimit !== oldSettings.disableSizeLimit || - oldSettings.baseUrl !== newSettings.baseUrl || - !equalOwnProperties(oldSettings.paths, newSettings.paths)); // Now create a new compiler const compilerHost: CompilerHost = { @@ -1172,7 +1161,8 @@ namespace ts { }, getDirectories: path => { return host.getDirectories ? host.getDirectories(path) : []; - } + }, + onReleaseOldSourceFile }; if (host.trace) { compilerHost.trace = message => host.trace(message); @@ -1188,36 +1178,29 @@ namespace ts { } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program); - - // Release any files we have acquired in the old program but are - // not part of the new program. - if (program) { - const oldSourceFiles = program.getSourceFiles(); - const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldSettings); - for (const oldSourceFile of oldSourceFiles) { - if (!newProgram.getSourceFile(oldSourceFile.fileName) || shouldCreateNewSourceFiles) { - documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey); - } - } - } + program = createProgram(rootFileNames, newSettings, compilerHost, program); // hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point. // It needs to be cleared to allow all collected snapshots to be released hostCache = undefined; - program = newProgram; - // Make sure all the nodes in the program are both bound, and have their parent // pointers set property. program.getTypeChecker(); return; - function getOrCreateSourceFile(fileName: string): SourceFile { - return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName)); + // Release any files we have acquired in the old program but are + // not part of the new program. + function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) { + const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions); + documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey); } - function getOrCreateSourceFileByPath(fileName: string, path: Path): SourceFile { + function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + } + + function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { Debug.assert(hostCache !== undefined); // The program is asking for this file, check first if the host can locate it. // If the host can not locate the file, then it does not exist. return undefined @@ -1230,7 +1213,7 @@ namespace ts { // Check if the language version has changed since we last created a program; if they are the same, // it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile // can not be reused. we have to dump all syntax trees and create new ones. - if (!shouldCreateNewSourceFiles) { + if (!shouldCreateNewSourceFile) { // Check if the old program had this file already const oldSourceFile = program && program.getSourceFileByPath(path); if (oldSourceFile) { @@ -1270,49 +1253,6 @@ namespace ts { // Could not find this file in the old program, create a new SourceFile for it. return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); } - - function sourceFileUpToDate(sourceFile: SourceFile): boolean { - if (!sourceFile) { - return false; - } - const path = sourceFile.path || toPath(sourceFile.fileName, currentDirectory, getCanonicalFileName); - return sourceFile.version === hostCache.getVersion(path); - } - - function programUpToDate(): boolean { - // If we haven't create a program yet, then it is not up-to-date - if (!program) { - return false; - } - - // If number of files in the program do not match, it is not up-to-date - const rootFileNames = hostCache.getRootFileNames(); - if (program.getSourceFiles().length !== rootFileNames.length) { - return false; - } - - // If any file is not up-to-date, then the whole program is not up-to-date - for (const fileName of rootFileNames) { - if (!sourceFileUpToDate(program.getSourceFile(fileName))) { - return false; - } - } - - const currentOptions = program.getCompilerOptions(); - const newOptions = hostCache.compilationSettings(); - // If the compilation settings do no match, then the program is not up-to-date - if (!compareDataObjects(currentOptions, newOptions)) { - return false; - } - - // If everything matches but the text of config file is changed, - // error locations can change for program options, so update the program - if (currentOptions.configFile && newOptions.configFile) { - return currentOptions.configFile.text === newOptions.configFile.text; - } - - return true; - } } function getProgram(): Program { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index a5b035154be..05d2ebf477c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -994,26 +994,6 @@ namespace ts { return result; } - export function compareDataObjects(dst: any, src: any): boolean { - if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) { - return false; - } - - for (const e in dst) { - if (typeof dst[e] === "object") { - if (!compareDataObjects(dst[e], src[e])) { - return false; - } - } - else if (typeof dst[e] !== "function") { - if (dst[e] !== src[e]) { - return false; - } - } - } - return true; - } - export function isArrayLiteralOrObjectLiteralDestructuringPattern(node: Node) { if (node.kind === SyntaxKind.ArrayLiteralExpression || node.kind === SyntaxKind.ObjectLiteralExpression) {