• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.projectSystem {
2    describe("unittests:: tsserver:: ConfiguredProjects", () => {
3        it("create configured project without file list", () => {
4            const configFile: File = {
5                path: "/a/b/tsconfig.json",
6                content: `
7                {
8                    "compilerOptions": {},
9                    "exclude": [
10                        "e"
11                    ]
12                }`
13            };
14            const file1: File = {
15                path: "/a/b/c/f1.ts",
16                content: "let x = 1"
17            };
18            const file2: File = {
19                path: "/a/b/d/f2.ts",
20                content: "let y = 1"
21            };
22            const file3: File = {
23                path: "/a/b/e/f3.ts",
24                content: "let z = 1"
25            };
26
27            const host = createServerHost([configFile, libFile, file1, file2, file3]);
28            const projectService = createProjectService(host);
29            const { configFileName, configFileErrors } = projectService.openClientFile(file1.path);
30
31            assert(configFileName, "should find config file");
32            assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`);
33            checkNumberOfInferredProjects(projectService, 0);
34            checkNumberOfConfiguredProjects(projectService, 1);
35
36            const project = configuredProjectAt(projectService, 0);
37            checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]);
38            checkProjectRootFiles(project, [file1.path, file2.path]);
39            // watching all files except one that was open
40            checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]);
41            const configFileDirectory = getDirectoryPath(configFile.path);
42            checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true);
43        });
44
45        it("create configured project with the file list", () => {
46            const configFile: File = {
47                path: "/a/b/tsconfig.json",
48                content: `
49                {
50                    "compilerOptions": {},
51                    "include": ["*.ts"]
52                }`
53            };
54            const file1: File = {
55                path: "/a/b/f1.ts",
56                content: "let x = 1"
57            };
58            const file2: File = {
59                path: "/a/b/f2.ts",
60                content: "let y = 1"
61            };
62            const file3: File = {
63                path: "/a/b/c/f3.ts",
64                content: "let z = 1"
65            };
66
67            const host = createServerHost([configFile, libFile, file1, file2, file3]);
68            const projectService = createProjectService(host);
69            const { configFileName, configFileErrors } = projectService.openClientFile(file1.path);
70
71            assert(configFileName, "should find config file");
72            assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`);
73            checkNumberOfInferredProjects(projectService, 0);
74            checkNumberOfConfiguredProjects(projectService, 1);
75
76            const project = configuredProjectAt(projectService, 0);
77            checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]);
78            checkProjectRootFiles(project, [file1.path, file2.path]);
79            // watching all files except one that was open
80            checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]);
81            checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false);
82        });
83
84        it("add and then remove a config file in a folder with loose files", () => {
85            const configFile: File = {
86                path: `${tscWatch.projectRoot}/tsconfig.json`,
87                content: `{
88                    "files": ["commonFile1.ts"]
89                }`
90            };
91            const commonFile1: File = {
92                path: `${tscWatch.projectRoot}/commonFile1.ts`,
93                content: "let x = 1"
94            };
95            const commonFile2: File = {
96                path: `${tscWatch.projectRoot}/commonFile2.ts`,
97                content: "let y = 1"
98            };
99
100            const host = createServerHost([libFile, commonFile1, commonFile2]);
101
102            const projectService = createProjectService(host);
103            projectService.openClientFile(commonFile1.path);
104            projectService.openClientFile(commonFile2.path);
105
106            projectService.checkNumberOfProjects({ inferredProjects: 2 });
107            checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]);
108            checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]);
109
110            const watchedFiles = getConfigFilesToWatch(tscWatch.projectRoot).concat(libFile.path);
111            checkWatchedFiles(host, watchedFiles);
112
113            // Add a tsconfig file
114            host.writeFile(configFile.path, configFile.content);
115            host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles
116
117            projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 });
118            assert.isTrue(projectService.inferredProjects[0].isOrphan());
119            checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]);
120            checkProjectActualFiles(projectService.configuredProjects.get(configFile.path)!, [libFile.path, commonFile1.path, configFile.path]);
121
122            checkWatchedFiles(host, watchedFiles);
123
124            // remove the tsconfig file
125            host.deleteFile(configFile.path);
126
127            projectService.checkNumberOfProjects({ inferredProjects: 2 });
128            assert.isTrue(projectService.inferredProjects[0].isOrphan());
129            checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]);
130
131            host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects
132
133            projectService.checkNumberOfProjects({ inferredProjects: 2 });
134            checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]);
135            checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]);
136            checkWatchedFiles(host, watchedFiles);
137        });
138
139        it("add new files to a configured project without file list", () => {
140            const configFile: File = {
141                path: "/a/b/tsconfig.json",
142                content: `{}`
143            };
144            const host = createServerHost([commonFile1, libFile, configFile]);
145            const projectService = createProjectService(host);
146            projectService.openClientFile(commonFile1.path);
147            const configFileDir = getDirectoryPath(configFile.path);
148            checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true);
149            checkNumberOfConfiguredProjects(projectService, 1);
150
151            const project = configuredProjectAt(projectService, 0);
152            checkProjectRootFiles(project, [commonFile1.path]);
153
154            // add a new ts file
155            host.writeFile(commonFile2.path, commonFile2.content);
156            host.checkTimeoutQueueLengthAndRun(2);
157            // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer.
158            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
159        });
160
161        it("should ignore non-existing files specified in the config file", () => {
162            const configFile: File = {
163                path: "/a/b/tsconfig.json",
164                content: `{
165                    "compilerOptions": {},
166                    "files": [
167                        "commonFile1.ts",
168                        "commonFile3.ts"
169                    ]
170                }`
171            };
172            const host = createServerHost([commonFile1, commonFile2, configFile]);
173            const projectService = createProjectService(host);
174            projectService.openClientFile(commonFile1.path);
175            projectService.openClientFile(commonFile2.path);
176
177            checkNumberOfConfiguredProjects(projectService, 1);
178            const project = configuredProjectAt(projectService, 0);
179            checkProjectRootFiles(project, [commonFile1.path]);
180            checkNumberOfInferredProjects(projectService, 1);
181        });
182
183        it("handle recreated files correctly", () => {
184            const configFile: File = {
185                path: "/a/b/tsconfig.json",
186                content: `{}`
187            };
188            const host = createServerHost([commonFile1, commonFile2, configFile]);
189            const projectService = createProjectService(host);
190            projectService.openClientFile(commonFile1.path);
191
192            checkNumberOfConfiguredProjects(projectService, 1);
193            const project = configuredProjectAt(projectService, 0);
194            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
195
196            // delete commonFile2
197            host.deleteFile(commonFile2.path);
198            host.checkTimeoutQueueLengthAndRun(2);
199            checkProjectRootFiles(project, [commonFile1.path]);
200
201            // re-add commonFile2
202            host.writeFile(commonFile2.path, commonFile2.content);
203            host.checkTimeoutQueueLengthAndRun(2);
204            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
205        });
206
207        it("files explicitly excluded in config file", () => {
208            const configFile: File = {
209                path: "/a/b/tsconfig.json",
210                content: `{
211                    "compilerOptions": {},
212                    "exclude": ["/a/c"]
213                }`
214            };
215            const excludedFile1: File = {
216                path: "/a/c/excluedFile1.ts",
217                content: `let t = 1;`
218            };
219
220            const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]);
221            const projectService = createProjectService(host);
222
223            projectService.openClientFile(commonFile1.path);
224            checkNumberOfConfiguredProjects(projectService, 1);
225            const project = configuredProjectAt(projectService, 0);
226            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
227            projectService.openClientFile(excludedFile1.path);
228            checkNumberOfInferredProjects(projectService, 1);
229        });
230
231        it("should properly handle module resolution changes in config file", () => {
232            const file1: File = {
233                path: "/a/b/file1.ts",
234                content: `import { T } from "module1";`
235            };
236            const nodeModuleFile: File = {
237                path: "/a/b/node_modules/module1.ts",
238                content: `export interface T {}`
239            };
240            const classicModuleFile: File = {
241                path: "/a/module1.ts",
242                content: `export interface T {}`
243            };
244            const randomFile: File = {
245                path: "/a/file1.ts",
246                content: `export interface T {}`
247            };
248            const configFile: File = {
249                path: "/a/b/tsconfig.json",
250                content: `{
251                    "compilerOptions": {
252                        "moduleResolution": "node"
253                    },
254                    "files": ["${file1.path}"]
255                }`
256            };
257            const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile];
258            const host = createServerHost(files);
259            const projectService = createProjectService(host);
260            projectService.openClientFile(file1.path);
261            projectService.openClientFile(nodeModuleFile.path);
262            projectService.openClientFile(classicModuleFile.path);
263
264            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
265            const project = configuredProjectAt(projectService, 0);
266            const inferredProject0 = projectService.inferredProjects[0];
267            checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]);
268            checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]);
269
270            host.writeFile(configFile.path, `{
271                "compilerOptions": {
272                    "moduleResolution": "classic"
273                },
274                "files": ["${file1.path}"]
275            }`);
276            host.checkTimeoutQueueLengthAndRun(2);
277
278            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1
279            checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]);
280            assert.strictEqual(projectService.inferredProjects[0], inferredProject0);
281            assert.isTrue(projectService.inferredProjects[0].isOrphan());
282            const inferredProject1 = projectService.inferredProjects[1];
283            checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]);
284
285            // Open random file and it will reuse first inferred project
286            projectService.openClientFile(randomFile.path);
287            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 });
288            checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]);
289            assert.strictEqual(projectService.inferredProjects[0], inferredProject0);
290            checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project
291            assert.strictEqual(projectService.inferredProjects[1], inferredProject1);
292            checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]);
293        });
294
295        it("should keep the configured project when the opened file is referenced by the project but not its root", () => {
296            const file1: File = {
297                path: "/a/b/main.ts",
298                content: "import { objA } from './obj-a';"
299            };
300            const file2: File = {
301                path: "/a/b/obj-a.ts",
302                content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});`
303            };
304            const configFile: File = {
305                path: "/a/b/tsconfig.json",
306                content: `{
307                    "compilerOptions": {
308                        "target": "es6"
309                    },
310                    "files": [ "main.ts" ]
311                }`
312            };
313            const host = createServerHost([file1, file2, configFile]);
314            const projectService = createProjectService(host);
315            projectService.openClientFile(file1.path);
316            projectService.closeClientFile(file1.path);
317            projectService.openClientFile(file2.path);
318            checkNumberOfConfiguredProjects(projectService, 1);
319            checkNumberOfInferredProjects(projectService, 0);
320        });
321
322        it("should keep the configured project when the opened file is referenced by the project but not its root", () => {
323            const file1: File = {
324                path: "/a/b/main.ts",
325                content: "import { objA } from './obj-a';"
326            };
327            const file2: File = {
328                path: "/a/b/obj-a.ts",
329                content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});`
330            };
331            const configFile: File = {
332                path: "/a/b/tsconfig.json",
333                content: `{
334                    "compilerOptions": {
335                        "target": "es6"
336                    },
337                    "files": [ "main.ts" ]
338                }`
339            };
340            const host = createServerHost([file1, file2, configFile]);
341            const projectService = createProjectService(host);
342            projectService.openClientFile(file1.path);
343            projectService.closeClientFile(file1.path);
344            projectService.openClientFile(file2.path);
345            checkNumberOfConfiguredProjects(projectService, 1);
346            checkNumberOfInferredProjects(projectService, 0);
347        });
348
349        it("should tolerate config file errors and still try to build a project", () => {
350            const configFile: File = {
351                path: "/a/b/tsconfig.json",
352                content: `{
353                    "compilerOptions": {
354                        "target": "es6",
355                        "allowAnything": true
356                    },
357                    "someOtherProperty": {}
358                }`
359            };
360            const host = createServerHost([commonFile1, commonFile2, libFile, configFile]);
361            const projectService = createProjectService(host);
362            projectService.openClientFile(commonFile1.path);
363            checkNumberOfConfiguredProjects(projectService, 1);
364            checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]);
365        });
366
367        it("should reuse same project if file is opened from the configured project that has no open files", () => {
368            const file1 = {
369                path: "/a/b/main.ts",
370                content: "let x =1;"
371            };
372            const file2 = {
373                path: "/a/b/main2.ts",
374                content: "let y =1;"
375            };
376            const configFile: File = {
377                path: "/a/b/tsconfig.json",
378                content: `{
379                    "compilerOptions": {
380                        "target": "es6"
381                    },
382                    "files": [ "main.ts", "main2.ts" ]
383                }`
384            };
385            const host = createServerHost([file1, file2, configFile, libFile]);
386            const projectService = createProjectService(host, { useSingleInferredProject: true });
387            projectService.openClientFile(file1.path);
388            checkNumberOfConfiguredProjects(projectService, 1);
389            const project = projectService.configuredProjects.get(configFile.path)!;
390            assert.isTrue(project.hasOpenRef()); // file1
391
392            projectService.closeClientFile(file1.path);
393            checkNumberOfConfiguredProjects(projectService, 1);
394            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
395            assert.isFalse(project.hasOpenRef()); // No open files
396            assert.isFalse(project.isClosed());
397
398            projectService.openClientFile(file2.path);
399            checkNumberOfConfiguredProjects(projectService, 1);
400            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
401            assert.isTrue(project.hasOpenRef()); // file2
402            assert.isFalse(project.isClosed());
403        });
404
405        it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => {
406            const file1 = {
407                path: "/a/b/main.ts",
408                content: "let x =1;"
409            };
410            const configFile: File = {
411                path: "/a/b/tsconfig.json",
412                content: `{
413                    "compilerOptions": {
414                        "target": "es6"
415                    },
416                    "files": [ "main.ts" ]
417                }`
418            };
419            const host = createServerHost([file1, configFile, libFile]);
420            const projectService = createProjectService(host, { useSingleInferredProject: true });
421            projectService.openClientFile(file1.path);
422            checkNumberOfConfiguredProjects(projectService, 1);
423            const project = projectService.configuredProjects.get(configFile.path)!;
424            assert.isTrue(project.hasOpenRef()); // file1
425
426            projectService.closeClientFile(file1.path);
427            checkNumberOfConfiguredProjects(projectService, 1);
428            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
429            assert.isFalse(project.hasOpenRef()); // No files
430            assert.isFalse(project.isClosed());
431
432            projectService.openClientFile(libFile.path);
433            checkNumberOfConfiguredProjects(projectService, 0);
434            assert.isFalse(project.hasOpenRef()); // No files + project closed
435            assert.isTrue(project.isClosed());
436        });
437
438        it("open file become a part of configured project if it is referenced from root file", () => {
439            const file1 = {
440                path: `${tscWatch.projectRoot}/a/b/f1.ts`,
441                content: "export let x = 5"
442            };
443            const file2 = {
444                path: `${tscWatch.projectRoot}/a/c/f2.ts`,
445                content: `import {x} from "../b/f1"`
446            };
447            const file3 = {
448                path: `${tscWatch.projectRoot}/a/c/f3.ts`,
449                content: "export let y = 1"
450            };
451            const configFile = {
452                path: `${tscWatch.projectRoot}/a/c/tsconfig.json`,
453                content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] })
454            };
455
456            const host = createServerHost([file1, file2, file3]);
457            const projectService = createProjectService(host);
458
459            projectService.openClientFile(file1.path);
460            checkNumberOfProjects(projectService, { inferredProjects: 1 });
461            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]);
462
463            projectService.openClientFile(file3.path);
464            checkNumberOfProjects(projectService, { inferredProjects: 2 });
465            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]);
466            checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
467
468            host.writeFile(configFile.path, configFile.content);
469            host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles
470            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 });
471            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]);
472            assert.isTrue(projectService.inferredProjects[0].isOrphan());
473            assert.isTrue(projectService.inferredProjects[1].isOrphan());
474        });
475
476        it("can correctly update configured project when set of root files has changed (new file on disk)", () => {
477            const file1 = {
478                path: "/a/b/f1.ts",
479                content: "let x = 1"
480            };
481            const file2 = {
482                path: "/a/b/f2.ts",
483                content: "let y = 1"
484            };
485            const configFile = {
486                path: "/a/b/tsconfig.json",
487                content: JSON.stringify({ compilerOptions: {} })
488            };
489
490            const host = createServerHost([file1, configFile]);
491            const projectService = createProjectService(host);
492
493            projectService.openClientFile(file1.path);
494            checkNumberOfProjects(projectService, { configuredProjects: 1 });
495            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]);
496
497            host.writeFile(file2.path, file2.content);
498
499            host.checkTimeoutQueueLengthAndRun(2);
500
501            checkNumberOfProjects(projectService, { configuredProjects: 1 });
502            checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]);
503        });
504
505        it("can correctly update configured project when set of root files has changed (new file in list of files)", () => {
506            const file1 = {
507                path: "/a/b/f1.ts",
508                content: "let x = 1"
509            };
510            const file2 = {
511                path: "/a/b/f2.ts",
512                content: "let y = 1"
513            };
514            const configFile = {
515                path: "/a/b/tsconfig.json",
516                content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] })
517            };
518
519            const host = createServerHost([file1, file2, configFile]);
520            const projectService = createProjectService(host);
521
522            projectService.openClientFile(file1.path);
523            checkNumberOfProjects(projectService, { configuredProjects: 1 });
524            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]);
525
526            host.writeFile(configFile.path, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }));
527
528            checkNumberOfProjects(projectService, { configuredProjects: 1 });
529            host.checkTimeoutQueueLengthAndRun(2);
530            checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]);
531        });
532
533        it("can update configured project when set of root files was not changed", () => {
534            const file1 = {
535                path: "/a/b/f1.ts",
536                content: "let x = 1"
537            };
538            const file2 = {
539                path: "/a/b/f2.ts",
540                content: "let y = 1"
541            };
542            const configFile = {
543                path: "/a/b/tsconfig.json",
544                content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })
545            };
546
547            const host = createServerHost([file1, file2, configFile]);
548            const projectService = createProjectService(host);
549
550            projectService.openClientFile(file1.path);
551            checkNumberOfProjects(projectService, { configuredProjects: 1 });
552            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]);
553
554            host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }));
555
556            checkNumberOfProjects(projectService, { configuredProjects: 1 });
557            checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]);
558        });
559
560        it("Open ref of configured project when open file gets added to the project as part of configured file update", () => {
561            const file1: File = {
562                path: "/a/b/src/file1.ts",
563                content: "let x = 1;"
564            };
565            const file2: File = {
566                path: "/a/b/src/file2.ts",
567                content: "let y = 1;"
568            };
569            const file3: File = {
570                path: "/a/b/file3.ts",
571                content: "let z = 1;"
572            };
573            const file4: File = {
574                path: "/a/file4.ts",
575                content: "let z = 1;"
576            };
577            const configFile = {
578                path: "/a/b/tsconfig.json",
579                content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] })
580            };
581
582            const files = [file1, file2, file3, file4];
583            const host = createServerHost(files.concat(configFile));
584            const projectService = createProjectService(host);
585
586            projectService.openClientFile(file1.path);
587            projectService.openClientFile(file2.path);
588            projectService.openClientFile(file3.path);
589            projectService.openClientFile(file4.path);
590
591            const infos = files.map(file => projectService.getScriptInfoForPath(file.path as Path)!);
592            checkOpenFiles(projectService, files);
593            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 });
594            const configProject1 = projectService.configuredProjects.get(configFile.path)!;
595            assert.isTrue(configProject1.hasOpenRef()); // file1 and file3
596            checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]);
597            const inferredProject1 = projectService.inferredProjects[0];
598            checkProjectActualFiles(inferredProject1, [file2.path]);
599            const inferredProject2 = projectService.inferredProjects[1];
600            checkProjectActualFiles(inferredProject2, [file4.path]);
601
602            host.writeFile(configFile.path, "{}");
603            host.runQueuedTimeoutCallbacks();
604
605            verifyScriptInfos();
606            checkOpenFiles(projectService, files);
607            verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file1, file2, file3
608            assert.isTrue(projectService.inferredProjects[0].isOrphan());
609            const inferredProject3 = projectService.inferredProjects[1];
610            checkProjectActualFiles(inferredProject3, [file4.path]);
611            assert.strictEqual(inferredProject3, inferredProject2);
612
613            projectService.closeClientFile(file1.path);
614            projectService.closeClientFile(file2.path);
615            projectService.closeClientFile(file4.path);
616
617            verifyScriptInfos();
618            checkOpenFiles(projectService, [file3]);
619            verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file3
620            assert.isTrue(projectService.inferredProjects[0].isOrphan());
621            assert.isTrue(projectService.inferredProjects[1].isOrphan());
622
623            projectService.openClientFile(file4.path);
624            verifyScriptInfos();
625            checkOpenFiles(projectService, [file3, file4]);
626            verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3
627            const inferredProject4 = projectService.inferredProjects[0];
628            checkProjectActualFiles(inferredProject4, [file4.path]);
629
630            projectService.closeClientFile(file3.path);
631            verifyScriptInfos();
632            checkOpenFiles(projectService, [file4]);
633            verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files
634            const inferredProject5 = projectService.inferredProjects[0];
635            checkProjectActualFiles(inferredProject4, [file4.path]);
636            assert.strictEqual(inferredProject5, inferredProject4);
637
638            const file5: File = {
639                path: "/file5.ts",
640                content: "let zz = 1;"
641            };
642            host.writeFile(file5.path, file5.content);
643            projectService.openClientFile(file5.path);
644            verifyScriptInfosAreUndefined([file1, file2, file3]);
645            assert.strictEqual(projectService.getScriptInfoForPath(file4.path as Path), find(infos, info => info.path === file4.path));
646            assert.isDefined(projectService.getScriptInfoForPath(file5.path as Path));
647            checkOpenFiles(projectService, [file4, file5]);
648            checkNumberOfProjects(projectService, { inferredProjects: 2 });
649            checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]);
650            checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]);
651
652            function verifyScriptInfos() {
653                infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info));
654            }
655
656            function verifyScriptInfosAreUndefined(files: File[]) {
657                for (const file of files) {
658                    assert.isUndefined(projectService.getScriptInfoForPath(file.path as Path));
659                }
660            }
661
662            function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) {
663                checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects });
664                const configProject2 = projectService.configuredProjects.get(configFile.path)!;
665                assert.strictEqual(configProject2, configProject1);
666                checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]);
667                assert.equal(configProject2.hasOpenRef(), hasOpenRef);
668            }
669        });
670
671        it("Open ref of configured project when open file gets added to the project as part of configured file update buts its open file references are all closed when the update happens", () => {
672            const file1: File = {
673                path: "/a/b/src/file1.ts",
674                content: "let x = 1;"
675            };
676            const file2: File = {
677                path: "/a/b/src/file2.ts",
678                content: "let y = 1;"
679            };
680            const file3: File = {
681                path: "/a/b/file3.ts",
682                content: "let z = 1;"
683            };
684            const file4: File = {
685                path: "/a/file4.ts",
686                content: "let z = 1;"
687            };
688            const configFile = {
689                path: "/a/b/tsconfig.json",
690                content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] })
691            };
692
693            const files = [file1, file2, file3];
694            const hostFiles = files.concat(file4, configFile);
695            const host = createServerHost(hostFiles);
696            const projectService = createProjectService(host);
697
698            projectService.openClientFile(file1.path);
699            projectService.openClientFile(file2.path);
700            projectService.openClientFile(file3.path);
701
702            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 });
703            const configuredProject = projectService.configuredProjects.get(configFile.path)!;
704            assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3
705            checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]);
706            const inferredProject1 = projectService.inferredProjects[0];
707            checkProjectActualFiles(inferredProject1, [file2.path]);
708
709            projectService.closeClientFile(file1.path);
710            projectService.closeClientFile(file3.path);
711            assert.isFalse(configuredProject.hasOpenRef()); // No files
712
713            host.writeFile(configFile.path, "{}");
714            // Time out is not yet run so there is project update pending
715            assert.isTrue(configuredProject.hasOpenRef()); // Pending update and file2 might get into the project
716
717            projectService.openClientFile(file4.path);
718
719            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 });
720            assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject);
721            assert.isTrue(configuredProject.hasOpenRef()); // Pending update and F2 might get into the project
722            assert.strictEqual(projectService.inferredProjects[0], inferredProject1);
723            const inferredProject2 = projectService.inferredProjects[1];
724            checkProjectActualFiles(inferredProject2, [file4.path]);
725
726            host.runQueuedTimeoutCallbacks();
727            checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 });
728            assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject);
729            assert.isTrue(configuredProject.hasOpenRef()); // file2
730            checkProjectActualFiles(configuredProject, [file1.path, file2.path, file3.path, configFile.path]);
731            assert.strictEqual(projectService.inferredProjects[0], inferredProject1);
732            assert.isTrue(inferredProject1.isOrphan());
733            assert.strictEqual(projectService.inferredProjects[1], inferredProject2);
734            checkProjectActualFiles(inferredProject2, [file4.path]);
735        });
736
737        it("files are properly detached when language service is disabled", () => {
738            const f1 = {
739                path: "/a/app.js",
740                content: "var x = 1"
741            };
742            const f2 = {
743                path: "/a/largefile.js",
744                content: ""
745            };
746            const f3 = {
747                path: "/a/lib.js",
748                content: "var x = 1"
749            };
750            const config = {
751                path: "/a/tsconfig.json",
752                content: JSON.stringify({ compilerOptions: { allowJs: true } })
753            };
754            const host = createServerHost([f1, f2, f3, config]);
755            const originalGetFileSize = host.getFileSize;
756            host.getFileSize = (filePath: string) =>
757                filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
758
759            const projectService = createProjectService(host);
760            projectService.openClientFile(f1.path);
761            projectService.checkNumberOfProjects({ configuredProjects: 1 });
762            const project = projectService.configuredProjects.get(config.path)!;
763            assert.isTrue(project.hasOpenRef()); // f1
764            assert.isFalse(project.isClosed());
765
766            projectService.closeClientFile(f1.path);
767            projectService.checkNumberOfProjects({ configuredProjects: 1 });
768            assert.strictEqual(projectService.configuredProjects.get(config.path), project);
769            assert.isFalse(project.hasOpenRef()); // No files
770            assert.isFalse(project.isClosed());
771
772            for (const f of [f1, f2, f3]) {
773                // All the script infos should be present and contain the project since it is still alive.
774                const scriptInfo = projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))!;
775                assert.equal(scriptInfo.containingProjects.length, 1, `expect 1 containing projects for '${f.path}'`);
776                assert.equal(scriptInfo.containingProjects[0], project, `expect configured project to be the only containing project for '${f.path}'`);
777            }
778
779            const f4 = {
780                path: "/aa.js",
781                content: "var x = 1"
782            };
783            host.writeFile(f4.path, f4.content);
784            projectService.openClientFile(f4.path);
785            projectService.checkNumberOfProjects({ inferredProjects: 1 });
786            assert.isFalse(project.hasOpenRef()); // No files
787            assert.isTrue(project.isClosed());
788
789            for (const f of [f1, f2, f3]) {
790                // All the script infos should not be present since the project is closed and orphan script infos are collected
791                assert.isUndefined(projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path)));
792            }
793        });
794
795        it("syntactic features work even if language service is disabled", () => {
796            const f1 = {
797                path: "/a/app.js",
798                content: "let x =   1;"
799            };
800            const f2 = {
801                path: "/a/largefile.js",
802                content: ""
803            };
804            const config = {
805                path: "/a/jsconfig.json",
806                content: "{}"
807            };
808            const host = createServerHost([f1, f2, config]);
809            const originalGetFileSize = host.getFileSize;
810            host.getFileSize = (filePath: string) =>
811                filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
812            const { session, events } = createSessionWithEventTracking<server.ProjectLanguageServiceStateEvent>(host, server.ProjectLanguageServiceStateEvent);
813            session.executeCommand(<protocol.OpenRequest>{
814                seq: 0,
815                type: "request",
816                command: "open",
817                arguments: { file: f1.path }
818            });
819
820            const projectService = session.getProjectService();
821            checkNumberOfProjects(projectService, { configuredProjects: 1 });
822            const project = configuredProjectAt(projectService, 0);
823            assert.isFalse(project.languageServiceEnabled, "Language service enabled");
824            assert.equal(events.length, 1, "should receive event");
825            assert.equal(events[0].data.project, project, "project name");
826            assert.isFalse(events[0].data.languageServiceEnabled, "Language service state");
827
828            const options = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath);
829            const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options);
830            assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]);
831        });
832
833        it("when multiple projects are open, detects correct default project", () => {
834            const barConfig: File = {
835                path: `${tscWatch.projectRoot}/bar/tsconfig.json`,
836                content: JSON.stringify({
837                    include: ["index.ts"],
838                    compilerOptions: {
839                        lib: ["dom", "es2017"]
840                    }
841                })
842            };
843            const barIndex: File = {
844                path: `${tscWatch.projectRoot}/bar/index.ts`,
845                content: `
846export function bar() {
847  console.log("hello world");
848}`
849            };
850            const fooConfig: File = {
851                path: `${tscWatch.projectRoot}/foo/tsconfig.json`,
852                content: JSON.stringify({
853                    include: ["index.ts"],
854                    compilerOptions: {
855                        lib: ["es2017"]
856                    }
857                })
858            };
859            const fooIndex: File = {
860                path: `${tscWatch.projectRoot}/foo/index.ts`,
861                content: `
862import { bar } from "bar";
863bar();`
864            };
865            const barSymLink: SymLink = {
866                path: `${tscWatch.projectRoot}/foo/node_modules/bar`,
867                symLink: `${tscWatch.projectRoot}/bar`
868            };
869
870            const lib2017: File = {
871                path: `${getDirectoryPath(libFile.path)}/lib.es2017.d.ts`,
872                content: libFile.content
873            };
874            const libDom: File = {
875                path: `${getDirectoryPath(libFile.path)}/lib.dom.d.ts`,
876                content: `
877declare var console: {
878    log(...args: any[]): void;
879};`
880            };
881            const host = createServerHost([barConfig, barIndex, fooConfig, fooIndex, barSymLink, lib2017, libDom]);
882            const session = createSession(host, { canUseEvents: true, });
883            openFilesForSession([fooIndex, barIndex], session);
884            verifyGetErrRequestNoErrors({ session, host, files: [barIndex, fooIndex] });
885        });
886
887        it("when file name starts with ^", () => {
888            const file: File = {
889                path: `${tscWatch.projectRoot}/file.ts`,
890                content: "const x = 10;"
891            };
892            const app: File = {
893                path: `${tscWatch.projectRoot}/^app.ts`,
894                content: "const y = 10;"
895            };
896            const tsconfig: File = {
897                path: `${tscWatch.projectRoot}/tsconfig.json`,
898                content: "{}"
899            };
900            const host = createServerHost([file, app, tsconfig, libFile]);
901            const service = createProjectService(host);
902            service.openClientFile(file.path);
903        });
904
905        describe("when creating new file", () => {
906            const foo: File = {
907                path: `${tscWatch.projectRoot}/src/foo.ts`,
908                content: "export function foo() { }"
909            };
910            const bar: File = {
911                path: `${tscWatch.projectRoot}/src/bar.ts`,
912                content: "export function bar() { }"
913            };
914            const config: File = {
915                path: `${tscWatch.projectRoot}/tsconfig.json`,
916                content: JSON.stringify({
917                    include: ["./src"]
918                })
919            };
920            const fooBar: File = {
921                path: `${tscWatch.projectRoot}/src/sub/fooBar.ts`,
922                content: "export function fooBar() { }"
923            };
924            function verifySessionWorker({ withExclude, openFileBeforeCreating, checkProjectBeforeError, checkProjectAfterError, }: VerifySession, errorOnNewFileBeforeOldFile: boolean) {
925                const host = createServerHost([
926                    foo, bar, libFile, { path: `${tscWatch.projectRoot}/src/sub` },
927                    withExclude ?
928                        {
929                            path: config.path,
930                            content: JSON.stringify({
931                                include: ["./src"],
932                                exclude: ["./src/sub"]
933                            })
934                        } :
935                        config
936                ]);
937                const session = createSession(host, {
938                    canUseEvents: true
939                });
940                session.executeCommandSeq<protocol.OpenRequest>({
941                    command: protocol.CommandTypes.Open,
942                    arguments: {
943                        file: foo.path,
944                        fileContent: foo.content,
945                        projectRootPath: tscWatch.projectRoot
946                    }
947                });
948                if (!openFileBeforeCreating) {
949                    host.writeFile(fooBar.path, fooBar.content);
950                }
951                session.executeCommandSeq<protocol.OpenRequest>({
952                    command: protocol.CommandTypes.Open,
953                    arguments: {
954                        file: fooBar.path,
955                        fileContent: fooBar.content,
956                        projectRootPath: tscWatch.projectRoot
957                    }
958                });
959                if (openFileBeforeCreating) {
960                    host.writeFile(fooBar.path, fooBar.content);
961                }
962                const service = session.getProjectService();
963                checkProjectBeforeError(service);
964                verifyGetErrRequestNoErrors({
965                    session,
966                    host,
967                    files: errorOnNewFileBeforeOldFile ?
968                        [fooBar, foo] :
969                        [foo, fooBar],
970                    existingTimeouts: withExclude ? 0 : 2
971                });
972                checkProjectAfterError(service);
973            }
974            interface VerifySession {
975                withExclude?: boolean;
976                openFileBeforeCreating: boolean;
977                checkProjectBeforeError: (service: server.ProjectService) => void;
978                checkProjectAfterError: (service: server.ProjectService) => void;
979            }
980            function verifySession(input: VerifySession) {
981                it("when error on new file are asked before old one", () => {
982                    verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ true);
983                });
984
985                it("when error on new file are asked after old one", () => {
986                    verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ false);
987                });
988            }
989            function checkFooBarInInferredProject(service: server.ProjectService) {
990                checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 });
991                checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, libFile.path, config.path]);
992                checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]);
993            }
994            function checkFooBarInConfiguredProject(service: server.ProjectService) {
995                checkNumberOfProjects(service, { configuredProjects: 1 });
996                checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]);
997            }
998            describe("when new file creation directory watcher is invoked before file is opened in editor", () => {
999                verifySession({
1000                    openFileBeforeCreating: false,
1001                    checkProjectBeforeError: checkFooBarInConfiguredProject,
1002                    checkProjectAfterError: checkFooBarInConfiguredProject
1003                });
1004                describe("when new file is excluded from config", () => {
1005                    verifySession({
1006                        withExclude: true,
1007                        openFileBeforeCreating: false,
1008                        checkProjectBeforeError: checkFooBarInInferredProject,
1009                        checkProjectAfterError: checkFooBarInInferredProject
1010                    });
1011                });
1012            });
1013
1014            describe("when new file creation directory watcher is invoked after file is opened in editor", () => {
1015                verifySession({
1016                    openFileBeforeCreating: true,
1017                    checkProjectBeforeError: checkFooBarInInferredProject,
1018                    checkProjectAfterError: service => {
1019                        // Both projects exist but fooBar is in configured project after the update
1020                        // Inferred project is yet to be updated so still has fooBar
1021                        checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 });
1022                        checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]);
1023                        checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]);
1024                        assert.isTrue(service.inferredProjects[0].dirty);
1025                        assert.equal(service.inferredProjects[0].getRootFilesMap().size, 0);
1026                    }
1027                });
1028                describe("when new file is excluded from config", () => {
1029                    verifySession({
1030                        withExclude: true,
1031                        openFileBeforeCreating: true,
1032                        checkProjectBeforeError: checkFooBarInInferredProject,
1033                        checkProjectAfterError: checkFooBarInInferredProject
1034                    });
1035                });
1036            });
1037        });
1038
1039        it("when default configured project does not contain the file", () => {
1040            const barConfig: File = {
1041                path: `${tscWatch.projectRoot}/bar/tsconfig.json`,
1042                content: "{}"
1043            };
1044            const barIndex: File = {
1045                path: `${tscWatch.projectRoot}/bar/index.ts`,
1046                content: `import {foo} from "../foo/lib";
1047foo();`
1048            };
1049            const fooBarConfig: File = {
1050                path: `${tscWatch.projectRoot}/foobar/tsconfig.json`,
1051                content: barConfig.path
1052            };
1053            const fooBarIndex: File = {
1054                path: `${tscWatch.projectRoot}/foobar/index.ts`,
1055                content: barIndex.content
1056            };
1057            const fooConfig: File = {
1058                path: `${tscWatch.projectRoot}/foo/tsconfig.json`,
1059                content: JSON.stringify({
1060                    include: ["index.ts"],
1061                    compilerOptions: {
1062                        declaration: true,
1063                        outDir: "lib"
1064                    }
1065                })
1066            };
1067            const fooIndex: File = {
1068                path: `${tscWatch.projectRoot}/foo/index.ts`,
1069                content: `export function foo() {}`
1070            };
1071            const host = createServerHost([barConfig, barIndex, fooBarConfig, fooBarIndex, fooConfig, fooIndex, libFile]);
1072            tscWatch.ensureErrorFreeBuild(host, [fooConfig.path]);
1073            const fooDts = `${tscWatch.projectRoot}/foo/lib/index.d.ts`;
1074            assert.isTrue(host.fileExists(fooDts));
1075            const session = createSession(host);
1076            const service = session.getProjectService();
1077            service.openClientFile(barIndex.path);
1078            checkProjectActualFiles(service.configuredProjects.get(barConfig.path)!, [barIndex.path, fooDts, libFile.path, barConfig.path]);
1079            service.openClientFile(fooBarIndex.path);
1080            checkProjectActualFiles(service.configuredProjects.get(fooBarConfig.path)!, [fooBarIndex.path, fooDts, libFile.path, fooBarConfig.path]);
1081            service.openClientFile(fooIndex.path);
1082            checkProjectActualFiles(service.configuredProjects.get(fooConfig.path)!, [fooIndex.path, libFile.path, fooConfig.path]);
1083            service.openClientFile(fooDts);
1084            session.executeCommandSeq<protocol.GetApplicableRefactorsRequest>({
1085                command: protocol.CommandTypes.GetApplicableRefactors,
1086                arguments: {
1087                    file: fooDts,
1088                    startLine: 1,
1089                    startOffset: 1,
1090                    endLine: 1,
1091                    endOffset: 1
1092                }
1093            });
1094            assert.equal(service.tryGetDefaultProjectForFile(server.toNormalizedPath(fooDts)), service.configuredProjects.get(barConfig.path));
1095        });
1096
1097        describe("watches extended config files", () => {
1098            function getService(additionalFiles?: File[]) {
1099                const alphaExtendedConfig: File = {
1100                    path: `${tscWatch.projectRoot}/extended/alpha.tsconfig.json`,
1101                    content: "{}"
1102                };
1103                const bravoExtendedConfig: File = {
1104                    path: `${tscWatch.projectRoot}/extended/bravo.tsconfig.json`,
1105                    content: JSON.stringify({
1106                        extends: "./alpha.tsconfig.json"
1107                    })
1108                };
1109                const aConfig: File = {
1110                    path: `${tscWatch.projectRoot}/a/tsconfig.json`,
1111                    content: JSON.stringify({
1112                        extends: "../extended/alpha.tsconfig.json",
1113                        files: ["a.ts"]
1114                    })
1115                };
1116                const aFile: File = {
1117                    path: `${tscWatch.projectRoot}/a/a.ts`,
1118                    content: `let a = 1;`
1119                };
1120                const bConfig: File = {
1121                    path: `${tscWatch.projectRoot}/b/tsconfig.json`,
1122                    content: JSON.stringify({
1123                        extends: "../extended/bravo.tsconfig.json",
1124                        files: ["b.ts"]
1125                    })
1126                };
1127                const bFile: File = {
1128                    path: `${tscWatch.projectRoot}/b/b.ts`,
1129                    content: `let b = 1;`
1130                };
1131
1132                const host = createServerHost([alphaExtendedConfig, aConfig, aFile, bravoExtendedConfig, bConfig, bFile, ...(additionalFiles || emptyArray)]);
1133                const projectService = createProjectService(host);
1134                return { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig };
1135            }
1136
1137            it("should watch the extended configs of multiple projects", () => {
1138                const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService();
1139
1140                projectService.openClientFile(aFile.path);
1141                projectService.openClientFile(bFile.path);
1142                checkNumberOfConfiguredProjects(projectService, 2);
1143                const aProject = projectService.configuredProjects.get(aConfig.path)!;
1144                const bProject = projectService.configuredProjects.get(bConfig.path)!;
1145                checkProjectActualFiles(aProject, [aFile.path, aConfig.path, alphaExtendedConfig.path]);
1146                checkProjectActualFiles(bProject, [bFile.path, bConfig.path, bravoExtendedConfig.path, alphaExtendedConfig.path]);
1147                assert.isUndefined(aProject.getCompilerOptions().strict);
1148                assert.isUndefined(bProject.getCompilerOptions().strict);
1149                checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]);
1150
1151                host.writeFile(alphaExtendedConfig.path, JSON.stringify({
1152                    compilerOptions: {
1153                        strict: true
1154                    }
1155                }));
1156                assert.isTrue(projectService.hasPendingProjectUpdate(aProject));
1157                assert.isTrue(projectService.hasPendingProjectUpdate(bProject));
1158                host.checkTimeoutQueueLengthAndRun(3);
1159                assert.isTrue(aProject.getCompilerOptions().strict);
1160                assert.isTrue(bProject.getCompilerOptions().strict);
1161                checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]);
1162
1163                host.writeFile(bravoExtendedConfig.path, JSON.stringify({
1164                    extends: "./alpha.tsconfig.json",
1165                    compilerOptions: {
1166                        strict: false
1167                    }
1168                }));
1169                assert.isFalse(projectService.hasPendingProjectUpdate(aProject));
1170                assert.isTrue(projectService.hasPendingProjectUpdate(bProject));
1171                host.checkTimeoutQueueLengthAndRun(2);
1172                assert.isTrue(aProject.getCompilerOptions().strict);
1173                assert.isFalse(bProject.getCompilerOptions().strict);
1174                checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]);
1175
1176                host.writeFile(bConfig.path, JSON.stringify({
1177                    extends: "../extended/alpha.tsconfig.json",
1178                }));
1179                assert.isFalse(projectService.hasPendingProjectUpdate(aProject));
1180                assert.isTrue(projectService.hasPendingProjectUpdate(bProject));
1181                host.checkTimeoutQueueLengthAndRun(2);
1182                assert.isTrue(aProject.getCompilerOptions().strict);
1183                assert.isTrue(bProject.getCompilerOptions().strict);
1184                checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, alphaExtendedConfig.path]);
1185
1186                host.writeFile(alphaExtendedConfig.path, "{}");
1187                assert.isTrue(projectService.hasPendingProjectUpdate(aProject));
1188                assert.isTrue(projectService.hasPendingProjectUpdate(bProject));
1189                host.checkTimeoutQueueLengthAndRun(3);
1190                assert.isUndefined(aProject.getCompilerOptions().strict);
1191                assert.isUndefined(bProject.getCompilerOptions().strict);
1192                checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, alphaExtendedConfig.path]);
1193            });
1194
1195            it("should stop watching the extended configs of closed projects", () => {
1196                const dummy: File = {
1197                    path: `${tscWatch.projectRoot}/dummy/dummy.ts`,
1198                    content: `let dummy = 1;`
1199                };
1200                const dummyConfig: File = {
1201                    path: `${tscWatch.projectRoot}/dummy/tsconfig.json`,
1202                    content: "{}"
1203                };
1204                const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService([dummy, dummyConfig]);
1205
1206                projectService.openClientFile(aFile.path);
1207                projectService.openClientFile(bFile.path);
1208                projectService.openClientFile(dummy.path);
1209                checkNumberOfConfiguredProjects(projectService, 3);
1210                checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path, dummyConfig.path]);
1211
1212                projectService.closeClientFile(bFile.path);
1213                projectService.closeClientFile(dummy.path);
1214                projectService.openClientFile(dummy.path);
1215
1216                checkNumberOfConfiguredProjects(projectService, 2);
1217                checkWatchedFiles(host, [aConfig.path, libFile.path, alphaExtendedConfig.path, dummyConfig.path]);
1218
1219                projectService.closeClientFile(aFile.path);
1220                projectService.closeClientFile(dummy.path);
1221                projectService.openClientFile(dummy.path);
1222
1223                checkNumberOfConfiguredProjects(projectService, 1);
1224                checkWatchedFiles(host, [libFile.path, dummyConfig.path]);
1225            });
1226        });
1227    });
1228
1229    describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => {
1230        it("should be tolerated without crashing the server", () => {
1231            const configFile = {
1232                path: "/a/b/tsconfig.json",
1233                content: `{
1234                    "compilerOptions": {},
1235                    "include": ["app/*", "test/**/*", "something"]
1236                }`
1237            };
1238            const file1 = {
1239                path: "/a/b/file1.ts",
1240                content: "let t = 10;"
1241            };
1242
1243            const host = createServerHost([file1, configFile]);
1244            const projectService = createProjectService(host);
1245            projectService.openClientFile(file1.path);
1246            host.runQueuedTimeoutCallbacks();
1247
1248            // Since file1 refers to config file as the default project, it needs to be kept alive
1249            checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
1250            const inferredProject = projectService.inferredProjects[0];
1251            assert.isTrue(inferredProject.containsFile(<server.NormalizedPath>file1.path));
1252            assert.isFalse(projectService.configuredProjects.get(configFile.path)!.containsFile(<server.NormalizedPath>file1.path));
1253        });
1254
1255        it("should be able to handle @types if input file list is empty", () => {
1256            const f = {
1257                path: "/a/app.ts",
1258                content: "let x = 1"
1259            };
1260            const config = {
1261                path: "/a/tsconfig.json",
1262                content: JSON.stringify({
1263                    compiler: {},
1264                    files: []
1265                })
1266            };
1267            const t1 = {
1268                path: "/a/node_modules/@types/typings/index.d.ts",
1269                content: `export * from "./lib"`
1270            };
1271            const t2 = {
1272                path: "/a/node_modules/@types/typings/lib.d.ts",
1273                content: `export const x: number`
1274            };
1275            const host = createServerHost([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) });
1276            const projectService = createProjectService(host);
1277
1278            projectService.openClientFile(f.path);
1279            // Since f refers to config file as the default project, it needs to be kept alive
1280            projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
1281        });
1282
1283        it("should tolerate invalid include files that start in subDirectory", () => {
1284            const f = {
1285                path: `${tscWatch.projectRoot}/src/server/index.ts`,
1286                content: "let x = 1"
1287            };
1288            const config = {
1289                path: `${tscWatch.projectRoot}/src/server/tsconfig.json`,
1290                content: JSON.stringify({
1291                    compiler: {
1292                        module: "commonjs",
1293                        outDir: "../../build"
1294                    },
1295                    include: [
1296                        "../src/**/*.ts"
1297                    ]
1298                })
1299            };
1300            const host = createServerHost([f, config, libFile], { useCaseSensitiveFileNames: true });
1301            const projectService = createProjectService(host);
1302
1303            projectService.openClientFile(f.path);
1304            // Since f refers to config file as the default project, it needs to be kept alive
1305            projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
1306        });
1307
1308        it("Changed module resolution reflected when specifying files list", () => {
1309            const file1: File = {
1310                path: "/a/b/file1.ts",
1311                content: 'import classc from "file2"'
1312            };
1313            const file2a: File = {
1314                path: "/a/file2.ts",
1315                content: "export classc { method2a() { return 10; } }"
1316            };
1317            const file2: File = {
1318                path: "/a/b/file2.ts",
1319                content: "export classc { method2() { return 10; } }"
1320            };
1321            const configFile: File = {
1322                path: "/a/b/tsconfig.json",
1323                content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } })
1324            };
1325            const files = [file1, file2a, configFile, libFile];
1326            const host = createServerHost(files);
1327            const projectService = createProjectService(host);
1328            projectService.openClientFile(file1.path);
1329            checkNumberOfProjects(projectService, { configuredProjects: 1 });
1330            const project = projectService.configuredProjects.get(configFile.path)!;
1331            assert.isDefined(project);
1332            checkProjectActualFiles(project, map(files, file => file.path));
1333            checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path));
1334            checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false);
1335            checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true);
1336
1337            files.push(file2);
1338            host.writeFile(file2.path, file2.content);
1339            host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions
1340            host.runQueuedTimeoutCallbacks(); // Actual update
1341            checkNumberOfProjects(projectService, { configuredProjects: 1 });
1342            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
1343            checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path));
1344            checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path));
1345            checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
1346            checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true);
1347
1348            // On next file open the files file2a should be closed and not watched any more
1349            projectService.openClientFile(file2.path);
1350            checkNumberOfProjects(projectService, { configuredProjects: 1 });
1351            assert.strictEqual(projectService.configuredProjects.get(configFile.path), project);
1352            checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path));
1353            checkWatchedFiles(host, [libFile.path, configFile.path]);
1354            checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
1355            checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true);
1356        });
1357
1358        it("Failed lookup locations uses parent most node_modules directory", () => {
1359            const root = "/user/username/rootfolder";
1360            const file1: File = {
1361                path: "/a/b/src/file1.ts",
1362                content: 'import { classc } from "module1"'
1363            };
1364            const module1: File = {
1365                path: "/a/b/node_modules/module1/index.d.ts",
1366                content: `import { class2 } from "module2";
1367                          export classc { method2a(): class2; }`
1368            };
1369            const module2: File = {
1370                path: "/a/b/node_modules/module2/index.d.ts",
1371                content: "export class2 { method2() { return 10; } }"
1372            };
1373            const module3: File = {
1374                path: "/a/b/node_modules/module/node_modules/module3/index.d.ts",
1375                content: "export class3 { method2() { return 10; } }"
1376            };
1377            const configFile: File = {
1378                path: "/a/b/src/tsconfig.json",
1379                content: JSON.stringify({ files: ["file1.ts"] })
1380            };
1381            const nonLibFiles = [file1, module1, module2, module3, configFile];
1382            nonLibFiles.forEach(f => f.path = root + f.path);
1383            const files = nonLibFiles.concat(libFile);
1384            const host = createServerHost(files);
1385            const projectService = createProjectService(host);
1386            projectService.openClientFile(file1.path);
1387            checkNumberOfProjects(projectService, { configuredProjects: 1 });
1388            const project = projectService.configuredProjects.get(configFile.path)!;
1389            assert.isDefined(project);
1390            checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]);
1391            checkWatchedFiles(host, [libFile.path, configFile.path]);
1392            checkWatchedDirectories(host, [], /*recursive*/ false);
1393            const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src");
1394            watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`);
1395            checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true);
1396        });
1397    });
1398
1399    describe("unittests:: tsserver:: ConfiguredProjects:: when reading tsconfig file fails", () => {
1400        it("should be tolerated without crashing the server", () => {
1401            const configFile = {
1402                path: `${tscWatch.projectRoot}/tsconfig.json`,
1403                content: ""
1404            };
1405            const file1 = {
1406                path: `${tscWatch.projectRoot}/file1.ts`,
1407                content: "let t = 10;"
1408            };
1409
1410            const host = createServerHost([file1, libFile, configFile]);
1411            const { session, events } = createSessionWithEventTracking<server.ConfigFileDiagEvent>(host, server.ConfigFileDiagEvent);
1412            const originalReadFile = host.readFile;
1413            host.readFile = f => {
1414                return f === configFile.path ?
1415                    undefined :
1416                    originalReadFile.call(host, f);
1417            };
1418            openFilesForSession([file1], session);
1419
1420            assert.deepEqual(events, [{
1421                eventName: server.ConfigFileDiagEvent,
1422                data: {
1423                    triggerFile: file1.path,
1424                    configFileName: configFile.path,
1425                    diagnostics: [
1426                        createCompilerDiagnostic(Diagnostics.Cannot_read_file_0, configFile.path)
1427                    ]
1428                }
1429            }]);
1430        });
1431    });
1432}
1433