diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index fd0f7350abe..9eada3eeb6f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -399,7 +399,9 @@ namespace ts { let reportFileChangeDetected = false; // Watches for the solution - const existingWatchersForWildcards = createFileMap>(toPath); + const allWatchedWildcardDirectories = createFileMap>(toPath); + const allWatchedInputFiles = createFileMap>(toPath); + const allWatchedConfigFiles = createFileMap(toPath); return { buildAllProjects, @@ -439,7 +441,9 @@ namespace ts { timerToBuildInvalidatedProject = undefined; } reportFileChangeDetected = false; - existingWatchersForWildcards.forEach(wildCardWatches => clearMap(wildCardWatches, closeFileWatcherOf)); + clearMap(allWatchedWildcardDirectories, wildCardWatches => clearMap(wildCardWatches, closeFileWatcherOf)); + clearMap(allWatchedInputFiles, inputFileWatches => clearMap(inputFileWatches, closeFileWatcher)); + clearMap(allWatchedConfigFiles, closeFileWatcher); } function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { @@ -493,48 +497,73 @@ namespace ts { const cfg = parseConfigFile(resolved); if (cfg) { // Watch this file - hostWithWatch.watchFile(resolved, () => { - configFileCache.removeKey(resolved); - invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Full); - }); + watchConfigFile(resolved); // Update watchers for wildcard directories - if (cfg.configFileSpecs) { - const existingWatches = existingWatchersForWildcards.getValue(resolved); - let newWatches: Map | undefined; - if (!existingWatches) { - newWatches = createMap(); - existingWatchersForWildcards.setValue(resolved, newWatches); - } - updateWatchingWildcardDirectories(existingWatches || newWatches!, createMapFromTemplate(cfg.configFileSpecs.wildcardDirectories), (dir, flags) => { - return hostWithWatch.watchDirectory(dir, fileOrDirectory => { - const fileOrDirectoryPath = toPath(fileOrDirectory); - if (fileOrDirectoryPath !== toPath(dir) && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, cfg.options)) { - // writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrDirectory}`); - return; - } - - if (isOutputFile(fileOrDirectory, cfg)) { - // writeLog(`${fileOrDirectory} is output file`); - return; - } - - invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Partial); - }, !!(flags & WatchDirectoryFlags.Recursive)); - }); - } + watchWildCardDirectories(resolved, cfg); // Watch input files - for (const input of cfg.fileNames) { - hostWithWatch.watchFile(input, () => { - invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.None); - }); - } + watchInputFiles(resolved, cfg); } } } + function watchConfigFile(resolved: ResolvedConfigFileName) { + if (!allWatchedConfigFiles.hasKey(resolved)) { + allWatchedConfigFiles.setValue(resolved, hostWithWatch.watchFile(resolved, () => { + configFileCache.removeKey(resolved); + invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Full); + })); + } + } + + function getOrCreateExistingWatches(resolved: ResolvedConfigFileName, allWatches: ConfigFileMap>) { + const existingWatches = allWatches.getValue(resolved); + let newWatches: Map | undefined; + if (!existingWatches) { + newWatches = createMap(); + allWatches.setValue(resolved, newWatches); + } + return existingWatches || newWatches!; + } + + function watchWildCardDirectories(resolved: ResolvedConfigFileName, parsed: ParsedCommandLine) { + updateWatchingWildcardDirectories( + getOrCreateExistingWatches(resolved, allWatchedWildcardDirectories), + createMapFromTemplate(parsed.configFileSpecs!.wildcardDirectories), + (dir, flags) => { + return hostWithWatch.watchDirectory(dir, fileOrDirectory => { + const fileOrDirectoryPath = toPath(fileOrDirectory); + if (fileOrDirectoryPath !== toPath(dir) && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, parsed.options)) { + // writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrDirectory}`); + return; + } + + if (isOutputFile(fileOrDirectory, parsed)) { + // writeLog(`${fileOrDirectory} is output file`); + return; + } + + invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Partial); + }, !!(flags & WatchDirectoryFlags.Recursive)); + } + ); + } + + function watchInputFiles(resolved: ResolvedConfigFileName, parsed: ParsedCommandLine) { + mutateMap( + getOrCreateExistingWatches(resolved, allWatchedInputFiles), + arrayToMap(parsed.fileNames, toPath), + { + createNewValue: (_key, input) => hostWithWatch.watchFile(input, () => { + invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.None); + }), + onDeleteValue: closeFileWatcher, + } + ); + } + function isOutputFile(fileName: string, configFile: ParsedCommandLine) { if (configFile.options.noEmit) return false; @@ -879,10 +908,12 @@ namespace ts { if (!resolved) return; // ?? const proj = parseConfigFile(resolved); if (!proj) return; // ? - // TODO:: If full reload , update watch for wild cards - // TODO:: If full or partial reload, update watch for input files - - if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + watchConfigFile(resolved); + watchWildCardDirectories(resolved, proj); + watchInputFiles(resolved, proj); + } + else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { // Update file names const result = getFileNamesFromConfigSpecs(proj.configFileSpecs!, getDirectoryPath(project), proj.options, parseConfigFileHost); if (result.fileNames.length !== 0) { @@ -892,6 +923,7 @@ namespace ts { proj.errors.push(getErrorForNoInputFiles(proj.configFileSpecs!, resolved)); } proj.fileNames = result.fileNames; + watchInputFiles(resolved, proj); } const status = getUpToDateStatus(proj); diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 5191fe1cc47..baf45da1516 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4329,7 +4329,7 @@ namespace ts { /** * clears already present map by calling onDeleteExistingValue callback before deleting that key/value */ - export function clearMap(map: Map, onDeleteValue: (valueInMap: T, key: string) => void) { + export function clearMap(map: { forEach: Map["forEach"]; clear: Map["clear"]; }, onDeleteValue: (valueInMap: T, key: string) => void) { // Remove all map.forEach(onDeleteValue); map.clear(); diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index c9c57379ec2..bc1bc4f373d 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -62,13 +62,17 @@ namespace ts.tscWatch { return getOutputFileNames(subProject, baseFileNameWithoutExtension).map(f => [f, host.getModifiedTime(f)] as OutputFileStamp); } - function getOutputFileStamps(host: WatchedSystem): OutputFileStamp[] { - return [ + function getOutputFileStamps(host: WatchedSystem, additionalFiles?: ReadonlyArray<[SubProject, string]>): OutputFileStamp[] { + const result = [ ...getOutputStamps(host, SubProject.core, "anotherModule"), ...getOutputStamps(host, SubProject.core, "index"), ...getOutputStamps(host, SubProject.logic, "index"), ...getOutputStamps(host, SubProject.tests, "index"), ]; + if (additionalFiles) { + additionalFiles.forEach(([subProject, baseFileNameWithoutExtension]) => result.push(...getOutputStamps(host, subProject, baseFileNameWithoutExtension))); + } + return result; } function verifyChangedFiles(actualStamps: OutputFileStamp[], oldTimeStamps: OutputFileStamp[], changedFiles: string[]) { @@ -108,49 +112,89 @@ namespace ts.tscWatch { createSolutionInWatchMode(); }); - it("change builds changes and reports found errors message", () => { - const host = createSolutionInWatchMode(); - verifyChange(`${core[1].content} + describe("validates the changes and watched files", () => { + const newFileWithoutExtension = "newFile"; + const newFile: File = { + path: projectFilePath(SubProject.core, `${newFileWithoutExtension}.ts`), + content: `export const newFileConst = 30;` + }; + + function createSolutionInWatchModeToVerifyChanges(additionalFiles?: ReadonlyArray<[SubProject, string]>) { + const host = createSolutionInWatchMode(); + return { host, verifyChangeWithFile, verifyChangeAfterTimeout, verifyWatches }; + + function verifyChangeWithFile(fileName: string, content: string) { + const outputFileStamps = getOutputFileStamps(host, additionalFiles); + host.writeFile(fileName, content); + verifyChangeAfterTimeout(outputFileStamps); + } + + function verifyChangeAfterTimeout(outputFileStamps: OutputFileStamp[]) { + host.checkTimeoutQueueLengthAndRun(1); // Builds core + const changedCore = getOutputFileStamps(host, additionalFiles); + verifyChangedFiles(changedCore, outputFileStamps, [ + ...getOutputFileNames(SubProject.core, "anotherModule"), // This should not be written really + ...getOutputFileNames(SubProject.core, "index"), + ...(additionalFiles ? getOutputFileNames(SubProject.core, newFileWithoutExtension) : emptyArray) + ]); + host.checkTimeoutQueueLengthAndRun(1); // Builds tests + const changedTests = getOutputFileStamps(host, additionalFiles); + verifyChangedFiles(changedTests, changedCore, [ + ...getOutputFileNames(SubProject.tests, "index") // Again these need not be written + ]); + host.checkTimeoutQueueLengthAndRun(1); // Builds logic + const changedLogic = getOutputFileStamps(host, additionalFiles); + verifyChangedFiles(changedLogic, changedTests, [ + ...getOutputFileNames(SubProject.logic, "index") // Again these need not be written + ]); + host.checkTimeoutQueueLength(0); + checkOutputErrorsIncremental(host, emptyArray); + verifyWatches(); + } + + function verifyWatches() { + checkWatchedFiles(host, additionalFiles ? testProjectExpectedWatchedFiles.concat(newFile.path) : testProjectExpectedWatchedFiles); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, [projectPath(SubProject.core), projectPath(SubProject.logic)], /*recursive*/ true); + } + } + + it("change builds changes and reports found errors message", () => { + const { host, verifyChangeWithFile, verifyChangeAfterTimeout } = createSolutionInWatchModeToVerifyChanges(); + verifyChange(`${core[1].content} export class someClass { }`); - // Another change requeues and builds it - verifyChange(core[1].content); + // Another change requeues and builds it + verifyChange(core[1].content); - // Two changes together report only single time message: File change detected. Starting incremental compilation... - const outputFileStamps = getOutputFileStamps(host); - const change1 = `${core[1].content} -export class someClass { }`; - host.writeFile(core[1].path, change1); - host.writeFile(core[1].path, `${change1} -export class someClass2 { }`); - verifyChangeAfterTimeout(outputFileStamps); - - function verifyChange(coreContent: string) { + // Two changes together report only single time message: File change detected. Starting incremental compilation... const outputFileStamps = getOutputFileStamps(host); - host.writeFile(core[1].path, coreContent); + const change1 = `${core[1].content} +export class someClass { }`; + host.writeFile(core[1].path, change1); + host.writeFile(core[1].path, `${change1} +export class someClass2 { }`); verifyChangeAfterTimeout(outputFileStamps); - } - function verifyChangeAfterTimeout(outputFileStamps: OutputFileStamp[]) { - host.checkTimeoutQueueLengthAndRun(1); // Builds core - const changedCore = getOutputFileStamps(host); - verifyChangedFiles(changedCore, outputFileStamps, [ - ...getOutputFileNames(SubProject.core, "anotherModule"), // This should not be written really - ...getOutputFileNames(SubProject.core, "index") - ]); - host.checkTimeoutQueueLengthAndRun(1); // Builds tests - const changedTests = getOutputFileStamps(host); - verifyChangedFiles(changedTests, changedCore, [ - ...getOutputFileNames(SubProject.tests, "index") // Again these need not be written - ]); - host.checkTimeoutQueueLengthAndRun(1); // Builds logic - const changedLogic = getOutputFileStamps(host); - verifyChangedFiles(changedLogic, changedTests, [ - ...getOutputFileNames(SubProject.logic, "index") // Again these need not be written - ]); - host.checkTimeoutQueueLength(0); - checkOutputErrorsIncremental(host, emptyArray); - } + function verifyChange(coreContent: string) { + verifyChangeWithFile(core[1].path, coreContent); + } + }); + + it("builds when new file is added, and its subsequent updates", () => { + const additinalFiles: ReadonlyArray<[SubProject, string]> = [[SubProject.core, newFileWithoutExtension]]; + const { verifyChangeWithFile } = createSolutionInWatchModeToVerifyChanges(additinalFiles); + verifyChange(newFile.content); + + // Another change requeues and builds it + verifyChange(`${newFile.content} +export class someClass2 { }`); + + function verifyChange(newFileContent: string) { + verifyChangeWithFile(newFile.path, newFileContent); + } + }); + }); // TODO: write tests reporting errors but that will have more involved work since file