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