diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 29911f42906..ba051ac23b2 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -564,8 +564,51 @@ namespace ts { } function getBuildOrder(state: SolutionBuilderState) { - return state.buildOrder || - (state.buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)))); + return state.buildOrder || createStateBuildOrder(state); + } + + function createStateBuildOrder(state: SolutionBuilderState) { + const buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f))); + if (arrayIsEqualTo(state.buildOrder, buildOrder)) return state.buildOrder!; + + // Clear all to ResolvedConfigFilePaths cache to start fresh + state.resolvedConfigFilePaths.clear(); + const currentProjects = arrayToSet( + buildOrder, + resolved => toResolvedConfigFilePath(state, resolved) + ) as ConfigFileMap; + + const noopOnDelete = { onDeleteValue: noop }; + // Config file cache + mutateMapSkippingNewValues(state.configFileCache, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectStatus, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.buildInfoChecked, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.builderPrograms, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.diagnostics, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectPendingBuild, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectErrorsReported, currentProjects, noopOnDelete); + + // Remove watches for the program no longer in the solution + if (state.watch) { + mutateMapSkippingNewValues( + state.allWatchedConfigFiles, + currentProjects, + { onDeleteValue: closeFileWatcher } + ); + + mutateMapSkippingNewValues( + state.allWatchedWildcardDirectories, + currentProjects, + { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcherOf) } + ); + + mutateMapSkippingNewValues( + state.allWatchedInputFiles, + currentProjects, + { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) } + ); + } + return state.buildOrder = buildOrder; } function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index d13e12a6f19..aa6e974c8e8 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4466,8 +4466,7 @@ namespace ts { map.clear(); } - export interface MutateMapOptions { - createNewValue(key: string, valueInNewMap: U): T; + export interface MutateMapSkippingNewValuesOptions { onDeleteValue(existingValue: T, key: string): void; /** @@ -4482,8 +4481,12 @@ namespace ts { /** * Mutates the map with newMap such that keys in map will be same as newMap. */ - export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { - const { createNewValue, onDeleteValue, onExistingValue } = options; + export function mutateMapSkippingNewValues( + map: Map, + newMap: ReadonlyMap, + options: MutateMapSkippingNewValuesOptions + ) { + const { onDeleteValue, onExistingValue } = options; // Needs update map.forEach((existingValue, key) => { const valueInNewMap = newMap.get(key); @@ -4497,7 +4500,20 @@ namespace ts { onExistingValue(existingValue, valueInNewMap, key); } }); + } + export interface MutateMapOptions extends MutateMapSkippingNewValuesOptions { + createNewValue(key: string, valueInNewMap: U): T; + } + + /** + * Mutates the map with newMap such that keys in map will be same as newMap. + */ + export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { + // Needs update + mutateMapSkippingNewValues(map, newMap, options); + + const { createNewValue } = options; // Add new values that are not already present newMap.forEach((valueInNewMap, key) => { if (!map.has(key)) { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index b8e3d189781..0535d832374 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -405,7 +405,7 @@ interface Array {}` return s; } - private now() { + now() { this.time += timeIncrements; return new Date(this.time); } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index a2ae6e478f3..7d0cea1ea64 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -102,6 +102,7 @@ "unittests/tsbuild/resolveJsonModule.ts", "unittests/tsbuild/sample.ts", "unittests/tsbuild/transitiveReferences.ts", + "unittests/tsbuild/watchEnvironment.ts", "unittests/tsbuild/watchMode.ts", "unittests/tscWatch/consoleClearing.ts", "unittests/tscWatch/emit.ts", diff --git a/src/testRunner/unittests/tsbuild/watchEnvironment.ts b/src/testRunner/unittests/tsbuild/watchEnvironment.ts new file mode 100644 index 00000000000..f97c92a89d4 --- /dev/null +++ b/src/testRunner/unittests/tsbuild/watchEnvironment.ts @@ -0,0 +1,111 @@ +namespace ts.tscWatch { + describe("unittests:: tsbuild:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => { + it("watchFile on same file multiple times because file is part of multiple projects", () => { + const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`; + let maxPkgs = 4; + const configPath = `${project}/tsconfig.json`; + const typing: File = { + path: `${project}/typings/xterm.d.ts`, + content: "export const typing = 10;" + }; + + const allPkgFiles = pkgs(pkgFiles); + const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project }); + writePkgReferences(); + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); + solutionBuilder.build(); + checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [ + `Projects in this build: \r\n${ + concatenate( + pkgs(index => ` * pkg${index}/tsconfig.json`), + [" * tsconfig.json"] + ).join("\r\n")}\n\n`, + ...flatArray(pkgs(index => [ + `Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n` + ])) + ]); + + const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1); + watchFilesDetailed.set(configPath, 1); + watchFilesDetailed.set(typing.path, maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + verifyInvoke(); + + // Make change + maxPkgs--; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, emptyArray); + const lastFiles = last(allPkgFiles); + lastFiles.forEach(f => watchFilesDetailed.delete(f.path)); + watchFilesDetailed.set(typing.path, maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, typing.content); + verifyInvoke(); + + // Make change to remove all the watches + maxPkgs = 0; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, [ + `tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n` + ]); + checkWatchedFilesDetailed(system, [configPath], 1); + + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + system.checkTimeoutQueueLength(0); + + function flatArray(arr: T[][]): readonly T[] { + return flatMap(arr, identity); + } + function pkgs(cb: (index: number) => T): T[] { + const result: T[] = []; + for (let index = 0; index < maxPkgs; index++) { + result.push(cb(index)); + } + return result; + } + function createPkgReference(index: number) { + return { path: `./pkg${index}` }; + } + function pkgFiles(index: number): File[] { + return [ + { + path: `${project}/pkg${index}/index.ts`, + content: `export const pkg${index} = ${index};` + }, + { + path: `${project}/pkg${index}/tsconfig.json`, + content: JSON.stringify({ + complerOptions: { composite: true }, + include: [ + "**/*.ts", + "../typings/xterm.d.ts" + ] + }) + } + ]; + } + function writePkgReferences() { + system.writeFile(configPath, JSON.stringify({ + files: [], + include: [], + references: pkgs(createPkgReference) + })); + } + function verifyInvoke() { + pkgs(() => system.checkTimeoutQueueLengthAndRun(1)); + checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [ + ...flatArray(pkgs(index => [ + `Project 'pkg${index}/tsconfig.json' is out of date because oldest output 'pkg${index}/index.js' is older than newest input 'typings/xterm.d.ts'\n\n`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n`, + `Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n` + ])) + ]); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsbuild/watchMode.ts b/src/testRunner/unittests/tsbuild/watchMode.ts index 4546e4e75c9..bd7653b6bea 100644 --- a/src/testRunner/unittests/tsbuild/watchMode.ts +++ b/src/testRunner/unittests/tsbuild/watchMode.ts @@ -16,11 +16,19 @@ namespace ts.tscWatch { return host; } + export function createSolutionBuilder(system: WatchedSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { const host = createSolutionBuilderHost(system); + host.now = system.now.bind(system); return ts.createSolutionBuilder(host, rootNames, defaultOptions || {}); } + export function createSolutionBuilderWithWatchHost(system: WatchedSystem) { + const host = ts.createSolutionBuilderWithWatchHost(system); + host.now = system.now.bind(system); + return host; + } + function createSolutionBuilderWithWatch(system: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { const host = createSolutionBuilderWithWatchHost(system); const solutionBuilder = ts.createSolutionBuilderWithWatch(host, rootNames, defaultOptions || { watch: true });