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