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