• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.projectSystem {
2    describe("unittests:: tsserver:: ExternalProjects", () => {
3        describe("can handle tsconfig file name with difference casing", () => {
4            function verifyConfigFileCasing(lazyConfiguredProjectsFromExternalProject: boolean) {
5                const f1 = {
6                    path: "/a/b/app.ts",
7                    content: "let x = 1"
8                };
9                const config = {
10                    path: "/a/b/tsconfig.json",
11                    content: JSON.stringify({
12                        include: []
13                    })
14                };
15
16                const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false });
17                const service = createProjectService(host);
18                service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
19                const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path));
20                service.openExternalProject({
21                    projectFileName: "/a/b/project.csproj",
22                    rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]),
23                    options: {}
24                } as protocol.ExternalProject);
25                service.checkNumberOfProjects({ configuredProjects: 1 });
26                const project = service.configuredProjects.get(config.path)!;
27                if (lazyConfiguredProjectsFromExternalProject) {
28                    assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded
29                    checkProjectActualFiles(project, emptyArray);
30                }
31                else {
32                    assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded
33                    checkProjectActualFiles(project, [upperCaseConfigFilePath]);
34                }
35
36                service.openClientFile(f1.path);
37                service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
38
39                assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated
40                checkProjectActualFiles(project, [upperCaseConfigFilePath]);
41                checkProjectActualFiles(service.inferredProjects[0], [f1.path]);
42            }
43
44            it("when lazyConfiguredProjectsFromExternalProject not set", () => {
45                verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false);
46            });
47
48            it("when lazyConfiguredProjectsFromExternalProject is set", () => {
49                verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true);
50            });
51        });
52
53        it("load global plugins", () => {
54            const f1 = {
55                path: "/a/file1.ts",
56                content: "let x = [1, 2];"
57            };
58            const p1 = { projectFileName: "/a/proj1.csproj", rootFiles: [toExternalFile(f1.path)], options: {} };
59
60            const host = createServerHost([f1]);
61            host.require = (_initialPath, moduleName) => {
62                assert.equal(moduleName, "myplugin");
63                return {
64                    module: () => ({
65                        create(info: server.PluginCreateInfo) {
66                            const proxy = Harness.LanguageService.makeDefaultProxy(info);
67                            proxy.getSemanticDiagnostics = filename => {
68                                const prev = info.languageService.getSemanticDiagnostics(filename);
69                                const sourceFile: SourceFile = info.project.getSourceFile(toPath(filename, /*basePath*/ undefined, createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!;
70                                prev.push({
71                                    category: DiagnosticCategory.Warning,
72                                    file: sourceFile,
73                                    code: 9999,
74                                    length: 3,
75                                    messageText: `Plugin diagnostic`,
76                                    start: 0
77                                });
78                                return prev;
79                            };
80                            return proxy;
81                        }
82                    }),
83                    error: undefined
84                };
85            };
86            const session = createSession(host, { globalPlugins: ["myplugin"] });
87
88            session.executeCommand({
89                seq: 1,
90                type: "request",
91                command: "openExternalProjects",
92                arguments: { projects: [p1] }
93            } as protocol.OpenExternalProjectsRequest);
94
95            const projectService = session.getProjectService();
96            checkNumberOfProjects(projectService, { externalProjects: 1 });
97            assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName);
98
99            const handlerResponse = session.executeCommand({
100                seq: 2,
101                type: "request",
102                command: "semanticDiagnosticsSync",
103                arguments: {
104                    file: f1.path,
105                    projectFileName: p1.projectFileName
106                }
107            } as protocol.SemanticDiagnosticsSyncRequest);
108
109            assert.isDefined(handlerResponse.response);
110            const response = handlerResponse.response as protocol.Diagnostic[];
111            assert.equal(response.length, 1);
112            assert.equal(response[0].text, "Plugin diagnostic");
113        });
114
115        it("remove not-listed external projects", () => {
116            const f1 = {
117                path: "/a/app.ts",
118                content: "let x = 1"
119            };
120            const f2 = {
121                path: "/b/app.ts",
122                content: "let x = 1"
123            };
124            const f3 = {
125                path: "/c/app.ts",
126                content: "let x = 1"
127            };
128            const makeProject = (f: File) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} });
129            const p1 = makeProject(f1);
130            const p2 = makeProject(f2);
131            const p3 = makeProject(f3);
132
133            const host = createServerHost([f1, f2, f3]);
134            const session = createSession(host);
135
136            session.executeCommand({
137                seq: 1,
138                type: "request",
139                command: "openExternalProjects",
140                arguments: { projects: [p1, p2] }
141            } as protocol.OpenExternalProjectsRequest);
142
143            const projectService = session.getProjectService();
144            checkNumberOfProjects(projectService, { externalProjects: 2 });
145            assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName);
146            assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName);
147
148            session.executeCommand({
149                seq: 2,
150                type: "request",
151                command: "openExternalProjects",
152                arguments: { projects: [p1, p3] }
153            } as protocol.OpenExternalProjectsRequest);
154            checkNumberOfProjects(projectService, { externalProjects: 2 });
155            assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName);
156            assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName);
157
158            session.executeCommand({
159                seq: 3,
160                type: "request",
161                command: "openExternalProjects",
162                arguments: { projects: [] }
163            } as protocol.OpenExternalProjectsRequest);
164            checkNumberOfProjects(projectService, { externalProjects: 0 });
165
166            session.executeCommand({
167                seq: 3,
168                type: "request",
169                command: "openExternalProjects",
170                arguments: { projects: [p2] }
171            } as protocol.OpenExternalProjectsRequest);
172            assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName);
173        });
174
175        it("should not close external project with no open files", () => {
176            const file1 = {
177                path: "/a/b/f1.ts",
178                content: "let x =1;"
179            };
180            const file2 = {
181                path: "/a/b/f2.ts",
182                content: "let y =1;"
183            };
184            const externalProjectName = "externalproject";
185            const host = createServerHost([file1, file2]);
186            const projectService = createProjectService(host);
187            projectService.openExternalProject({
188                rootFiles: toExternalFiles([file1.path, file2.path]),
189                options: {},
190                projectFileName: externalProjectName
191            });
192
193            checkNumberOfExternalProjects(projectService, 1);
194            checkNumberOfInferredProjects(projectService, 0);
195
196            // open client file - should not lead to creation of inferred project
197            projectService.openClientFile(file1.path, file1.content);
198            checkNumberOfExternalProjects(projectService, 1);
199            checkNumberOfInferredProjects(projectService, 0);
200
201            // close client file - external project should still exists
202            projectService.closeClientFile(file1.path);
203            checkNumberOfExternalProjects(projectService, 1);
204            checkNumberOfInferredProjects(projectService, 0);
205
206            projectService.closeExternalProject(externalProjectName);
207            checkNumberOfExternalProjects(projectService, 0);
208            checkNumberOfInferredProjects(projectService, 0);
209        });
210
211        it("external project for dynamic file", () => {
212            const externalProjectName = "^ScriptDocument1 file1.ts";
213            const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]);
214            const host = createServerHost([]);
215            const projectService = createProjectService(host);
216            projectService.openExternalProject({
217                rootFiles: externalFiles,
218                options: {},
219                projectFileName: externalProjectName
220            });
221
222            checkNumberOfExternalProjects(projectService, 1);
223            checkNumberOfInferredProjects(projectService, 0);
224            verifyDynamic(projectService, "/^scriptdocument1 file1.ts");
225
226            externalFiles[0].content = "let x =1;";
227            projectService.applyChangesInOpenFiles(arrayIterator(externalFiles));
228        });
229
230        it("when file name starts with ^", () => {
231            const file: File = {
232                path: `${tscWatch.projectRoot}/file.ts`,
233                content: "const x = 10;"
234            };
235            const app: File = {
236                path: `${tscWatch.projectRoot}/^app.ts`,
237                content: "const y = 10;"
238            };
239            const host = createServerHost([file, app, libFile]);
240            const service = createProjectService(host);
241            service.openExternalProjects([{
242                projectFileName: `${tscWatch.projectRoot}/myproject.njsproj`,
243                rootFiles: [
244                    toExternalFile(file.path),
245                    toExternalFile(app.path)
246                ],
247                options: { },
248            }]);
249        });
250
251        it("external project that included config files", () => {
252            const file1 = {
253                path: "/a/b/f1.ts",
254                content: "let x =1;"
255            };
256            const config1 = {
257                path: "/a/b/tsconfig.json",
258                content: JSON.stringify(
259                    {
260                        compilerOptions: {},
261                        files: ["f1.ts"]
262                    }
263                )
264            };
265            const file2 = {
266                path: "/a/c/f2.ts",
267                content: "let y =1;"
268            };
269            const config2 = {
270                path: "/a/c/tsconfig.json",
271                content: JSON.stringify(
272                    {
273                        compilerOptions: {},
274                        files: ["f2.ts"]
275                    }
276                )
277            };
278            const file3 = {
279                path: "/a/d/f3.ts",
280                content: "let z =1;"
281            };
282            const externalProjectName = "externalproject";
283            const host = createServerHost([file1, file2, file3, config1, config2]);
284            const projectService = createProjectService(host);
285            projectService.openExternalProject({
286                rootFiles: toExternalFiles([config1.path, config2.path, file3.path]),
287                options: {},
288                projectFileName: externalProjectName
289            });
290
291            checkNumberOfProjects(projectService, { configuredProjects: 2 });
292            const proj1 = projectService.configuredProjects.get(config1.path);
293            const proj2 = projectService.configuredProjects.get(config2.path);
294            assert.isDefined(proj1);
295            assert.isDefined(proj2);
296
297            // open client file - should not lead to creation of inferred project
298            projectService.openClientFile(file1.path, file1.content);
299            checkNumberOfProjects(projectService, { configuredProjects: 2 });
300            assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
301            assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2);
302
303            projectService.openClientFile(file3.path, file3.content);
304            checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 });
305            assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
306            assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2);
307
308            projectService.closeExternalProject(externalProjectName);
309            // open file 'file1' from configured project keeps project alive
310            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
311            assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
312            assert.isUndefined(projectService.configuredProjects.get(config2.path));
313
314            projectService.closeClientFile(file3.path);
315            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
316            assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
317            assert.isUndefined(projectService.configuredProjects.get(config2.path));
318            assert.isTrue(projectService.inferredProjects[0].isOrphan());
319
320            projectService.closeClientFile(file1.path);
321            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
322            assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1);
323            assert.isUndefined(projectService.configuredProjects.get(config2.path));
324            assert.isTrue(projectService.inferredProjects[0].isOrphan());
325
326            projectService.openClientFile(file2.path, file2.content);
327            checkNumberOfProjects(projectService, { configuredProjects: 1 });
328            assert.isUndefined(projectService.configuredProjects.get(config1.path));
329            assert.isDefined(projectService.configuredProjects.get(config2.path));
330        });
331
332        it("external project with included config file opened after configured project", () => {
333            const file1 = {
334                path: "/a/b/f1.ts",
335                content: "let x = 1"
336            };
337            const configFile = {
338                path: "/a/b/tsconfig.json",
339                content: JSON.stringify({ compilerOptions: {} })
340            };
341            const externalProjectName = "externalproject";
342            const host = createServerHost([file1, configFile]);
343            const projectService = createProjectService(host);
344
345            projectService.openClientFile(file1.path);
346            checkNumberOfProjects(projectService, { configuredProjects: 1 });
347
348            projectService.openExternalProject({
349                rootFiles: toExternalFiles([configFile.path]),
350                options: {},
351                projectFileName: externalProjectName
352            });
353
354            checkNumberOfProjects(projectService, { configuredProjects: 1 });
355
356            projectService.closeClientFile(file1.path);
357            // configured project is alive since it is opened as part of external project
358            checkNumberOfProjects(projectService, { configuredProjects: 1 });
359
360            projectService.closeExternalProject(externalProjectName);
361            checkNumberOfProjects(projectService, { configuredProjects: 0 });
362        });
363
364        it("external project with included config file opened after configured project and then closed", () => {
365            const file1 = {
366                path: "/a/b/f1.ts",
367                content: "let x = 1"
368            };
369            const file2 = {
370                path: "/a/f2.ts",
371                content: "let x = 1"
372            };
373            const configFile = {
374                path: "/a/b/tsconfig.json",
375                content: JSON.stringify({ compilerOptions: {} })
376            };
377            const externalProjectName = "externalproject";
378            const host = createServerHost([file1, file2, libFile, configFile]);
379            const projectService = createProjectService(host);
380
381            projectService.openClientFile(file1.path);
382            checkNumberOfProjects(projectService, { configuredProjects: 1 });
383            const project = projectService.configuredProjects.get(configFile.path);
384
385            projectService.openExternalProject({
386                rootFiles: toExternalFiles([configFile.path]),
387                options: {},
388                projectFileName: externalProjectName
389            });
390
391            checkNumberOfProjects(projectService, { configuredProjects: 1 });
392            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
393
394            projectService.closeExternalProject(externalProjectName);
395            // configured project is alive since file is still open
396            checkNumberOfProjects(projectService, { configuredProjects: 1 });
397            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
398
399            projectService.closeClientFile(file1.path);
400            checkNumberOfProjects(projectService, { configuredProjects: 1 });
401            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
402
403            projectService.openClientFile(file2.path);
404            checkNumberOfProjects(projectService, { inferredProjects: 1 });
405            assert.isUndefined(projectService.configuredProjects.get(configFile.path));
406        });
407
408        it("can correctly update external project when set of root files has changed", () => {
409            const file1 = {
410                path: "/a/b/f1.ts",
411                content: "let x = 1"
412            };
413            const file2 = {
414                path: "/a/b/f2.ts",
415                content: "let y = 1"
416            };
417            const host = createServerHost([file1, file2]);
418            const projectService = createProjectService(host);
419
420            projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) });
421            checkNumberOfProjects(projectService, { externalProjects: 1 });
422            checkProjectActualFiles(projectService.externalProjects[0], [file1.path]);
423
424            projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) });
425            checkNumberOfProjects(projectService, { externalProjects: 1 });
426            checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]);
427        });
428
429        it("can update external project when set of root files was not changed", () => {
430            const file1 = {
431                path: "/a/b/f1.ts",
432                content: `export * from "m"`
433            };
434            const file2 = {
435                path: "/a/b/f2.ts",
436                content: "export let y = 1"
437            };
438            const file3 = {
439                path: "/a/m.ts",
440                content: "export let y = 1"
441            };
442
443            const host = createServerHost([file1, file2, file3]);
444            const projectService = createProjectService(host);
445
446            projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) });
447            checkNumberOfProjects(projectService, { externalProjects: 1 });
448            checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]);
449            checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]);
450
451            projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) });
452            checkNumberOfProjects(projectService, { externalProjects: 1 });
453            checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]);
454            checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]);
455        });
456
457        it("language service disabled state is updated in external projects", () => {
458            const f1 = {
459                path: "/a/app.js",
460                content: "var x = 1"
461            };
462            const f2 = {
463                path: "/a/largefile.js",
464                content: ""
465            };
466            const host = createServerHost([f1, f2]);
467            const originalGetFileSize = host.getFileSize;
468            host.getFileSize = (filePath: string) =>
469                filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
470
471            const service = createProjectService(host);
472            const projectFileName = "/a/proj.csproj";
473
474            service.openExternalProject({
475                projectFileName,
476                rootFiles: toExternalFiles([f1.path, f2.path]),
477                options: {}
478            });
479            service.checkNumberOfProjects({ externalProjects: 1 });
480            assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1");
481
482            service.openExternalProject({
483                projectFileName,
484                rootFiles: toExternalFiles([f1.path]),
485                options: {}
486            });
487            service.checkNumberOfProjects({ externalProjects: 1 });
488            assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled");
489
490            service.openExternalProject({
491                projectFileName,
492                rootFiles: toExternalFiles([f1.path, f2.path]),
493                options: {}
494            });
495            service.checkNumberOfProjects({ externalProjects: 1 });
496            assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2");
497        });
498
499        describe("deleting config file opened from the external project works", () => {
500            function verifyDeletingConfigFile(lazyConfiguredProjectsFromExternalProject: boolean) {
501                const site = {
502                    path: "/user/someuser/project/js/site.js",
503                    content: ""
504                };
505                const configFile = {
506                    path: "/user/someuser/project/tsconfig.json",
507                    content: "{}"
508                };
509                const projectFileName = "/user/someuser/project/WebApplication6.csproj";
510                const host = createServerHost([libFile, site, configFile]);
511                const projectService = createProjectService(host);
512                projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
513
514                const externalProject: protocol.ExternalProject = {
515                    projectFileName,
516                    rootFiles: [toExternalFile(site.path), toExternalFile(configFile.path)],
517                    options: { allowJs: false },
518                    typeAcquisition: { include: [] }
519                };
520
521                projectService.openExternalProjects([externalProject]);
522
523                let knownProjects = projectService.synchronizeProjectList([]);
524                checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 });
525
526                const configProject = configuredProjectAt(projectService, 0);
527                checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ?
528                    emptyArray : // Since no files opened from this project, its not loaded
529                    [configFile.path]);
530
531                host.deleteFile(configFile.path);
532
533                knownProjects = projectService.synchronizeProjectList(map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039
534                checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 });
535
536                externalProject.rootFiles.length = 1;
537                projectService.openExternalProjects([externalProject]);
538
539                checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 });
540                checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]);
541            }
542            it("when lazyConfiguredProjectsFromExternalProject not set", () => {
543                verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false);
544            });
545            it("when lazyConfiguredProjectsFromExternalProject is set", () => {
546                verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true);
547            });
548        });
549
550        describe("correctly handling add/remove tsconfig - 1", () => {
551            function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) {
552                const f1 = {
553                    path: "/a/b/app.ts",
554                    content: "let x = 1;"
555                };
556                const f2 = {
557                    path: "/a/b/lib.ts",
558                    content: ""
559                };
560                const tsconfig = {
561                    path: "/a/b/tsconfig.json",
562                    content: ""
563                };
564                const host = createServerHost([f1, f2]);
565                const projectService = createProjectService(host);
566                projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
567
568                // open external project
569                const projectName = "/a/b/proj1";
570                projectService.openExternalProject({
571                    projectFileName: projectName,
572                    rootFiles: toExternalFiles([f1.path, f2.path]),
573                    options: {}
574                });
575                projectService.openClientFile(f1.path);
576                projectService.checkNumberOfProjects({ externalProjects: 1 });
577                checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]);
578
579                // rename lib.ts to tsconfig.json
580                host.renameFile(f2.path, tsconfig.path);
581                projectService.openExternalProject({
582                    projectFileName: projectName,
583                    rootFiles: toExternalFiles([f1.path, tsconfig.path]),
584                    options: {}
585                });
586                projectService.checkNumberOfProjects({ configuredProjects: 1 });
587                if (lazyConfiguredProjectsFromExternalProject) {
588                    checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed
589                    projectService.ensureInferredProjectsUpToDate_TestOnly();
590                }
591                checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]);
592
593                // rename tsconfig.json back to lib.ts
594                host.renameFile(tsconfig.path, f2.path);
595                projectService.openExternalProject({
596                    projectFileName: projectName,
597                    rootFiles: toExternalFiles([f1.path, f2.path]),
598                    options: {}
599                });
600
601                projectService.checkNumberOfProjects({ externalProjects: 1 });
602                checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]);
603            }
604            it("when lazyConfiguredProjectsFromExternalProject not set", () => {
605                verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false);
606            });
607            it("when lazyConfiguredProjectsFromExternalProject is set", () => {
608                verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true);
609            });
610        });
611
612        describe("correctly handling add/remove tsconfig - 2", () => {
613            function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) {
614                const f1 = {
615                    path: "/a/b/app.ts",
616                    content: "let x = 1;"
617                };
618                const cLib = {
619                    path: "/a/b/c/lib.ts",
620                    content: ""
621                };
622                const cTsconfig = {
623                    path: "/a/b/c/tsconfig.json",
624                    content: "{}"
625                };
626                const dLib = {
627                    path: "/a/b/d/lib.ts",
628                    content: ""
629                };
630                const dTsconfig = {
631                    path: "/a/b/d/tsconfig.json",
632                    content: "{}"
633                };
634                const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]);
635                const projectService = createProjectService(host);
636                projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
637
638                // open external project
639                const projectName = "/a/b/proj1";
640                projectService.openExternalProject({
641                    projectFileName: projectName,
642                    rootFiles: toExternalFiles([f1.path]),
643                    options: {}
644                });
645
646                projectService.checkNumberOfProjects({ externalProjects: 1 });
647                checkProjectActualFiles(projectService.externalProjects[0], [f1.path]);
648
649                // add two config file as root files
650                projectService.openExternalProject({
651                    projectFileName: projectName,
652                    rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]),
653                    options: {}
654                });
655                projectService.checkNumberOfProjects({ configuredProjects: 2 });
656                if (lazyConfiguredProjectsFromExternalProject) {
657                    checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed
658                    checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed
659                    projectService.ensureInferredProjectsUpToDate_TestOnly();
660                }
661                checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]);
662                checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]);
663
664                // remove one config file
665                projectService.openExternalProject({
666                    projectFileName: projectName,
667                    rootFiles: toExternalFiles([f1.path, dTsconfig.path]),
668                    options: {}
669                });
670
671                projectService.checkNumberOfProjects({ configuredProjects: 1 });
672                checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]);
673
674                // remove second config file
675                projectService.openExternalProject({
676                    projectFileName: projectName,
677                    rootFiles: toExternalFiles([f1.path]),
678                    options: {}
679                });
680
681                projectService.checkNumberOfProjects({ externalProjects: 1 });
682                checkProjectActualFiles(projectService.externalProjects[0], [f1.path]);
683
684                // open two config files
685                // add two config file as root files
686                projectService.openExternalProject({
687                    projectFileName: projectName,
688                    rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]),
689                    options: {}
690                });
691                projectService.checkNumberOfProjects({ configuredProjects: 2 });
692                if (lazyConfiguredProjectsFromExternalProject) {
693                    checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed
694                    checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed
695                    projectService.ensureInferredProjectsUpToDate_TestOnly();
696                }
697                checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]);
698                checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]);
699
700                // close all projects - no projects should be opened
701                projectService.closeExternalProject(projectName);
702                projectService.checkNumberOfProjects({});
703            }
704
705            it("when lazyConfiguredProjectsFromExternalProject not set", () => {
706                verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false);
707            });
708            it("when lazyConfiguredProjectsFromExternalProject is set", () => {
709                verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true);
710            });
711        });
712
713        it("correctly handles changes in lib section of config file", () => {
714            const libES5 = {
715                path: "/compiler/lib.es5.d.ts",
716                content: "declare const eval: any"
717            };
718            const libES2015Promise = {
719                path: "/compiler/lib.es2015.promise.d.ts",
720                content: "declare class Promise<T> {}"
721            };
722            const app = {
723                path: "/src/app.ts",
724                content: "var x: Promise<string>;"
725            };
726            const config1 = {
727                path: "/src/tsconfig.json",
728                content: JSON.stringify(
729                    {
730                        compilerOptions: {
731                            module: "commonjs",
732                            target: "es5",
733                            noImplicitAny: true,
734                            sourceMap: false,
735                            lib: [
736                                "es5"
737                            ]
738                        }
739                    })
740            };
741            const config2 = {
742                path: config1.path,
743                content: JSON.stringify(
744                    {
745                        compilerOptions: {
746                            module: "commonjs",
747                            target: "es5",
748                            noImplicitAny: true,
749                            sourceMap: false,
750                            lib: [
751                                "es5",
752                                "es2015.promise"
753                            ]
754                        }
755                    })
756            };
757            const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" });
758            const projectService = createProjectService(host);
759            projectService.openClientFile(app.path);
760
761            projectService.checkNumberOfProjects({ configuredProjects: 1 });
762            checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]);
763
764            host.writeFile(config2.path, config2.content);
765            host.checkTimeoutQueueLengthAndRun(2);
766
767            projectService.checkNumberOfProjects({ configuredProjects: 1 });
768            checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]);
769        });
770
771        it("should handle non-existing directories in config file", () => {
772            const f = {
773                path: "/a/src/app.ts",
774                content: "let x = 1;"
775            };
776            const config = {
777                path: "/a/tsconfig.json",
778                content: JSON.stringify({
779                    compilerOptions: {},
780                    include: [
781                        "src/**/*",
782                        "notexistingfolder/*"
783                    ]
784                })
785            };
786            const host = createServerHost([f, config]);
787            const projectService = createProjectService(host);
788            projectService.openClientFile(f.path);
789            projectService.checkNumberOfProjects({ configuredProjects: 1 });
790            const project = projectService.configuredProjects.get(config.path)!;
791            assert.isTrue(project.hasOpenRef()); // f
792
793            projectService.closeClientFile(f.path);
794            projectService.checkNumberOfProjects({ configuredProjects: 1 });
795            assert.strictEqual(projectService.configuredProjects.get(config.path), project);
796            assert.isFalse(project.hasOpenRef()); // No files
797            assert.isFalse(project.isClosed());
798
799            projectService.openClientFile(f.path);
800            projectService.checkNumberOfProjects({ configuredProjects: 1 });
801            assert.strictEqual(projectService.configuredProjects.get(config.path), project);
802            assert.isTrue(project.hasOpenRef()); // f
803            assert.isFalse(project.isClosed());
804        });
805
806        it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => {
807            const f1 = {
808                path: "/a/b/app.ts",
809                content: "let x = 1"
810            };
811            const config = {
812                path: "/a/b/tsconfig.json",
813                content: JSON.stringify({})
814            };
815            const projectFileName = "/a/b/project.csproj";
816            const host = createServerHost([f1, config]);
817            const service = createProjectService(host);
818            service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } });
819            service.openExternalProject({
820                projectFileName,
821                rootFiles: toExternalFiles([f1.path, config.path]),
822                options: {}
823            } as protocol.ExternalProject);
824            service.checkNumberOfProjects({ configuredProjects: 1 });
825            const project = service.configuredProjects.get(config.path)!;
826            assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded
827            checkProjectActualFiles(project, emptyArray);
828
829            service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } });
830            assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded
831            checkProjectActualFiles(project, [config.path, f1.path]);
832
833            service.closeExternalProject(projectFileName);
834            service.checkNumberOfProjects({});
835
836            service.openExternalProject({
837                projectFileName,
838                rootFiles: toExternalFiles([f1.path, config.path]),
839                options: {}
840            } as protocol.ExternalProject);
841            service.checkNumberOfProjects({ configuredProjects: 1 });
842            const project2 = service.configuredProjects.get(config.path)!;
843            assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded
844            checkProjectActualFiles(project2, [config.path, f1.path]);
845        });
846
847        it("handles creation of external project with jsconfig before jsconfig creation watcher is invoked", () => {
848            const projectFileName = `${tscWatch.projectRoot}/WebApplication36.csproj`;
849            const tsconfig: File = {
850                path: `${tscWatch.projectRoot}/tsconfig.json`,
851                content: "{}"
852            };
853            const files = [libFile, tsconfig];
854            const host = createServerHost(files);
855            const service = createProjectService(host);
856
857            // Create external project
858            service.openExternalProjects([{
859                projectFileName,
860                rootFiles: [{ fileName: tsconfig.path }],
861                options: { allowJs: false }
862            }]);
863            checkNumberOfProjects(service, { configuredProjects: 1 });
864            const configProject = service.configuredProjects.get(tsconfig.path.toLowerCase())!;
865            checkProjectActualFiles(configProject, [tsconfig.path]);
866
867            // write js file, open external project and open it for edit
868            const jsFilePath = `${tscWatch.projectRoot}/javascript.js`;
869            host.writeFile(jsFilePath, "");
870            service.openExternalProjects([{
871                projectFileName,
872                rootFiles: [{ fileName: tsconfig.path }, { fileName: jsFilePath }],
873                options: { allowJs: false }
874            }]);
875            service.applyChangesInOpenFiles(singleIterator({ fileName: jsFilePath, scriptKind: ScriptKind.JS, content: "" }));
876            checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 });
877            checkProjectActualFiles(configProject, [tsconfig.path]);
878            const inferredProject = service.inferredProjects[0];
879            checkProjectActualFiles(inferredProject, [libFile.path, jsFilePath]);
880
881            // write jsconfig file
882            const jsConfig: File = {
883                path: `${tscWatch.projectRoot}/jsconfig.json`,
884                content: "{}"
885            };
886            // Dont invoke file creation watchers as the repro suggests
887            host.ensureFileOrFolder(jsConfig, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true);
888
889            // Open external project
890            service.openExternalProjects([{
891                projectFileName,
892                rootFiles: [{ fileName: jsConfig.path }, { fileName: tsconfig.path }, { fileName: jsFilePath }],
893                options: { allowJs: false }
894            }]);
895            checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 });
896            checkProjectActualFiles(configProject, [tsconfig.path]);
897            assert.isTrue(inferredProject.isOrphan());
898            const jsConfigProject = service.configuredProjects.get(jsConfig.path.toLowerCase())!;
899            checkProjectActualFiles(jsConfigProject, [jsConfig.path, jsFilePath, libFile.path]);
900        });
901
902        it("does not crash if external file does not exist", () => {
903            const f1 = {
904                path: "/a/file1.ts",
905                content: "let x = [1, 2];",
906            };
907            const p1 = {
908                projectFileName: "/a/proj1.csproj",
909                rootFiles: [toExternalFile(f1.path)],
910                options: {},
911            };
912
913            const host = createServerHost([f1]);
914            host.require = (_initialPath, moduleName) => {
915                assert.equal(moduleName, "myplugin");
916                return {
917                    module: () => ({
918                        create(info: server.PluginCreateInfo) {
919                            return Harness.LanguageService.makeDefaultProxy(info);
920                        },
921                        getExternalFiles() {
922                            return ["/does/not/exist"];
923                        },
924                    }),
925                    error: undefined,
926                };
927            };
928            const session = createSession(host, {
929                globalPlugins: ["myplugin"],
930            });
931            const projectService = session.getProjectService();
932            // When the external project is opened, the graph will be updated,
933            // and in the process getExternalFiles() above will be called.
934            // Since the external file does not exist, there will not be a script
935            // info for it. If tsserver does not handle this case, the following
936            // method call will crash.
937            projectService.openExternalProject(p1);
938            checkNumberOfProjects(projectService, { externalProjects: 1 });
939        });
940    });
941}
942