Skip to content

Commit

Permalink
Store projects in extended config file watcher
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
molisani committed Dec 9, 2020
1 parent 86905be commit 1acd39c
Show file tree
Hide file tree
Showing 9 changed files with 486 additions and 45 deletions.
64 changes: 64 additions & 0 deletions src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ namespace ts {
originalGetSourceFile: CompilerHost["getSourceFile"];
}

interface SharedExtendedConfigFileWatcher extends FileWatcher {
projects: Set<ResolvedConfigFilePath>;
}

interface SolutionBuilderState<T extends BuilderProgram = BuilderProgram> extends WatchFactory<WatchType, ResolvedConfigFileName> {
readonly host: SolutionBuilderHost<T>;
readonly hostWithWatch: SolutionBuilderWithWatchHost<T>;
Expand Down Expand Up @@ -253,6 +257,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<ResolvedConfigFilePath, SharedExtendedConfigFileWatcher>;

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

timerToBuildInvalidatedProject: undefined,
reportFileChangeDetected: false,
Expand Down Expand Up @@ -461,6 +467,18 @@ namespace ts {
{ onDeleteValue: closeFileWatcher }
);

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

mutateMapSkippingNewValues(
state.allWatchedWildcardDirectories,
currentProjects,
Expand Down Expand Up @@ -1164,6 +1182,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 @@ -1790,6 +1809,49 @@ namespace ts {
));
}

function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
const extendedSourceFiles = parsed?.options.configFile?.extendedSourceFiles || emptyArray;
const extendedConfigs = new Map(extendedSourceFiles.map((extendedSourceFile) => {
const extendedConfigFileName = extendedSourceFile as ResolvedConfigFileName;
const extendedConfigFilePath = toResolvedConfigFilePath(state, extendedConfigFileName);
return [extendedConfigFilePath, extendedConfigFileName] as const;
}));
extendedConfigs.forEach((extendedConfigFileName, extendedConfigFilePath) => {
// start watching previously unseen extended config
if (!state.allWatchedExtendedConfigFiles.has(extendedConfigFilePath)) {
const projects = new Set<ResolvedConfigFilePath>([resolvedPath]);
const fileWatcher = state.watchFile(
extendedConfigFileName,
() => {
projects.forEach((projectConfigFilePath) => {
invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full);
});
},
PollingInterval.High,
parsed?.watchOptions,
WatchType.ExtendedConfigFile,
extendedConfigFileName
);
state.allWatchedExtendedConfigFiles.set(extendedConfigFilePath, {
close: () => fileWatcher.close(),
projects,
});
}
});
state.allWatchedExtendedConfigFiles.forEach((watcher, extendedConfigFilePath) => {
if (extendedConfigs.has(extendedConfigFilePath)) {
watcher.projects.add(resolvedPath);
}
else {
watcher.projects.delete(resolvedPath);
if (watcher.projects.size === 0) {
watcher.close();
state.allWatchedExtendedConfigFiles.delete(extendedConfigFilePath);
}
}
});
}

function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
if (!state.watch) return;
updateWatchingWildcardDirectories(
Expand Down Expand Up @@ -1848,6 +1910,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 @@ -1860,6 +1923,7 @@ namespace ts {

function stopWatching(state: SolutionBuilderState) {
clearMap(state.allWatchedConfigFiles, closeFileWatcher);
clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcher);
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
}
Expand Down
12 changes: 5 additions & 7 deletions src/compiler/watchPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,13 +793,11 @@ namespace ts {

function watchExtendedConfigFiles() {
const { configFile } = builderProgram.getCompilerOptions();
if (configFile) {
updateExtendedConfigFilesMap(
configFile,
extendedConfigFilesMap || (extendedConfigFilesMap = new Map()),
watchExtendedConfigFile
);
}
updateExtendedConfigFilesMap(
configFile,
extendedConfigFilesMap || (extendedConfigFilesMap = new Map()),
watchExtendedConfigFile
);
}

function watchExtendedConfigFile(extendedConfigFile: string) {
Expand Down
6 changes: 3 additions & 3 deletions src/compiler/watchUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,11 @@ namespace ts {
* Updates the map of extended config file watches with a new set of extended config files from a base config file
*/
export function updateExtendedConfigFilesMap(
configFile: TsConfigSourceFile,
extendedConfigFilesMap: ESMap<string, FileWatcher>,
configFile: TsConfigSourceFile | undefined,
extendedConfigFilesMap: ESMap<Path, FileWatcher>,
createExtendedConfigFileWatch: (extendedConfigPath: string) => FileWatcher,
) {
const extendedSourceFiles = configFile.extendedSourceFiles || emptyArray;
const extendedSourceFiles = configFile?.extendedSourceFiles ?? emptyArray;
// TODO(rbuckton): Should be a `Set` but that requires changing the below code that uses `mutateMap`
const newExtendedConfigFilesMap = arrayToMap(extendedSourceFiles, identity, returnTrue);
// Update the extended config files watcher
Expand Down
77 changes: 43 additions & 34 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,10 @@ namespace ts.server {
errors: Diagnostic[] | undefined;
}

interface SharedExtendedConfigFileWatcher extends FileWatcher {
projects: Set<ConfiguredProject>;
}

export class ProjectService {

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

/*@internal*/
private sharedExtendedConfigFileMap = createMultiMap<Path, ConfiguredProject>();
/*@internal*/
private sharedExtendedConfigFileWatchers = new Map<Path, FileWatcher>();
private readonly sharedExtendedConfigFileWatchers = new Map<Path, SharedExtendedConfigFileWatcher>();

/*@internal*/
readonly packageJsonCache: PackageJsonCache;
Expand Down Expand Up @@ -1358,53 +1360,60 @@ namespace ts.server {

/*@internal*/
private updateSharedExtendedConfigFileMap(project: ConfiguredProject) {
const extendedSourceFiles = project.getCompilerOptions().configFile?.extendedSourceFiles || emptyArray;
extendedSourceFiles.forEach((extendedSourceFile: string) => {
const extendedConfigPath = this.toPath(extendedSourceFile);
if (!this.sharedExtendedConfigFileMap.has(extendedConfigPath)) {
const watcher = this.watchFactory.watchFile(
const extendedConfigPaths: readonly Path[] = project.getCompilerOptions().configFile?.extendedSourceFiles
?.map((file) => this.toPath(file)) ?? emptyArray;
for (const extendedConfigPath of extendedConfigPaths) {
// start watching previously unseen extended config
if (!this.sharedExtendedConfigFileWatchers.has(extendedConfigPath)) {
const projects = new Set<ConfiguredProject>([project]);
const fileWatcherCallback = () => {
const reason = `Change in extended config file ${extendedConfigPath} detected`;
projects.forEach((project: ConfiguredProject) => {
// Skip refresh if project is not yet loaded
if (project.isInitialLoadPending()) return;
project.pendingReload = ConfigFileProgramReloadLevel.Full;
project.pendingReloadReason = reason;
this.delayUpdateProjectGraph(project);
});
};
const fileWatcher = this.watchFactory.watchFile(
extendedConfigPath,
() => this.onSharedExtendedConfigChanged(extendedConfigPath),
fileWatcherCallback,
PollingInterval.High,
this.hostConfiguration.watchOptions,
WatchType.ExtendedConfigFile
);
this.sharedExtendedConfigFileWatchers.set(extendedConfigPath, watcher);
this.sharedExtendedConfigFileWatchers.set(extendedConfigPath, {
close: () => fileWatcher.close(),
projects,
});
}
const otherProjects = this.sharedExtendedConfigFileMap.get(extendedConfigPath);
if (!otherProjects || !otherProjects.includes(project)) {
this.sharedExtendedConfigFileMap.add(extendedConfigPath, project);
}
this.sharedExtendedConfigFileWatchers.forEach((watcher, extendedConfigPath) => {
if (extendedConfigPaths.includes(extendedConfigPath)) {
watcher.projects.add(project);
}
else {
watcher.projects.delete(project);
if (watcher.projects.size === 0) {
watcher.close();
this.sharedExtendedConfigFileWatchers.delete(extendedConfigPath);
}
}
});
}

/*@internal*/
private removeProjectFromSharedExtendedConfigFileMap(project: ConfiguredProject) {
for (const key of arrayFrom(this.sharedExtendedConfigFileMap.keys())) {
this.sharedExtendedConfigFileMap.remove(key, project);
const otherProjects = this.sharedExtendedConfigFileMap.get(key) || emptyArray;
if (otherProjects.length === 0) {
const watcher = this.sharedExtendedConfigFileWatchers.get(key);
if (watcher) {
watcher.close();
this.sharedExtendedConfigFileWatchers.delete(key);
}
for (const [sharedConfigPath, watcher] of arrayFrom(this.sharedExtendedConfigFileWatchers.entries())) {
watcher.projects.delete(project);
if (watcher.projects.size === 0) {
watcher.close();
this.sharedExtendedConfigFileWatchers.delete(sharedConfigPath);
}
}
}

/*@internal*/
private onSharedExtendedConfigChanged(extendedConfigPath: Path) {
const projects = this.sharedExtendedConfigFileMap.get(extendedConfigPath) || emptyArray;
projects.forEach((project: ConfiguredProject) => {
// Skip refresh if project is not yet loaded
if (project.isInitialLoadPending()) return;
project.pendingReload = ConfigFileProgramReloadLevel.Full;
project.pendingReloadReason = `Change in extended config file ${extendedConfigPath} detected`;
this.delayUpdateProjectGraph(project);
});
}

/**
* 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
77 changes: 77 additions & 0 deletions src/testRunner/unittests/tscWatch/programUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1766,5 +1766,82 @@ import { x } from "../b";`),
}
]
});

verifyTscWatch({
scenario,
subScenario: "works correctly when project with extended config is removed",
commandLineArgs: ["-b", "-w", configFilePath],
sys: () => {
const alphaExtendedConfigFile: File = {
path: "/a/b/alpha.tsconfig.json",
content: JSON.stringify({
strict: true
})
};
const project1Config: File = {
path: "/a/b/project1.tsconfig.json",
content: JSON.stringify({
extends: "./alpha.tsconfig.json",
compilerOptions: {
composite: true,
},
files: [commonFile1.path, commonFile2.path]
})
};
const bravoExtendedConfigFile: File = {
path: "/a/b/bravo.tsconfig.json",
content: JSON.stringify({
strict: true
})
};
const otherFile: File = {
path: "/a/b/other.ts",
content: "let z = 0;",
};
const project2Config: File = {
path: "/a/b/project2.tsconfig.json",
content: JSON.stringify({
extends: "./bravo.tsconfig.json",
compilerOptions: {
composite: true,
},
files: [otherFile.path]
})
};
const configFile: File = {
path: configFilePath,
content: JSON.stringify({
references: [
{
path: "./project1.tsconfig.json",
},
{
path: "./project2.tsconfig.json",
},
],
files: [],
})
};
return createWatchedSystem([
libFile, configFile,
alphaExtendedConfigFile, project1Config, commonFile1, commonFile2,
bravoExtendedConfigFile, project2Config, otherFile
]);
},
changes: [
{
caption: "Remove project2 from base config",
change: sys => sys.modifyFile(configFilePath, JSON.stringify({
references: [
{
path: "./project1.tsconfig.json",
},
],
files: [],
})),
timeouts: checkSingleTimeoutQueueLengthAndRun,
}
]
});
});
}
Loading

0 comments on commit 1acd39c

Please sign in to comment.