/// /// /// namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: PartialSystem, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; export interface WatchingSystemHost { // FS system to use system: System; // parse config file parseConfigFile: ParseConfigFile; // Reporting errors reportDiagnostic: DiagnosticReporter; reportWatchDiagnostic: DiagnosticReporter; // Callbacks to do custom action before creating program and after creating program beforeCompile(compilerOptions: CompilerOptions): void; afterCompile(host: PartialSystem, program: Program, builder: Builder): void; } const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { getCurrentDirectory: () => sys.getCurrentDirectory(), getNewLine: () => sys.newLine, getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames) } : undefined; export function createDiagnosticReporter(system = sys, worker = reportDiagnosticSimply, formatDiagnosticsHost?: FormatDiagnosticsHost): DiagnosticReporter { return diagnostic => worker(diagnostic, getFormatDiagnosticsHost(), system); function getFormatDiagnosticsHost() { return formatDiagnosticsHost || (formatDiagnosticsHost = system === sys ? defaultFormatDiagnosticsHost : { getCurrentDirectory: () => system.getCurrentDirectory(), getNewLine: () => system.newLine, getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), }); } } export function createWatchDiagnosticReporter(system = sys): DiagnosticReporter { return diagnostic => { let output = new Date().toLocaleTimeString() + " - "; output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine + system.newLine}`; system.write(output); }; } export function reportDiagnostics(diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter): void { for (const diagnostic of diagnostics) { reportDiagnostic(diagnostic); } } export function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void { system.write(ts.formatDiagnostic(diagnostic, host)); } export function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void { system.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); } export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: PartialSystem, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine { let configFileText: string; try { configFileText = system.readFile(configFileName); } catch (e) { const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); reportWatchDiagnostic(error); system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } if (!configFileText) { const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); reportDiagnostics([error], reportDiagnostic); system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } const result = parseJsonText(configFileName, configFileText); reportDiagnostics(result.parseDiagnostics, reportDiagnostic); const cwd = system.getCurrentDirectory(); const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); reportDiagnostics(configParseResult.errors, reportDiagnostic); return configParseResult; } function reportEmittedFiles(files: string[], system: PartialSystem): void { if (!files || files.length === 0) { return; } const currentDir = system.getCurrentDirectory(); for (const file of files) { const filepath = getNormalizedAbsolutePath(file, currentDir); system.write(`TSFILE: ${filepath}${system.newLine}`); } } export function handleEmitOutputAndReportErrors(system: PartialSystem, program: Program, emittedFiles: string[], emitSkipped: boolean, diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter ): ExitStatus { reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), reportDiagnostic); reportEmittedFiles(emittedFiles, system); if (program.getCompilerOptions().listFiles) { forEach(program.getSourceFiles(), file => { system.write(file.fileName + system.newLine); }); } if (emitSkipped && diagnostics.length > 0) { // If the emitter didn't emit anything, then pass that value along. return ExitStatus.DiagnosticsPresent_OutputsSkipped; } else if (diagnostics.length > 0) { // The emitter emitted something, inform the caller if that happened in the presence // of diagnostics or not. return ExitStatus.DiagnosticsPresent_OutputsGenerated; } return ExitStatus.Success; } export function createWatchingSystemHost(pretty?: DiagnosticStyle, system = sys, parseConfigFile?: ParseConfigFile, reportDiagnostic?: DiagnosticReporter, reportWatchDiagnostic?: DiagnosticReporter ): WatchingSystemHost { reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply); reportWatchDiagnostic = reportWatchDiagnostic || createWatchDiagnosticReporter(system); parseConfigFile = parseConfigFile || ts.parseConfigFile; return { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile: noop, afterCompile: compileWatchedProgram, }; function compileWatchedProgram(host: PartialSystem, program: Program, builder: Builder) { // First get and report any syntactic errors. let diagnostics = program.getSyntacticDiagnostics(); let reportSemanticDiagnostics = false; // If we didn't have any syntactic errors, then also try getting the global and // semantic errors. if (diagnostics.length === 0) { diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); if (diagnostics.length === 0) { reportSemanticDiagnostics = true; } } // Emit and report any errors we ran into. const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; let sourceMaps: SourceMapData[]; let emitSkipped: boolean; const result = builder.emitChangedFiles(program); if (result.length === 0) { emitSkipped = true; } else { for (const emitOutput of result) { if (emitOutput.emitSkipped) { emitSkipped = true; } diagnostics = concatenate(diagnostics, emitOutput.diagnostics); sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); writeOutputFiles(emitOutput.outputFiles); } } if (reportSemanticDiagnostics) { diagnostics = diagnostics.concat(builder.getSemanticDiagnostics(program)); } return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic); function ensureDirectoriesExist(directoryPath: string) { if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { const parentDirectory = getDirectoryPath(directoryPath); ensureDirectoriesExist(parentDirectory); host.createDirectory(directoryPath); } } function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { try { performance.mark("beforeIOWrite"); ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); host.writeFile(fileName, data, writeByteOrderMark); performance.mark("afterIOWrite"); performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); } catch (e) { return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); } } function writeOutputFiles(outputFiles: OutputFile[]) { if (outputFiles) { for (const outputFile of outputFiles) { const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); if (error) { diagnostics.push(error); } if (emittedFiles) { emittedFiles.push(outputFile.name); } } } } } } export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions = {}, watchingHost?: WatchingSystemHost) { return createWatchMode(configParseResult.fileNames, configParseResult.options, watchingHost, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); } export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost) { return createWatchMode(rootFileNames, compilerOptions, watchingHost); } interface HostFileInfo { version: number; sourceFile: SourceFile; fileWatcher: FileWatcher; } function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { let program: Program; let needsReload: boolean; // true if the config file changed and needs to reload it from the disk let missingFilesMap: Map; // Map of file watchers for the missing files let configFileWatcher: FileWatcher; // watcher for the config file let watchedWildcardDirectories: Map; // map of watchers for the wild card directories in the config file let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // Cache that stores the source file and version info let missingFilePathsRequestedForRelease: Path[]; // These paths are held temparirly so that we can remove the entry from source file cache if the file is not tracked by missing files let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations let changedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed const loggingEnabled = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; const writeLog: (s: string) => void = loggingEnabled ? s => system.write(s) : noop; const watchFile = loggingEnabled ? ts.addFileWatcherWithLogging : ts.addFileWatcher; const watchFilePath = loggingEnabled ? ts.addFilePathWatcherWithLogging : ts.addFilePathWatcher; const watchDirectoryWorker = loggingEnabled ? ts.addDirectoryWatcherWithLogging : ts.addDirectoryWatcher; watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; const partialSystem = configFileName ? createCachedPartialSystem(system) : system; if (configFileName) { configFileWatcher = watchFile(system, configFileName, scheduleProgramReload, writeLog); } const getCurrentDirectory = memoize(() => partialSystem.getCurrentDirectory()); const realpath = system.realpath && ((path: string) => system.realpath(path)); const getCachedPartialSystem = configFileName && (() => partialSystem as CachedPartialSystem); const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); let newLine = getNewLineCharacter(compilerOptions, system); const compilerHost: CompilerHost & ResolutionCacheHost = { // Members for CompilerHost getSourceFile: getVersionedSourceFile, getSourceFileByPath: getVersionedSourceFileByPath, getDefaultLibLocation, getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, getCurrentDirectory, useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, getCanonicalFileName, getNewLine: () => newLine, fileExists, readFile, trace, directoryExists, getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", getDirectories, realpath, resolveTypeReferenceDirectives, resolveModuleNames, onReleaseOldSourceFile, hasChangedAutomaticTypeDirectiveNames, // Members for ResolutionCacheHost toPath, getCompilationSettings: () => compilerOptions, watchDirectoryOfFailedLookupLocation: watchDirectory, watchTypeRootsDirectory: watchDirectory, getCachedPartialSystem, onInvalidatedResolution: scheduleProgramUpdate, onChangedAutomaticTypeDirectiveNames, writeLog }; // Cache for the module resolution const resolutionCache = createResolutionCache(compilerHost); resolutionCache.setRootDirectory(configFileName ? getDirectoryPath(getNormalizedAbsolutePath(configFileName, getCurrentDirectory())) : getCurrentDirectory() ); // There is no extra check needed since we can just rely on the program to decide emit const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); synchronizeProgram(); // Update the wild card directory watch watchConfigFileWildCardDirectories(); return () => program; function synchronizeProgram() { writeLog(`Synchronizing program`); if (hasChangedCompilerOptions) { newLine = getNewLineCharacter(compilerOptions, system); } const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(); if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames)) { return; } if (hasChangedCompilerOptions && changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { resolutionCache.clear(); } const needsUpdateInTypeRootWatch = hasChangedCompilerOptions || !program; hasChangedCompilerOptions = false; beforeCompile(compilerOptions); // Compile the program resolutionCache.startCachingPerDirectoryResolution(); compilerHost.hasInvalidatedResolution = hasInvalidatedResolution; program = createProgram(rootFileNames, compilerOptions, compilerHost, program); resolutionCache.finishCachingPerDirectoryResolution(); builder.onProgramUpdateGraph(program, hasInvalidatedResolution); // Update watches updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath); if (needsUpdateInTypeRootWatch) { resolutionCache.updateTypeRootsWatch(); } if (missingFilePathsRequestedForRelease) { // These are the paths that program creater told us as not in use any more but were missing on the disk. // We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO, // if there is already watcher for it (for missing files) // At this point our watches were updated, hence now we know that these paths are not tracked and need to be removed // so that at later time we have correct result of their presence for (const missingFilePath of missingFilePathsRequestedForRelease) { if (!missingFilesMap.has(missingFilePath)) { sourceFilesCache.delete(missingFilePath); } } missingFilePathsRequestedForRelease = undefined; } afterCompile(partialSystem, program, builder); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); } function toPath(fileName: string) { return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); } function fileExists(fileName: string) { const path = toPath(fileName); const hostSourceFileInfo = sourceFilesCache.get(path); if (hostSourceFileInfo !== undefined) { return !isString(hostSourceFileInfo); } return partialSystem.fileExists(fileName); } function directoryExists(directoryName: string) { return partialSystem.directoryExists(directoryName); } function readFile(fileName: string) { return system.readFile(fileName); } function trace(s: string) { return system.write(s + newLine); } function getDirectories(path: string) { return partialSystem.getDirectories(path); } function resolveModuleNames(moduleNames: string[], containingFile: string) { return resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false); } function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string) { return resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); } function getDefaultLibLocation(): string { return getDirectoryPath(normalizePath(system.getExecutingFilePath())); } function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { return getVersionedSourceFileByPath(fileName, toPath(fileName), 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 || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { const sourceFile = getNewSourceFile(); if (hostSourceFile) { if (shouldCreateNewSourceFile) { hostSourceFile.version++; } if (sourceFile) { hostSourceFile.sourceFile = sourceFile; sourceFile.version = hostSourceFile.version.toString(); if (!hostSourceFile.fileWatcher) { hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); } } else { // There is no source file on host any more, close the watch, missing file paths will track it hostSourceFile.fileWatcher.close(); sourceFilesCache.set(path, hostSourceFile.version.toString()); } } else { let fileWatcher: FileWatcher; if (sourceFile) { sourceFile.version = "0"; fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); } else { sourceFilesCache.set(path, "0"); } } return sourceFile; } return hostSourceFile.sourceFile; function getNewSourceFile() { let text: string; try { performance.mark("beforeIORead"); text = system.readFile(fileName, compilerOptions.charset); performance.mark("afterIORead"); performance.measure("I/O Read", "beforeIORead", "afterIORead"); } catch (e) { if (onError) { onError(e.message); } } 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(); resolutionCache.invalidateResolutionOfFile(path); } 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) { // record the missing file paths so they can be removed later if watchers arent tracking them if (isString(hostSourceFileInfo)) { (missingFilePathsRequestedForRelease || (missingFilePathsRequestedForRelease = [])).push(oldSourceFile.path); } else if (hostSourceFileInfo.sourceFile === oldSourceFile) { sourceFilesCache.delete(oldSourceFile.path); resolutionCache.invalidateResolutionOfFile(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 scheduleProgramUpdate() { if (!system.setTimeout || !system.clearTimeout) { return; } if (timerToUpdateProgram) { system.clearTimeout(timerToUpdateProgram); } timerToUpdateProgram = system.setTimeout(updateProgram, 250); } function scheduleProgramReload() { Debug.assert(!!configFileName); needsReload = true; scheduleProgramUpdate(); } function updateProgram() { timerToUpdateProgram = undefined; reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); if (needsReload) { reloadConfigFile(); } else { synchronizeProgram(); } } function reloadConfigFile() { writeLog(`Reloading config file: ${configFileName}`); needsReload = false; const cachedHost = partialSystem as CachedPartialSystem; cachedHost.clearCache(); const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); rootFileNames = configParseResult.fileNames; compilerOptions = configParseResult.options; hasChangedCompilerOptions = true; configFileSpecs = configParseResult.configFileSpecs; configFileWildCardDirectories = configParseResult.wildcardDirectories; synchronizeProgram(); // Update the wild card directory watch watchConfigFileWildCardDirectories(); } function onSourceFileChange(fileName: string, eventKind: FileWatcherEventKind, path: Path) { updateCachedSystemWithFile(fileName, path, eventKind); const hostSourceFile = sourceFilesCache.get(path); if (hostSourceFile) { // Update the cache if (eventKind === FileWatcherEventKind.Deleted) { resolutionCache.invalidateResolutionOfFile(path); if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); sourceFilesCache.set(path, (hostSourceFile.version++).toString()); } } else { // Deleted file created if (isString(hostSourceFile)) { sourceFilesCache.delete(path); } else { // file changed - just update the version hostSourceFile.version++; } } } // Update the program scheduleProgramUpdate(); } function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) { if (configFileName) { (partialSystem as CachedPartialSystem).addOrDeleteFile(fileName, path, eventKind); } } function watchDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { return watchDirectoryWorker(system, directory, cb, flags, writeLog); } function onChangedAutomaticTypeDirectiveNames() { changedAutomaticTypeDirectiveNames = true; scheduleProgramUpdate(); } function hasChangedAutomaticTypeDirectiveNames() { return changedAutomaticTypeDirectiveNames; } function watchMissingFilePath(missingFilePath: Path) { return watchFilePath(system, missingFilePath, onMissingFileChange, missingFilePath, writeLog); } function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { updateCachedSystemWithFile(fileName, missingFilePath, eventKind); if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { missingFilesMap.get(missingFilePath).close(); missingFilesMap.delete(missingFilePath); // 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() { updateWatchingWildcardDirectories( watchedWildcardDirectories || (watchedWildcardDirectories = createMap()), createMapFromTemplate(configFileWildCardDirectories), watchWildcardDirectory ); } function watchWildcardDirectory(directory: string, flags: WatchDirectoryFlags) { return watchDirectory( directory, fileOrFolder => { Debug.assert(!!configFileName); const fileOrFolderPath = toPath(fileOrFolder); // Since the file existance changed, update the sourceFiles cache (partialSystem as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); removeSourceFile(fileOrFolderPath); // If the the added or created file or folder is not supported file name, ignore the file // But when watched directory is added/removed, we need to reload the file list if (fileOrFolderPath !== directory && !isSupportedSourceFileName(fileOrFolder, compilerOptions)) { writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrFolder}`); return; } // Reload is pending, do the reload if (!needsReload) { const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, partialSystem); if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) { reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); } rootFileNames = result.fileNames; // Schedule Update the program scheduleProgramUpdate(); } }, flags ); } function computeHash(data: string) { return system.createHash ? system.createHash(data) : data; } } }