• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.projectSystem {
2    const angularFormsDts: File = {
3        path: "/node_modules/@angular/forms/forms.d.ts",
4        content: "export declare class PatternValidator {}",
5    };
6    const angularFormsPackageJson: File = {
7        path: "/node_modules/@angular/forms/package.json",
8        content: `{ "name": "@angular/forms", "typings": "./forms.d.ts" }`,
9    };
10    const angularCoreDts: File = {
11        path: "/node_modules/@angular/core/core.d.ts",
12        content: "",
13    };
14    const angularCorePackageJson: File = {
15        path: "/node_modules/@angular/core/package.json",
16        content: `{ "name": "@angular/core", "typings": "./core.d.ts" }`,
17    };
18    const tsconfig: File = {
19        path: "/tsconfig.json",
20        content: `{ "compilerOptions": { "module": "commonjs" } }`,
21    };
22    const packageJson: File = {
23        path: "/package.json",
24        content: `{ "dependencies": { "@angular/forms": "*", "@angular/core": "*" } }`
25    };
26    const indexTs: File = {
27        path: "/index.ts",
28        content: ""
29    };
30
31    describe("unittests:: tsserver:: autoImportProvider", () => {
32        it("Auto import provider program is not created without dependencies listed in package.json", () => {
33            const { projectService, session } = setup([
34                angularFormsDts,
35                angularFormsPackageJson,
36                tsconfig,
37                { path: packageJson.path, content: `{ "dependencies": {} }` },
38                indexTs
39            ]);
40            openFilesForSession([indexTs], session);
41            assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider());
42        });
43
44        it("Auto import provider program is not created if dependencies are already in main program", () => {
45            const { projectService, session } = setup([
46                angularFormsDts,
47                angularFormsPackageJson,
48                tsconfig,
49                packageJson,
50                { path: indexTs.path, content: "import '@angular/forms';" }
51            ]);
52            openFilesForSession([indexTs], session);
53            assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider());
54        });
55
56        it("Auto-import program is not created for projects already inside node_modules", () => {
57            // Simulate browsing typings files inside node_modules: no point creating auto import program
58            // for the InferredProject that gets created in there.
59            const { projectService, session } = setup([
60                angularFormsDts,
61                { path: angularFormsPackageJson.path, content: `{ "dependencies": { "@angular/core": "*" } }` },
62                { path: "/node_modules/@angular/core/package.json", content: `{ "typings": "./core.d.ts" }` },
63                { path: "/node_modules/@angular/core/core.d.ts", content: `export namespace angular {};` },
64            ]);
65
66            openFilesForSession([angularFormsDts], session);
67            checkNumberOfInferredProjects(projectService, 1);
68            checkNumberOfConfiguredProjects(projectService, 0);
69            assert.isUndefined(projectService
70                .getDefaultProjectForFile(angularFormsDts.path as server.NormalizedPath, /*ensureProject*/ true)!
71                .getLanguageService()
72                .getAutoImportProvider());
73        });
74
75        it("Auto-importable file is in inferred project until imported", () => {
76            const { projectService, session, updateFile } = setup([angularFormsDts, angularFormsPackageJson, tsconfig, packageJson, indexTs]);
77            checkNumberOfInferredProjects(projectService, 0);
78            openFilesForSession([angularFormsDts], session);
79            checkNumberOfInferredProjects(projectService, 1);
80            assert.equal(
81                projectService.getDefaultProjectForFile(angularFormsDts.path as server.NormalizedPath, /*ensureProject*/ true)?.projectKind,
82                server.ProjectKind.Inferred);
83
84            updateFile(indexTs.path, "import '@angular/forms'");
85            assert.equal(
86                projectService.getDefaultProjectForFile(angularFormsDts.path as server.NormalizedPath, /*ensureProject*/ true)?.projectKind,
87                server.ProjectKind.Configured);
88
89            assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider());
90        });
91
92        it("Responds to package.json changes", () => {
93            const { projectService, session, host } = setup([
94                angularFormsDts,
95                angularFormsPackageJson,
96                tsconfig,
97                { path: "/package.json", content: "{}" },
98                indexTs
99            ]);
100
101            openFilesForSession([indexTs], session);
102            assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider());
103
104            host.writeFile(packageJson.path, packageJson.content);
105            assert.ok(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider());
106        });
107
108        it("Reuses autoImportProvider when program structure is unchanged", () => {
109            const { projectService, session, updateFile } = setup([
110                angularFormsDts,
111                angularFormsPackageJson,
112                tsconfig,
113                packageJson,
114                indexTs
115            ]);
116
117            openFilesForSession([indexTs], session);
118            const autoImportProvider = projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider();
119            assert.ok(autoImportProvider);
120
121            updateFile(indexTs.path, "console.log(0)");
122            assert.strictEqual(
123                projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider(),
124                autoImportProvider);
125        });
126
127        it("Closes AutoImportProviderProject when host project closes", () => {
128            const { projectService, session } = setup([
129                angularFormsDts,
130                angularFormsPackageJson,
131                tsconfig,
132                packageJson,
133                indexTs
134            ]);
135
136            openFilesForSession([indexTs], session);
137            const hostProject = projectService.configuredProjects.get(tsconfig.path)!;
138            hostProject.getPackageJsonAutoImportProvider();
139            const autoImportProviderProject = hostProject.autoImportProviderHost;
140            assert.ok(autoImportProviderProject);
141
142            hostProject.close();
143            assert.ok(autoImportProviderProject && autoImportProviderProject.isClosed());
144            assert.isUndefined(hostProject.autoImportProviderHost);
145        });
146
147        it("Does not schedule ensureProjectForOpenFiles on AutoImportProviderProject creation", () => {
148            const { projectService, session, host } = setup([
149                angularFormsDts,
150                angularFormsPackageJson,
151                tsconfig,
152                indexTs
153            ]);
154
155            // Create configured project only, ensure !projectService.pendingEnsureProjectForOpenFiles
156            openFilesForSession([indexTs], session);
157            const hostProject = projectService.configuredProjects.get(tsconfig.path)!;
158            projectService.delayEnsureProjectForOpenFiles();
159            host.runQueuedTimeoutCallbacks();
160            assert.isFalse(projectService.pendingEnsureProjectForOpenFiles);
161
162            // Create auto import provider project, ensure still !projectService.pendingEnsureProjectForOpenFiles
163            host.writeFile(packageJson.path, packageJson.content);
164            hostProject.getPackageJsonAutoImportProvider();
165            assert.isFalse(projectService.pendingEnsureProjectForOpenFiles);
166        });
167
168        it("Responds to automatic changes in node_modules", () => {
169            const { projectService, session, host } = setup([
170                angularFormsDts,
171                angularFormsPackageJson,
172                angularCoreDts,
173                angularCorePackageJson,
174                tsconfig,
175                packageJson,
176                indexTs
177            ]);
178
179            openFilesForSession([indexTs], session);
180            const project = projectService.configuredProjects.get(tsconfig.path)!;
181            const completionsBefore = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true });
182            assert.isTrue(completionsBefore?.entries.some(c => c.name === "PatternValidator"));
183
184            // Directory watchers only fire for add/remove, not change.
185            // This is ok since a real `npm install` will always trigger add/remove events.
186            host.deleteFile(angularFormsDts.path);
187            host.writeFile(angularFormsDts.path, "");
188
189            const autoImportProvider = project.getLanguageService().getAutoImportProvider();
190            const completionsAfter = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true });
191            assert.equal(autoImportProvider!.getSourceFile(angularFormsDts.path)!.getText(), "");
192            assert.isFalse(completionsAfter?.entries.some(c => c.name === "PatternValidator"));
193        });
194
195        it("Responds to manual changes in node_modules", () => {
196            const { projectService, session, updateFile } = setup([
197                angularFormsDts,
198                angularFormsPackageJson,
199                angularCoreDts,
200                angularCorePackageJson,
201                tsconfig,
202                packageJson,
203                indexTs
204            ]);
205
206            openFilesForSession([indexTs, angularFormsDts], session);
207            const project = projectService.configuredProjects.get(tsconfig.path)!;
208            const completionsBefore = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true });
209            assert.isTrue(completionsBefore?.entries.some(c => c.name === "PatternValidator"));
210
211            updateFile(angularFormsDts.path, "export class ValidatorPattern {}");
212            const completionsAfter = project.getLanguageService().getCompletionsAtPosition(indexTs.path, 0, { includeCompletionsForModuleExports: true });
213            assert.isFalse(completionsAfter?.entries.some(c => c.name === "PatternValidator"));
214            assert.isTrue(completionsAfter?.entries.some(c => c.name === "ValidatorPattern"));
215        });
216
217        it("Recovers from an unparseable package.json", () => {
218            const { projectService, session, host } = setup([
219                angularFormsDts,
220                angularFormsPackageJson,
221                tsconfig,
222                { path: packageJson.path, content: "{" },
223                indexTs
224            ]);
225
226            openFilesForSession([indexTs], session);
227            assert.isUndefined(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider());
228
229            host.writeFile(packageJson.path, packageJson.content);
230            assert.ok(projectService.configuredProjects.get(tsconfig.path)!.getLanguageService().getAutoImportProvider());
231        });
232
233        it("Does not create an auto import provider if there are too many dependencies", () => {
234            const createPackage = (i: number): File[] => ([
235                { path: `/node_modules/package${i}/package.json`, content: `{ "name": "package${i}" }` },
236                { path: `/node_modules/package${i}/index.d.ts`, content: `` }
237            ]);
238
239            const packages = [];
240            for (let i = 0; i < 11; i++) {
241                packages.push(createPackage(i));
242            }
243
244            const dependencies = packages.reduce((hash, p) => ({ ...hash, [JSON.parse(p[0].content).name]: "*" }), {});
245            const packageJson: File = { path: "/package.json", content: JSON.stringify(dependencies) };
246            const { projectService, session } = setup([ ...flatten(packages), indexTs, tsconfig, packageJson ]);
247
248            openFilesForSession([indexTs], session);
249            const project = projectService.configuredProjects.get(tsconfig.path)!;
250            assert.isUndefined(project.getPackageJsonAutoImportProvider());
251        });
252    });
253
254    describe("unittests:: tsserver:: autoImportProvider - monorepo", () => {
255        it("Does not create auto import providers upon opening projects for find-all-references", () => {
256            const files = [
257                // node_modules
258                angularFormsDts,
259                angularFormsPackageJson,
260
261                // root
262                { path: tsconfig.path, content: `{ "references": [{ "path": "packages/a" }, { "path": "packages/b" }] }` },
263                { path: packageJson.path, content: `{ "private": true }` },
264
265                // packages/a
266                { path: "/packages/a/package.json", content: packageJson.content },
267                { path: "/packages/a/tsconfig.json", content: `{ "compilerOptions": { "composite": true }, "references": [{ "path": "../b" }] }` },
268                { path: "/packages/a/index.ts", content: "import { B } from '../b';" },
269
270                // packages/b
271                { path: "/packages/b/package.json", content: packageJson.content },
272                { path: "/packages/b/tsconfig.json", content: `{ "compilerOptions": { "composite": true } }` },
273                { path: "/packages/b/index.ts", content: `export class B {}` }
274            ];
275
276            const { projectService, session, findAllReferences } = setup(files);
277
278            openFilesForSession([files.find(f => f.path === "/packages/b/index.ts")!], session);
279            checkNumberOfConfiguredProjects(projectService, 2); // Solution (no files), B
280            findAllReferences("/packages/b/index.ts", 1, "export class B".length - 1);
281            checkNumberOfConfiguredProjects(projectService, 3); // Solution (no files), A, B
282
283            // Project for A is created - ensure it doesn't have an autoImportProvider
284            assert.isUndefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getLanguageService().getAutoImportProvider());
285        });
286
287        it("Does not close when root files are redirects that don't actually exist", () => {
288            const files = [
289                // packages/a
290                { path: "/packages/a/package.json", content: `{ "dependencies": { "b": "*" } }` },
291                { path: "/packages/a/tsconfig.json", content: `{ "compilerOptions": { "composite": true }, "references": [{ "path": "./node_modules/b" }] }` },
292                { path: "/packages/a/index.ts", content: "" },
293
294                // packages/b
295                { path: "/packages/a/node_modules/b/package.json", content: `{ "types": "dist/index.d.ts" }` },
296                { path: "/packages/a/node_modules/b/tsconfig.json", content: `{ "compilerOptions": { "composite": true, "outDir": "dist" } }` },
297                { path: "/packages/a/node_modules/b/index.ts", content: `export class B {}` }
298            ];
299
300            const { projectService, session } = setup(files);
301            openFilesForSession([files[2]], session);
302            assert.isDefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getPackageJsonAutoImportProvider());
303            assert.isDefined(projectService.configuredProjects.get("/packages/a/tsconfig.json")!.getPackageJsonAutoImportProvider());
304        });
305
306        it("Can use the same document registry bucket key as main program", () => {
307            for (const option of sourceFileAffectingCompilerOptions) {
308                assert(
309                    !hasProperty(server.AutoImportProviderProject.compilerOptionsOverrides, option.name),
310                    `'${option.name}' may cause AutoImportProviderProject not to share source files with main program`
311                );
312            }
313        });
314    });
315
316    function setup(files: File[]) {
317        const host = createServerHost(files);
318        const session = createSession(host);
319        const projectService = session.getProjectService();
320        return {
321            host,
322            projectService,
323            session,
324            updateFile,
325            findAllReferences
326        };
327
328        function updateFile(path: string, newText: string) {
329            Debug.assertIsDefined(files.find(f => f.path === path));
330            session.executeCommandSeq<protocol.ApplyChangedToOpenFilesRequest>({
331                command: protocol.CommandTypes.ApplyChangedToOpenFiles,
332                arguments: {
333                    openFiles: [{
334                        fileName: path,
335                        content: newText
336                    }]
337                }
338            });
339        }
340
341        function findAllReferences(file: string, line: number, offset: number) {
342            Debug.assertIsDefined(files.find(f => f.path === file));
343            session.executeCommandSeq<protocol.ReferencesRequest>({
344                command: protocol.CommandTypes.References,
345                arguments: {
346                    file,
347                    line,
348                    offset
349                }
350            });
351        }
352    }
353}
354