• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.projectSystem {
2    describe("unittests:: tsserver:: Inferred projects", () => {
3        it("create inferred project", () => {
4            const appFile: File = {
5                path: `${tscWatch.projectRoot}/app.ts`,
6                content: `
7                import {f} from "./module"
8                console.log(f)
9                `
10            };
11
12            const moduleFile: File = {
13                path: `${tscWatch.projectRoot}/module.d.ts`,
14                content: `export let x: number`
15            };
16            const host = createServerHost([appFile, moduleFile, libFile]);
17            const projectService = createProjectService(host);
18            const { configFileName } = projectService.openClientFile(appFile.path);
19
20            assert(!configFileName, `should not find config, got: '${configFileName}`);
21            checkNumberOfConfiguredProjects(projectService, 0);
22            checkNumberOfInferredProjects(projectService, 1);
23
24            const project = projectService.inferredProjects[0];
25
26            checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]);
27            checkWatchedFiles(host, getConfigFilesToWatch(tscWatch.projectRoot).concat(libFile.path, moduleFile.path));
28            checkWatchedDirectories(host, [tscWatch.projectRoot], /*recursive*/ false);
29            checkWatchedDirectories(host, [combinePaths(tscWatch.projectRoot, nodeModulesAtTypes)], /*recursive*/ true);
30        });
31
32        it("should use only one inferred project if 'useOneInferredProject' is set", () => {
33            const file1 = {
34                path: `${tscWatch.projectRoot}/a/b/main.ts`,
35                content: "let x =1;"
36            };
37            const configFile: File = {
38                path: `${tscWatch.projectRoot}/a/b/tsconfig.json`,
39                content: `{
40                    "compilerOptions": {
41                        "target": "es6"
42                    },
43                    "files": [ "main.ts" ]
44                }`
45            };
46            const file2 = {
47                path: `${tscWatch.projectRoot}/a/c/main.ts`,
48                content: "let x =1;"
49            };
50
51            const file3 = {
52                path: `${tscWatch.projectRoot}/a/d/main.ts`,
53                content: "let x =1;"
54            };
55
56            const host = createServerHost([file1, file2, file3, libFile]);
57            const projectService = createProjectService(host, { useSingleInferredProject: true });
58            projectService.openClientFile(file1.path);
59            projectService.openClientFile(file2.path);
60            projectService.openClientFile(file3.path);
61
62            checkNumberOfConfiguredProjects(projectService, 0);
63            checkNumberOfInferredProjects(projectService, 1);
64            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]);
65
66
67            host.writeFile(configFile.path, configFile.content);
68            host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles
69            checkNumberOfConfiguredProjects(projectService, 1);
70            checkNumberOfInferredProjects(projectService, 1);
71            checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]);
72        });
73
74        it("disable inferred project", () => {
75            const file1 = {
76                path: "/a/b/f1.ts",
77                content: "let x =1;"
78            };
79
80            const host = createServerHost([file1]);
81            const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true });
82
83            projectService.openClientFile(file1.path, file1.content);
84
85            checkNumberOfProjects(projectService, { inferredProjects: 1 });
86            const proj = projectService.inferredProjects[0];
87            assert.isDefined(proj);
88
89            assert.isFalse(proj.languageServiceEnabled);
90        });
91
92        it("project settings for inferred projects", () => {
93            const file1 = {
94                path: "/a/b/app.ts",
95                content: `import {x} from "mod"`
96            };
97            const modFile = {
98                path: "/a/mod.ts",
99                content: "export let x: number"
100            };
101            const host = createServerHost([file1, modFile]);
102            const projectService = createProjectService(host);
103
104            projectService.openClientFile(file1.path);
105            projectService.openClientFile(modFile.path);
106
107            checkNumberOfProjects(projectService, { inferredProjects: 2 });
108            const inferredProjects = projectService.inferredProjects.slice();
109            checkProjectActualFiles(inferredProjects[0], [file1.path]);
110            checkProjectActualFiles(inferredProjects[1], [modFile.path]);
111
112            projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic });
113            host.checkTimeoutQueueLengthAndRun(3);
114            checkNumberOfProjects(projectService, { inferredProjects: 2 });
115            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
116            assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]);
117            checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]);
118            assert.isTrue(inferredProjects[1].isOrphan());
119        });
120
121        it("should support files without extensions", () => {
122            const f = {
123                path: "/a/compile",
124                content: "let x = 1"
125            };
126            const host = createServerHost([f]);
127            const session = createSession(host);
128            session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
129                seq: 1,
130                type: "request",
131                command: "compilerOptionsForInferredProjects",
132                arguments: {
133                    options: {
134                        allowJs: true
135                    }
136                }
137            });
138            session.executeCommand(<server.protocol.OpenRequest>{
139                seq: 2,
140                type: "request",
141                command: "open",
142                arguments: {
143                    file: f.path,
144                    fileContent: f.content,
145                    scriptKindName: "JS"
146                }
147            });
148            const projectService = session.getProjectService();
149            checkNumberOfProjects(projectService, { inferredProjects: 1 });
150            checkProjectActualFiles(projectService.inferredProjects[0], [f.path]);
151        });
152
153        it("inferred projects per project root", () => {
154            const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" };
155            const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" };
156            const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" };
157            const file4 = { path: "/c/file3.ts", content: "let z = 4;" };
158            const host = createServerHost([file1, file2, file3, file4]);
159            const session = createSession(host, {
160                useSingleInferredProject: true,
161                useInferredProjectPerProjectRoot: true
162            });
163            session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
164                seq: 1,
165                type: "request",
166                command: CommandNames.CompilerOptionsForInferredProjects,
167                arguments: {
168                    options: {
169                        allowJs: true,
170                        target: ScriptTarget.ESNext
171                    }
172                }
173            });
174            session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
175                seq: 2,
176                type: "request",
177                command: CommandNames.CompilerOptionsForInferredProjects,
178                arguments: {
179                    options: {
180                        allowJs: true,
181                        target: ScriptTarget.ES2015
182                    },
183                    projectRootPath: "/b"
184                }
185            });
186            session.executeCommand(<server.protocol.OpenRequest>{
187                seq: 3,
188                type: "request",
189                command: CommandNames.Open,
190                arguments: {
191                    file: file1.path,
192                    fileContent: file1.content,
193                    scriptKindName: "JS",
194                    projectRootPath: file1.projectRootPath
195                }
196            });
197            session.executeCommand(<server.protocol.OpenRequest>{
198                seq: 4,
199                type: "request",
200                command: CommandNames.Open,
201                arguments: {
202                    file: file2.path,
203                    fileContent: file2.content,
204                    scriptKindName: "JS",
205                    projectRootPath: file2.projectRootPath
206                }
207            });
208            session.executeCommand(<server.protocol.OpenRequest>{
209                seq: 5,
210                type: "request",
211                command: CommandNames.Open,
212                arguments: {
213                    file: file3.path,
214                    fileContent: file3.content,
215                    scriptKindName: "JS",
216                    projectRootPath: file3.projectRootPath
217                }
218            });
219            session.executeCommand(<server.protocol.OpenRequest>{
220                seq: 6,
221                type: "request",
222                command: CommandNames.Open,
223                arguments: {
224                    file: file4.path,
225                    fileContent: file4.content,
226                    scriptKindName: "JS"
227                }
228            });
229
230            const projectService = session.getProjectService();
231            checkNumberOfProjects(projectService, { inferredProjects: 3 });
232            checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]);
233            checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]);
234            checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]);
235            assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext);
236            assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext);
237            assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015);
238        });
239
240        function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) {
241            checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path));
242            assert.equal(inferredProject.getCompilationSettings().target, target);
243        }
244
245        function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) {
246            const files: [File, File, File, File] = [
247                { path: "/a/file1.ts", content: "let x = 1;" },
248                { path: "/A/file2.ts", content: "let y = 2;" },
249                { path: "/b/file2.ts", content: "let x = 3;" },
250                { path: "/c/file3.ts", content: "let z = 4;" }
251            ];
252            const host = createServerHost(files, { useCaseSensitiveFileNames });
253            const projectService = createProjectService(host, { useSingleInferredProject: true, useInferredProjectPerProjectRoot: true });
254            projectService.setCompilerOptionsForInferredProjects({
255                allowJs: true,
256                target: ScriptTarget.ESNext
257            });
258            projectService.setCompilerOptionsForInferredProjects({
259                allowJs: true,
260                target: ScriptTarget.ES2015
261            }, "/a");
262
263            openClientFiles(["/a", "/a", "/b", undefined]);
264            verifyInferredProjectsState([
265                [[files[3]], ScriptTarget.ESNext],
266                [[files[0], files[1]], ScriptTarget.ES2015],
267                [[files[2]], ScriptTarget.ESNext]
268            ]);
269            closeClientFiles();
270
271            openClientFiles(["/a", "/A", "/b", undefined]);
272            if (useCaseSensitiveFileNames) {
273                verifyInferredProjectsState([
274                    [[files[3]], ScriptTarget.ESNext],
275                    [[files[0]], ScriptTarget.ES2015],
276                    [[files[1]], ScriptTarget.ESNext],
277                    [[files[2]], ScriptTarget.ESNext]
278                ]);
279            }
280            else {
281                verifyInferredProjectsState([
282                    [[files[3]], ScriptTarget.ESNext],
283                    [[files[0], files[1]], ScriptTarget.ES2015],
284                    [[files[2]], ScriptTarget.ESNext]
285                ]);
286            }
287            closeClientFiles();
288
289            projectService.setCompilerOptionsForInferredProjects({
290                allowJs: true,
291                target: ScriptTarget.ES2017
292            }, "/A");
293
294            openClientFiles(["/a", "/a", "/b", undefined]);
295            verifyInferredProjectsState([
296                [[files[3]], ScriptTarget.ESNext],
297                [[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017],
298                [[files[2]], ScriptTarget.ESNext]
299            ]);
300            closeClientFiles();
301
302            openClientFiles(["/a", "/A", "/b", undefined]);
303            if (useCaseSensitiveFileNames) {
304                verifyInferredProjectsState([
305                    [[files[3]], ScriptTarget.ESNext],
306                    [[files[0]], ScriptTarget.ES2015],
307                    [[files[1]], ScriptTarget.ES2017],
308                    [[files[2]], ScriptTarget.ESNext]
309                ]);
310            }
311            else {
312                verifyInferredProjectsState([
313                    [[files[3]], ScriptTarget.ESNext],
314                    [[files[0], files[1]], ScriptTarget.ES2017],
315                    [[files[2]], ScriptTarget.ESNext]
316                ]);
317            }
318            closeClientFiles();
319
320            function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) {
321                files.forEach((file, index) => {
322                    projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]);
323                });
324            }
325
326            function closeClientFiles() {
327                files.forEach(file => projectService.closeClientFile(file.path));
328            }
329
330            function verifyInferredProjectsState(expected: [File[], ScriptTarget][]) {
331                checkNumberOfProjects(projectService, { inferredProjects: expected.length });
332                projectService.inferredProjects.forEach((p, index) => {
333                    const [actualFiles, target] = expected[index];
334                    checkInferredProject(p, actualFiles, target);
335                });
336            }
337        }
338
339        it("inferred projects per project root with case sensitive system", () => {
340            verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true);
341        });
342
343        it("inferred projects per project root with case insensitive system", () => {
344            verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false);
345        });
346
347        it("should still retain configured project created while opening the file", () => {
348            const appFile: File = {
349                path: `${tscWatch.projectRoot}/app.ts`,
350                content: `const app = 20;`
351            };
352            const config: File = {
353                path: `${tscWatch.projectRoot}/tsconfig.json`,
354                content: "{}"
355            };
356            const jsFile1: File = {
357                path: `${tscWatch.projectRoot}/jsFile1.js`,
358                content: `const jsFile1 = 10;`
359            };
360            const jsFile2: File = {
361                path: `${tscWatch.projectRoot}/jsFile2.js`,
362                content: `const jsFile2 = 10;`
363            };
364            const host = createServerHost([appFile, libFile, config, jsFile1, jsFile2]);
365            const projectService = createProjectService(host);
366            const originalSet = projectService.configuredProjects.set;
367            const originalDelete = projectService.configuredProjects.delete;
368            const configuredCreated = new Map<string, true>();
369            const configuredRemoved = new Map<string, true>();
370            projectService.configuredProjects.set = (key, value) => {
371                assert.isFalse(configuredCreated.has(key));
372                configuredCreated.set(key, true);
373                return originalSet.call(projectService.configuredProjects, key, value);
374            };
375            projectService.configuredProjects.delete = key => {
376                assert.isFalse(configuredRemoved.has(key));
377                configuredRemoved.set(key, true);
378                return originalDelete.call(projectService.configuredProjects, key);
379            };
380
381            // Do not remove config project when opening jsFile that is not present as part of config project
382            projectService.openClientFile(jsFile1.path);
383            checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
384            checkProjectActualFiles(projectService.inferredProjects[0], [jsFile1.path, libFile.path]);
385            const project = projectService.configuredProjects.get(config.path)!;
386            checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]);
387            checkConfiguredProjectCreatedAndNotDeleted();
388
389            // Do not remove config project when opening jsFile that is not present as part of config project
390            projectService.closeClientFile(jsFile1.path);
391            checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
392            projectService.openClientFile(jsFile2.path);
393            checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
394            checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]);
395            checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]);
396            checkConfiguredProjectNotCreatedAndNotDeleted();
397
398            // Do not remove config project when opening jsFile that is not present as part of config project
399            projectService.openClientFile(jsFile1.path);
400            checkNumberOfProjects(projectService, { inferredProjects: 2, configuredProjects: 1 });
401            checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]);
402            checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]);
403            checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]);
404            checkConfiguredProjectNotCreatedAndNotDeleted();
405
406            // When opening file that doesnt fall back to the config file, we remove the config project
407            projectService.openClientFile(libFile.path);
408            checkNumberOfProjects(projectService, { inferredProjects: 2 });
409            checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]);
410            checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]);
411            checkConfiguredProjectNotCreatedButDeleted();
412
413            function checkConfiguredProjectCreatedAndNotDeleted() {
414                assert.equal(configuredCreated.size, 1);
415                assert.isTrue(configuredCreated.has(config.path));
416                assert.equal(configuredRemoved.size, 0);
417                configuredCreated.clear();
418            }
419
420            function checkConfiguredProjectNotCreatedAndNotDeleted() {
421                assert.equal(configuredCreated.size, 0);
422                assert.equal(configuredRemoved.size, 0);
423            }
424
425            function checkConfiguredProjectNotCreatedButDeleted() {
426                assert.equal(configuredCreated.size, 0);
427                assert.equal(configuredRemoved.size, 1);
428                assert.isTrue(configuredRemoved.has(config.path));
429                configuredRemoved.clear();
430            }
431        });
432
433        it("regression test - should infer typeAcquisition for inferred projects when set undefined", () => {
434            const file1 = { path: "/a/file1.js", content: "" };
435            const host = createServerHost([file1]);
436
437            const projectService = createProjectService(host);
438
439            projectService.openClientFile(file1.path);
440
441            checkNumberOfProjects(projectService, { inferredProjects: 1 });
442            const inferredProject = projectService.inferredProjects[0];
443            checkProjectActualFiles(inferredProject, [file1.path]);
444            inferredProject.setTypeAcquisition(undefined);
445
446            const expected = {
447                enable: true,
448                include: [],
449                exclude: []
450            };
451            assert.deepEqual(inferredProject.getTypeAcquisition(), expected, "typeAcquisition should be inferred for inferred projects");
452        });
453
454        it("Setting compiler options for inferred projects when there are no open files should not schedule any refresh", () => {
455            const host = createServerHost([commonFile1, libFile]);
456            const projectService = createProjectService(host);
457            projectService.setCompilerOptionsForInferredProjects({
458                allowJs: true,
459                target: ScriptTarget.ES2015
460            });
461            host.checkTimeoutQueueLength(0);
462        });
463    });
464}
465