Skip to content

Commit 5c604f9

Browse files
committed
correctly update external project if config file is added or removed
1 parent 3a9d850 commit 5c604f9

2 files changed

Lines changed: 204 additions & 13 deletions

File tree

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,4 +1747,138 @@ namespace ts.projectSystem {
17471747
assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`);
17481748
});
17491749
});
1750+
1751+
describe("external projects", () => {
1752+
it("correctly handling add/remove tsconfig - 1", () => {
1753+
const f1 = {
1754+
path: "/a/b/app.ts",
1755+
content: "let x = 1;"
1756+
};
1757+
const f2 = {
1758+
path: "/a/b/lib.ts",
1759+
content: ""
1760+
};
1761+
const tsconfig = {
1762+
path: "/a/b/tsconfig.json",
1763+
content: ""
1764+
};
1765+
const host = createServerHost([f1, f2]);
1766+
const projectService = createProjectService(host);
1767+
1768+
// open external project
1769+
const projectName = "/a/b/proj1";
1770+
projectService.openExternalProject({
1771+
projectFileName: projectName,
1772+
rootFiles: toExternalFiles([f1.path, f2.path]),
1773+
options: {}
1774+
});
1775+
projectService.openClientFile(f1.path);
1776+
projectService.checkNumberOfProjects({ externalProjects: 1 });
1777+
checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]);
1778+
1779+
// rename lib.ts to tsconfig.json
1780+
host.reloadFS([f1, tsconfig]);
1781+
projectService.openExternalProject({
1782+
projectFileName: projectName,
1783+
rootFiles: toExternalFiles([f1.path, tsconfig.path]),
1784+
options: {}
1785+
});
1786+
projectService.checkNumberOfProjects({ configuredProjects: 1 });
1787+
checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]);
1788+
1789+
// rename tsconfig.json back to lib.ts
1790+
host.reloadFS([f1, f2]);
1791+
host.triggerFileWatcherCallback(tsconfig.path, /*removed*/ true);
1792+
projectService.openExternalProject({
1793+
projectFileName: projectName,
1794+
rootFiles: toExternalFiles([f1.path, f2.path]),
1795+
options: {}
1796+
});
1797+
1798+
projectService.checkNumberOfProjects({ externalProjects: 1 });
1799+
checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]);
1800+
});
1801+
1802+
1803+
it("correctly handling add/remove tsconfig - 2", () => {
1804+
const f1 = {
1805+
path: "/a/b/app.ts",
1806+
content: "let x = 1;"
1807+
};
1808+
const cLib = {
1809+
path: "/a/b/c/lib.ts",
1810+
content: ""
1811+
};
1812+
const cTsconfig = {
1813+
path: "/a/b/c/tsconfig.json",
1814+
content: "{}"
1815+
};
1816+
const dLib = {
1817+
path: "/a/b/d/lib.ts",
1818+
content: ""
1819+
};
1820+
const dTsconfig = {
1821+
path: "/a/b/d/tsconfig.json",
1822+
content: "{}"
1823+
};
1824+
const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]);
1825+
const projectService = createProjectService(host);
1826+
1827+
// open external project
1828+
const projectName = "/a/b/proj1";
1829+
projectService.openExternalProject({
1830+
projectFileName: projectName,
1831+
rootFiles: toExternalFiles([f1.path]),
1832+
options: {}
1833+
});
1834+
1835+
projectService.checkNumberOfProjects({ externalProjects: 1 });
1836+
checkProjectActualFiles(projectService.externalProjects[0], [f1.path]);
1837+
1838+
// add two config file as root files
1839+
projectService.openExternalProject({
1840+
projectFileName: projectName,
1841+
rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]),
1842+
options: {}
1843+
});
1844+
projectService.checkNumberOfProjects({ configuredProjects: 2 });
1845+
checkProjectActualFiles(projectService.configuredProjects[0], [cLib.path]);
1846+
checkProjectActualFiles(projectService.configuredProjects[1], [dLib.path]);
1847+
1848+
// remove one config file
1849+
projectService.openExternalProject({
1850+
projectFileName: projectName,
1851+
rootFiles: toExternalFiles([f1.path, dTsconfig.path]),
1852+
options: {}
1853+
});
1854+
1855+
projectService.checkNumberOfProjects({ configuredProjects: 1 });
1856+
checkProjectActualFiles(projectService.configuredProjects[0], [dLib.path]);
1857+
1858+
// remove second config file
1859+
projectService.openExternalProject({
1860+
projectFileName: projectName,
1861+
rootFiles: toExternalFiles([f1.path]),
1862+
options: {}
1863+
});
1864+
1865+
projectService.checkNumberOfProjects({ externalProjects: 1 });
1866+
checkProjectActualFiles(projectService.externalProjects[0], [f1.path]);
1867+
1868+
// open two config files
1869+
// add two config file as root files
1870+
projectService.openExternalProject({
1871+
projectFileName: projectName,
1872+
rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]),
1873+
options: {}
1874+
});
1875+
projectService.checkNumberOfProjects({ configuredProjects: 2 });
1876+
checkProjectActualFiles(projectService.configuredProjects[0], [cLib.path]);
1877+
checkProjectActualFiles(projectService.configuredProjects[1], [dLib.path]);
1878+
1879+
// close all projects - no projects should be opened
1880+
projectService.closeExternalProject(projectName);
1881+
projectService.checkNumberOfProjects({});
1882+
});
1883+
});
17501884
}

src/server/editorServices.ts

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,19 +1156,25 @@ namespace ts.server {
11561156
}
11571157
}
11581158

1159-
closeExternalProject(uncheckedFileName: string): void {
1159+
private closeConfiguredProject(configFile: NormalizedPath): void {
1160+
const configuredProject = this.findConfiguredProjectByProjectName(configFile);
1161+
if (configuredProject && configuredProject.deleteOpenRef() === 0) {
1162+
this.removeProject(configuredProject);
1163+
}
1164+
}
1165+
1166+
closeExternalProject(uncheckedFileName: string, suppressRefresh = false): void {
11601167
const fileName = toNormalizedPath(uncheckedFileName);
11611168
const configFiles = this.externalProjectToConfiguredProjectMap[fileName];
11621169
if (configFiles) {
11631170
let shouldRefreshInferredProjects = false;
11641171
for (const configFile of configFiles) {
1165-
const configuredProject = this.findConfiguredProjectByProjectName(configFile);
1166-
if (configuredProject && configuredProject.deleteOpenRef() === 0) {
1167-
this.removeProject(configuredProject);
1172+
if (this.closeConfiguredProject(configFile)) {
11681173
shouldRefreshInferredProjects = true;
11691174
}
11701175
}
1171-
if (shouldRefreshInferredProjects) {
1176+
delete this.externalProjectToConfiguredProjectMap[fileName];
1177+
if (shouldRefreshInferredProjects && !suppressRefresh) {
11721178
this.refreshInferredProjects();
11731179
}
11741180
}
@@ -1177,18 +1183,14 @@ namespace ts.server {
11771183
const externalProject = this.findExternalProjectByProjectName(uncheckedFileName);
11781184
if (externalProject) {
11791185
this.removeProject(externalProject);
1180-
this.refreshInferredProjects();
1186+
if (!suppressRefresh) {
1187+
this.refreshInferredProjects();
1188+
}
11811189
}
11821190
}
11831191
}
11841192

11851193
openExternalProject(proj: protocol.ExternalProject): void {
1186-
const externalProject = this.findExternalProjectByProjectName(proj.projectFileName);
1187-
if (externalProject) {
1188-
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave, /*configFileErrors*/ undefined);
1189-
return;
1190-
}
1191-
11921194
let tsConfigFiles: NormalizedPath[];
11931195
const rootFiles: protocol.ExternalFile[] = [];
11941196
for (const file of proj.rootFiles) {
@@ -1200,6 +1202,58 @@ namespace ts.server {
12001202
rootFiles.push(file);
12011203
}
12021204
}
1205+
1206+
// sort config files to simplify comparison later
1207+
if (tsConfigFiles) {
1208+
tsConfigFiles.sort();
1209+
}
1210+
1211+
const externalProject = this.findExternalProjectByProjectName(proj.projectFileName);
1212+
let exisingConfigFiles: string[];
1213+
if (externalProject) {
1214+
if (!tsConfigFiles) {
1215+
// external project already exists and not config files were added - update the project and return;
1216+
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave, /*configFileErrors*/ undefined);
1217+
return;
1218+
}
1219+
// some config files were added to external project (that previously were not there)
1220+
// close existing project and later we'll open a set of configured projects for these files
1221+
this.closeExternalProject(proj.projectFileName, /*suppressRefresh*/ true);
1222+
}
1223+
else if (this.externalProjectToConfiguredProjectMap[proj.projectFileName]) {
1224+
// this project used to include config files
1225+
if (!tsConfigFiles) {
1226+
// config files were removed from the project - close existing external project which in turn will close configured projects
1227+
this.closeExternalProject(proj.projectFileName, /*suppressRefresh*/ true);
1228+
}
1229+
else {
1230+
// project previously had some config files - compare them with new set of files and close all configured projects that correspond to unused files
1231+
const oldConfigFiles = this.externalProjectToConfiguredProjectMap[proj.projectFileName];
1232+
let iNew = 0;
1233+
let iOld = 0;
1234+
while (iNew < tsConfigFiles.length && iOld < oldConfigFiles.length) {
1235+
const newConfig = tsConfigFiles[iNew];
1236+
const oldConfig = oldConfigFiles[iOld];
1237+
if (oldConfig < newConfig) {
1238+
this.closeConfiguredProject(oldConfig);
1239+
iOld++;
1240+
}
1241+
else if (oldConfig > newConfig) {
1242+
iNew++;
1243+
}
1244+
else {
1245+
// record existing config files so avoid extra add-refs
1246+
(exisingConfigFiles || (exisingConfigFiles = [])).push(oldConfig);
1247+
iOld++;
1248+
iNew++;
1249+
}
1250+
}
1251+
for (let i = iOld; i < oldConfigFiles.length; i++) {
1252+
// projects for all remaining old config files should be closed
1253+
this.closeConfiguredProject(oldConfigFiles[i]);
1254+
}
1255+
}
1256+
}
12031257
if (tsConfigFiles) {
12041258
// store the list of tsconfig files that belong to the external project
12051259
this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles;
@@ -1210,15 +1264,18 @@ namespace ts.server {
12101264
// TODO: save errors
12111265
project = result.success && result.project;
12121266
}
1213-
if (project) {
1267+
if (project && !contains(exisingConfigFiles, tsconfigFile)) {
12141268
// keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project
12151269
project.addOpenRef();
12161270
}
12171271
}
12181272
}
12191273
else {
1274+
// no config files - remove the item from the collection
1275+
delete this.externalProjectToConfiguredProjectMap[proj.projectFileName];
12201276
this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typingOptions);
12211277
}
1278+
this.refreshInferredProjects();
12221279
}
12231280
}
12241281
}

0 commit comments

Comments
 (0)