Initial refactoring so that watch from tsc follows the tsserver projects

This commit is contained in:
Sheetal Nandi 2017-07-24 16:57:49 -07:00
parent 94a589b3bb
commit ef5935b52c
12 changed files with 970 additions and 518 deletions

View File

@ -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<JsFileExtensionInfo>): ExpandResult {
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray<JsFileExtensionInfo> = []): ExpandResult {
basePath = normalizePath(basePath);
const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper;

View File

@ -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<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, 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<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[];
addOrDeleteFileOrFolder(fileOrFolder: string): void;
clearCache(): void;
}
export function createCachedHost(host: HostForCaching): CachedHost {
const cachedReadDirectoryResult = createMap<FileSystemEntries>();
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<string>, name: string) {
return some(entries, file => fileNameEqual(file, name));
}
function updateFileSystemEntry(entries: ReadonlyArray<string>, 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<string>, excludes?: ReadonlyArray<string>, includes?: ReadonlyArray<string>, 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();
}
}
}

View File

@ -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<FileWatcher>,
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<WildCardDirectoryWatchers>, wildcardDirectories: Map<WatchDirectoryFlags>,
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<SourceFile>() : 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) {

View File

@ -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<boolean>;
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<boolean>();
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<WatchDirectoryFlags>) {
let program: Program;
let needsReload: boolean;
let missingFilesMap: Map<FileWatcher>;
let configFileWatcher: FileWatcher;
let watchedWildCardDirectories: Map<WildCardDirectoryWatchers>;
let timerToUpdateProgram: any;
let compilerOptions: CompilerOptions;
let rootFileNames: string[];
const sourceFilesCache = createMap<HostFileInfo | string>();
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<boolean>();
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<OutputFingerprint>;
function writeFileIfUpdated(fileName: string, data: string, writeByteOrderMark: boolean): void {
if (!outputFingerprints) {
outputFingerprints = createMap<OutputFingerprint>();
}
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);

View File

@ -2326,6 +2326,7 @@ namespace ts {
/* @internal */ patternAmbientModules?: PatternAmbientModule[];
/* @internal */ ambientModuleNames: ReadonlyArray<string>;
/* @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 */

View File

@ -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<T>(
existingMap: Map<T>,
onDeleteExistingValue: (key: string, existingValue: T) => void) {
if (existingMap) {
// Remove all
existingMap.forEach((existingValue, key) => {
existingMap.delete(key);
onDeleteExistingValue(key, existingValue);
});
}
}
export function mutateExistingMapWithNewSet<T>(
existingMap: Map<T>, newMap: Map<true>,
createNewValue: (key: string) => T,
onDeleteExistingValue: (key: string, existingValue: T) => void
): Map<T> {
return mutateExistingMap(
existingMap, newMap,
/*createNewValue*/(key, _valueInNewMap) => createNewValue(key),
onDeleteExistingValue,
);
}
export function mutateExistingMap<T, U>(
existingMap: Map<T>, newMap: Map<U>,
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<T> {
// 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<T>();
}
// 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 {

View File

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

View File

@ -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<FileSystemEntries>();
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<string>, name: string) {
return some(entries, file => this.fileNameEqual(file, name));
}
private updateFileSystemEntry(entries: ReadonlyArray<string>, 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<FileSystemEntries>();
return this.cachedHost.clearCache();
}
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) {

View File

@ -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<WatchDirectoryFlags>) {
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
)
);
}

View File

@ -292,77 +292,4 @@ namespace ts.server {
deleted(oldItems[oldIndex++]);
}
}
export function cleanExistingMap<T>(
existingMap: Map<T>,
onDeleteExistingValue: (key: string, existingValue: T) => void) {
if (existingMap) {
// Remove all
existingMap.forEach((existingValue, key) => {
existingMap.delete(key);
onDeleteExistingValue(key, existingValue);
});
}
}
export function mutateExistingMapWithNewSet<T>(
existingMap: Map<T>, newMap: Map<true>,
createNewValue: (key: string) => T,
onDeleteExistingValue: (key: string, existingValue: T) => void
): Map<T> {
return mutateExistingMap(
existingMap, newMap,
/*createNewValue*/(key, _valueInNewMap) => createNewValue(key),
onDeleteExistingValue,
);
}
export function mutateExistingMap<T, U>(
existingMap: Map<T>, newMap: Map<U>,
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<T> {
// 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<T>();
}
// 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;
}
}

View File

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

View File

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