Skip to content

Commit 1acd39c

Browse files
committed
Store projects in extended config file watcher
Creates SharedExtendedConfigFileWatcher in both editorServices (tsserver) and tsbuildPublic. The file watcher is responsible for triggering a full project reload for the contained projects. Upon reload, any configs that are no longer related to a project have their watchers updated to match. New test cases to confirm that the file watchers for extended configs are closed when the project is closed.
1 parent 86905be commit 1acd39c

File tree

9 files changed

+486
-45
lines changed

9 files changed

+486
-45
lines changed

src/compiler/tsbuildPublic.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ namespace ts {
209209
originalGetSourceFile: CompilerHost["getSourceFile"];
210210
}
211211

212+
interface SharedExtendedConfigFileWatcher extends FileWatcher {
213+
projects: Set<ResolvedConfigFilePath>;
214+
}
215+
212216
interface SolutionBuilderState<T extends BuilderProgram = BuilderProgram> extends WatchFactory<WatchType, ResolvedConfigFileName> {
213217
readonly host: SolutionBuilderHost<T>;
214218
readonly hostWithWatch: SolutionBuilderWithWatchHost<T>;
@@ -253,6 +257,7 @@ namespace ts {
253257
readonly allWatchedWildcardDirectories: ESMap<ResolvedConfigFilePath, ESMap<string, WildcardDirectoryWatcher>>;
254258
readonly allWatchedInputFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
255259
readonly allWatchedConfigFiles: ESMap<ResolvedConfigFilePath, FileWatcher>;
260+
readonly allWatchedExtendedConfigFiles: ESMap<ResolvedConfigFilePath, SharedExtendedConfigFileWatcher>;
256261

257262
timerToBuildInvalidatedProject: any;
258263
reportFileChangeDetected: boolean;
@@ -324,6 +329,7 @@ namespace ts {
324329
allWatchedWildcardDirectories: new Map(),
325330
allWatchedInputFiles: new Map(),
326331
allWatchedConfigFiles: new Map(),
332+
allWatchedExtendedConfigFiles: new Map(),
327333

328334
timerToBuildInvalidatedProject: undefined,
329335
reportFileChangeDetected: false,
@@ -461,6 +467,18 @@ namespace ts {
461467
{ onDeleteValue: closeFileWatcher }
462468
);
463469

470+
state.allWatchedExtendedConfigFiles.forEach((watcher, extendedConfigFilePath) => {
471+
watcher.projects.forEach((project) => {
472+
if (!currentProjects.has(project)) {
473+
watcher.projects.delete(project);
474+
}
475+
});
476+
if (watcher.projects.size === 0) {
477+
watcher.close();
478+
state.allWatchedExtendedConfigFiles.delete(extendedConfigFilePath);
479+
}
480+
});
481+
464482
mutateMapSkippingNewValues(
465483
state.allWatchedWildcardDirectories,
466484
currentProjects,
@@ -1164,6 +1182,7 @@ namespace ts {
11641182

11651183
if (reloadLevel === ConfigFileProgramReloadLevel.Full) {
11661184
watchConfigFile(state, project, projectPath, config);
1185+
watchExtendedConfigFiles(state, projectPath, config);
11671186
watchWildCardDirectories(state, project, projectPath, config);
11681187
watchInputFiles(state, project, projectPath, config);
11691188
}
@@ -1790,6 +1809,49 @@ namespace ts {
17901809
));
17911810
}
17921811

1812+
function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
1813+
const extendedSourceFiles = parsed?.options.configFile?.extendedSourceFiles || emptyArray;
1814+
const extendedConfigs = new Map(extendedSourceFiles.map((extendedSourceFile) => {
1815+
const extendedConfigFileName = extendedSourceFile as ResolvedConfigFileName;
1816+
const extendedConfigFilePath = toResolvedConfigFilePath(state, extendedConfigFileName);
1817+
return [extendedConfigFilePath, extendedConfigFileName] as const;
1818+
}));
1819+
extendedConfigs.forEach((extendedConfigFileName, extendedConfigFilePath) => {
1820+
// start watching previously unseen extended config
1821+
if (!state.allWatchedExtendedConfigFiles.has(extendedConfigFilePath)) {
1822+
const projects = new Set<ResolvedConfigFilePath>([resolvedPath]);
1823+
const fileWatcher = state.watchFile(
1824+
extendedConfigFileName,
1825+
() => {
1826+
projects.forEach((projectConfigFilePath) => {
1827+
invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full);
1828+
});
1829+
},
1830+
PollingInterval.High,
1831+
parsed?.watchOptions,
1832+
WatchType.ExtendedConfigFile,
1833+
extendedConfigFileName
1834+
);
1835+
state.allWatchedExtendedConfigFiles.set(extendedConfigFilePath, {
1836+
close: () => fileWatcher.close(),
1837+
projects,
1838+
});
1839+
}
1840+
});
1841+
state.allWatchedExtendedConfigFiles.forEach((watcher, extendedConfigFilePath) => {
1842+
if (extendedConfigs.has(extendedConfigFilePath)) {
1843+
watcher.projects.add(resolvedPath);
1844+
}
1845+
else {
1846+
watcher.projects.delete(resolvedPath);
1847+
if (watcher.projects.size === 0) {
1848+
watcher.close();
1849+
state.allWatchedExtendedConfigFiles.delete(extendedConfigFilePath);
1850+
}
1851+
}
1852+
});
1853+
}
1854+
17931855
function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
17941856
if (!state.watch) return;
17951857
updateWatchingWildcardDirectories(
@@ -1848,6 +1910,7 @@ namespace ts {
18481910
const cfg = parseConfigFile(state, resolved, resolvedPath);
18491911
// Watch this file
18501912
watchConfigFile(state, resolved, resolvedPath, cfg);
1913+
watchExtendedConfigFiles(state, resolvedPath, cfg);
18511914
if (cfg) {
18521915
// Update watchers for wildcard directories
18531916
watchWildCardDirectories(state, resolved, resolvedPath, cfg);
@@ -1860,6 +1923,7 @@ namespace ts {
18601923

18611924
function stopWatching(state: SolutionBuilderState) {
18621925
clearMap(state.allWatchedConfigFiles, closeFileWatcher);
1926+
clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcher);
18631927
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
18641928
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
18651929
}

src/compiler/watchPublic.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -793,13 +793,11 @@ namespace ts {
793793

794794
function watchExtendedConfigFiles() {
795795
const { configFile } = builderProgram.getCompilerOptions();
796-
if (configFile) {
797-
updateExtendedConfigFilesMap(
798-
configFile,
799-
extendedConfigFilesMap || (extendedConfigFilesMap = new Map()),
800-
watchExtendedConfigFile
801-
);
802-
}
796+
updateExtendedConfigFilesMap(
797+
configFile,
798+
extendedConfigFilesMap || (extendedConfigFilesMap = new Map()),
799+
watchExtendedConfigFile
800+
);
803801
}
804802

805803
function watchExtendedConfigFile(extendedConfigFile: string) {

src/compiler/watchUtilities.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,11 @@ namespace ts {
261261
* Updates the map of extended config file watches with a new set of extended config files from a base config file
262262
*/
263263
export function updateExtendedConfigFilesMap(
264-
configFile: TsConfigSourceFile,
265-
extendedConfigFilesMap: ESMap<string, FileWatcher>,
264+
configFile: TsConfigSourceFile | undefined,
265+
extendedConfigFilesMap: ESMap<Path, FileWatcher>,
266266
createExtendedConfigFileWatch: (extendedConfigPath: string) => FileWatcher,
267267
) {
268-
const extendedSourceFiles = configFile.extendedSourceFiles || emptyArray;
268+
const extendedSourceFiles = configFile?.extendedSourceFiles ?? emptyArray;
269269
// TODO(rbuckton): Should be a `Set` but that requires changing the below code that uses `mutateMap`
270270
const newExtendedConfigFilesMap = arrayToMap(extendedSourceFiles, identity, returnTrue);
271271
// Update the extended config files watcher

src/server/editorServices.ts

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,10 @@ namespace ts.server {
641641
errors: Diagnostic[] | undefined;
642642
}
643643

644+
interface SharedExtendedConfigFileWatcher extends FileWatcher {
645+
projects: Set<ConfiguredProject>;
646+
}
647+
644648
export class ProjectService {
645649

646650
/*@internal*/
@@ -757,9 +761,7 @@ namespace ts.server {
757761
readonly watchFactory: WatchFactory<WatchType, Project>;
758762

759763
/*@internal*/
760-
private sharedExtendedConfigFileMap = createMultiMap<Path, ConfiguredProject>();
761-
/*@internal*/
762-
private sharedExtendedConfigFileWatchers = new Map<Path, FileWatcher>();
764+
private readonly sharedExtendedConfigFileWatchers = new Map<Path, SharedExtendedConfigFileWatcher>();
763765

764766
/*@internal*/
765767
readonly packageJsonCache: PackageJsonCache;
@@ -1358,53 +1360,60 @@ namespace ts.server {
13581360

13591361
/*@internal*/
13601362
private updateSharedExtendedConfigFileMap(project: ConfiguredProject) {
1361-
const extendedSourceFiles = project.getCompilerOptions().configFile?.extendedSourceFiles || emptyArray;
1362-
extendedSourceFiles.forEach((extendedSourceFile: string) => {
1363-
const extendedConfigPath = this.toPath(extendedSourceFile);
1364-
if (!this.sharedExtendedConfigFileMap.has(extendedConfigPath)) {
1365-
const watcher = this.watchFactory.watchFile(
1363+
const extendedConfigPaths: readonly Path[] = project.getCompilerOptions().configFile?.extendedSourceFiles
1364+
?.map((file) => this.toPath(file)) ?? emptyArray;
1365+
for (const extendedConfigPath of extendedConfigPaths) {
1366+
// start watching previously unseen extended config
1367+
if (!this.sharedExtendedConfigFileWatchers.has(extendedConfigPath)) {
1368+
const projects = new Set<ConfiguredProject>([project]);
1369+
const fileWatcherCallback = () => {
1370+
const reason = `Change in extended config file ${extendedConfigPath} detected`;
1371+
projects.forEach((project: ConfiguredProject) => {
1372+
// Skip refresh if project is not yet loaded
1373+
if (project.isInitialLoadPending()) return;
1374+
project.pendingReload = ConfigFileProgramReloadLevel.Full;
1375+
project.pendingReloadReason = reason;
1376+
this.delayUpdateProjectGraph(project);
1377+
});
1378+
};
1379+
const fileWatcher = this.watchFactory.watchFile(
13661380
extendedConfigPath,
1367-
() => this.onSharedExtendedConfigChanged(extendedConfigPath),
1381+
fileWatcherCallback,
13681382
PollingInterval.High,
13691383
this.hostConfiguration.watchOptions,
13701384
WatchType.ExtendedConfigFile
13711385
);
1372-
this.sharedExtendedConfigFileWatchers.set(extendedConfigPath, watcher);
1386+
this.sharedExtendedConfigFileWatchers.set(extendedConfigPath, {
1387+
close: () => fileWatcher.close(),
1388+
projects,
1389+
});
13731390
}
1374-
const otherProjects = this.sharedExtendedConfigFileMap.get(extendedConfigPath);
1375-
if (!otherProjects || !otherProjects.includes(project)) {
1376-
this.sharedExtendedConfigFileMap.add(extendedConfigPath, project);
1391+
}
1392+
this.sharedExtendedConfigFileWatchers.forEach((watcher, extendedConfigPath) => {
1393+
if (extendedConfigPaths.includes(extendedConfigPath)) {
1394+
watcher.projects.add(project);
1395+
}
1396+
else {
1397+
watcher.projects.delete(project);
1398+
if (watcher.projects.size === 0) {
1399+
watcher.close();
1400+
this.sharedExtendedConfigFileWatchers.delete(extendedConfigPath);
1401+
}
13771402
}
13781403
});
13791404
}
13801405

13811406
/*@internal*/
13821407
private removeProjectFromSharedExtendedConfigFileMap(project: ConfiguredProject) {
1383-
for (const key of arrayFrom(this.sharedExtendedConfigFileMap.keys())) {
1384-
this.sharedExtendedConfigFileMap.remove(key, project);
1385-
const otherProjects = this.sharedExtendedConfigFileMap.get(key) || emptyArray;
1386-
if (otherProjects.length === 0) {
1387-
const watcher = this.sharedExtendedConfigFileWatchers.get(key);
1388-
if (watcher) {
1389-
watcher.close();
1390-
this.sharedExtendedConfigFileWatchers.delete(key);
1391-
}
1408+
for (const [sharedConfigPath, watcher] of arrayFrom(this.sharedExtendedConfigFileWatchers.entries())) {
1409+
watcher.projects.delete(project);
1410+
if (watcher.projects.size === 0) {
1411+
watcher.close();
1412+
this.sharedExtendedConfigFileWatchers.delete(sharedConfigPath);
13921413
}
13931414
}
13941415
}
13951416

1396-
/*@internal*/
1397-
private onSharedExtendedConfigChanged(extendedConfigPath: Path) {
1398-
const projects = this.sharedExtendedConfigFileMap.get(extendedConfigPath) || emptyArray;
1399-
projects.forEach((project: ConfiguredProject) => {
1400-
// Skip refresh if project is not yet loaded
1401-
if (project.isInitialLoadPending()) return;
1402-
project.pendingReload = ConfigFileProgramReloadLevel.Full;
1403-
project.pendingReloadReason = `Change in extended config file ${extendedConfigPath} detected`;
1404-
this.delayUpdateProjectGraph(project);
1405-
});
1406-
}
1407-
14081417
/**
14091418
* This is the callback function for the config file add/remove/change at any location
14101419
* that matters to open script info but doesnt have configured project open

src/testRunner/unittests/tscWatch/programUpdates.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,5 +1766,82 @@ import { x } from "../b";`),
17661766
}
17671767
]
17681768
});
1769+
1770+
verifyTscWatch({
1771+
scenario,
1772+
subScenario: "works correctly when project with extended config is removed",
1773+
commandLineArgs: ["-b", "-w", configFilePath],
1774+
sys: () => {
1775+
const alphaExtendedConfigFile: File = {
1776+
path: "/a/b/alpha.tsconfig.json",
1777+
content: JSON.stringify({
1778+
strict: true
1779+
})
1780+
};
1781+
const project1Config: File = {
1782+
path: "/a/b/project1.tsconfig.json",
1783+
content: JSON.stringify({
1784+
extends: "./alpha.tsconfig.json",
1785+
compilerOptions: {
1786+
composite: true,
1787+
},
1788+
files: [commonFile1.path, commonFile2.path]
1789+
})
1790+
};
1791+
const bravoExtendedConfigFile: File = {
1792+
path: "/a/b/bravo.tsconfig.json",
1793+
content: JSON.stringify({
1794+
strict: true
1795+
})
1796+
};
1797+
const otherFile: File = {
1798+
path: "/a/b/other.ts",
1799+
content: "let z = 0;",
1800+
};
1801+
const project2Config: File = {
1802+
path: "/a/b/project2.tsconfig.json",
1803+
content: JSON.stringify({
1804+
extends: "./bravo.tsconfig.json",
1805+
compilerOptions: {
1806+
composite: true,
1807+
},
1808+
files: [otherFile.path]
1809+
})
1810+
};
1811+
const configFile: File = {
1812+
path: configFilePath,
1813+
content: JSON.stringify({
1814+
references: [
1815+
{
1816+
path: "./project1.tsconfig.json",
1817+
},
1818+
{
1819+
path: "./project2.tsconfig.json",
1820+
},
1821+
],
1822+
files: [],
1823+
})
1824+
};
1825+
return createWatchedSystem([
1826+
libFile, configFile,
1827+
alphaExtendedConfigFile, project1Config, commonFile1, commonFile2,
1828+
bravoExtendedConfigFile, project2Config, otherFile
1829+
]);
1830+
},
1831+
changes: [
1832+
{
1833+
caption: "Remove project2 from base config",
1834+
change: sys => sys.modifyFile(configFilePath, JSON.stringify({
1835+
references: [
1836+
{
1837+
path: "./project1.tsconfig.json",
1838+
},
1839+
],
1840+
files: [],
1841+
})),
1842+
timeouts: checkSingleTimeoutQueueLengthAndRun,
1843+
}
1844+
]
1845+
});
17691846
});
17701847
}

0 commit comments

Comments
 (0)