Skip to content

Commit 716b167

Browse files
Watch extended configs if present (#41493)
* Watch extended configs if present * Address code review comments Added new `WatchType` for extended config files. Refactored watch map update to separate function, relocated call sites. Removed unnecessary test cases and relocated with new tests in programUpdates. * Unify extended config file watching between tsc/tsserver Update `updateExtendedConfigFilesWatch` to read from a `TsConfigSourceFile` to get `extendedSourceFiles`. Add watcher map to `ConfiguredProject` in the server. New test cases to verify correct events triggered and extended files are being watched properly. * Simplify watcher callback, fix tests Removes unnecessary actions in extended config watcher callback function. Updates tests to match. * Share extended config watchers across projects in server New shared watcher map in ProjectService that stores callbacks per project to be invoked when the file watcher is triggered. The FileWatcher is created with the watch options of the first Project to watch the extended config. * Refactor shared extended config map and watchers Remove all server-related utility functions/types from watchUtilities. Store config-project mapping and config file watchers inside ProjectService with new private methods to add or remove projects. * 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. * Apply suggestions from code review Co-authored-by: Sheetal Nandi <shkamat@microsoft.com> * Map extended config files by path * Move shared watcher into utilities and add more tests Co-authored-by: Sheetal Nandi <shkamat@microsoft.com>
1 parent 3e72526 commit 716b167

File tree

15 files changed

+1614
-1
lines changed

15 files changed

+1614
-1
lines changed

src/compiler/tsbuildPublic.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ namespace ts {
254254
readonly allWatchedWildcardDirectories: ESMap<ResolvedConfigFilePath, ESMap<string, WildcardDirectoryWatcher>>;
255255
readonly allWatchedInputFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
256256
readonly allWatchedConfigFiles: ESMap<ResolvedConfigFilePath, FileWatcher>;
257+
readonly allWatchedExtendedConfigFiles: ESMap<Path, SharedExtendedConfigFileWatcher<ResolvedConfigFilePath>>;
257258

258259
timerToBuildInvalidatedProject: any;
259260
reportFileChangeDetected: boolean;
@@ -325,6 +326,7 @@ namespace ts {
325326
allWatchedWildcardDirectories: new Map(),
326327
allWatchedInputFiles: new Map(),
327328
allWatchedConfigFiles: new Map(),
329+
allWatchedExtendedConfigFiles: new Map(),
328330

329331
timerToBuildInvalidatedProject: undefined,
330332
reportFileChangeDetected: false,
@@ -462,6 +464,15 @@ namespace ts {
462464
{ onDeleteValue: closeFileWatcher }
463465
);
464466

467+
state.allWatchedExtendedConfigFiles.forEach(watcher => {
468+
watcher.projects.forEach(project => {
469+
if (!currentProjects.has(project)) {
470+
watcher.projects.delete(project);
471+
}
472+
});
473+
watcher.close();
474+
});
475+
465476
mutateMapSkippingNewValues(
466477
state.allWatchedWildcardDirectories,
467478
currentProjects,
@@ -1165,6 +1176,7 @@ namespace ts {
11651176

11661177
if (reloadLevel === ConfigFileProgramReloadLevel.Full) {
11671178
watchConfigFile(state, project, projectPath, config);
1179+
watchExtendedConfigFiles(state, projectPath, config);
11681180
watchWildCardDirectories(state, project, projectPath, config);
11691181
watchInputFiles(state, project, projectPath, config);
11701182
}
@@ -1789,6 +1801,24 @@ namespace ts {
17891801
));
17901802
}
17911803

1804+
function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
1805+
updateSharedExtendedConfigFileWatcher(
1806+
resolvedPath,
1807+
parsed,
1808+
state.allWatchedExtendedConfigFiles,
1809+
(extendedConfigFileName, extendedConfigFilePath) => state.watchFile(
1810+
extendedConfigFileName,
1811+
() => state.allWatchedExtendedConfigFiles.get(extendedConfigFilePath)?.projects.forEach(projectConfigFilePath =>
1812+
invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full)
1813+
),
1814+
PollingInterval.High,
1815+
parsed?.watchOptions,
1816+
WatchType.ExtendedConfigFile,
1817+
),
1818+
fileName => toPath(state, fileName),
1819+
);
1820+
}
1821+
17921822
function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
17931823
if (!state.watch) return;
17941824
updateWatchingWildcardDirectories(
@@ -1846,6 +1876,7 @@ namespace ts {
18461876
const cfg = parseConfigFile(state, resolved, resolvedPath);
18471877
// Watch this file
18481878
watchConfigFile(state, resolved, resolvedPath, cfg);
1879+
watchExtendedConfigFiles(state, resolvedPath, cfg);
18491880
if (cfg) {
18501881
// Update watchers for wildcard directories
18511882
watchWildCardDirectories(state, resolved, resolvedPath, cfg);
@@ -1858,6 +1889,10 @@ namespace ts {
18581889

18591890
function stopWatching(state: SolutionBuilderState) {
18601891
clearMap(state.allWatchedConfigFiles, closeFileWatcher);
1892+
clearMap(state.allWatchedExtendedConfigFiles, watcher => {
1893+
watcher.projects.clear();
1894+
watcher.close();
1895+
});
18611896
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
18621897
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
18631898
}

src/compiler/watch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ namespace ts {
409409
export type WatchType = WatchTypeRegistry[keyof WatchTypeRegistry];
410410
export const WatchType: WatchTypeRegistry = {
411411
ConfigFile: "Config file",
412+
ExtendedConfigFile: "Extended config file",
412413
SourceFile: "Source file",
413414
MissingFile: "Missing file",
414415
WildcardDirectory: "Wild card directory",
@@ -418,6 +419,7 @@ namespace ts {
418419

419420
export interface WatchTypeRegistry {
420421
ConfigFile: "Config file",
422+
ExtendedConfigFile: "Extended config file",
421423
SourceFile: "Source file",
422424
MissingFile: "Missing file",
423425
WildcardDirectory: "Wild card directory",

src/compiler/watchPublic.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ namespace ts {
246246

247247
let builderProgram: T;
248248
let reloadLevel: ConfigFileProgramReloadLevel; // level to indicate if the program needs to be reloaded from config file/just filenames etc
249+
let extendedConfigFilesMap: ESMap<Path, FileWatcher>; // Map of file watchers for the extended config files
249250
let missingFilesMap: ESMap<Path, FileWatcher>; // Map of file watchers for the missing files
250251
let watchedWildcardDirectories: ESMap<string, WildcardDirectoryWatcher>; // map of watchers for the wild card directories in the config file
251252
let timerToUpdateProgram: any; // timer callback to recompile the program
@@ -337,6 +338,9 @@ namespace ts {
337338
// Update the wild card directory watch
338339
watchConfigFileWildCardDirectories();
339340

341+
// Update extended config file watch
342+
watchExtendedConfigFiles();
343+
340344
return configFileName ?
341345
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close } :
342346
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close };
@@ -354,6 +358,10 @@ namespace ts {
354358
configFileWatcher.close();
355359
configFileWatcher = undefined;
356360
}
361+
if (extendedConfigFilesMap) {
362+
clearMap(extendedConfigFilesMap, closeFileWatcher);
363+
extendedConfigFilesMap = undefined!;
364+
}
357365
if (watchedWildcardDirectories) {
358366
clearMap(watchedWildcardDirectories, closeFileWatcherOf);
359367
watchedWildcardDirectories = undefined!;
@@ -657,6 +665,9 @@ namespace ts {
657665

658666
// Update the wild card directory watch
659667
watchConfigFileWildCardDirectories();
668+
669+
// Update extended config file watch
670+
watchExtendedConfigFiles();
660671
}
661672

662673
function parseConfigFile() {
@@ -777,5 +788,23 @@ namespace ts {
777788
WatchType.WildcardDirectory
778789
);
779790
}
791+
792+
function watchExtendedConfigFiles() {
793+
// Update the extended config files watcher
794+
mutateMap(
795+
extendedConfigFilesMap ||= new Map(),
796+
arrayToMap(compilerOptions.configFile?.extendedSourceFiles || emptyArray, toPath),
797+
{
798+
// Watch the extended config files
799+
createNewValue: watchExtendedConfigFile,
800+
// Config files that are no longer extended should no longer be watched.
801+
onDeleteValue: closeFileWatcher
802+
}
803+
);
804+
}
805+
806+
function watchExtendedConfigFile(extendedConfigFile: Path) {
807+
return watchFile(extendedConfigFile, scheduleProgramReload, PollingInterval.High, watchOptions, WatchType.ExtendedConfigFile);
808+
}
780809
}
781810
}

src/compiler/watchUtilities.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,51 @@ namespace ts {
257257
Full
258258
}
259259

260+
export interface SharedExtendedConfigFileWatcher<T> extends FileWatcher {
261+
fileWatcher: FileWatcher;
262+
projects: Set<T>;
263+
}
264+
265+
/**
266+
* Updates the map of shared extended config file watches with a new set of extended config files from a base config file of the project
267+
*/
268+
export function updateSharedExtendedConfigFileWatcher<T>(
269+
projectPath: T,
270+
parsed: ParsedCommandLine | undefined,
271+
extendedConfigFilesMap: ESMap<Path, SharedExtendedConfigFileWatcher<T>>,
272+
createExtendedConfigFileWatch: (extendedConfigPath: string, extendedConfigFilePath: Path) => FileWatcher,
273+
toPath: (fileName: string) => Path,
274+
) {
275+
const extendedConfigs = arrayToMap(parsed?.options.configFile?.extendedSourceFiles || emptyArray, toPath);
276+
// remove project from all unrelated watchers
277+
extendedConfigFilesMap.forEach((watcher, extendedConfigFilePath) => {
278+
if (!extendedConfigs.has(extendedConfigFilePath)) {
279+
watcher.projects.delete(projectPath);
280+
watcher.close();
281+
}
282+
});
283+
// Update the extended config files watcher
284+
extendedConfigs.forEach((extendedConfigFileName, extendedConfigFilePath) => {
285+
const existing = extendedConfigFilesMap.get(extendedConfigFilePath);
286+
if (existing) {
287+
existing.projects.add(projectPath);
288+
}
289+
else {
290+
// start watching previously unseen extended config
291+
extendedConfigFilesMap.set(extendedConfigFilePath, {
292+
projects: new Set([projectPath]),
293+
fileWatcher: createExtendedConfigFileWatch(extendedConfigFileName, extendedConfigFilePath),
294+
close: () => {
295+
const existing = extendedConfigFilesMap.get(extendedConfigFilePath);
296+
if (!existing || existing.projects.size !== 0) return;
297+
existing.fileWatcher.close();
298+
extendedConfigFilesMap.delete(extendedConfigFilePath);
299+
},
300+
});
301+
}
302+
});
303+
}
304+
260305
/**
261306
* Updates the existing missing file watches with the new set of missing files after new program is created
262307
*/

src/server/editorServices.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,9 @@ namespace ts.server {
756756
/*@internal*/
757757
readonly watchFactory: WatchFactory<WatchType, Project>;
758758

759+
/*@internal*/
760+
private readonly sharedExtendedConfigFileWatchers = new Map<Path, SharedExtendedConfigFileWatcher<NormalizedPath>>();
761+
759762
/*@internal*/
760763
readonly packageJsonCache: PackageJsonCache;
761764
/*@internal*/
@@ -1350,6 +1353,43 @@ namespace ts.server {
13501353
}
13511354
}
13521355

1356+
/*@internal*/
1357+
updateSharedExtendedConfigFileMap({ canonicalConfigFilePath }: ConfiguredProject, parsedCommandLine: ParsedCommandLine) {
1358+
updateSharedExtendedConfigFileWatcher(
1359+
canonicalConfigFilePath,
1360+
parsedCommandLine,
1361+
this.sharedExtendedConfigFileWatchers,
1362+
(extendedConfigFileName, extendedConfigFilePath) => this.watchFactory.watchFile(
1363+
extendedConfigFileName,
1364+
() => {
1365+
let ensureProjectsForOpenFiles = false;
1366+
this.sharedExtendedConfigFileWatchers.get(extendedConfigFilePath)?.projects.forEach(canonicalPath => {
1367+
const project = this.configuredProjects.get(canonicalPath);
1368+
// Skip refresh if project is not yet loaded
1369+
if (!project || project.isInitialLoadPending()) return;
1370+
project.pendingReload = ConfigFileProgramReloadLevel.Full;
1371+
project.pendingReloadReason = `Change in extended config file ${extendedConfigFileName} detected`;
1372+
this.delayUpdateProjectGraph(project);
1373+
ensureProjectsForOpenFiles = true;
1374+
});
1375+
if (ensureProjectsForOpenFiles) this.delayEnsureProjectForOpenFiles();
1376+
},
1377+
PollingInterval.High,
1378+
this.hostConfiguration.watchOptions,
1379+
WatchType.ExtendedConfigFile
1380+
),
1381+
fileName => this.toPath(fileName),
1382+
);
1383+
}
1384+
1385+
/*@internal*/
1386+
removeProjectFromSharedExtendedConfigFileMap(project: ConfiguredProject) {
1387+
this.sharedExtendedConfigFileWatchers.forEach(watcher => {
1388+
watcher.projects.delete(project.canonicalConfigFilePath);
1389+
watcher.close();
1390+
});
1391+
}
1392+
13531393
/**
13541394
* This is the callback function for the config file add/remove/change at any location
13551395
* that matters to open script info but doesnt have configured project open
@@ -2051,7 +2091,6 @@ namespace ts.server {
20512091
this,
20522092
this.documentRegistry,
20532093
cachedDirectoryStructureHost);
2054-
// TODO: We probably should also watch the configFiles that are extended
20552094
project.createConfigFileWatcher();
20562095
this.configuredProjects.set(project.canonicalConfigFilePath, project);
20572096
this.setConfigFileExistenceByNewConfiguredProject(project);
@@ -2134,12 +2173,14 @@ namespace ts.server {
21342173
if (lastFileExceededProgramSize) {
21352174
project.disableLanguageService(lastFileExceededProgramSize);
21362175
project.stopWatchingWildCards();
2176+
this.removeProjectFromSharedExtendedConfigFileMap(project);
21372177
}
21382178
else {
21392179
project.setCompilerOptions(compilerOptions);
21402180
project.setWatchOptions(parsedCommandLine.watchOptions);
21412181
project.enableLanguageService();
21422182
project.watchWildcards(new Map(getEntries(parsedCommandLine.wildcardDirectories!))); // TODO: GH#18217
2183+
this.updateSharedExtendedConfigFileMap(project, parsedCommandLine);
21432184
}
21442185
project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides);
21452186
const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles());

src/server/project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,6 +2292,7 @@ namespace ts.server {
22922292
}
22932293

22942294
this.stopWatchingWildCards();
2295+
this.projectService.removeProjectFromSharedExtendedConfigFileMap(this);
22952296
this.projectErrors = undefined;
22962297
this.openFileWatchTriggered.clear();
22972298
this.compilerHost = undefined;

0 commit comments

Comments
 (0)