Skip to content
35 changes: 35 additions & 0 deletions src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ namespace ts {
readonly allWatchedWildcardDirectories: ESMap<ResolvedConfigFilePath, ESMap<string, WildcardDirectoryWatcher>>;
readonly allWatchedInputFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
readonly allWatchedConfigFiles: ESMap<ResolvedConfigFilePath, FileWatcher>;
readonly allWatchedExtendedConfigFiles: ESMap<Path, SharedExtendedConfigFileWatcher<ResolvedConfigFilePath>>;

timerToBuildInvalidatedProject: any;
reportFileChangeDetected: boolean;
Expand Down Expand Up @@ -325,6 +326,7 @@ namespace ts {
allWatchedWildcardDirectories: new Map(),
allWatchedInputFiles: new Map(),
allWatchedConfigFiles: new Map(),
allWatchedExtendedConfigFiles: new Map(),

timerToBuildInvalidatedProject: undefined,
reportFileChangeDetected: false,
Expand Down Expand Up @@ -462,6 +464,15 @@ namespace ts {
{ onDeleteValue: closeFileWatcher }
);

state.allWatchedExtendedConfigFiles.forEach(watcher => {
watcher.projects.forEach(project => {
if (!currentProjects.has(project)) {
watcher.projects.delete(project);
}
});
watcher.close();
});

mutateMapSkippingNewValues(
state.allWatchedWildcardDirectories,
currentProjects,
Expand Down Expand Up @@ -1165,6 +1176,7 @@ namespace ts {

if (reloadLevel === ConfigFileProgramReloadLevel.Full) {
watchConfigFile(state, project, projectPath, config);
watchExtendedConfigFiles(state, projectPath, config);
watchWildCardDirectories(state, project, projectPath, config);
watchInputFiles(state, project, projectPath, config);
}
Expand Down Expand Up @@ -1789,6 +1801,24 @@ namespace ts {
));
}

function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
updateSharedExtendedConfigFileWatcher(
resolvedPath,
parsed,
state.allWatchedExtendedConfigFiles,
(extendedConfigFileName, extendedConfigFilePath) => state.watchFile(
extendedConfigFileName,
() => state.allWatchedExtendedConfigFiles.get(extendedConfigFilePath)?.projects.forEach(projectConfigFilePath =>
invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full)
),
PollingInterval.High,
parsed?.watchOptions,
WatchType.ExtendedConfigFile,
),
fileName => toPath(state, fileName),
);
}

function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
if (!state.watch) return;
updateWatchingWildcardDirectories(
Expand Down Expand Up @@ -1846,6 +1876,7 @@ namespace ts {
const cfg = parseConfigFile(state, resolved, resolvedPath);
// Watch this file
watchConfigFile(state, resolved, resolvedPath, cfg);
watchExtendedConfigFiles(state, resolvedPath, cfg);
if (cfg) {
// Update watchers for wildcard directories
watchWildCardDirectories(state, resolved, resolvedPath, cfg);
Expand All @@ -1858,6 +1889,10 @@ namespace ts {

function stopWatching(state: SolutionBuilderState) {
clearMap(state.allWatchedConfigFiles, closeFileWatcher);
clearMap(state.allWatchedExtendedConfigFiles, watcher => {
watcher.projects.clear();
watcher.close();
});
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
}
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ namespace ts {
export type WatchType = WatchTypeRegistry[keyof WatchTypeRegistry];
export const WatchType: WatchTypeRegistry = {
ConfigFile: "Config file",
ExtendedConfigFile: "Extended config file",
SourceFile: "Source file",
MissingFile: "Missing file",
WildcardDirectory: "Wild card directory",
Expand All @@ -418,6 +419,7 @@ namespace ts {

export interface WatchTypeRegistry {
ConfigFile: "Config file",
ExtendedConfigFile: "Extended config file",
SourceFile: "Source file",
MissingFile: "Missing file",
WildcardDirectory: "Wild card directory",
Expand Down
29 changes: 29 additions & 0 deletions src/compiler/watchPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ namespace ts {

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

// Update extended config file watch
watchExtendedConfigFiles();

return configFileName ?
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close } :
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close };
Expand All @@ -354,6 +358,10 @@ namespace ts {
configFileWatcher.close();
configFileWatcher = undefined;
}
if (extendedConfigFilesMap) {
clearMap(extendedConfigFilesMap, closeFileWatcher);
extendedConfigFilesMap = undefined!;
}
if (watchedWildcardDirectories) {
clearMap(watchedWildcardDirectories, closeFileWatcherOf);
watchedWildcardDirectories = undefined!;
Expand Down Expand Up @@ -657,6 +665,9 @@ namespace ts {

// Update the wild card directory watch
watchConfigFileWildCardDirectories();

// Update extended config file watch
watchExtendedConfigFiles();
}

function parseConfigFile() {
Expand Down Expand Up @@ -777,5 +788,23 @@ namespace ts {
WatchType.WildcardDirectory
);
}

function watchExtendedConfigFiles() {
// Update the extended config files watcher
mutateMap(
extendedConfigFilesMap ||= new Map(),
arrayToMap(compilerOptions.configFile?.extendedSourceFiles || emptyArray, toPath),
{
// Watch the extended config files
createNewValue: watchExtendedConfigFile,
// Config files that are no longer extended should no longer be watched.
onDeleteValue: closeFileWatcher
}
);
}

function watchExtendedConfigFile(extendedConfigFile: Path) {
return watchFile(extendedConfigFile, scheduleProgramReload, PollingInterval.High, watchOptions, WatchType.ExtendedConfigFile);
}
}
}
45 changes: 45 additions & 0 deletions src/compiler/watchUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,51 @@ namespace ts {
Full
}

export interface SharedExtendedConfigFileWatcher<T> extends FileWatcher {
fileWatcher: FileWatcher;
projects: Set<T>;
}

/**
* 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
*/
export function updateSharedExtendedConfigFileWatcher<T>(
projectPath: T,
parsed: ParsedCommandLine | undefined,
extendedConfigFilesMap: ESMap<Path, SharedExtendedConfigFileWatcher<T>>,
createExtendedConfigFileWatch: (extendedConfigPath: string, extendedConfigFilePath: Path) => FileWatcher,
toPath: (fileName: string) => Path,
) {
const extendedConfigs = arrayToMap(parsed?.options.configFile?.extendedSourceFiles || emptyArray, toPath);
// remove project from all unrelated watchers
extendedConfigFilesMap.forEach((watcher, extendedConfigFilePath) => {
if (!extendedConfigs.has(extendedConfigFilePath)) {
watcher.projects.delete(projectPath);
watcher.close();
}
});
// Update the extended config files watcher
extendedConfigs.forEach((extendedConfigFileName, extendedConfigFilePath) => {
const existing = extendedConfigFilesMap.get(extendedConfigFilePath);
if (existing) {
existing.projects.add(projectPath);
}
else {
// start watching previously unseen extended config
extendedConfigFilesMap.set(extendedConfigFilePath, {
projects: new Set([projectPath]),
fileWatcher: createExtendedConfigFileWatch(extendedConfigFileName, extendedConfigFilePath),
close: () => {
const existing = extendedConfigFilesMap.get(extendedConfigFilePath);
if (!existing || existing.projects.size !== 0) return;
existing.fileWatcher.close();
extendedConfigFilesMap.delete(extendedConfigFilePath);
},
});
}
});
}

/**
* Updates the existing missing file watches with the new set of missing files after new program is created
*/
Expand Down
43 changes: 42 additions & 1 deletion src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,9 @@ namespace ts.server {
/*@internal*/
readonly watchFactory: WatchFactory<WatchType, Project>;

/*@internal*/
private readonly sharedExtendedConfigFileWatchers = new Map<Path, SharedExtendedConfigFileWatcher<NormalizedPath>>();

/*@internal*/
readonly packageJsonCache: PackageJsonCache;
/*@internal*/
Expand Down Expand Up @@ -1350,6 +1353,43 @@ namespace ts.server {
}
}

/*@internal*/
updateSharedExtendedConfigFileMap({ canonicalConfigFilePath }: ConfiguredProject, parsedCommandLine: ParsedCommandLine) {
updateSharedExtendedConfigFileWatcher(
canonicalConfigFilePath,
parsedCommandLine,
this.sharedExtendedConfigFileWatchers,
(extendedConfigFileName, extendedConfigFilePath) => this.watchFactory.watchFile(
extendedConfigFileName,
() => {
let ensureProjectsForOpenFiles = false;
this.sharedExtendedConfigFileWatchers.get(extendedConfigFilePath)?.projects.forEach(canonicalPath => {
const project = this.configuredProjects.get(canonicalPath);
// Skip refresh if project is not yet loaded
if (!project || project.isInitialLoadPending()) return;
project.pendingReload = ConfigFileProgramReloadLevel.Full;
project.pendingReloadReason = `Change in extended config file ${extendedConfigFileName} detected`;
this.delayUpdateProjectGraph(project);
ensureProjectsForOpenFiles = true;
});
if (ensureProjectsForOpenFiles) this.delayEnsureProjectForOpenFiles();
},
PollingInterval.High,
this.hostConfiguration.watchOptions,
WatchType.ExtendedConfigFile
),
fileName => this.toPath(fileName),
);
}

/*@internal*/
removeProjectFromSharedExtendedConfigFileMap(project: ConfiguredProject) {
this.sharedExtendedConfigFileWatchers.forEach(watcher => {
watcher.projects.delete(project.canonicalConfigFilePath);
watcher.close();
});
}

/**
* This is the callback function for the config file add/remove/change at any location
* that matters to open script info but doesnt have configured project open
Expand Down Expand Up @@ -2051,7 +2091,6 @@ namespace ts.server {
this,
this.documentRegistry,
cachedDirectoryStructureHost);
// TODO: We probably should also watch the configFiles that are extended
project.createConfigFileWatcher();
this.configuredProjects.set(project.canonicalConfigFilePath, project);
this.setConfigFileExistenceByNewConfiguredProject(project);
Expand Down Expand Up @@ -2134,12 +2173,14 @@ namespace ts.server {
if (lastFileExceededProgramSize) {
project.disableLanguageService(lastFileExceededProgramSize);
project.stopWatchingWildCards();
this.removeProjectFromSharedExtendedConfigFileMap(project);
}
else {
project.setCompilerOptions(compilerOptions);
project.setWatchOptions(parsedCommandLine.watchOptions);
project.enableLanguageService();
project.watchWildcards(new Map(getEntries(parsedCommandLine.wildcardDirectories!))); // TODO: GH#18217
this.updateSharedExtendedConfigFileMap(project, parsedCommandLine);
}
project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides);
const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles());
Expand Down
1 change: 1 addition & 0 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2292,6 +2292,7 @@ namespace ts.server {
}

this.stopWatchingWildCards();
this.projectService.removeProjectFromSharedExtendedConfigFileMap(this);
this.projectErrors = undefined;
this.openFileWatchTriggered.clear();
this.compilerHost = undefined;
Expand Down
Loading