1namespace ts.projectSystem { 2 import CommandNames = server.CommandNames; 3 function createTestTypingsInstaller(host: server.ServerHost) { 4 return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); 5 } 6 7 describe("unittests:: tsserver:: compileOnSave:: affected list", () => { 8 function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: File[] }[]) { 9 const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; 10 const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); 11 expectedFileList = expectedFileList.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); 12 13 assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`); 14 15 for (let i = 0; i < actualResult.length; i++) { 16 const actualResultSingleProject = actualResult[i]; 17 const expectedResultSingleProject = expectedFileList[i]; 18 assert.equal(actualResultSingleProject.projectFileName, expectedResultSingleProject.projectFileName, `Actual result contains different projects than the expected result`); 19 20 const actualResultSingleProjectFileNameList = actualResultSingleProject.fileNames.sort(); 21 const expectedResultSingleProjectFileNameList = map(expectedResultSingleProject.files, f => f.path).sort(); 22 assert.isTrue( 23 arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList), 24 `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`); 25 } 26 } 27 28 describe("for configured projects", () => { 29 let moduleFile1: File; 30 let file1Consumer1: File; 31 let file1Consumer2: File; 32 let moduleFile2: File; 33 let globalFile3: File; 34 let configFile: File; 35 let changeModuleFile1ShapeRequest1: server.protocol.Request; 36 let changeModuleFile1InternalRequest1: server.protocol.Request; 37 // A compile on save affected file request using file1 38 let moduleFile1FileListRequest: server.protocol.Request; 39 40 beforeEach(() => { 41 moduleFile1 = { 42 path: "/a/b/moduleFile1.ts", 43 content: "export function Foo() { };" 44 }; 45 46 file1Consumer1 = { 47 path: "/a/b/file1Consumer1.ts", 48 content: `import {Foo} from "./moduleFile1"; export var y = 10;` 49 }; 50 51 file1Consumer2 = { 52 path: "/a/b/file1Consumer2.ts", 53 content: `import {Foo} from "./moduleFile1"; let z = 10;` 54 }; 55 56 moduleFile2 = { 57 path: "/a/b/moduleFile2.ts", 58 content: `export var Foo4 = 10;` 59 }; 60 61 globalFile3 = { 62 path: "/a/b/globalFile3.ts", 63 content: `interface GlobalFoo { age: number }` 64 }; 65 66 configFile = { 67 path: "/a/b/tsconfig.json", 68 content: `{ 69 "compileOnSave": true 70 }` 71 }; 72 73 // Change the content of file1 to `export var T: number;export function Foo() { };` 74 changeModuleFile1ShapeRequest1 = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 75 file: moduleFile1.path, 76 line: 1, 77 offset: 1, 78 endLine: 1, 79 endOffset: 1, 80 insertString: `export var T: number;` 81 }); 82 83 // Change the content of file1 to `export var T: number;export function Foo() { };` 84 changeModuleFile1InternalRequest1 = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 85 file: moduleFile1.path, 86 line: 1, 87 offset: 1, 88 endLine: 1, 89 endOffset: 1, 90 insertString: `var T1: number;` 91 }); 92 93 moduleFile1FileListRequest = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path, projectFileName: configFile.path }); 94 }); 95 96 it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { 97 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); 98 const typingsInstaller = createTestTypingsInstaller(host); 99 const session = createSession(host, { typingsInstaller }); 100 101 openFilesForSession([moduleFile1, file1Consumer1], session); 102 103 // Send an initial compileOnSave request 104 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 105 session.executeCommand(changeModuleFile1ShapeRequest1); 106 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 107 108 // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };` 109 const changeFile1InternalRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 110 file: moduleFile1.path, 111 line: 1, 112 offset: 46, 113 endLine: 1, 114 endOffset: 46, 115 insertString: `console.log('hi');` 116 }); 117 session.executeCommand(changeFile1InternalRequest); 118 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); 119 }); 120 121 it("should be up-to-date with the reference map changes", () => { 122 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); 123 const typingsInstaller = createTestTypingsInstaller(host); 124 const session = createSession(host, { typingsInstaller }); 125 126 openFilesForSession([moduleFile1, file1Consumer1], session); 127 128 // Send an initial compileOnSave request 129 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 130 131 // Change file2 content to `let y = Foo();` 132 const removeFile1Consumer1ImportRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 133 file: file1Consumer1.path, 134 line: 1, 135 offset: 1, 136 endLine: 1, 137 endOffset: 28, 138 insertString: "" 139 }); 140 session.executeCommand(removeFile1Consumer1ImportRequest); 141 session.executeCommand(changeModuleFile1ShapeRequest1); 142 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); 143 144 // Add the import statements back to file2 145 const addFile2ImportRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 146 file: file1Consumer1.path, 147 line: 1, 148 offset: 1, 149 endLine: 1, 150 endOffset: 1, 151 insertString: `import {Foo} from "./moduleFile1";` 152 }); 153 session.executeCommand(addFile2ImportRequest); 154 155 // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };` 156 const changeModuleFile1ShapeRequest2 = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 157 file: moduleFile1.path, 158 line: 1, 159 offset: 1, 160 endLine: 1, 161 endOffset: 1, 162 insertString: `export var T2: string;` 163 }); 164 session.executeCommand(changeModuleFile1ShapeRequest2); 165 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 166 }); 167 168 it("should be up-to-date with changes made in non-open files", () => { 169 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); 170 const typingsInstaller = createTestTypingsInstaller(host); 171 const session = createSession(host, { typingsInstaller }); 172 173 openFilesForSession([moduleFile1], session); 174 175 // Send an initial compileOnSave request 176 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 177 178 host.writeFile(file1Consumer1.path, `let y = 10;`); 179 180 session.executeCommand(changeModuleFile1ShapeRequest1); 181 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); 182 }); 183 184 it("should be up-to-date with deleted files", () => { 185 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); 186 const typingsInstaller = createTestTypingsInstaller(host); 187 const session = createSession(host, { typingsInstaller }); 188 189 openFilesForSession([moduleFile1], session); 190 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 191 192 session.executeCommand(changeModuleFile1ShapeRequest1); 193 // Delete file1Consumer2 194 host.deleteFile(file1Consumer2.path); 195 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); 196 }); 197 198 it("should be up-to-date with newly created files", () => { 199 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); 200 const typingsInstaller = createTestTypingsInstaller(host); 201 const session = createSession(host, { typingsInstaller }); 202 203 openFilesForSession([moduleFile1], session); 204 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 205 206 const file1Consumer3: File = { 207 path: "/a/b/file1Consumer3.ts", 208 content: `import {Foo} from "./moduleFile1"; let y = Foo();` 209 }; 210 host.writeFile(file1Consumer3.path, file1Consumer3.content); 211 host.runQueuedTimeoutCallbacks(); 212 session.executeCommand(changeModuleFile1ShapeRequest1); 213 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]); 214 }); 215 216 it("should detect changes in non-root files", () => { 217 moduleFile1 = { 218 path: "/a/b/moduleFile1.ts", 219 content: "export function Foo() { };" 220 }; 221 222 file1Consumer1 = { 223 path: "/a/b/file1Consumer1.ts", 224 content: `import {Foo} from "./moduleFile1"; let y = Foo();` 225 }; 226 227 configFile = { 228 path: "/a/b/tsconfig.json", 229 content: `{ 230 "compileOnSave": true, 231 "files": ["${file1Consumer1.path}"] 232 }` 233 }; 234 235 const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); 236 const typingsInstaller = createTestTypingsInstaller(host); 237 const session = createSession(host, { typingsInstaller }); 238 239 openFilesForSession([moduleFile1, file1Consumer1], session); 240 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); 241 242 // change file1 shape now, and verify both files are affected 243 session.executeCommand(changeModuleFile1ShapeRequest1); 244 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); 245 246 // change file1 internal, and verify only file1 is affected 247 session.executeCommand(changeModuleFile1InternalRequest1); 248 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); 249 }); 250 251 it("should return all files if a global file changed shape", () => { 252 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); 253 const typingsInstaller = createTestTypingsInstaller(host); 254 const session = createSession(host, { typingsInstaller }); 255 256 openFilesForSession([globalFile3], session); 257 const changeGlobalFile3ShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 258 file: globalFile3.path, 259 line: 1, 260 offset: 1, 261 endLine: 1, 262 endOffset: 1, 263 insertString: `var T2: string;` 264 }); 265 266 // check after file1 shape changes 267 session.executeCommand(changeGlobalFile3ShapeRequest); 268 const globalFile3FileListRequest = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path }); 269 sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }]); 270 }); 271 272 it("should return empty array if CompileOnSave is not enabled", () => { 273 configFile = { 274 path: "/a/b/tsconfig.json", 275 content: `{}` 276 }; 277 278 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); 279 const typingsInstaller = createTestTypingsInstaller(host); 280 const session = createSession(host, { typingsInstaller }); 281 openFilesForSession([moduleFile1], session); 282 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); 283 }); 284 285 it("should return empty array if noEmit is set", () => { 286 configFile = { 287 path: "/a/b/tsconfig.json", 288 content: `{ 289 "compileOnSave": true, 290 "compilerOptions": { 291 "noEmit": true 292 } 293 }` 294 }; 295 296 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); 297 const typingsInstaller = createTestTypingsInstaller(host); 298 const session = createSession(host, { typingsInstaller }); 299 openFilesForSession([moduleFile1], session); 300 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); 301 }); 302 303 it("should save when compileOnSave is enabled in base tsconfig.json", () => { 304 configFile = { 305 path: "/a/b/tsconfig.json", 306 content: `{ 307 "extends": "/a/tsconfig.json" 308 }` 309 }; 310 311 const configFile2: File = { 312 path: "/a/tsconfig.json", 313 content: `{ 314 "compileOnSave": true 315 }` 316 }; 317 318 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, libFile]); 319 const typingsInstaller = createTestTypingsInstaller(host); 320 const session = createSession(host, { typingsInstaller }); 321 322 openFilesForSession([moduleFile1, file1Consumer1], session); 323 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); 324 }); 325 326 it("should always return the file itself if '--isolatedModules' is specified", () => { 327 configFile = { 328 path: "/a/b/tsconfig.json", 329 content: `{ 330 "compileOnSave": true, 331 "compilerOptions": { 332 "isolatedModules": true 333 } 334 }` 335 }; 336 337 const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); 338 const typingsInstaller = createTestTypingsInstaller(host); 339 const session = createSession(host, { typingsInstaller }); 340 openFilesForSession([moduleFile1], session); 341 342 const file1ChangeShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 343 file: moduleFile1.path, 344 line: 1, 345 offset: 27, 346 endLine: 1, 347 endOffset: 27, 348 insertString: `Point,` 349 }); 350 session.executeCommand(file1ChangeShapeRequest); 351 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); 352 }); 353 354 it("should always return the file itself if '--out' or '--outFile' is specified", () => { 355 configFile = { 356 path: "/a/b/tsconfig.json", 357 content: `{ 358 "compileOnSave": true, 359 "compilerOptions": { 360 "module": "system", 361 "outFile": "/a/b/out.js" 362 } 363 }` 364 }; 365 366 const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); 367 const typingsInstaller = createTestTypingsInstaller(host); 368 const session = createSession(host, { typingsInstaller }); 369 openFilesForSession([moduleFile1], session); 370 371 const file1ChangeShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 372 file: moduleFile1.path, 373 line: 1, 374 offset: 27, 375 endLine: 1, 376 endOffset: 27, 377 insertString: `Point,` 378 }); 379 session.executeCommand(file1ChangeShapeRequest); 380 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); 381 }); 382 383 it("should return cascaded affected file list", () => { 384 const file1Consumer1Consumer1: File = { 385 path: "/a/b/file1Consumer1Consumer1.ts", 386 content: `import {y} from "./file1Consumer1";` 387 }; 388 const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]); 389 const typingsInstaller = createTestTypingsInstaller(host); 390 const session = createSession(host, { typingsInstaller }); 391 392 openFilesForSession([moduleFile1, file1Consumer1], session); 393 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); 394 395 const changeFile1Consumer1ShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(CommandNames.Change, { 396 file: file1Consumer1.path, 397 line: 2, 398 offset: 1, 399 endLine: 2, 400 endOffset: 1, 401 insertString: `export var T: number;` 402 }); 403 session.executeCommand(changeModuleFile1ShapeRequest1); 404 session.executeCommand(changeFile1Consumer1ShapeRequest); 405 sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); 406 }); 407 408 it("should work fine for files with circular references", () => { 409 const file1: File = { 410 path: "/a/b/file1.ts", 411 content: ` 412 /// <reference path="./file2.ts" /> 413 export var t1 = 10;` 414 }; 415 const file2: File = { 416 path: "/a/b/file2.ts", 417 content: ` 418 /// <reference path="./file1.ts" /> 419 export var t2 = 10;` 420 }; 421 const host = createServerHost([file1, file2, configFile]); 422 const typingsInstaller = createTestTypingsInstaller(host); 423 const session = createSession(host, { typingsInstaller }); 424 425 openFilesForSession([file1, file2], session); 426 const file1AffectedListRequest = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); 427 sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); 428 }); 429 430 it("should return results for all projects if not specifying projectFileName", () => { 431 const file1: File = { path: "/a/b/file1.ts", content: "export var t = 10;" }; 432 const file2: File = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; 433 const file3: File = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; 434 const configFile1: File = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; 435 const configFile2: File = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; 436 437 const host = createServerHost([file1, file2, file3, configFile1, configFile2]); 438 const session = createSession(host); 439 440 openFilesForSession([file1, file2, file3], session); 441 const file1AffectedListRequest = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); 442 443 sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ 444 { projectFileName: configFile1.path, files: [file1, file2] }, 445 { projectFileName: configFile2.path, files: [file1, file3] } 446 ]); 447 }); 448 449 it("should detect removed code file", () => { 450 const referenceFile1: File = { 451 path: "/a/b/referenceFile1.ts", 452 content: ` 453 /// <reference path="./moduleFile1.ts" /> 454 export var x = Foo();` 455 }; 456 const host = createServerHost([moduleFile1, referenceFile1, configFile]); 457 const session = createSession(host); 458 459 openFilesForSession([referenceFile1], session); 460 host.deleteFile(moduleFile1.path); 461 462 const request = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); 463 sendAffectedFileRequestAndCheckResult(session, request, [ 464 { projectFileName: configFile.path, files: [referenceFile1] } 465 ]); 466 const requestForMissingFile = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); 467 sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); 468 }); 469 470 it("should detect non-existing code file", () => { 471 const referenceFile1: File = { 472 path: "/a/b/referenceFile1.ts", 473 content: ` 474 /// <reference path="./moduleFile2.ts" /> 475 export var x = Foo();` 476 }; 477 const host = createServerHost([referenceFile1, configFile]); 478 const session = createSession(host); 479 480 openFilesForSession([referenceFile1], session); 481 const request = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); 482 sendAffectedFileRequestAndCheckResult(session, request, [ 483 { projectFileName: configFile.path, files: [referenceFile1] } 484 ]); 485 }); 486 }); 487 488 describe("for changes in declaration files", () => { 489 function testDTS(dtsFileContents: string, tsFileContents: string, opts: CompilerOptions, expectDTSEmit: boolean) { 490 const dtsFile = { 491 path: "/a/runtime/a.d.ts", 492 content: dtsFileContents 493 }; 494 const f2 = { 495 path: "/a/b.ts", 496 content: tsFileContents 497 }; 498 const config = { 499 path: "/a/tsconfig.json", 500 content: JSON.stringify({ 501 compilerOptions: opts, 502 compileOnSave: true 503 }) 504 }; 505 const host = createServerHost([dtsFile, f2, config]); 506 const session = createSession(host); 507 session.executeCommand({ 508 seq: 1, 509 type: "request", 510 command: "open", 511 arguments: { file: dtsFile.path } 512 } as protocol.OpenRequest); 513 const projectService = session.getProjectService(); 514 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 515 const project = projectService.configuredProjects.get(config.path)!; 516 checkProjectRootFiles(project, [dtsFile.path, f2.path]); 517 session.executeCommand({ 518 seq: 2, 519 type: "request", 520 command: "open", 521 arguments: { file: f2.path } 522 } as protocol.OpenRequest); 523 checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); 524 const { response } = session.executeCommand({ 525 seq: 3, 526 type: "request", 527 command: "compileOnSaveAffectedFileList", 528 arguments: { file: dtsFile.path } 529 } as protocol.CompileOnSaveAffectedFileListRequest); 530 if (expectDTSEmit) { 531 assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); 532 assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected to affect 2 files"); 533 } 534 else { 535 assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 0, "expected no output"); 536 } 537 538 539 const { response: response2 } = session.executeCommand({ 540 seq: 4, 541 type: "request", 542 command: "compileOnSaveAffectedFileList", 543 arguments: { file: f2.path } 544 } as protocol.CompileOnSaveAffectedFileListRequest); 545 assert.equal((response2 as protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); 546 } 547 548 it("should return empty array if change is made in a global declaration file", () => { 549 testDTS( 550 /*dtsFileContents*/ "declare const x: string;", 551 /*tsFileContents*/ "var y = 1;", 552 /*opts*/ {}, 553 /*expectDTSEmit*/ false 554 ); 555 }); 556 557 it("should return empty array if change is made in a module declaration file", () => { 558 testDTS( 559 /*dtsFileContents*/ "export const x: string;", 560 /*tsFileContents*/ "import { x } from './runtime/a;", 561 /*opts*/ {}, 562 /*expectDTSEmit*/ false 563 ); 564 }); 565 566 it("should return results if change is made in a global declaration file with declaration emit", () => { 567 testDTS( 568 /*dtsFileContents*/ "declare const x: string;", 569 /*tsFileContents*/ "var y = 1;", 570 /*opts*/ { declaration: true }, 571 /*expectDTSEmit*/ true 572 ); 573 }); 574 575 it("should return results if change is made in a global declaration file with composite enabled", () => { 576 testDTS( 577 /*dtsFileContents*/ "declare const x: string;", 578 /*tsFileContents*/ "var y = 1;", 579 /*opts*/ { composite: true }, 580 /*expectDTSEmit*/ true 581 ); 582 }); 583 584 it("should return results if change is made in a global declaration file with decorator emit enabled", () => { 585 testDTS( 586 /*dtsFileContents*/ "declare const x: string;", 587 /*tsFileContents*/ "var y = 1;", 588 /*opts*/ { experimentalDecorators: true, emitDecoratorMetadata: true }, 589 /*expectDTSEmit*/ true 590 ); 591 }); 592 }); 593 594 describe("tsserverProjectSystem emit with outFile or out setting", () => { 595 function test(opts: CompilerOptions, expectedUsesOutFile: boolean) { 596 const f1 = { 597 path: "/a/a.ts", 598 content: "let x = 1" 599 }; 600 const f2 = { 601 path: "/a/b.ts", 602 content: "let y = 1" 603 }; 604 const config = { 605 path: "/a/tsconfig.json", 606 content: JSON.stringify({ 607 compilerOptions: opts, 608 compileOnSave: true 609 }) 610 }; 611 const host = createServerHost([f1, f2, config]); 612 const session = createSession(host); 613 session.executeCommand({ 614 seq: 1, 615 type: "request", 616 command: "open", 617 arguments: { file: f1.path } 618 } as protocol.OpenRequest); 619 checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); 620 const { response } = session.executeCommand({ 621 seq: 2, 622 type: "request", 623 command: "compileOnSaveAffectedFileList", 624 arguments: { file: f1.path } 625 } as protocol.CompileOnSaveAffectedFileListRequest); 626 assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output for 1 project"); 627 assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected output for 1 project"); 628 assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[])[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); 629 } 630 631 it("projectUsesOutFile should not be returned if not set", () => { 632 test({}, /*expectedUsesOutFile*/ false); 633 }); 634 it("projectUsesOutFile should be true if outFile is set", () => { 635 test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true); 636 }); 637 it("projectUsesOutFile should be true if out is set", () => { 638 test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true); 639 }); 640 }); 641 }); 642 643 describe("unittests:: tsserver:: compileOnSave:: EmitFile test", () => { 644 it("should respect line endings", () => { 645 test("\n"); 646 test("\r\n"); 647 648 function test(newLine: string) { 649 const lines = ["var x = 1;", "var y = 2;"]; 650 const path = "/a/app"; 651 const f = { 652 path: path + Extension.Ts, 653 content: lines.join(newLine) 654 }; 655 const host = createServerHost([f], { newLine }); 656 const session = createSession(host); 657 const openRequest: server.protocol.OpenRequest = { 658 seq: 1, 659 type: "request", 660 command: server.protocol.CommandTypes.Open, 661 arguments: { file: f.path } 662 }; 663 session.executeCommand(openRequest); 664 const emitFileRequest: server.protocol.CompileOnSaveEmitFileRequest = { 665 seq: 2, 666 type: "request", 667 command: server.protocol.CommandTypes.CompileOnSaveEmitFile, 668 arguments: { file: f.path } 669 }; 670 session.executeCommand(emitFileRequest); 671 const emitOutput = host.readFile(path + Extension.Js); 672 assert.equal(emitOutput, f.content + newLine, "content of emit output should be identical with the input + newline"); 673 } 674 }); 675 676 it("should emit specified file", () => { 677 const file1 = { 678 path: "/a/b/f1.ts", 679 content: `export function Foo() { return 10; }` 680 }; 681 const file2 = { 682 path: "/a/b/f2.ts", 683 content: `import {Foo} from "./f1"; let y = Foo();` 684 }; 685 const configFile = { 686 path: "/a/b/tsconfig.json", 687 content: `{}` 688 }; 689 const host = createServerHost([file1, file2, configFile, libFile], { newLine: "\r\n" }); 690 const typingsInstaller = createTestTypingsInstaller(host); 691 const session = createSession(host, { typingsInstaller }); 692 693 openFilesForSession([file1, file2], session); 694 const compileFileRequest = makeSessionRequest<server.protocol.CompileOnSaveEmitFileRequestArgs>(CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path }); 695 session.executeCommand(compileFileRequest); 696 697 const expectedEmittedFileName = "/a/b/f1.js"; 698 assert.isTrue(host.fileExists(expectedEmittedFileName)); 699 assert.equal(host.readFile(expectedEmittedFileName), `"use strict";\r\nexports.__esModule = true;\r\nexports.Foo = void 0;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`); 700 }); 701 702 it("shoud not emit js files in external projects", () => { 703 const file1 = { 704 path: "/a/b/file1.ts", 705 content: "consonle.log('file1');" 706 }; 707 // file2 has errors. The emitting should not be blocked. 708 const file2 = { 709 path: "/a/b/file2.js", 710 content: "console.log'file2');" 711 }; 712 const file3 = { 713 path: "/a/b/file3.js", 714 content: "console.log('file3');" 715 }; 716 const externalProjectName = "/a/b/externalproject"; 717 const host = createServerHost([file1, file2, file3, libFile]); 718 const session = createSession(host); 719 const projectService = session.getProjectService(); 720 721 projectService.openExternalProject({ 722 rootFiles: toExternalFiles([file1.path, file2.path]), 723 options: { 724 allowJs: true, 725 outFile: "dist.js", 726 compileOnSave: true 727 }, 728 projectFileName: externalProjectName 729 }); 730 731 const emitRequest = makeSessionRequest<server.protocol.CompileOnSaveEmitFileRequestArgs>(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); 732 session.executeCommand(emitRequest); 733 734 const expectedOutFileName = "/a/b/dist.js"; 735 assert.isTrue(host.fileExists(expectedOutFileName)); 736 const outFileContent = host.readFile(expectedOutFileName)!; 737 assert.isTrue(outFileContent.indexOf(file1.content) !== -1); 738 assert.isTrue(outFileContent.indexOf(file2.content) === -1); 739 assert.isTrue(outFileContent.indexOf(file3.content) === -1); 740 }); 741 742 it("should use project root as current directory so that compile on save results in correct file mapping", () => { 743 const inputFileName = "Foo.ts"; 744 const file1 = { 745 path: `/root/TypeScriptProject3/TypeScriptProject3/${inputFileName}`, 746 content: "consonle.log('file1');" 747 }; 748 const externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; 749 const host = createServerHost([file1, libFile]); 750 const session = createSession(host); 751 const projectService = session.getProjectService(); 752 753 const outFileName = "bar.js"; 754 projectService.openExternalProject({ 755 rootFiles: toExternalFiles([file1.path]), 756 options: { 757 outFile: outFileName, 758 sourceMap: true, 759 compileOnSave: true 760 }, 761 projectFileName: externalProjectName 762 }); 763 764 const emitRequest = makeSessionRequest<server.protocol.CompileOnSaveEmitFileRequestArgs>(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); 765 session.executeCommand(emitRequest); 766 767 // Verify js file 768 const expectedOutFileName = "/root/TypeScriptProject3/TypeScriptProject3/" + outFileName; 769 assert.isTrue(host.fileExists(expectedOutFileName)); 770 const outFileContent = host.readFile(expectedOutFileName)!; 771 verifyContentHasString(outFileContent, file1.content); 772 verifyContentHasString(outFileContent, `//# ${"sourceMappingURL"}=${outFileName}.map`); // Sometimes tools can sometimes see this line as a source mapping url comment, so we obfuscate it a little 773 774 // Verify map file 775 const expectedMapFileName = expectedOutFileName + ".map"; 776 assert.isTrue(host.fileExists(expectedMapFileName)); 777 const mapFileContent = host.readFile(expectedMapFileName)!; 778 verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); 779 780 function verifyContentHasString(content: string, str: string) { 781 assert.isTrue(stringContains(content, str), `Expected "${content}" to have "${str}"`); 782 } 783 }); 784 785 describe("compile on save emit with and without richResponse", () => { 786 it("without rich Response", () => { 787 verify(/*richRepsonse*/ undefined); 788 }); 789 it("with rich Response set to false", () => { 790 verify(/*richRepsonse*/ false); 791 }); 792 it("with rich Repsonse", () => { 793 verify(/*richRepsonse*/ true); 794 }); 795 796 function verify(richResponse: boolean | undefined) { 797 const config: File = { 798 path: `${tscWatch.projectRoot}/tsconfig.json`, 799 content: JSON.stringify({ 800 compileOnSave: true, 801 compilerOptions: { 802 outDir: "test", 803 noEmitOnError: true, 804 declaration: true, 805 }, 806 exclude: ["node_modules"] 807 }) 808 }; 809 const file1: File = { 810 path: `${tscWatch.projectRoot}/file1.ts`, 811 content: "const x = 1;" 812 }; 813 const file2: File = { 814 path: `${tscWatch.projectRoot}/file2.ts`, 815 content: "const y = 2;" 816 }; 817 const host = createServerHost([file1, file2, config, libFile]); 818 const session = createSession(host); 819 openFilesForSession([file1], session); 820 821 const affectedFileResponse = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({ 822 command: protocol.CommandTypes.CompileOnSaveAffectedFileList, 823 arguments: { file: file1.path } 824 }).response as protocol.CompileOnSaveAffectedFileListSingleProject[]; 825 assert.deepEqual(affectedFileResponse, [ 826 { fileNames: [file1.path, file2.path], projectFileName: config.path, projectUsesOutFile: false } 827 ]); 828 const file1SaveResponse = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({ 829 command: protocol.CommandTypes.CompileOnSaveEmitFile, 830 arguments: { file: file1.path, richResponse } 831 }).response; 832 if (richResponse) { 833 assert.deepEqual(file1SaveResponse, { emitSkipped: false, diagnostics: emptyArray }); 834 } 835 else { 836 assert.isTrue(file1SaveResponse); 837 } 838 assert.strictEqual(host.readFile(`${tscWatch.projectRoot}/test/file1.d.ts`), "declare const x = 1;\n"); 839 const file2SaveResponse = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({ 840 command: protocol.CommandTypes.CompileOnSaveEmitFile, 841 arguments: { file: file2.path, richResponse } 842 }).response; 843 if (richResponse) { 844 assert.deepEqual(file2SaveResponse, { 845 emitSkipped: true, 846 diagnostics: [{ 847 start: undefined, 848 end: undefined, 849 fileName: undefined, 850 text: formatStringFromArgs(Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.message, [`${tscWatch.projectRoot}/test/file1.d.ts`]), 851 code: Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file.code, 852 category: diagnosticCategoryName(Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file), 853 reportsUnnecessary: undefined, 854 reportsDeprecated: undefined, 855 relatedInformation: undefined, 856 source: undefined 857 }] 858 }); 859 } 860 else { 861 assert.isFalse(file2SaveResponse); 862 } 863 assert.isFalse(host.fileExists(`${tscWatch.projectRoot}/test/file2.d.ts`)); 864 } 865 }); 866 867 describe("compile on save in global files", () => { 868 describe("when program contains module", () => { 869 it("when d.ts emit is enabled", () => { 870 verifyGlobalSave(/*declaration*/ true, /*hasModule*/ true); 871 }); 872 it("when d.ts emit is not enabled", () => { 873 verifyGlobalSave(/*declaration*/ false, /*hasModule*/ true); 874 }); 875 }); 876 describe("when program doesnt have module", () => { 877 it("when d.ts emit is enabled", () => { 878 verifyGlobalSave(/*declaration*/ true, /*hasModule*/ false); 879 }); 880 it("when d.ts emit is not enabled", () => { 881 verifyGlobalSave(/*declaration*/ false, /*hasModule*/ false); 882 }); 883 }); 884 function verifyGlobalSave(declaration: boolean,hasModule: boolean) { 885 const config: File = { 886 path: `${tscWatch.projectRoot}/tsconfig.json`, 887 content: JSON.stringify({ 888 compileOnSave: true, 889 compilerOptions: { 890 declaration, 891 module: hasModule ? undefined : "none" 892 }, 893 }) 894 }; 895 const file1: File = { 896 path: `${tscWatch.projectRoot}/file1.ts`, 897 content: `const x = 1; 898function foo() { 899 return "hello"; 900}` 901 }; 902 const file2: File = { 903 path: `${tscWatch.projectRoot}/file2.ts`, 904 content: `const y = 2; 905function bar() { 906 return "world"; 907}` 908 }; 909 const file3: File = { 910 path: `${tscWatch.projectRoot}/file3.ts`, 911 content: "const xy = 3;" 912 }; 913 const module: File = { 914 path: `${tscWatch.projectRoot}/module.ts`, 915 content: "export const xyz = 4;" 916 }; 917 const files = [file1, file2, file3, ...(hasModule ? [module] : emptyArray)]; 918 const host = createServerHost([...files, config, libFile]); 919 const session = createSession(host); 920 openFilesForSession([file1, file2], session); 921 922 const affectedFileResponse = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({ 923 command: protocol.CommandTypes.CompileOnSaveAffectedFileList, 924 arguments: { file: file1.path } 925 }).response as protocol.CompileOnSaveAffectedFileListSingleProject[]; 926 assert.deepEqual(affectedFileResponse, [ 927 { fileNames: files.map(f => f.path), projectFileName: config.path, projectUsesOutFile: false } 928 ]); 929 verifyFileSave(file1); 930 verifyFileSave(file2); 931 verifyFileSave(file3); 932 if (hasModule) { 933 verifyFileSave(module); 934 } 935 936 // Change file1 get affected file list 937 verifyLocalEdit(file1, "hello", "world"); 938 939 // Change file2 get affected file list = will return only file2 if --declaration otherwise all files 940 verifyLocalEdit(file2, "world", "hello", /*returnsAllFilesAsAffected*/ !declaration); 941 942 function verifyFileSave(file: File) { 943 const response = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({ 944 command: protocol.CommandTypes.CompileOnSaveEmitFile, 945 arguments: { file: file.path } 946 }).response; 947 assert.isTrue(response); 948 assert.strictEqual( 949 host.readFile(changeExtension(file.path, ".js")), 950 file === module ? 951 `"use strict";\nexports.__esModule = true;\nexports.xyz = void 0;\nexports.xyz = 4;\n` : 952 `${file.content.replace("const", "var")}\n` 953 ); 954 if (declaration) { 955 assert.strictEqual( 956 host.readFile(changeExtension(file.path, ".d.ts")), 957 (file.content.substr(0, file.content.indexOf(" {") === -1 ? file.content.length : file.content.indexOf(" {")) 958 .replace("const ", "declare const ") 959 .replace("function ", "declare function ") 960 .replace(")", "): string;")) + "\n" 961 ); 962 } 963 } 964 965 function verifyLocalEdit(file: File, oldText: string, newText: string, returnsAllFilesAsAffected?: boolean) { 966 // Change file1 get affected file list 967 session.executeCommandSeq<protocol.UpdateOpenRequest>({ 968 command: protocol.CommandTypes.UpdateOpen, 969 arguments: { 970 changedFiles: [{ 971 fileName: file.path, 972 textChanges: [{ 973 newText, 974 ...protocolTextSpanFromSubstring(file.content, oldText) 975 }] 976 }] 977 } 978 }); 979 const affectedFileResponse = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({ 980 command: protocol.CommandTypes.CompileOnSaveAffectedFileList, 981 arguments: { file: file.path } 982 }).response as protocol.CompileOnSaveAffectedFileListSingleProject[]; 983 assert.deepEqual(affectedFileResponse, [ 984 { fileNames: [file.path, ...(returnsAllFilesAsAffected ? files.filter(f => f !== file).map(f => f.path) : emptyArray)], projectFileName: config.path, projectUsesOutFile: false } 985 ]); 986 file.content = file.content.replace(oldText, newText); 987 verifyFileSave(file); 988 } 989 } 990 }); 991 }); 992 993 describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { 994 const core: File = { 995 path: `${tscWatch.projectRoot}/core/core.ts`, 996 content: "let z = 10;" 997 }; 998 const app1: File = { 999 path: `${tscWatch.projectRoot}/app1/app.ts`, 1000 content: "let x = 10;" 1001 }; 1002 const app2: File = { 1003 path: `${tscWatch.projectRoot}/app2/app.ts`, 1004 content: "let y = 10;" 1005 }; 1006 const app1Config: File = { 1007 path: `${tscWatch.projectRoot}/app1/tsconfig.json`, 1008 content: JSON.stringify({ 1009 files: ["app.ts", "../core/core.ts"], 1010 compilerOptions: { outFile: "build/output.js" }, 1011 compileOnSave: true 1012 }) 1013 }; 1014 const app2Config: File = { 1015 path: `${tscWatch.projectRoot}/app2/tsconfig.json`, 1016 content: JSON.stringify({ 1017 files: ["app.ts", "../core/core.ts"], 1018 compilerOptions: { outFile: "build/output.js" }, 1019 compileOnSave: true 1020 }) 1021 }; 1022 const files = [libFile, core, app1, app2, app1Config, app2Config]; 1023 1024 function insertString(session: TestSession, file: File) { 1025 session.executeCommandSeq<protocol.ChangeRequest>({ 1026 command: protocol.CommandTypes.Change, 1027 arguments: { 1028 file: file.path, 1029 line: 1, 1030 offset: 1, 1031 endLine: 1, 1032 endOffset: 1, 1033 insertString: "let k = 1" 1034 } 1035 }); 1036 } 1037 1038 function getSession() { 1039 const host = createServerHost(files); 1040 const session = createSession(host); 1041 openFilesForSession([app1, app2, core], session); 1042 const service = session.getProjectService(); 1043 checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); 1044 const project1 = service.configuredProjects.get(app1Config.path)!; 1045 const project2 = service.configuredProjects.get(app2Config.path)!; 1046 checkProjectActualFiles(project1, [libFile.path, app1.path, core.path, app1Config.path]); 1047 checkProjectActualFiles(project2, [libFile.path, app2.path, core.path, app2Config.path]); 1048 insertString(session, app1); 1049 insertString(session, app2); 1050 assert.equal(project1.dirty, true); 1051 assert.equal(project2.dirty, true); 1052 return session; 1053 } 1054 1055 it("when projectFile is specified", () => { 1056 const session = getSession(); 1057 const response = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({ 1058 command: protocol.CommandTypes.CompileOnSaveAffectedFileList, 1059 arguments: { 1060 file: core.path, 1061 projectFileName: app1Config.path 1062 } 1063 }).response; 1064 assert.deepEqual(response, [ 1065 { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true } 1066 ]); 1067 assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); 1068 assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, true); 1069 }); 1070 1071 it("when projectFile is not specified", () => { 1072 const session = getSession(); 1073 const response = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({ 1074 command: protocol.CommandTypes.CompileOnSaveAffectedFileList, 1075 arguments: { 1076 file: core.path 1077 } 1078 }).response; 1079 assert.deepEqual(response, [ 1080 { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true }, 1081 { projectFileName: app2Config.path, fileNames: [core.path, app2.path], projectUsesOutFile: true } 1082 ]); 1083 assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); 1084 assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, false); 1085 }); 1086 }); 1087} 1088