diff --git a/Jakefile.js b/Jakefile.js index 31243f77c42..ba3e18f3e56 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -91,6 +91,7 @@ var languageServiceLibrarySources = filesFromConfig(path.join(serverDirectory, " var harnessCoreSources = [ "harness.ts", "virtualFileSystem.ts", + "virtualFileSystemWithWatch.ts", "sourceMapRecorder.ts", "harnessLanguageService.ts", "fourslash.ts", @@ -128,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([ "convertCompilerOptionsFromJson.ts", "convertTypeAcquisitionFromJson.ts", "tsserverProjectSystem.ts", + "tscWatchMode.ts", "compileOnSave.ts", "typingsInstaller.ts", "projectErrors.ts", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index e2bfa647eb7..80bd089429a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -387,7 +387,8 @@ namespace ts { allDiagnostics?: Diagnostic[]; } - export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, getSourceVersion: (path: Path) => string): boolean { + export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, + getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean): boolean { // If we haven't create a program yet, then it is not up-to-date if (!program) { return false; @@ -398,14 +399,18 @@ namespace ts { 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))) { + for (const file of program.getSourceFiles()) { + if (!sourceFileUpToDate(program.getSourceFile(file.fileName))) { return false; } } + // If any of the missing file paths are now created + if (program.getMissingFilePaths().some(missingFilePath => fileExists(missingFilePath))) { + 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)) { @@ -445,14 +450,10 @@ namespace ts { /** * 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) { + closeExistingMissingFilePathFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void) { const missingFilePaths = program.getMissingFilePaths(); const newMissingFilePathMap = arrayToSet(missingFilePaths); @@ -463,12 +464,15 @@ namespace ts { createMissingFileWatch, // Files that are no longer missing (e.g. because they are no longer required) // should no longer be watched. - closeExistingFileWatcher + closeExistingMissingFilePathFileWatcher ); } export type WildCardDirectoryWatchers = { watcher: FileWatcher, recursive: boolean }; + /** + * Updates the existing wild card directory watcyhes with the new set of wild card directories from the config file after new program is created + */ export function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, recursive: boolean) => FileWatcher, closeDirectoryWatcher: (directory: string, watcher: FileWatcher, recursive: boolean, recursiveChanged: boolean) => void) { diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index ec93b61bbdf..3a672fca727 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -1,3 +1,6 @@ /// +if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { + ts.sys.tryEnableSourceMapsForHost(); +} ts.executeCommandLine(ts.sys.args); diff --git a/src/compiler/tscLib.ts b/src/compiler/tscLib.ts index 3dba8961674..b9234906176 100644 --- a/src/compiler/tscLib.ts +++ b/src/compiler/tscLib.ts @@ -12,35 +12,49 @@ namespace ts { value: string; } - const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = { + export interface FormatDiagnosticsHostWithWrite extends FormatDiagnosticsHost { + write?(s: string): void; + } + + const defaultFormatDiagnosticsHost: FormatDiagnosticsHostWithWrite = sys ? { getCurrentDirectory: () => sys.getCurrentDirectory(), getNewLine: () => sys.newLine, - getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames) - }; + getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames), + write: s => sys.write(s) + } : undefined; + + function getDefaultFormatDiagnosticsHost(system: System): FormatDiagnosticsHostWithWrite { + return system === sys ? defaultFormatDiagnosticsHost : { + getCurrentDirectory: () => system.getCurrentDirectory(), + getNewLine: () => system.newLine, + getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), + write: s => system.write(s) + }; + } let reportDiagnosticWorker = reportDiagnosticSimply; - function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost) { + function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite) { reportDiagnosticWorker(diagnostic, host || defaultFormatDiagnosticsHost); } - function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHost): void { + function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHostWithWrite): void { for (const diagnostic of diagnostics) { reportDiagnostic(diagnostic, host); } } - function reportEmittedFiles(files: string[]): void { + function reportEmittedFiles(files: string[], system: System): void { if (!files || files.length === 0) { return; } - const currentDir = sys.getCurrentDirectory(); + const currentDir = system.getCurrentDirectory(); for (const file of files) { const filepath = getNormalizedAbsolutePath(file, currentDir); - sys.write(`TSFILE: ${filepath}${sys.newLine}`); + system.write(`TSFILE: ${filepath}${system.newLine}`); } } @@ -57,15 +71,15 @@ namespace ts { return diagnostic.messageText; } - function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { - sys.write(ts.formatDiagnostics([diagnostic], host)); + function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite): void { + host.write(ts.formatDiagnostics([diagnostic], host)); } - function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { - sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + sys.newLine); + function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite): void { + host.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); } - function reportWatchDiagnostic(diagnostic: Diagnostic) { + function reportWatchDiagnostic(diagnostic: Diagnostic, system: System) { let output = new Date().toLocaleTimeString() + " - "; if (diagnostic.file) { @@ -73,9 +87,9 @@ namespace ts { output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `; } - output += `${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }${ sys.newLine + sys.newLine + sys.newLine }`; + output += `${ flattenDiagnosticMessageText(diagnostic.messageText, system.newLine) }${ system.newLine + system.newLine + system.newLine }`; - sys.write(output); + system.write(output); } function padLeft(s: string, length: number) { @@ -175,7 +189,7 @@ namespace ts { const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys); if (isWatchSet(configParseResult.options)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithConfigFile(configParseResult, commandLineOptions); + createWatchModeWithConfigFile(configParseResult, commandLineOptions, sys); } else { performCompilation(configParseResult.fileNames, configParseResult.options); @@ -184,7 +198,7 @@ namespace ts { else { if (isWatchSet(commandLine.options)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions); + createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, sys); } else { performCompilation(commandLine.fileNames, commandLineOptions); @@ -204,7 +218,7 @@ namespace ts { } const compilerHost = createCompilerHost(compilerOptions); - const compileResult = compile(rootFileNames, compilerOptions, compilerHost); + const compileResult = compile(rootFileNames, compilerOptions, compilerHost, sys); return sys.exit(compileResult.exitStatus); } } @@ -216,16 +230,16 @@ namespace ts { } /* @internal */ - export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend?: CompilerOptions) { - return createWatchMode(configParseResult.fileNames, configParseResult.options, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); + export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions, system: System) { + return createWatchMode(configParseResult.fileNames, configParseResult.options, system, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); } /* @internal */ - export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions) { - return createWatchMode(rootFileNames, compilerOptions); + export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, system: System) { + return createWatchMode(rootFileNames, compilerOptions, system); } - function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { + function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, system: System, 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 @@ -238,11 +252,11 @@ namespace ts { let host: System; if (configFileName) { - host = createCachedSystem(sys); - configFileWatcher = sys.watchFile(configFileName, onConfigFileChanged); + host = createCachedSystem(system); + configFileWatcher = system.watchFile(configFileName, onConfigFileChanged); } else { - host = sys; + host = system; } const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); @@ -259,21 +273,23 @@ namespace ts { // Update the wild card directory watch watchConfigFileWildCardDirectories(); + return () => program; + function synchronizeProgram() { writeLog(`Synchronizing program`); - if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion)) { + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists)) { return; } // Create the compiler host const compilerHost = createWatchedCompilerHost(compilerOptions); - program = compile(rootFileNames, compilerOptions, compilerHost, program).program; + program = compile(rootFileNames, compilerOptions, compilerHost, system, program).program; // Update watches missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes), system); } function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { @@ -302,14 +318,14 @@ namespace ts { }; // 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); - // }; - //} + // if (host.resolveModuleNames) { + // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + // } + // if (host.resolveTypeReferenceDirectives) { + // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { + // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); + // }; + // } function ensureDirectoriesExist(directoryPath: string) { if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { @@ -342,7 +358,7 @@ namespace ts { const seenFiles = createMap(); let emitSkipped: boolean; - let diagnostics: Diagnostic[]; + const diagnostics: Diagnostic[] = []; const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; let sourceMaps: SourceMapData[]; while (filesPendingToEmit.length) { @@ -366,7 +382,7 @@ namespace ts { emitSkipped = true; } - diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + diagnostics.push(...emitOutput.diagnostics); sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); // If it emitted more than one source files, just mark all those source files as seen if (emitOutput.emittedSourceFiles && emitOutput.emittedSourceFiles.length > 1) { @@ -377,7 +393,7 @@ namespace ts { for (const outputFile of emitOutput.outputFiles) { const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); if (error) { - (diagnostics || (diagnostics = [])).push(error); + diagnostics.push(error); } if (emittedFiles) { emittedFiles.push(outputFile.name); @@ -413,41 +429,43 @@ namespace ts { } // Create new source file if requested or the versions dont match - if (!hostSourceFile) { + if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { changedFilePaths.push(path); - const sourceFile = getSourceFile(fileName, languageVersion, onError); - if (sourceFile) { - sourceFile.version = "0"; - const fileWatcher = watchSourceFileForChanges(sourceFile.path); - sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + + const sourceFile = getNewSourceFile(); + if (hostSourceFile) { + if (shouldCreateNewSourceFile) { + hostSourceFile.version++; + } + if (sourceFile) { + hostSourceFile.sourceFile = sourceFile; + sourceFile.version = hostSourceFile.version.toString(); + if (!hostSourceFile.fileWatcher) { + hostSourceFile.fileWatcher = watchSourceFileForChanges(path); + } + } + 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 { - sourceFilesCache.set(path, "0"); + let fileWatcher: FileWatcher; + if (sourceFile) { + sourceFile.version = "0"; + fileWatcher = watchSourceFileForChanges(path); + sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + } + else { + sourceFilesCache.set(path, "0"); + } } return sourceFile; } - else if (shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { - changedFilePaths.push(path); - 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 { + function getNewSourceFile() { let text: string; try { performance.mark("beforeIORead"); @@ -459,7 +477,6 @@ namespace ts { if (onError) { onError(e.message); } - text = ""; } return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; @@ -496,14 +513,14 @@ namespace ts { // operations (such as saving all modified files in an editor) a chance to complete before we kick // off a new compilation. function scheduleProgramUpdate() { - if (!sys.setTimeout || !sys.clearTimeout) { + if (!system.setTimeout || !system.clearTimeout) { return; } if (timerToUpdateProgram) { - sys.clearTimeout(timerToUpdateProgram); + system.clearTimeout(timerToUpdateProgram); } - timerToUpdateProgram = sys.setTimeout(updateProgram, 250); + timerToUpdateProgram = system.setTimeout(updateProgram, 250); } function scheduleProgramReload() { @@ -514,7 +531,7 @@ namespace ts { function updateProgram() { timerToUpdateProgram = undefined; - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), system); if (needsReload) { reloadConfigFile(); @@ -526,8 +543,6 @@ namespace ts { function reloadConfigFile() { writeLog(`Reloading config file: ${configFileName}`); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - needsReload = false; const cachedHost = host as CachedSystem; @@ -550,6 +565,8 @@ namespace ts { function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + + updateCachedSystem(fileName, path); const hostSourceFile = sourceFilesCache.get(path); if (hostSourceFile) { // Update the cache @@ -570,10 +587,18 @@ namespace ts { } } } + // Update the program scheduleProgramUpdate(); } + function updateCachedSystem(fileName: string, path: Path) { + if (configFileName) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(fileName, getDirectoryPath(path)); + (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); + } + } + function watchMissingFilePath(missingFilePath: Path) { return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); } @@ -588,10 +613,7 @@ namespace ts { closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); missingFilesMap.delete(missingFilePath); - if (configFileName) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); - } + updateCachedSystem(filename, missingFilePath); // Delete the entry in the source files cache so that new source file is created removeSourceFile(missingFilePath); @@ -621,10 +643,12 @@ namespace ts { function onFileAddOrRemoveInWatchedDirectory(fileName: string) { Debug.assert(!!configFileName); - (host as CachedSystem).addOrDeleteFileOrFolder(fileName); + + const path = toPath(fileName, currentDirectory, getCanonicalFileName); // Since the file existance changed, update the sourceFiles cache - removeSourceFile(toPath(fileName, currentDirectory, getCanonicalFileName)); + updateCachedSystem(fileName, path); + removeSourceFile(path); // 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". @@ -640,7 +664,7 @@ namespace ts { if (!needsReload) { const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtensions*/ []); if (!configFileSpecs.filesSpecs) { - reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], /*host*/ undefined); + reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], getDefaultFormatDiagnosticsHost(system)); } rootFileNames = result.fileNames; @@ -662,7 +686,7 @@ namespace ts { } function computeHash(data: string) { - return sys.createHash ? sys.createHash(data) : data; + return system.createHash ? system.createHash(data) : data; } } @@ -718,35 +742,35 @@ namespace ts { } /* @internal */ - export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: System): ParsedCommandLine { + export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: System): ParsedCommandLine { let configFileText: string; try { - configFileText = host.readFile(configFileName); + configFileText = system.readFile(configFileName); } catch (e) { const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - reportWatchDiagnostic(error); - host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + reportWatchDiagnostic(error, system); + system.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); + reportDiagnostics([error], getDefaultFormatDiagnosticsHost(system)); + system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } const result = parseJsonText(configFileName, configFileText); - reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); + reportDiagnostics(result.parseDiagnostics, getDefaultFormatDiagnosticsHost(system)); - const cwd = host.getCurrentDirectory(); - const configParseResult = parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); - reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); + const cwd = system.getCurrentDirectory(); + const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); + reportDiagnostics(configParseResult.errors, getDefaultFormatDiagnosticsHost(system)); return configParseResult; } - function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, oldProgram?: Program) { + function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, system: System, oldProgram?: Program) { const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; let statistics: Statistic[]; if (hasDiagnostics) { @@ -759,12 +783,12 @@ namespace ts { if (compilerOptions.listFiles) { forEach(program.getSourceFiles(), file => { - sys.write(file.fileName + sys.newLine); + system.write(file.fileName + system.newLine); }); } if (hasDiagnostics) { - const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1; + const memoryUsed = system.getMemoryUsage ? system.getMemoryUsage() : -1; reportCountStatistic("Files", program.getSourceFiles().length); reportCountStatistic("Lines", countLines(program)); reportCountStatistic("Nodes", program.getNodeCount()); @@ -830,9 +854,9 @@ namespace ts { } diagnostics = diagnostics.concat(emitOutput.diagnostics); - reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), compilerHost); + reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), getDefaultFormatDiagnosticsHost(system)); - reportEmittedFiles(emitOutput.emittedFiles); + reportEmittedFiles(emitOutput.emittedFiles, system); if (emitOutput.emitSkipped && diagnostics.length > 0) { // If the emitter didn't emit anything, then pass that value along. return ExitStatus.DiagnosticsPresent_OutputsSkipped; @@ -859,7 +883,7 @@ namespace ts { } for (const { name, value } of statistics) { - sys.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + sys.newLine); + system.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + system.newLine); } } @@ -1012,7 +1036,3 @@ namespace ts { if (ts.Debug.isDebugging) { ts.Debug.enableDebugInfo(); } - -if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { - ts.sys.tryEnableSourceMapsForHost(); -} diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index dfdd0a2fee8..7c3ad0bc5a3 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -93,6 +93,7 @@ "rwcRunner.ts", "test262Runner.ts", "runner.ts", + "virtualFileSystemWithWatch.ts", "../server/protocol.ts", "../server/session.ts", "../server/client.ts", @@ -116,6 +117,7 @@ "./unittests/convertCompilerOptionsFromJson.ts", "./unittests/convertTypeAcquisitionFromJson.ts", "./unittests/tsserverProjectSystem.ts", + "./unittests/tscWatchMode.ts", "./unittests/matchFiles.ts", "./unittests/initializeTSConfig.ts", "./unittests/compileOnSave.ts", diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts new file mode 100644 index 00000000000..630d9a71d20 --- /dev/null +++ b/src/harness/unittests/tscWatchMode.ts @@ -0,0 +1,1000 @@ +/// +/// +/// + +namespace ts.tscWatch { + + export import WatchedSystem = ts.TestFSWithWatch.TestServerHost; + export type TestServerHostCreationParameters = ts.TestFSWithWatch.TestServerHostCreationParameters; + export type File = ts.TestFSWithWatch.File; + export type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + export type Folder = ts.TestFSWithWatch.Folder; + export type FSEntry = ts.TestFSWithWatch.FSEntry; + export import createWatchedSystem = ts.TestFSWithWatch.createWatchedSystem; + export import checkFileNames = ts.TestFSWithWatch.checkFileNames; + export import libFile = ts.TestFSWithWatch.libFile; + export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; + export import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; + export import checkOutputContains = ts.TestFSWithWatch.checkOutputContains; + export import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain; + + export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { + checkFileNames(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + } + + export function checkProgramRootFiles(program: Program, expectedFiles: string[]) { + checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + } + + export function createWatchWithConfig(configFilePath: string, host: WatchedSystem) { + const configFileResult = parseConfigFile(configFilePath, {}, host); + return createWatchModeWithConfigFile(configFileResult, {}, host); + } + + describe("tsc-watch program updates", () => { + const commonFile1: FileOrFolder = { + path: "/a/b/commonFile1.ts", + content: "let x = 1" + }; + const commonFile2: FileOrFolder = { + path: "/a/b/commonFile2.ts", + content: "let y = 1" + }; + + it("create watch without config file", () => { + const appFile: FileOrFolder = { + path: "/a/b/c/app.ts", + content: ` + import {f} from "./module" + console.log(f) + ` + }; + + const moduleFile: FileOrFolder = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + const host = createWatchedSystem([appFile, moduleFile, libFile]); + const watch = createWatchModeWithoutConfigFile([appFile.path], {}, host); + + checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); + + // TODO: Should we watch creation of config files in the root file's file hierarchy? + + // const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + // const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + // checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + }); + + it("can handle tsconfig file name with difference casing", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + include: ["app.ts"] + }) + }; + + const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); + const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); + const watch = createWatchWithConfig(upperCaseConfigFilePath, host); + checkProgramActualFiles(watch(), [f1.path]); + }); + + it("create configured project without file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "exclude": [ + "e" + ] + }` + }; + const file1: FileOrFolder = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: FileOrFolder = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: FileOrFolder = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + + const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); + const configFileResult = parseConfigFile(configFile.path, {}, host); + assert.equal(configFileResult.errors.length, 0, `expect no errors in config file, got ${JSON.stringify(configFileResult.errors)}`); + + const watch = createWatchModeWithConfigFile(configFileResult, {}, host); + + checkProgramActualFiles(watch(), [file1.path, libFile.path, file2.path]); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); + }); + + // TODO: if watching for config file creation + // it("add and then remove a config file in a folder with loose files", () => { + // }); + + it("add new files to a configured program without file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([commonFile1, libFile, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkWatchedDirectories(host, ["/a/b"], /*recursive*/ true); + + checkProgramRootFiles(watch(), [commonFile1.path]); + + // add a new ts file + host.reloadFS([commonFile1, commonFile2, libFile, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("should ignore non-existing files specified in the config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": [ + "commonFile1.ts", + "commonFile3.ts" + ] + }` + }; + const host = createWatchedSystem([commonFile1, commonFile2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + + const commonFile3 = "/a/b/commonFile3.ts"; + checkProgramRootFiles(watch(), [commonFile1.path, commonFile3]); + checkProgramActualFiles(watch(), [commonFile1.path]); + }); + + it("handle recreated files correctly", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([commonFile1, commonFile2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + + // delete commonFile2 + host.reloadFS([commonFile1, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path]); + + // re-add commonFile2 + host.reloadFS([commonFile1, commonFile2, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("handles the missing files - that were added to program because they were added with /// { + const file1: FileOrFolder = { + path: "/a/b/commonFile1.ts", + content: `/// + let x = y` + }; + const host = createWatchedSystem([file1, libFile]); + const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, libFile.path]); + const errors = [ + `a/b/commonFile1.ts(1,22): error TS6053: File '${commonFile2.path}' not found.${host.newLine}`, + `a/b/commonFile1.ts(2,29): error TS2304: Cannot find name 'y'.${host.newLine}` + ]; + checkOutputContains(host, errors); + host.clearOutput(); + + host.reloadFS([file1, commonFile2, libFile]); + host.runQueuedTimeoutCallbacks(); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); + checkOutputDoesNotContain(host, errors); + }); + + it("should reflect change in config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + configFile.content = `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}"] + }`; + + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); // reload the configured project + checkProgramRootFiles(watch(), [commonFile1.path]); + }); + + it("files explicitly excluded in config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "exclude": ["/a/c"] + }` + }; + const excludedFile1: FileOrFolder = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + + const host = createWatchedSystem([commonFile1, commonFile2, excludedFile1, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("should properly handle module resolution changes in config file", () => { + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: FileOrFolder = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: FileOrFolder = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "moduleResolution": "node" + }, + "files": ["${file1.path}"] + }` + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, nodeModuleFile.path]); + + configFile.content = `{ + "compilerOptions": { + "moduleResolution": "classic" + }, + "files": ["${file1.path}"] + }`; + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, classicModuleFile.path]); + }); + + it("should tolerate config file errors and still try to build a project", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6", + "allowAnything": true + }, + "someOtherProperty": {} + }` + }; + const host = createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("changes in files are reflected in project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + const modifiedFile2 = { + path: file2.path, + content: `export * from "../c/f3"` // now inferred project should inclule file3 + }; + + host.reloadFS([file1, modifiedFile2, file3]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, modifiedFile2.path, file3.path]); + }); + + it("deleted files affect project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path]); + }); + + it("deleted files affect project structure - 2", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file1.path, file3.path], {}, host); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path, file3.path]); + }); + + it("config file includes the file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" + }; + const file2 = { + path: "/a/c/f2.ts", + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: "export let y = 1" + }; + const configFile = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, file3, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramRootFiles(watch(), [file2.path, file3.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + }); + + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2"; + export * from "../d/f3";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file2.path, file3.path], {}, host); + checkProgramActualFiles(watch(), [file2.path, file3.path]); + + const watch2 = createWatchModeWithoutConfigFile([file1.path], {}, host); + checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); + + // Previous program shouldnt be updated + checkProgramActualFiles(watch(), [file2.path, file3.path]); + host.checkTimeoutQueueLength(0); + }); + + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + + const host = createWatchedSystem([file1, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramActualFiles(watch(), [file1.path]); + + host.reloadFS([file1, file2, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path, file2.path]); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + }); + + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramActualFiles(watch(), [file1.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + }); + + it("can update configured project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + }); + + it("config file is deleted", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createWatchedSystem([file1, file2, config]); + const watch = createWatchWithConfig(config.path, host); + + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + host.clearOutput(); + host.reloadFS([file1, file2]); + host.checkTimeoutQueueLengthAndRun(1); + + assert.equal(host.exitCode, ExitStatus.DiagnosticsPresent_OutputsSkipped); + checkOutputContains(host, [`error TS6053: File '${config.path}' not found.${host.newLine}`]); + }); + + it("Proper errors: document is not contained in project", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const corruptedConfig = { + path: "/a/b/tsconfig.json", + content: "{" + }; + const host = createWatchedSystem([file1, corruptedConfig]); + const watch = createWatchWithConfig(corruptedConfig.path, host); + + checkProgramActualFiles(watch(), [file1.path]); + }); + + it("correctly handles changes in lib section of config file", () => { + const libES5 = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const libES2015Promise = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + const app = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const config1 = { + path: "/src/tsconfig.json", + content: JSON.stringify( + { + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "sourceMap": false, + "lib": [ + "es5" + ] + } + }) + }; + const config2 = { + path: config1.path, + content: JSON.stringify( + { + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "sourceMap": false, + "lib": [ + "es5", + "es2015.promise" + ] + } + }) + }; + const host = createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const watch = createWatchWithConfig(config1.path, host); + + checkProgramActualFiles(watch(), [libES5.path, app.path]); + + host.reloadFS([libES5, libES2015Promise, app, config2]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramActualFiles(watch(), [libES5.path, libES2015Promise.path, app.path]); + }); + + it("should handle non-existing directories in config file", () => { + const f = { + path: "/a/src/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + include: [ + "src/**/*", + "notexistingfolder/*" + ] + }) + }; + const host = createWatchedSystem([f, config]); + const watch = createWatchWithConfig(config.path, host); + checkProgramActualFiles(watch(), [f.path]); + }); + + it("rename a module file and rename back should restore the states for inferred projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createWatchedSystem([moduleFile, file1, libFile]); + createWatchModeWithoutConfigFile([file1.path], {}, host); + const error = "a/b/file1.ts(1,20): error TS2307: Cannot find module \'./moduleFile\'.\n"; + checkOutputDoesNotContain(host, [error]); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [error]); + + host.clearOutput(); + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("rename a module file and rename back should restore the states for configured projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + + const error = `error TS6053: File '${moduleFile.path}' not found.${host.newLine}`; + checkOutputDoesNotContain(host, [error]); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [error]); + + host.clearOutput(); + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("types should load from config file path if config exists", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) + }; + const node = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: "declare var process: any" + }; + const cwd = { + path: "/a/c" + }; + const host = createWatchedSystem([f1, config, node, cwd], { currentDirectory: cwd.path }); + const watch = createWatchWithConfig(config.path, host); + + checkProgramActualFiles(watch(), [f1.path, node.path]); + }); + + it("add the missing module file for inferred project: should remove the `module not found` error", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createWatchedSystem([file1, libFile]); + createWatchModeWithoutConfigFile([file1.path], {}, host); + + const error = `a/b/file1.ts(1,20): error TS2307: Cannot find module \'./moduleFile\'.${host.newLine}`; + checkOutputContains(host, [error]); + host.clearOutput(); + + host.reloadFS([file1, moduleFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("Configure file diagnostics events are generated when the config file has errors", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + checkOutputContains(host, [ + `a/b/tsconfig.json(3,29): error TS5023: Unknown compiler option \'foo\'.${host.newLine}`, + `a/b/tsconfig.json(4,29): error TS5023: Unknown compiler option \'allowJS\'.${host.newLine}` + ]); + }); + + it("Configure file diagnostics events are generated when the config file doesn't have errors", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + checkOutputDoesNotContain(host, [ + `a/b/tsconfig.json(3,29): error TS5023: Unknown compiler option \'foo\'.${host.newLine}`, + `a/b/tsconfig.json(4,29): error TS5023: Unknown compiler option \'allowJS\'.${host.newLine}` + ]); + }); + + it("Configure file diagnostics events are generated when the config file changes", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + const error = `a/b/tsconfig.json(3,25): error TS5023: Unknown compiler option 'haha'.${host.newLine}`; + checkOutputDoesNotContain(host, [error]); + + configFile.content = `{ + "compilerOptions": { + "haha": 123 + } + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [error]); + + host.clearOutput(); + configFile.content = `{ + "compilerOptions": {} + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "include": ["app/*", "test/**/*", "something"] + }` + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + + const host = createWatchedSystem([file1, configFile, libFile]); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramActualFiles(watch(), [libFile.path]); + }); + + it("non-existing directories listed in config file input array should be able to handle @types if input file list is empty", () => { + const f = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compiler: {}, + files: [] + }) + }; + const t1 = { + path: "/a/node_modules/@types/typings/index.d.ts", + content: `export * from "./lib"` + }; + const t2 = { + path: "/a/node_modules/@types/typings/lib.d.ts", + content: `export const x: number` + }; + const host = createWatchedSystem([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); + const watch = createWatchWithConfig(config.path, host); + + checkProgramActualFiles(watch(), [t1.path, t2.path]); + }); + + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = createWatchedSystem([f, libFile]); + const watch = createWatchModeWithoutConfigFile([f.path], { allowNonTsExtensions: true }, host); + checkProgramActualFiles(watch(), [f.path, libFile.path]); + }); + + it("Options Diagnostic locations reported correctly with changes in configFile contents when options change", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFileContentBeforeComment = `{`; + const configFileContentComment = ` + // comment + // More comment`; + const configFileContentAfterComment = ` + "compilerOptions": { + "allowJs": true, + "declaration": true + } + }`; + const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; + const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; + + const line = 5; + const errors = (line: number) => [ + `a/b/tsconfig.json(${line},25): error TS5053: Option \'allowJs\' cannot be specified with option \'declaration\'.\n`, + `a/b/tsconfig.json(${line + 1},25): error TS5053: Option \'allowJs\' cannot be specified with option \'declaration\'.\n` + ]; + + const configFile = { + path: "/a/b/tsconfig.json", + content: configFileContentWithComment + }; + + const host = createWatchedSystem([file, libFile, configFile]); + createWatchWithConfig(configFile.path, host); + checkOutputContains(host, errors(line)); + checkOutputDoesNotContain(host, errors(line - 2)); + host.clearOutput(); + + configFile.content = configFileContentWithoutCommentLine; + host.reloadFS([file, configFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, errors(line - 2)); + checkOutputDoesNotContain(host, errors(line)); + }); + }); + + describe("tsc-watch emit", () => { + it("emit with outFile or out setting projectUsesOutFile should not be returned if not set", () => { + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true } + }) + }; + const files = [f1, f2, config, libFile]; + const host = createWatchedSystem([f1, f2, config, libFile]); + createWatchWithConfig(config.path, host); + const emittedF1 = `TSFILE: ${f1.path.replace(".ts", ".js")}${host.newLine}`; + const emittedF2 = `TSFILE: ${f2.path.replace(".ts", ".js")}${host.newLine}`; + checkOutputContains(host, [emittedF1, emittedF2]); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [emittedF1]); + checkOutputDoesNotContain(host, [emittedF2]); + }); + + it("emit with outFile or out setting projectUsesOutFile should be true if out is set", () => { + const outJS = "/a/out.js"; + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true, out: outJS } + }) + }; + const files = [f1, f2, config, libFile]; + const host = createWatchedSystem([f1, f2, config, libFile]); + createWatchWithConfig(config.path, host); + const emittedF1 = `TSFILE: ${outJS}${host.newLine}`; + checkOutputContains(host, [emittedF1]); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [emittedF1]); + }); + + it("emit with outFile or out setting projectUsesOutFile should be true if outFile is set", () => { + const outJs = "/a/out.js"; + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true, outFile: outJs } + }) + }; + const files = [f1, f2, config, libFile]; + const host = createWatchedSystem([f1, f2, config, libFile]); + createWatchWithConfig(config.path, host); + const emittedF1 = `TSFILE: ${outJs}${host.newLine}`; + checkOutputContains(host, [emittedF1]); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [emittedF1]); + }); + }); +} diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index f37d685851c..45c0554bd9c 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -30,6 +30,19 @@ namespace ts.TestFSWithWatch { newLine?: string; } + export function createWatchedSystem(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost { + if (!params) { + params = {}; + } + const host = new TestServerHost(/*withSafelist*/ false, + params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, + params.executingFilePath || getExecutingFilePathFromLibFile(), + params.currentDirectory || "/", + fileOrFolderList, + params.newLine); + return host; + } + export function createServerHost(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost { if (!params) { params = {}; @@ -112,6 +125,23 @@ namespace ts.TestFSWithWatch { checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); } + export function checkOutputContains(host: TestServerHost, expected: string[] | ReadonlyArray) { + const mapExpected = arrayToSet(expected); + for (const f of host.getOutput()) { + if (mapExpected.has(f)) { + mapExpected.delete(f); + } + } + assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`); + } + + export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | ReadonlyArray) { + const mapExpectedToBeAbsent = arrayToSet(expectedToBeAbsent); + for (const f of host.getOutput()) { + assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(host.getOutput())}`); + } + } + export class Callbacks { private map: TimeOutCallback[] = []; private nextId = 1; @@ -172,7 +202,7 @@ namespace ts.TestFSWithWatch { this.reloadFS(fileOrFolderList); } - private toFullPath(s: string) { + toFullPath(s: string) { const fullPath = getNormalizedAbsolutePath(s, this.currentDirectory); return this.toPath(fullPath); } @@ -345,6 +375,11 @@ namespace ts.TestFSWithWatch { return isFile(this.fs.get(path)); } + readFile(s: string) { + const fsEntry = this.fs.get(this.toFullPath(s)); + return isFile(fsEntry) ? fsEntry.content : undefined; + } + getFileSize(s: string) { const path = this.toFullPath(s); const entry = this.fs.get(path); @@ -432,7 +467,15 @@ namespace ts.TestFSWithWatch { } runQueuedTimeoutCallbacks() { - this.timeoutCallbacks.invoke(); + try { + this.timeoutCallbacks.invoke(); + } + catch (e) { + if (e.message === this.existMessage) { + return; + } + throw e; + } } runQueuedImmediateCallbacks() { @@ -482,11 +525,15 @@ namespace ts.TestFSWithWatch { clear(this.output); } - readonly readFile = (s: string) => (this.fs.get(this.toFullPath(s))).content; + readonly existMessage = "System Exit"; + exitCode: number; readonly resolvePath = (s: string) => s; readonly getExecutingFilePath = () => this.executingFilePath; readonly getCurrentDirectory = () => this.currentDirectory; - readonly exit = notImplemented; + exit(exitCode?: number) { + this.exitCode = exitCode; + throw new Error(this.existMessage); + } readonly getEnvironmentVariable = notImplemented; } } diff --git a/src/services/services.ts b/src/services/services.ts index 091364b5c50..8fe158a4798 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1116,7 +1116,7 @@ namespace ts { let hostCache = new HostCache(host, getCanonicalFileName); const rootFileNames = hostCache.getRootFileNames(); // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (path) => hostCache.getVersion(path))) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists)) { return; } @@ -1139,14 +1139,7 @@ namespace ts { getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: noop, getCurrentDirectory: () => currentDirectory, - fileExists: (fileName): boolean => { - // stub missing host functionality - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache.getEntryByPath(path); - return entry ? - !isString(entry) : - (host.fileExists && host.fileExists(fileName)); - }, + fileExists, readFile(fileName) { // stub missing host functionality const path = toPath(fileName, currentDirectory, getCanonicalFileName); @@ -1189,6 +1182,14 @@ namespace ts { program.getTypeChecker(); return; + function fileExists(fileName: string) { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const entry = hostCache.getEntryByPath(path); + return entry ? + !isString(entry) : + (host.fileExists && host.fileExists(fileName)); + } + // Release any files we have acquired in the old program but are // not part of the new program. function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {