• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.projectSystem {
2    describe("unittests:: tsserver:: Projects", () => {
3        it("handles the missing files - that were added to program because they were added with ///<ref", () => {
4            const file1: File = {
5                path: "/a/b/commonFile1.ts",
6                content: `/// <reference path="commonFile2.ts"/>
7                    let x = y`
8            };
9            const host = createServerHost([file1, libFile]);
10            const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
11            openFilesForSession([file1], session);
12
13            const getErrRequest = makeSessionRequest<server.protocol.SemanticDiagnosticsSyncRequestArgs>(
14                server.CommandNames.SemanticDiagnosticsSync,
15                { file: file1.path }
16            );
17
18            // Two errors: CommonFile2 not found and cannot find name y
19            session.executeCommand(getErrRequest);
20
21            host.writeFile(commonFile2.path, commonFile2.content);
22            host.runQueuedTimeoutCallbacks();
23            session.executeCommand(getErrRequest);
24            baselineTsserverLogs("projects", "handles the missing files added with tripleslash ref", session);
25        });
26
27        it("should create new inferred projects for files excluded from a configured project", () => {
28            const configFile: File = {
29                path: "/a/b/tsconfig.json",
30                content: `{
31                    "compilerOptions": {},
32                    "files": ["${commonFile1.path}", "${commonFile2.path}"]
33                }`
34            };
35            const files = [commonFile1, commonFile2, configFile];
36            const host = createServerHost(files);
37            const projectService = createProjectService(host);
38            projectService.openClientFile(commonFile1.path);
39
40            const project = configuredProjectAt(projectService, 0);
41            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
42            configFile.content = `{
43                "compilerOptions": {},
44                "files": ["${commonFile1.path}"]
45            }`;
46            host.writeFile(configFile.path, configFile.content);
47
48            checkNumberOfConfiguredProjects(projectService, 1);
49            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
50            host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects
51            checkNumberOfConfiguredProjects(projectService, 1);
52            checkProjectRootFiles(project, [commonFile1.path]);
53
54            projectService.openClientFile(commonFile2.path);
55            checkNumberOfInferredProjects(projectService, 1);
56        });
57
58        it("should disable features when the files are too large", () => {
59            const file1 = {
60                path: "/a/b/f1.js",
61                content: "let x =1;",
62                fileSize: 10 * 1024 * 1024
63            };
64            const file2 = {
65                path: "/a/b/f2.js",
66                content: "let y =1;",
67                fileSize: 6 * 1024 * 1024
68            };
69            const file3 = {
70                path: "/a/b/f3.js",
71                content: "let y =1;",
72                fileSize: 6 * 1024 * 1024
73            };
74
75            const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3";
76
77            const host = createServerHost([file1, file2, file3]);
78            const projectService = createProjectService(host);
79
80            projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name });
81            const proj1 = projectService.findProject(proj1name)!;
82            assert.isTrue(proj1.languageServiceEnabled);
83
84            projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name });
85            const proj2 = projectService.findProject(proj2name)!;
86            assert.isTrue(proj2.languageServiceEnabled);
87
88            projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name });
89            const proj3 = projectService.findProject(proj3name)!;
90            assert.isFalse(proj3.languageServiceEnabled);
91        });
92
93        it("should not crash when opening a file in a project with a disabled language service", () => {
94            const file1 = {
95                path: "/a/b/f1.js",
96                content: "let x =1;",
97                fileSize: 50 * 1024 * 1024
98            };
99            const file2 = {
100                path: "/a/b/f2.js",
101                content: "let x =1;",
102                fileSize: 100
103            };
104
105            const projName = "proj1";
106
107            const host = createServerHost([file1, file2]);
108            const projectService = createProjectService(host, { useSingleInferredProject: true, eventHandler: noop });
109
110            projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path, file2.path]), options: {}, projectFileName: projName });
111            const proj1 = projectService.findProject(projName)!;
112            assert.isFalse(proj1.languageServiceEnabled);
113
114            assert.doesNotThrow(() => projectService.openClientFile(file2.path));
115        });
116
117        describe("ignoreConfigFiles", () => {
118            it("external project including config file", () => {
119                const file1 = {
120                    path: "/a/b/f1.ts",
121                    content: "let x =1;"
122                };
123                const config1 = {
124                    path: "/a/b/tsconfig.json",
125                    content: JSON.stringify(
126                        {
127                            compilerOptions: {},
128                            files: ["f1.ts"]
129                        }
130                    )
131                };
132
133                const externalProjectName = "externalproject";
134                const host = createServerHost([file1, config1]);
135                const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true });
136                projectService.openExternalProject({
137                    rootFiles: toExternalFiles([file1.path, config1.path]),
138                    options: {},
139                    projectFileName: externalProjectName
140                });
141
142                checkNumberOfProjects(projectService, { externalProjects: 1 });
143                const proj = projectService.externalProjects[0];
144                assert.isDefined(proj);
145
146                assert.isTrue(proj.fileExists(file1.path));
147            });
148
149            it("loose file included in config file (openClientFile)", () => {
150                const file1 = {
151                    path: "/a/b/f1.ts",
152                    content: "let x =1;"
153                };
154                const config1 = {
155                    path: "/a/b/tsconfig.json",
156                    content: JSON.stringify(
157                        {
158                            compilerOptions: {},
159                            files: ["f1.ts"]
160                        }
161                    )
162                };
163
164                const host = createServerHost([file1, config1]);
165                const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true });
166                projectService.openClientFile(file1.path, file1.content);
167
168                checkNumberOfProjects(projectService, { inferredProjects: 1 });
169                const proj = projectService.inferredProjects[0];
170                assert.isDefined(proj);
171
172                assert.isTrue(proj.fileExists(file1.path));
173            });
174
175            it("loose file included in config file (applyCodeChanges)", () => {
176                const file1 = {
177                    path: "/a/b/f1.ts",
178                    content: "let x =1;"
179                };
180                const config1 = {
181                    path: "/a/b/tsconfig.json",
182                    content: JSON.stringify(
183                        {
184                            compilerOptions: {},
185                            files: ["f1.ts"]
186                        }
187                    )
188                };
189
190                const host = createServerHost([file1, config1]);
191                const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true });
192                projectService.applyChangesInOpenFiles(singleIterator({ fileName: file1.path, content: file1.content }));
193
194                checkNumberOfProjects(projectService, { inferredProjects: 1 });
195                const proj = projectService.inferredProjects[0];
196                assert.isDefined(proj);
197
198                assert.isTrue(proj.fileExists(file1.path));
199            });
200        });
201
202        it("reload regular file after closing", () => {
203            const f1 = {
204                path: "/a/b/app.ts",
205                content: "x."
206            };
207            const f2 = {
208                path: "/a/b/lib.ts",
209                content: "let x: number;"
210            };
211
212            const host = createServerHost([f1, f2, libFile]);
213            const service = createProjectService(host);
214            service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} });
215
216            service.openClientFile(f1.path);
217            service.openClientFile(f2.path, "let x: string");
218
219            service.checkNumberOfProjects({ externalProjects: 1 });
220            checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]);
221
222            const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!;
223            // should contain completions for string
224            assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'");
225            assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'");
226
227            service.closeClientFile(f2.path);
228            const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!;
229            // should contain completions for string
230            assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'");
231            assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'");
232        });
233
234        it("clear mixed content file after closing", () => {
235            const f1 = {
236                path: "/a/b/app.ts",
237                content: " "
238            };
239            const f2 = {
240                path: "/a/b/lib.html",
241                content: "<html/>"
242            };
243
244            const host = createServerHost([f1, f2, libFile]);
245            const service = createProjectService(host);
246            service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} });
247
248            service.openClientFile(f1.path);
249            service.openClientFile(f2.path, "let somelongname: string");
250
251            service.checkNumberOfProjects({ externalProjects: 1 });
252            checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]);
253
254            const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!;
255            assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'");
256
257            service.closeClientFile(f2.path);
258            const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!;
259            assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'");
260            const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!;
261            assert.equal(sf2.text, "");
262        });
263
264        it("changes in closed files are reflected in project structure", () => {
265            const file1 = {
266                path: "/a/b/f1.ts",
267                content: `export * from "./f2"`
268            };
269            const file2 = {
270                path: "/a/b/f2.ts",
271                content: `export let x = 1`
272            };
273            const file3 = {
274                path: "/a/c/f3.ts",
275                content: `export let y = 1;`
276            };
277            const host = createServerHost([file1, file2, file3]);
278            const projectService = createProjectService(host);
279
280            projectService.openClientFile(file1.path);
281            checkNumberOfProjects(projectService, { inferredProjects: 1 });
282            const inferredProject0 = projectService.inferredProjects[0];
283            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]);
284
285            projectService.openClientFile(file3.path);
286            checkNumberOfProjects(projectService, { inferredProjects: 2 });
287            assert.strictEqual(projectService.inferredProjects[0], inferredProject0);
288            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]);
289            const inferredProject1 = projectService.inferredProjects[1];
290            checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
291
292            host.writeFile(file2.path, `export * from "../c/f3"`); // now inferred project should inclule file3
293            host.checkTimeoutQueueLengthAndRun(2);
294            checkNumberOfProjects(projectService, { inferredProjects: 2 });
295            assert.strictEqual(projectService.inferredProjects[0], inferredProject0);
296            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]);
297            assert.strictEqual(projectService.inferredProjects[1], inferredProject1);
298            assert.isTrue(inferredProject1.isOrphan());
299        });
300
301        it("deleted files affect project structure", () => {
302            const file1 = {
303                path: "/a/b/f1.ts",
304                content: `export * from "./f2"`
305            };
306            const file2 = {
307                path: "/a/b/f2.ts",
308                content: `export * from "../c/f3"`
309            };
310            const file3 = {
311                path: "/a/c/f3.ts",
312                content: `export let y = 1;`
313            };
314            const host = createServerHost([file1, file2, file3]);
315            const projectService = createProjectService(host);
316
317            projectService.openClientFile(file1.path);
318
319            checkNumberOfProjects(projectService, { inferredProjects: 1 });
320
321            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]);
322
323            projectService.openClientFile(file3.path);
324            checkNumberOfProjects(projectService, { inferredProjects: 1 });
325
326            host.deleteFile(file2.path);
327            host.checkTimeoutQueueLengthAndRun(2);
328
329            checkNumberOfProjects(projectService, { inferredProjects: 2 });
330
331            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]);
332            checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
333        });
334
335        it("ignores files excluded by a custom safe type list", () => {
336            const file1 = {
337                path: "/a/b/f1.js",
338                content: "export let x = 5"
339            };
340            const office = {
341                path: "/lib/duckquack-3.min.js",
342                content: "whoa do @@ not parse me ok thanks!!!"
343            };
344            const host = createServerHost([file1, office, customTypesMap]);
345            const projectService = createProjectService(host);
346            try {
347                projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) });
348                const proj = projectService.externalProjects[0];
349                assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]);
350                assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]);
351            }
352            finally {
353                projectService.resetSafeList();
354            }
355        });
356
357        it("file with name constructor.js doesnt cause issue with typeAcquisition when safe type list", () => {
358            const file1 = {
359                path: "/a/b/f1.js",
360                content: `export let x = 5; import { s } from "s"`
361            };
362            const constructorFile = {
363                path: "/a/b/constructor.js",
364                content: "const x = 10;"
365            };
366            const bliss = {
367                path: "/a/b/bliss.js",
368                content: "export function is() { return true; }"
369            };
370            const host = createServerHost([file1, libFile, constructorFile, bliss, customTypesMap]);
371            let request: string | undefined;
372            const cachePath = "/a/data";
373            const typingsInstaller: server.ITypingsInstaller = {
374                isKnownTypesPackageName: returnFalse,
375                installPackage: notImplemented,
376                enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => {
377                    assert.isUndefined(request);
378                    request = JSON.stringify(server.createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || server.emptyArray, cachePath));
379                },
380                attach: noop,
381                onProjectClosed: noop,
382                globalTypingsCacheLocation: cachePath
383            };
384
385            const projectName = "project";
386            const projectService = createProjectService(host, { typingsInstaller });
387            projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: toExternalFiles([file1.path, constructorFile.path, bliss.path]) });
388            assert.equal(request, JSON.stringify({
389                projectName,
390                fileNames: [libFile.path, file1.path, constructorFile.path, bliss.path],
391                compilerOptions: { allowNonTsExtensions: true, noEmitForJsFiles: true },
392                typeAcquisition: { include: ["blissfuljs"], exclude: [], enable: true },
393                unresolvedImports: ["s"],
394                projectRootPath: "/",
395                cachePath,
396                kind: "discover"
397            }));
398            const response = JSON.parse(request!);
399            request = undefined;
400            projectService.updateTypingsForProject({
401                kind: "action::set",
402                projectName: response.projectName,
403                typeAcquisition: response.typeAcquisition,
404                compilerOptions: response.compilerOptions,
405                typings: emptyArray,
406                unresolvedImports: response.unresolvedImports,
407            });
408
409            host.checkTimeoutQueueLength(0);
410            assert.isUndefined(request);
411        });
412
413        it("ignores files excluded by the default type list", () => {
414            const file1 = {
415                path: "/a/b/f1.js",
416                content: "export let x = 5"
417            };
418            const minFile = {
419                path: "/c/moment.min.js",
420                content: "unspecified"
421            };
422            const kendoFile1 = {
423                path: "/q/lib/kendo/kendo.all.min.js",
424                content: "unspecified"
425            };
426            const kendoFile2 = {
427                path: "/q/lib/kendo/kendo.ui.min.js",
428                content: "unspecified"
429            };
430            const kendoFile3 = {
431                path: "/q/lib/kendo-ui/kendo.all.js",
432                content: "unspecified"
433            };
434            const officeFile1 = {
435                path: "/scripts/Office/1/excel-15.debug.js",
436                content: "unspecified"
437            };
438            const officeFile2 = {
439                path: "/scripts/Office/1/powerpoint.js",
440                content: "unspecified"
441            };
442            const files = [file1, minFile, kendoFile1, kendoFile2, kendoFile3, officeFile1, officeFile2];
443            const host = createServerHost(files);
444            const projectService = createProjectService(host);
445            try {
446                projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) });
447                const proj = projectService.externalProjects[0];
448                assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]);
449                assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]);
450            }
451            finally {
452                projectService.resetSafeList();
453            }
454        });
455
456        it("removes version numbers correctly", () => {
457            const testData: [string, string][] = [
458                ["jquery-max", "jquery-max"],
459                ["jquery.min", "jquery"],
460                ["jquery-min.4.2.3", "jquery"],
461                ["jquery.min.4.2.1", "jquery"],
462                ["minimum", "minimum"],
463                ["min", "min"],
464                ["min.3.2", "min"],
465                ["jquery", "jquery"]
466            ];
467            for (const t of testData) {
468                assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]);
469            }
470        });
471
472        it("ignores files excluded by a legacy safe type list", () => {
473            const file1 = {
474                path: "/a/b/bliss.js",
475                content: "let x = 5"
476            };
477            const file2 = {
478                path: "/a/b/foo.js",
479                content: ""
480            };
481            const file3 = {
482                path: "/a/b/Bacon.js",
483                content: "let y = 5"
484            };
485            const host = createServerHost([file1, file2, file3, customTypesMap]);
486            const projectService = createProjectService(host);
487            try {
488                projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } });
489                const proj = projectService.externalProjects[0];
490                assert.deepEqual(proj.getFileNames(), [file2.path]);
491            }
492            finally {
493                projectService.resetSafeList();
494            }
495        });
496
497        it("correctly migrate files between projects", () => {
498            const file1 = {
499                path: "/a/b/f1.ts",
500                content: `
501                export * from "../c/f2";
502                export * from "../d/f3";`
503            };
504            const file2 = {
505                path: "/a/c/f2.ts",
506                content: "export let x = 1;"
507            };
508            const file3 = {
509                path: "/a/d/f3.ts",
510                content: "export let y = 1;"
511            };
512            const host = createServerHost([file1, file2, file3]);
513            const projectService = createProjectService(host);
514
515            projectService.openClientFile(file2.path);
516            checkNumberOfProjects(projectService, { inferredProjects: 1 });
517            checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]);
518            let inferredProjects = projectService.inferredProjects.slice();
519
520            projectService.openClientFile(file3.path);
521            checkNumberOfProjects(projectService, { inferredProjects: 2 });
522            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
523            checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]);
524            checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
525            inferredProjects = projectService.inferredProjects.slice();
526
527            projectService.openClientFile(file1.path);
528            checkNumberOfProjects(projectService, { inferredProjects: 1 });
529            assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]);
530            assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]);
531            checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]);
532            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]);
533            inferredProjects = projectService.inferredProjects.slice();
534
535            projectService.closeClientFile(file1.path);
536            checkNumberOfProjects(projectService, { inferredProjects: 3 });
537            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
538            assert.isTrue(projectService.inferredProjects[0].isOrphan());
539            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
540            checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]);
541            inferredProjects = projectService.inferredProjects.slice();
542
543            projectService.closeClientFile(file3.path);
544            checkNumberOfProjects(projectService, { inferredProjects: 3 });
545            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
546            assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]);
547            assert.strictEqual(projectService.inferredProjects[2], inferredProjects[2]);
548            assert.isTrue(projectService.inferredProjects[0].isOrphan());
549            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
550            assert.isTrue(projectService.inferredProjects[2].isOrphan());
551
552            projectService.openClientFile(file3.path);
553            checkNumberOfProjects(projectService, { inferredProjects: 2 });
554            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]);
555            assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]);
556            checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]);
557            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
558        });
559
560        it("regression test for crash in acquireOrUpdateDocument", () => {
561            const tsFile = {
562                fileName: "/a/b/file1.ts",
563                path: "/a/b/file1.ts",
564                content: ""
565            };
566            const jsFile = {
567                path: "/a/b/file1.js",
568                content: "var x = 10;",
569                fileName: "/a/b/file1.js",
570                scriptKind: "JS" as const
571            };
572
573            const host = createServerHost([]);
574            const projectService = createProjectService(host);
575            projectService.applyChangesInOpenFiles(singleIterator(tsFile));
576            const projs = projectService.synchronizeProjectList([]);
577            projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName);
578            projectService.synchronizeProjectList([projs[0].info!]);
579            projectService.applyChangesInOpenFiles(singleIterator(jsFile));
580        });
581
582        it("config file is deleted", () => {
583            const file1 = {
584                path: "/a/b/f1.ts",
585                content: "let x = 1;"
586            };
587            const file2 = {
588                path: "/a/b/f2.ts",
589                content: "let y = 2;"
590            };
591            const config = {
592                path: "/a/b/tsconfig.json",
593                content: JSON.stringify({ compilerOptions: {} })
594            };
595            const host = createServerHost([file1, file2, config]);
596            const projectService = createProjectService(host);
597
598            projectService.openClientFile(file1.path);
599            checkNumberOfProjects(projectService, { configuredProjects: 1 });
600            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
601
602            projectService.openClientFile(file2.path);
603            checkNumberOfProjects(projectService, { configuredProjects: 1 });
604            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
605
606            host.deleteFile(config.path);
607            host.checkTimeoutQueueLengthAndRun(1);
608            checkNumberOfProjects(projectService, { inferredProjects: 2 });
609            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]);
610            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
611        });
612
613        it("loading files with correct priority", () => {
614            const f1 = {
615                path: "/a/main.ts",
616                content: "let x = 1"
617            };
618            const f2 = {
619                path: "/a/main.js",
620                content: "var y = 1"
621            };
622            const f3 = {
623                path: "/main.js",
624                content: "var y = 1"
625            };
626            const config = {
627                path: "/a/tsconfig.json",
628                content: JSON.stringify({
629                    compilerOptions: { allowJs: true }
630                })
631            };
632            const host = createServerHost([f1, f2, f3, config]);
633            const projectService = createProjectService(host);
634            projectService.setHostConfiguration({
635                extraFileExtensions: [
636                    { extension: ".js", isMixedContent: false },
637                    { extension: ".html", isMixedContent: true }
638                ]
639            });
640            projectService.openClientFile(f1.path);
641            projectService.checkNumberOfProjects({ configuredProjects: 1 });
642            checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]);
643
644            // Since f2 refers to config file as the default project, it needs to be kept alive
645            projectService.closeClientFile(f1.path);
646            projectService.openClientFile(f2.path);
647            projectService.checkNumberOfProjects({ inferredProjects: 1, configuredProjects: 1 });
648            assert.isDefined(projectService.configuredProjects.get(config.path));
649            checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]);
650
651            // Should close configured project with next file open
652            projectService.closeClientFile(f2.path);
653            projectService.openClientFile(f3.path);
654            projectService.checkNumberOfProjects({ inferredProjects: 1 });
655            assert.isUndefined(projectService.configuredProjects.get(config.path));
656            checkProjectActualFiles(projectService.inferredProjects[0], [f3.path]);
657        });
658
659        it("tsconfig script block support", () => {
660            const file1 = {
661                path: "/a/b/f1.ts",
662                content: ` `
663            };
664            const file2 = {
665                path: "/a/b/f2.html",
666                content: `var hello = "hello";`
667            };
668            const config = {
669                path: "/a/b/tsconfig.json",
670                content: JSON.stringify({ compilerOptions: { allowJs: true } })
671            };
672            const host = createServerHost([file1, file2, config]);
673            const session = createSession(host);
674            openFilesForSession([file1], session);
675            const projectService = session.getProjectService();
676
677            // HTML file will not be included in any projects yet
678            checkNumberOfProjects(projectService, { configuredProjects: 1 });
679            const configuredProj = configuredProjectAt(projectService, 0);
680            checkProjectActualFiles(configuredProj, [file1.path, config.path]);
681
682            // Specify .html extension as mixed content
683            const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }];
684            const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions });
685            session.executeCommand(configureHostRequest);
686
687            // The configured project should now be updated to include html file
688            checkNumberOfProjects(projectService, { configuredProjects: 1 });
689            assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated");
690            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
691
692            // Open HTML file
693            projectService.applyChangesInOpenFiles(singleIterator({
694                fileName: file2.path,
695                hasMixedContent: true,
696                scriptKind: ScriptKind.JS,
697                content: `var hello = "hello";`
698            }));
699            // Now HTML file is included in the project
700            checkNumberOfProjects(projectService, { configuredProjects: 1 });
701            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
702
703            // Check identifiers defined in HTML content are available in .ts file
704            const project = configuredProjectAt(projectService, 0);
705            let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions);
706            assert(completions && some(completions.entries, e => e.name === "hello"), `expected entry hello to be in completion list`);
707
708            // Close HTML file
709            projectService.applyChangesInOpenFiles(
710                /*openFiles*/ undefined,
711                /*changedFiles*/ undefined,
712                /*closedFiles*/[file2.path]);
713
714            // HTML file is still included in project
715            checkNumberOfProjects(projectService, { configuredProjects: 1 });
716            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
717
718            // Check identifiers defined in HTML content are not available in .ts file
719            completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, emptyOptions);
720            assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`);
721        });
722
723        it("no tsconfig script block diagnostic errors", () => {
724
725            //  #1. Ensure no diagnostic errors when allowJs is true
726            const file1 = {
727                path: "/a/b/f1.ts",
728                content: ` `
729            };
730            const file2 = {
731                path: "/a/b/f2.html",
732                content: `var hello = "hello";`
733            };
734            const config1 = {
735                path: "/a/b/tsconfig.json",
736                content: JSON.stringify({ compilerOptions: { allowJs: true } })
737            };
738
739            let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
740            let session = createSession(host);
741
742            // Specify .html extension as mixed content in a configure host request
743            const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }];
744            const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions });
745            session.executeCommand(configureHostRequest);
746
747            openFilesForSession([file1], session);
748            let projectService = session.getProjectService();
749
750            checkNumberOfProjects(projectService, { configuredProjects: 1 });
751
752            let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
753            assert.deepEqual(diagnostics, []);
754
755            //  #2. Ensure no errors when allowJs is false
756            const config2 = {
757                path: "/a/b/tsconfig.json",
758                content: JSON.stringify({ compilerOptions: { allowJs: false } })
759            };
760
761            host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
762            session = createSession(host);
763
764            session.executeCommand(configureHostRequest);
765
766            openFilesForSession([file1], session);
767            projectService = session.getProjectService();
768
769            checkNumberOfProjects(projectService, { configuredProjects: 1 });
770
771            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
772            assert.deepEqual(diagnostics, []);
773
774            //  #3. Ensure no errors when compiler options aren't specified
775            const config3 = {
776                path: "/a/b/tsconfig.json",
777                content: JSON.stringify({})
778            };
779
780            host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
781            session = createSession(host);
782
783            session.executeCommand(configureHostRequest);
784
785            openFilesForSession([file1], session);
786            projectService = session.getProjectService();
787
788            checkNumberOfProjects(projectService, { configuredProjects: 1 });
789
790            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
791            assert.deepEqual(diagnostics, []);
792
793            //  #4. Ensure no errors when files are explicitly specified in tsconfig
794            const config4 = {
795                path: "/a/b/tsconfig.json",
796                content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] })
797            };
798
799            host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
800            session = createSession(host);
801
802            session.executeCommand(configureHostRequest);
803
804            openFilesForSession([file1], session);
805            projectService = session.getProjectService();
806
807            checkNumberOfProjects(projectService, { configuredProjects: 1 });
808
809            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
810            assert.deepEqual(diagnostics, []);
811
812            //  #4. Ensure no errors when files are explicitly excluded in tsconfig
813            const config5 = {
814                path: "/a/b/tsconfig.json",
815                content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] })
816            };
817
818            host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
819            session = createSession(host);
820
821            session.executeCommand(configureHostRequest);
822
823            openFilesForSession([file1], session);
824            projectService = session.getProjectService();
825
826            checkNumberOfProjects(projectService, { configuredProjects: 1 });
827
828            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
829            assert.deepEqual(diagnostics, []);
830        });
831
832        it("project structure update is deferred if files are not added\removed", () => {
833            const file1 = {
834                path: "/a/b/f1.ts",
835                content: `import {x} from "./f2"`
836            };
837            const file2 = {
838                path: "/a/b/f2.ts",
839                content: "export let x = 1"
840            };
841            const host = createServerHost([file1, file2]);
842            const projectService = createProjectService(host);
843
844            projectService.openClientFile(file1.path);
845            projectService.openClientFile(file2.path);
846
847            checkNumberOfProjects(projectService, { inferredProjects: 1 });
848            projectService.applyChangesInOpenFiles(
849                /*openFiles*/ undefined,
850                /*changedFiles*/singleIterator({ fileName: file1.path, changes: singleIterator({ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }) }),
851                /*closedFiles*/ undefined);
852
853            checkNumberOfProjects(projectService, { inferredProjects: 1 });
854            projectService.ensureInferredProjectsUpToDate_TestOnly();
855            checkNumberOfProjects(projectService, { inferredProjects: 2 });
856        });
857
858        it("files with mixed content are handled correctly", () => {
859            const file1 = {
860                path: "/a/b/f1.html",
861                content: `<html><script language="javascript">var x = 1;</></html>`
862            };
863            const host = createServerHost([file1]);
864            const projectService = createProjectService(host, { logger: createLoggerWithInMemoryLogs(host) });
865            const projectFileName = "projectFileName";
866            projectService.openExternalProject({ projectFileName, options: {}, rootFiles: [{ fileName: file1.path, scriptKind: ScriptKind.JS, hasMixedContent: true }] });
867
868
869            const project = projectService.externalProjects[0];
870
871            const scriptInfo = project.getScriptInfo(file1.path)!;
872            const snap = scriptInfo.getSnapshot();
873            const actualText = getSnapshotText(snap);
874            projectService.logger.info(`Text of${file1.path}: ${actualText}`);
875
876            projectService.openClientFile(file1.path, `var x = 1;`);
877            project.updateGraph();
878
879            const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file1.path, 4)!;
880            projectService.logger.info(`QuickInfo : ${quickInfo.kind}`);
881
882            projectService.closeClientFile(file1.path);
883
884            const scriptInfo2 = project.getScriptInfo(file1.path)!;
885            const actualText2 = getSnapshotText(scriptInfo2.getSnapshot());
886            projectService.logger.info(`Text of${file1.path}: ${actualText2}`);
887            baselineTsserverLogs("projects", "files with mixed content are handled correctly", projectService);
888        });
889
890        it("syntax tree cache handles changes in project settings", () => {
891            const file1 = {
892                path: "/a/b/app.ts",
893                content: "{x: 1}"
894            };
895            const host = createServerHost([file1]);
896            const projectService = createProjectService(host, { useSingleInferredProject: true });
897            projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES5, allowJs: false });
898            projectService.openClientFile(file1.path);
899            projectService.inferredProjects[0].getLanguageService(/*ensureSynchronized*/ false).getOutliningSpans(file1.path);
900            projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES5, allowJs: true });
901            projectService.getScriptInfo(file1.path)!.editContent(0, 0, " ");
902            projectService.inferredProjects[0].getLanguageService(/*ensureSynchronized*/ false).getOutliningSpans(file1.path);
903            projectService.closeClientFile(file1.path);
904        });
905
906        it("File in multiple projects at opened and closed correctly", () => {
907            const file1 = {
908                path: "/a/b/app.ts",
909                content: "let x = 1;"
910            };
911            const file2 = {
912                path: "/a/c/f.ts",
913                content: `/// <reference path="../b/app.ts"/>`
914            };
915            const tsconfig1 = {
916                path: "/a/c/tsconfig.json",
917                content: "{}"
918            };
919            const tsconfig2 = {
920                path: "/a/b/tsconfig.json",
921                content: "{}"
922            };
923            const host = createServerHost([file1, file2, tsconfig1, tsconfig2]);
924            const projectService = createProjectService(host);
925
926            projectService.openClientFile(file2.path);
927            checkNumberOfProjects(projectService, { configuredProjects: 1 });
928            const project1 = projectService.configuredProjects.get(tsconfig1.path)!;
929            assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 1"); // file2
930            assert.equal(project1.getScriptInfo(file2.path)!.containingProjects.length, 1, "containing projects count");
931            assert.isFalse(project1.isClosed());
932
933            projectService.openClientFile(file1.path);
934            checkNumberOfProjects(projectService, { configuredProjects: 2 });
935            assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 2"); // file2
936            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
937            assert.isFalse(project1.isClosed());
938
939            const project2 = projectService.configuredProjects.get(tsconfig2.path)!;
940            assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 2"); // file1
941            assert.isFalse(project2.isClosed());
942
943            assert.equal(project1.getScriptInfo(file1.path)!.containingProjects.length, 2, `${file1.path} containing projects count`);
944            assert.equal(project1.getScriptInfo(file2.path)!.containingProjects.length, 1, `${file2.path} containing projects count`);
945
946            projectService.closeClientFile(file2.path);
947            checkNumberOfProjects(projectService, { configuredProjects: 2 });
948            assert.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 3"); // No files
949            assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 3"); // file1
950            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
951            assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2);
952            assert.isFalse(project1.isClosed());
953            assert.isFalse(project2.isClosed());
954
955            projectService.closeClientFile(file1.path);
956            checkNumberOfProjects(projectService, { configuredProjects: 2 });
957            assert.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 4"); // No files
958            assert.isFalse(project2.hasOpenRef(), "Has open ref count in project2 - 4"); // No files
959            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
960            assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2);
961            assert.isFalse(project1.isClosed());
962            assert.isFalse(project2.isClosed());
963
964            projectService.openClientFile(file2.path);
965            checkNumberOfProjects(projectService, { configuredProjects: 1 });
966            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
967            assert.isUndefined(projectService.configuredProjects.get(tsconfig2.path));
968            assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 5"); // file2
969            assert.isFalse(project1.isClosed());
970            assert.isTrue(project2.isClosed());
971        });
972
973        it("snapshot from different caches are incompatible", () => {
974            const f1 = {
975                path: "/a/b/app.ts",
976                content: "let x = 1;"
977            };
978            const host = createServerHost([f1]);
979            const projectFileName = "/a/b/proj.csproj";
980            const projectService = createProjectService(host);
981            projectService.openExternalProject({
982                projectFileName,
983                rootFiles: [toExternalFile(f1.path)],
984                options: {}
985            });
986            projectService.openClientFile(f1.path, "let x = 1;\nlet y = 2;");
987
988            projectService.checkNumberOfProjects({ externalProjects: 1 });
989            projectService.externalProjects[0].getLanguageService(/*ensureSynchronized*/ false).getNavigationBarItems(f1.path);
990            projectService.closeClientFile(f1.path);
991
992            projectService.openClientFile(f1.path);
993            projectService.checkNumberOfProjects({ externalProjects: 1 });
994            const navbar = projectService.externalProjects[0].getLanguageService(/*ensureSynchronized*/ false).getNavigationBarItems(f1.path);
995            assert.equal(navbar[0].spans[0].length, f1.content.length);
996        });
997
998        it("Getting errors from closed script info does not throw exception (because of getting project from orphan script info)", () => {
999            const f1 = {
1000                path: "/a/b/app.ts",
1001                content: "let x = 1;"
1002            };
1003            const config = {
1004                path: "/a/b/tsconfig.json",
1005                content: JSON.stringify({ compilerOptions: {} })
1006            };
1007            const host = createServerHost([f1, libFile, config]);
1008            const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
1009            session.executeCommandSeq({
1010                command: server.CommandNames.Open,
1011                arguments: {
1012                    file: f1.path
1013                }
1014            } as protocol.OpenRequest);
1015            session.executeCommandSeq({
1016                command: server.CommandNames.Close,
1017                arguments: {
1018                    file: f1.path
1019                }
1020            } as protocol.CloseRequest);
1021            session.executeCommandSeq({
1022                command: server.CommandNames.Geterr,
1023                arguments: {
1024                    delay: 0,
1025                    files: [f1.path]
1026                }
1027            } as protocol.GeterrRequest);
1028            baselineTsserverLogs("projects", "getting errors from closed script info does not throw exception because of getting project from orphan script info", session);
1029        });
1030
1031        it("Properly handle Windows-style outDir", () => {
1032            const configFile: File = {
1033                path: "C:\\a\\tsconfig.json",
1034                content: JSON.stringify({
1035                    compilerOptions: {
1036                        outDir: `C:\\a\\b`
1037                    },
1038                    include: ["*.ts"]
1039                })
1040            };
1041            const file1: File = {
1042                path: "C:\\a\\f1.ts",
1043                content: "let x = 1;"
1044            };
1045
1046            const host = createServerHost([file1, configFile], { windowsStyleRoot: "c:/" });
1047            const projectService = createProjectService(host);
1048
1049            projectService.openClientFile(file1.path);
1050            checkNumberOfProjects(projectService, { configuredProjects: 1 });
1051            const project = configuredProjectAt(projectService, 0);
1052            checkProjectActualFiles(project, [normalizePath(file1.path), normalizePath(configFile.path)]);
1053            const options = project.getCompilerOptions();
1054            assert.equal(options.outDir, "C:/a/b", "");
1055        });
1056
1057        it("files opened and closed affecting multiple projects", () => {
1058            const file: File = {
1059                path: "/a/b/projects/config/file.ts",
1060                content: `import {a} from "../files/file1"; export let b = a;`
1061            };
1062            const config: File = {
1063                path: "/a/b/projects/config/tsconfig.json",
1064                content: ""
1065            };
1066            const filesFile1: File = {
1067                path: "/a/b/projects/files/file1.ts",
1068                content: "export let a = 10;"
1069            };
1070            const filesFile2: File = {
1071                path: "/a/b/projects/files/file2.ts",
1072                content: "export let aa = 10;"
1073            };
1074
1075            const files = [config, file, filesFile1, filesFile2, libFile];
1076            const host = createServerHost(files);
1077            const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
1078            // Create configured project
1079            session.executeCommandSeq<protocol.OpenRequest>({
1080                command: protocol.CommandTypes.Open,
1081                arguments: {
1082                    file: file.path
1083                }
1084            });
1085
1086            // open files/file1 = should not create another project
1087            session.executeCommandSeq<protocol.OpenRequest>({
1088                command: protocol.CommandTypes.Open,
1089                arguments: {
1090                    file: filesFile1.path
1091                }
1092            });
1093
1094            // Close the file = should still have project
1095            session.executeCommandSeq<protocol.CloseRequest>({
1096                command: protocol.CommandTypes.Close,
1097                arguments: {
1098                    file: file.path
1099                }
1100            });
1101
1102            // Open files/file2 - should create inferred project and close configured project
1103            session.executeCommandSeq<protocol.OpenRequest>({
1104                command: protocol.CommandTypes.Open,
1105                arguments: {
1106                    file: filesFile2.path
1107                }
1108            });
1109
1110            // Actions on file1 would result in assert
1111            session.executeCommandSeq<protocol.OccurrencesRequest>({
1112                command: protocol.CommandTypes.Occurrences,
1113                arguments: {
1114                    file: filesFile1.path,
1115                    line: 1,
1116                    offset: filesFile1.content.indexOf("a")
1117                }
1118            });
1119
1120            baselineTsserverLogs("projects", "files opened and closed affecting multiple projects", session);
1121        });
1122
1123        it("requests are done on file on pendingReload but has svc for previous version", () => {
1124            const file1: File = {
1125                path: `${tscWatch.projectRoot}/src/file1.ts`,
1126                content: `import { y } from "./file2"; let x = 10;`
1127            };
1128            const file2: File = {
1129                path: `${tscWatch.projectRoot}/src/file2.ts`,
1130                content: "export let y = 10;"
1131            };
1132            const config: File = {
1133                path: `${tscWatch.projectRoot}/tsconfig.json`,
1134                content: "{}"
1135            };
1136            const files = [file1, file2, libFile, config];
1137            const host = createServerHost(files);
1138            const session = createSession(host);
1139            session.executeCommandSeq<protocol.OpenRequest>({
1140                command: protocol.CommandTypes.Open,
1141                arguments: { file: file2.path, fileContent: file2.content }
1142            });
1143            session.executeCommandSeq<protocol.OpenRequest>({
1144                command: protocol.CommandTypes.Open,
1145                arguments: { file: file1.path }
1146            });
1147            session.executeCommandSeq<protocol.CloseRequest>({
1148                command: protocol.CommandTypes.Close,
1149                arguments: { file: file2.path }
1150            });
1151
1152            file2.content += "export let z = 10;";
1153            host.writeFile(file2.path, file2.content);
1154            // Do not let the timeout runs, before executing command
1155            const startOffset = file2.content.indexOf("y") + 1;
1156            session.executeCommandSeq<protocol.GetApplicableRefactorsRequest>({
1157                command: protocol.CommandTypes.GetApplicableRefactors,
1158                arguments: { file: file2.path, startLine: 1, startOffset, endLine: 1, endOffset: startOffset + 1 }
1159            });
1160        });
1161
1162        describe("includes deferred files in the project context", () => {
1163            function verifyDeferredContext(lazyConfiguredProjectsFromExternalProject: boolean) {
1164                const file1 = {
1165                    path: "/a.deferred",
1166                    content: "const a = 1;"
1167                };
1168                // Deferred extensions should not affect JS files.
1169                const file2 = {
1170                    path: "/b.js",
1171                    content: "const b = 1;"
1172                };
1173                const tsconfig = {
1174                    path: "/tsconfig.json",
1175                    content: ""
1176                };
1177
1178                const host = createServerHost([file1, file2, tsconfig]);
1179                const session = createSession(host);
1180                const projectService = session.getProjectService();
1181                session.executeCommandSeq<protocol.ConfigureRequest>({
1182                    command: protocol.CommandTypes.Configure,
1183                    arguments: { preferences: { lazyConfiguredProjectsFromExternalProject } }
1184                });
1185
1186                // Configure the deferred extension.
1187                const extraFileExtensions = [{ extension: ".deferred", scriptKind: ScriptKind.Deferred, isMixedContent: true }];
1188                const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions });
1189                session.executeCommand(configureHostRequest);
1190
1191                // Open external project
1192                const projectName = "/proj1";
1193                projectService.openExternalProject({
1194                    projectFileName: projectName,
1195                    rootFiles: toExternalFiles([file1.path, file2.path, tsconfig.path]),
1196                    options: {}
1197                });
1198
1199                // Assert
1200                checkNumberOfProjects(projectService, { configuredProjects: 1 });
1201
1202                const configuredProject = configuredProjectAt(projectService, 0);
1203                if (lazyConfiguredProjectsFromExternalProject) {
1204                    // configured project is just created and not yet loaded
1205                    checkProjectActualFiles(configuredProject, emptyArray);
1206                    projectService.ensureInferredProjectsUpToDate_TestOnly();
1207                }
1208                checkProjectActualFiles(configuredProject, [file1.path, tsconfig.path]);
1209
1210                // Allow allowNonTsExtensions will be set to true for deferred extensions.
1211                assert.isTrue(configuredProject.getCompilerOptions().allowNonTsExtensions);
1212            }
1213
1214            it("when lazyConfiguredProjectsFromExternalProject not set", () => {
1215                verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ false);
1216            });
1217            it("when lazyConfiguredProjectsFromExternalProject is set", () => {
1218                verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ true);
1219            });
1220        });
1221
1222        it("Orphan source files are handled correctly on watch trigger", () => {
1223            const file1: File = {
1224                path: `${tscWatch.projectRoot}/src/file1.ts`,
1225                content: `export let x = 10;`
1226            };
1227            const file2: File = {
1228                path: `${tscWatch.projectRoot}/src/file2.ts`,
1229                content: "export let y = 10;"
1230            };
1231            const configContent1 = JSON.stringify({
1232                files: ["src/file1.ts", "src/file2.ts"]
1233            });
1234            const config: File = {
1235                path: `${tscWatch.projectRoot}/tsconfig.json`,
1236                content: configContent1
1237            };
1238            const files = [file1, file2, libFile, config];
1239            const host = createServerHost(files);
1240            const service = createProjectService(host);
1241            service.openClientFile(file1.path);
1242            checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, file2.path, libFile.path, config.path]);
1243
1244            const configContent2 = JSON.stringify({
1245                files: ["src/file1.ts"]
1246            });
1247            config.content = configContent2;
1248            host.writeFile(config.path, config.content);
1249            host.runQueuedTimeoutCallbacks();
1250
1251            checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, libFile.path, config.path]);
1252            verifyFile2InfoIsOrphan();
1253
1254            file2.content += "export let z = 10;";
1255            host.writeFile(file2.path, file2.content);
1256            host.runQueuedTimeoutCallbacks();
1257
1258            checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, libFile.path, config.path]);
1259            verifyFile2InfoIsOrphan();
1260
1261            function verifyFile2InfoIsOrphan() {
1262                const info = Debug.checkDefined(service.getScriptInfoForPath(file2.path as Path));
1263                assert.equal(info.containingProjects.length, 0);
1264            }
1265        });
1266
1267        it("no project structure update on directory watch invoke on open file save", () => {
1268            const projectRootPath = "/users/username/projects/project";
1269            const file1: File = {
1270                path: `${projectRootPath}/a.ts`,
1271                content: "export const a = 10;"
1272            };
1273            const config: File = {
1274                path: `${projectRootPath}/tsconfig.json`,
1275                content: "{}"
1276            };
1277            const files = [file1, config];
1278            const host = createServerHost(files);
1279            const service = createProjectService(host);
1280            service.openClientFile(file1.path);
1281            checkNumberOfProjects(service, { configuredProjects: 1 });
1282
1283            host.modifyFile(file1.path, file1.content, { invokeFileDeleteCreateAsPartInsteadOfChange: true });
1284            host.checkTimeoutQueueLength(0);
1285        });
1286
1287        it("synchronizeProjectList provides redirect info when requested", () => {
1288            const projectRootPath = "/users/username/projects/project";
1289            const fileA: File = {
1290                path: `${projectRootPath}/A/a.ts`,
1291                content: "export const foo: string = 5;"
1292            };
1293            const configA: File = {
1294                path: `${projectRootPath}/A/tsconfig.json`,
1295                content: `{
1296  "compilerOptions": {
1297    "composite": true,
1298    "declaration": true
1299  }
1300}`
1301            };
1302            const fileB: File = {
1303                path: `${projectRootPath}/B/b.ts`,
1304                content: "import { foo } from \"../A/a\"; console.log(foo);"
1305            };
1306            const configB: File = {
1307                path: `${projectRootPath}/B/tsconfig.json`,
1308                content: `{
1309  "compilerOptions": {
1310    "composite": true,
1311    "declaration": true
1312  },
1313  "references": [
1314    { "path": "../A" }
1315  ]
1316}`
1317            };
1318            const files = [fileA, fileB, configA, configB, libFile];
1319            const host = createServerHost(files);
1320            const projectService = createProjectService(host);
1321            projectService.openClientFile(fileA.path);
1322            projectService.openClientFile(fileB.path);
1323            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true);
1324            assert.deepEqual(knownProjects[0].files, [
1325                {
1326                    fileName: libFile.path,
1327                    isSourceOfProjectReferenceRedirect: false
1328                },
1329                {
1330                    fileName: fileA.path,
1331                    isSourceOfProjectReferenceRedirect: false
1332                },
1333                {
1334                    fileName: configA.path,
1335                    isSourceOfProjectReferenceRedirect: false
1336                }
1337            ]);
1338            assert.deepEqual(knownProjects[1].files, [
1339                {
1340                    fileName: libFile.path,
1341                    isSourceOfProjectReferenceRedirect: false
1342                },
1343                {
1344                    fileName: fileA.path,
1345                    isSourceOfProjectReferenceRedirect: true,
1346                },
1347                {
1348                    fileName: fileB.path,
1349                    isSourceOfProjectReferenceRedirect: false,
1350                },
1351                {
1352                    fileName: configB.path,
1353                    isSourceOfProjectReferenceRedirect: false
1354                }
1355            ]);
1356        });
1357
1358        it("synchronizeProjectList provides updates to redirect info when requested", () => {
1359            const projectRootPath = "/users/username/projects/project";
1360            const fileA: File = {
1361                path: `${projectRootPath}/A/a.ts`,
1362                content: "export const foo: string = 5;"
1363            };
1364            const configA: File = {
1365                path: `${projectRootPath}/A/tsconfig.json`,
1366                content: `{
1367  "compilerOptions": {
1368    "composite": true,
1369    "declaration": true
1370  }
1371}`
1372            };
1373            const fileB: File = {
1374                path: `${projectRootPath}/B/b.ts`,
1375                content: "import { foo } from \"../B/b2\"; console.log(foo);"
1376            };
1377            const fileB2: File = {
1378                path: `${projectRootPath}/B/b2.ts`,
1379                content: "export const foo: string = 5;"
1380            };
1381            const configB: File = {
1382                path: `${projectRootPath}/B/tsconfig.json`,
1383                content: `{
1384  "compilerOptions": {
1385    "composite": true,
1386    "declaration": true
1387  },
1388  "references": [
1389    { "path": "../A" }
1390  ]
1391}`
1392            };
1393            const files = [fileA, fileB, fileB2, configA, configB, libFile];
1394            const host = createServerHost(files);
1395            const projectService = createProjectService(host);
1396            projectService.openClientFile(fileA.path);
1397            projectService.openClientFile(fileB.path);
1398            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true);
1399            assert.deepEqual(knownProjects[0].files, [
1400                {
1401                    fileName: libFile.path,
1402                    isSourceOfProjectReferenceRedirect: false
1403                },
1404                {
1405                    fileName: fileA.path,
1406                    isSourceOfProjectReferenceRedirect: false
1407                },
1408                {
1409                    fileName: configA.path,
1410                    isSourceOfProjectReferenceRedirect: false
1411                }
1412            ]);
1413            assert.deepEqual(knownProjects[1].files, [
1414                {
1415                    fileName: libFile.path,
1416                    isSourceOfProjectReferenceRedirect: false
1417                },
1418                {
1419                    fileName: fileB2.path,
1420                    isSourceOfProjectReferenceRedirect: false,
1421                },
1422                {
1423                    fileName: fileB.path,
1424                    isSourceOfProjectReferenceRedirect: false,
1425                },
1426                {
1427                    fileName: configB.path,
1428                    isSourceOfProjectReferenceRedirect: false
1429                }
1430            ]);
1431
1432            host.modifyFile(configA.path, `{
1433  "compilerOptions": {
1434    "composite": true,
1435    "declaration": true
1436  },
1437  "include": [
1438      "**/*",
1439      "../B/b2.ts"
1440  ]
1441}`);
1442            const newKnownProjects = projectService.synchronizeProjectList(knownProjects.map(proj => proj.info!), /*includeProjectReferenceRedirectInfo*/ true);
1443            assert.deepEqual(newKnownProjects[0].changes?.added, [
1444                {
1445                    fileName: fileB2.path,
1446                    isSourceOfProjectReferenceRedirect: false
1447                }
1448            ]);
1449            assert.deepEqual(newKnownProjects[1].changes?.updatedRedirects, [
1450                {
1451                    fileName: fileB2.path,
1452                    isSourceOfProjectReferenceRedirect: true
1453                }
1454            ]);
1455        });
1456
1457        it("synchronizeProjectList returns correct information when base configuration file cannot be resolved", () => {
1458            const file: File = {
1459                path: `${tscWatch.projectRoot}/index.ts`,
1460                content: "export const foo = 5;"
1461            };
1462            const config: File = {
1463                path: `${tscWatch.projectRoot}/tsconfig.json`,
1464                content: JSON.stringify({ extends: "./tsconfig_base.json" })
1465            };
1466            const host = createServerHost([file, config, libFile]);
1467            const projectService = createProjectService(host);
1468            projectService.openClientFile(file.path);
1469            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ false);
1470            assert.deepEqual(knownProjects[0].files, [
1471                libFile.path,
1472                file.path,
1473                config.path,
1474                `${tscWatch.projectRoot}/tsconfig_base.json`,
1475            ]);
1476        });
1477
1478        it("synchronizeProjectList returns correct information when base configuration file cannot be resolved and redirect info is requested", () => {
1479            const file: File = {
1480                path: `${tscWatch.projectRoot}/index.ts`,
1481                content: "export const foo = 5;"
1482            };
1483            const config: File = {
1484                path: `${tscWatch.projectRoot}/tsconfig.json`,
1485                content: JSON.stringify({ extends: "./tsconfig_base.json" })
1486            };
1487            const host = createServerHost([file, config, libFile]);
1488            const projectService = createProjectService(host);
1489            projectService.openClientFile(file.path);
1490            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true);
1491            assert.deepEqual(knownProjects[0].files, [
1492                { fileName: libFile.path, isSourceOfProjectReferenceRedirect: false },
1493                { fileName: file.path, isSourceOfProjectReferenceRedirect: false },
1494                { fileName: config.path, isSourceOfProjectReferenceRedirect: false },
1495                { fileName: `${tscWatch.projectRoot}/tsconfig_base.json`, isSourceOfProjectReferenceRedirect: false },
1496            ]);
1497        });
1498
1499        it("handles delayed directory watch invoke on file creation", () => {
1500            const projectRootPath = "/users/username/projects/project";
1501            const fileB: File = {
1502                path: `${projectRootPath}/b.ts`,
1503                content: "export const b = 10;"
1504            };
1505            const fileA: File = {
1506                path: `${projectRootPath}/a.ts`,
1507                content: "export const a = 10;"
1508            };
1509            const fileSubA: File = {
1510                path: `${projectRootPath}/sub/a.ts`,
1511                content: fileA.content
1512            };
1513            const config: File = {
1514                path: `${projectRootPath}/tsconfig.json`,
1515                content: "{}"
1516            };
1517            const files = [fileSubA, fileB, config, libFile];
1518            const host = createServerHost(files);
1519            const session = createSession(host, { canUseEvents: true, noGetErrOnBackgroundUpdate: true, logger: createLoggerWithInMemoryLogs(host) });
1520            openFile(fileB);
1521            openFile(fileSubA);
1522
1523            host.checkTimeoutQueueLengthAndRun(0);
1524
1525            // This should schedule 2 timeouts for ensuring project structure and ensuring projects for open file
1526            host.deleteFile(fileSubA.path);
1527            host.deleteFolder(getDirectoryPath(fileSubA.path));
1528            host.writeFile(fileA.path, fileA.content);
1529            host.checkTimeoutQueueLength(2);
1530
1531            closeFilesForSession([fileSubA], session);
1532            // This should cancel existing updates and schedule new ones
1533            host.checkTimeoutQueueLength(2);
1534
1535            // Open the fileA (as if rename)
1536            // config project is updated to check if fileA is present in it
1537            openFile(fileA);
1538
1539            // Run the timeout for updating configured project and ensuring projects for open file
1540            host.checkTimeoutQueueLengthAndRun(2);
1541
1542            // file is deleted but watches are not yet invoked
1543            const originalFileExists = host.fileExists;
1544            host.fileExists = s => s === fileA.path ? false : originalFileExists.call(host, s);
1545            closeFilesForSession([fileA], session);
1546            host.checkTimeoutQueueLength(2); // Update configured project and projects for open file
1547
1548            // This should create inferred project since fileSubA not on the disk
1549            openFile(fileSubA);
1550
1551            host.checkTimeoutQueueLengthAndRun(2); // Update configured project and projects for open file
1552            host.fileExists = originalFileExists;
1553
1554            // Actually trigger the file move
1555            host.deleteFile(fileA.path);
1556            host.ensureFileOrFolder(fileSubA);
1557            host.checkTimeoutQueueLength(2);
1558
1559            verifyGetErrRequest({ session, host, files: [fileB, fileSubA], existingTimeouts: 2 });
1560            baselineTsserverLogs("projects", "handles delayed directory watch invoke on file creation", session);
1561
1562            function openFile(file: File) {
1563                openFilesForSession([{ file, projectRootPath }], session);
1564            }
1565        });
1566
1567        it("assert when removing project", () => {
1568            const host = createServerHost([commonFile1, commonFile2, libFile]);
1569            const service = createProjectService(host);
1570            service.openClientFile(commonFile1.path);
1571            const project = service.inferredProjects[0];
1572            checkProjectActualFiles(project, [commonFile1.path, libFile.path]);
1573            // Intentionally create scriptinfo and attach it to project
1574            const info = service.getOrCreateScriptInfoForNormalizedPath(commonFile2.path as server.NormalizedPath, /*openedByClient*/ false)!;
1575            info.attachToProject(project);
1576            try {
1577                service.applyChangesInOpenFiles(/*openFiles*/ undefined, /*changedFiles*/ undefined, [commonFile1.path]);
1578            }
1579            catch (e) {
1580                assert.isTrue(e.message.indexOf("Debug Failure. False expression: Found script Info still attached to project") === 0);
1581            }
1582        });
1583        it("does not look beyond node_modules folders for default configured projects", () => {
1584            const rootFilePath = server.asNormalizedPath("/project/index.ts");
1585            const rootProjectPath = server.asNormalizedPath("/project/tsconfig.json");
1586            const nodeModulesFilePath1 = server.asNormalizedPath("/project/node_modules/@types/a/index.d.ts");
1587            const nodeModulesProjectPath1 = server.asNormalizedPath("/project/node_modules/@types/a/tsconfig.json");
1588            const nodeModulesFilePath2 = server.asNormalizedPath("/project/node_modules/@types/b/index.d.ts");
1589            const serverHost = createServerHost([
1590                { path: rootFilePath, content: "import 'a'; import 'b';" },
1591                { path: rootProjectPath, content: "{}" },
1592                { path: nodeModulesFilePath1, content: "{}" },
1593                { path: nodeModulesProjectPath1, content: "{}" },
1594                { path: nodeModulesFilePath2, content: "{}" },
1595            ]);
1596            const projectService = createProjectService(serverHost, { useSingleInferredProject: true });
1597
1598            const openRootFileResult = projectService.openClientFile(rootFilePath);
1599            assert.strictEqual(openRootFileResult.configFileName?.toString(), rootProjectPath);
1600
1601            const openNodeModulesFileResult1 = projectService.openClientFile(nodeModulesFilePath1);
1602            assert.strictEqual(openNodeModulesFileResult1.configFileName?.toString(), nodeModulesProjectPath1);
1603
1604            const openNodeModulesFileResult2 = projectService.openClientFile(nodeModulesFilePath2);
1605            assert.isUndefined(openNodeModulesFileResult2.configFileName);
1606
1607            const rootProject = projectService.findProject(rootProjectPath)!;
1608            checkProjectActualFiles(rootProject, [rootProjectPath, rootFilePath, nodeModulesFilePath1, nodeModulesFilePath2]);
1609
1610            checkNumberOfInferredProjects(projectService, 0);
1611        });
1612
1613        describe("file opened is in configured project that will be removed", () => {
1614            function runOnTs<T extends server.protocol.Request>(scenario: string, getRequest: (innerFile: File) => Partial<T>) {
1615                it(scenario, () => {
1616                    const testsConfig: File = {
1617                        path: `${tscWatch.projectRoot}/playground/tsconfig.json`,
1618                        content: "{}"
1619                    };
1620                    const testsFile: File = {
1621                        path: `${tscWatch.projectRoot}/playground/tests.ts`,
1622                        content: `export function foo() {}`
1623                    };
1624                    const innerFile: File = {
1625                        path: `${tscWatch.projectRoot}/playground/tsconfig-json/tests/spec.ts`,
1626                        content: `export function bar() { }`
1627                    };
1628                    const innerConfig: File = {
1629                        path: `${tscWatch.projectRoot}/playground/tsconfig-json/tsconfig.json`,
1630                        content: JSON.stringify({
1631                            include: ["./src"]
1632                        })
1633                    };
1634                    const innerSrcFile: File = {
1635                        path: `${tscWatch.projectRoot}/playground/tsconfig-json/src/src.ts`,
1636                        content: `export function foobar() { }`
1637                    };
1638                    const host = createServerHost([testsConfig, testsFile, innerFile, innerConfig, innerSrcFile, libFile]);
1639                    const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
1640                    openFilesForSession([testsFile], session);
1641                    closeFilesForSession([testsFile], session);
1642                    openFilesForSession([innerFile], session);
1643                    session.executeCommandSeq(getRequest(innerFile));
1644                    baselineTsserverLogs("projects", scenario, session);
1645                });
1646            }
1647            runOnTs<protocol.OutliningSpansRequest>(
1648                "file opened is in configured project that will be removed",
1649                innerFile => ({
1650                    command: protocol.CommandTypes.GetOutliningSpans,
1651                    arguments: { file: innerFile.path }
1652                })
1653            );
1654
1655            runOnTs<protocol.ReferencesRequest>(
1656                "references on file opened is in configured project that will be removed",
1657                innerFile => ({
1658                    command: protocol.CommandTypes.References,
1659                    arguments: protocolFileLocationFromSubstring(innerFile, "bar")
1660                })
1661            );
1662
1663            it("js file opened is in configured project that will be removed", () => {
1664                const rootConfig: File = {
1665                    path: `${tscWatch.projectRoot}/tsconfig.json`,
1666                    content: JSON.stringify({ compilerOptions: { allowJs: true } })
1667                };
1668                const mocksFile: File = {
1669                    path: `${tscWatch.projectRoot}/mocks/cssMock.js`,
1670                    content: `function foo() { }`
1671                };
1672                const innerFile: File = {
1673                    path: `${tscWatch.projectRoot}/apps/editor/scripts/createConfigVariable.js`,
1674                    content: `function bar() { }`
1675                };
1676                const innerConfig: File = {
1677                    path: `${tscWatch.projectRoot}/apps/editor/tsconfig.json`,
1678                    content: JSON.stringify({
1679                        extends: "../../tsconfig.json",
1680                        include: ["./src"],
1681                    })
1682                };
1683                const innerSrcFile: File = {
1684                    path: `${tscWatch.projectRoot}/apps/editor/src/src.js`,
1685                    content: `function fooBar() { }`
1686                };
1687                const host = createServerHost([rootConfig, mocksFile, innerFile, innerConfig, innerSrcFile, libFile]);
1688                const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
1689                openFilesForSession([mocksFile], session);
1690                closeFilesForSession([mocksFile], session);
1691                openFilesForSession([innerFile], session);
1692                baselineTsserverLogs("projects", "js file opened is in configured project that will be removed", session);
1693            });
1694        });
1695    });
1696}
1697