1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: with project references and compile on save", () => { 3 const dependecyLocation = `${tscWatch.projectRoot}/dependency`; 4 const usageLocation = `${tscWatch.projectRoot}/usage`; 5 const dependencyTs: File = { 6 path: `${dependecyLocation}/fns.ts`, 7 content: `export function fn1() { } 8export function fn2() { } 9` 10 }; 11 const dependencyConfig: File = { 12 path: `${dependecyLocation}/tsconfig.json`, 13 content: JSON.stringify({ 14 compilerOptions: { composite: true, declarationDir: "../decls" }, 15 compileOnSave: true 16 }) 17 }; 18 const usageTs: File = { 19 path: `${usageLocation}/usage.ts`, 20 content: `import { 21 fn1, 22 fn2, 23} from '../decls/fns' 24fn1(); 25fn2(); 26` 27 }; 28 const usageConfig: File = { 29 path: `${usageLocation}/tsconfig.json`, 30 content: JSON.stringify({ 31 compileOnSave: true, 32 references: [{ path: "../dependency" }] 33 }) 34 }; 35 36 interface VerifySingleScenarioWorker extends VerifySingleScenario { 37 withProject: boolean; 38 } 39 function verifySingleScenarioWorker({ 40 withProject, scenario, openFiles, requestArgs, change, expectedResult 41 }: VerifySingleScenarioWorker) { 42 it(scenario, () => { 43 const host = TestFSWithWatch.changeToHostTrackingWrittenFiles( 44 createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, libFile]) 45 ); 46 const session = createSession(host); 47 openFilesForSession(openFiles(), session); 48 const reqArgs = requestArgs(); 49 const { 50 expectedAffected, 51 expectedEmit: { expectedEmitSuccess, expectedFiles }, 52 expectedEmitOutput 53 } = expectedResult(withProject); 54 55 if (change) { 56 session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({ 57 command: protocol.CommandTypes.CompileOnSaveAffectedFileList, 58 arguments: { file: dependencyTs.path } 59 }); 60 const { file, insertString } = change(); 61 if (session.getProjectService().openFiles.has(file.path as Path)) { 62 const toLocation = protocolToLocation(file.content); 63 const location = toLocation(file.content.length); 64 session.executeCommandSeq<protocol.ChangeRequest>({ 65 command: protocol.CommandTypes.Change, 66 arguments: { 67 file: file.path, 68 ...location, 69 endLine: location.line, 70 endOffset: location.offset, 71 insertString 72 } 73 }); 74 } 75 else { 76 host.writeFile(file.path, `${file.content}${insertString}`); 77 } 78 host.writtenFiles.clear(); 79 } 80 81 const args = withProject ? reqArgs : { file: reqArgs.file }; 82 // Verify CompileOnSaveAffectedFileList 83 const actualAffectedFiles = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({ 84 command: protocol.CommandTypes.CompileOnSaveAffectedFileList, 85 arguments: args 86 }).response as protocol.CompileOnSaveAffectedFileListSingleProject[]; 87 assert.deepEqual(actualAffectedFiles, expectedAffected, "Affected files"); 88 89 // Verify CompileOnSaveEmit 90 const actualEmit = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({ 91 command: protocol.CommandTypes.CompileOnSaveEmitFile, 92 arguments: args 93 }).response; 94 assert.deepEqual(actualEmit, expectedEmitSuccess, "Emit files"); 95 assert.equal(host.writtenFiles.size, expectedFiles.length); 96 for (const file of expectedFiles) { 97 assert.equal(host.readFile(file.path), file.content, `Expected to write ${file.path}`); 98 assert.isTrue(host.writtenFiles.has(file.path as Path), `${file.path} is newly written`); 99 } 100 101 // Verify EmitOutput 102 const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq<protocol.EmitOutputRequest>({ 103 command: protocol.CommandTypes.EmitOutput, 104 arguments: args 105 }).response as EmitOutput; 106 assert.deepEqual(actualEmitOutput, expectedEmitOutput, "Emit output"); 107 }); 108 } 109 110 interface VerifySingleScenario { 111 scenario: string; 112 openFiles: () => readonly File[]; 113 requestArgs: () => protocol.FileRequestArgs; 114 skipWithoutProject?: boolean; 115 change?: () => SingleScenarioChange; 116 expectedResult: GetSingleScenarioResult; 117 } 118 function verifySingleScenario(scenario: VerifySingleScenario) { 119 if (!scenario.skipWithoutProject) { 120 describe("without specifying project file", () => { 121 verifySingleScenarioWorker({ 122 withProject: false, 123 ...scenario 124 }); 125 }); 126 } 127 describe("with specifying project file", () => { 128 verifySingleScenarioWorker({ 129 withProject: true, 130 ...scenario 131 }); 132 }); 133 } 134 135 interface SingleScenarioExpectedEmit { 136 expectedEmitSuccess: boolean; 137 expectedFiles: readonly File[]; 138 } 139 interface SingleScenarioResult { 140 expectedAffected: protocol.CompileOnSaveAffectedFileListSingleProject[]; 141 expectedEmit: SingleScenarioExpectedEmit; 142 expectedEmitOutput: EmitOutput; 143 } 144 type GetSingleScenarioResult = (withProject: boolean) => SingleScenarioResult; 145 interface SingleScenarioChange { 146 file: File; 147 insertString: string; 148 } 149 interface ScenarioDetails { 150 scenarioName: string; 151 requestArgs: () => protocol.FileRequestArgs; 152 skipWithoutProject?: boolean; 153 initial: GetSingleScenarioResult; 154 localChangeToDependency: GetSingleScenarioResult; 155 localChangeToUsage: GetSingleScenarioResult; 156 changeToDependency: GetSingleScenarioResult; 157 changeToUsage: GetSingleScenarioResult; 158 } 159 interface VerifyScenario { 160 openFiles: () => readonly File[]; 161 scenarios: readonly ScenarioDetails[]; 162 } 163 164 const localChange = "function fn3() { }"; 165 const change = `export ${localChange}`; 166 const changeJs = `function fn3() { } 167exports.fn3 = fn3;`; 168 const changeDts = "export declare function fn3(): void;"; 169 function verifyScenario({ openFiles, scenarios }: VerifyScenario) { 170 for (const { 171 scenarioName, requestArgs, skipWithoutProject, initial, 172 localChangeToDependency, localChangeToUsage, 173 changeToDependency, changeToUsage 174 } of scenarios) { 175 describe(scenarioName, () => { 176 verifySingleScenario({ 177 scenario: "with initial file open", 178 openFiles, 179 requestArgs, 180 skipWithoutProject, 181 expectedResult: initial 182 }); 183 184 verifySingleScenario({ 185 scenario: "with local change to dependency", 186 openFiles, 187 requestArgs, 188 skipWithoutProject, 189 change: () => ({ file: dependencyTs, insertString: localChange }), 190 expectedResult: localChangeToDependency 191 }); 192 193 verifySingleScenario({ 194 scenario: "with local change to usage", 195 openFiles, 196 requestArgs, 197 skipWithoutProject, 198 change: () => ({ file: usageTs, insertString: localChange }), 199 expectedResult: localChangeToUsage 200 }); 201 202 verifySingleScenario({ 203 scenario: "with change to dependency", 204 openFiles, 205 requestArgs, 206 skipWithoutProject, 207 change: () => ({ file: dependencyTs, insertString: change }), 208 expectedResult: changeToDependency 209 }); 210 211 verifySingleScenario({ 212 scenario: "with change to usage", 213 openFiles, 214 requestArgs, 215 skipWithoutProject, 216 change: () => ({ file: usageTs, insertString: change }), 217 expectedResult: changeToUsage 218 }); 219 }); 220 } 221 } 222 223 function expectedAffectedFiles(config: File, fileNames: File[]): protocol.CompileOnSaveAffectedFileListSingleProject { 224 return { 225 projectFileName: config.path, 226 fileNames: fileNames.map(f => f.path), 227 projectUsesOutFile: false 228 }; 229 } 230 231 function expectedUsageEmit(appendJsText?: string): SingleScenarioExpectedEmit { 232 const appendJs = appendJsText ? `${appendJsText} 233` : ""; 234 return { 235 expectedEmitSuccess: true, 236 expectedFiles: [{ 237 path: `${usageLocation}/usage.js`, 238 content: `"use strict"; 239exports.__esModule = true;${appendJsText === changeJs ? "\nexports.fn3 = void 0;" : ""} 240var fns_1 = require("../decls/fns"); 241fns_1.fn1(); 242fns_1.fn2(); 243${appendJs}` 244 }] 245 }; 246 } 247 248 function expectedEmitOutput({ expectedFiles }: SingleScenarioExpectedEmit): EmitOutput { 249 return { 250 outputFiles: expectedFiles.map(({ path, content }) => ({ 251 name: path, 252 text: content, 253 writeByteOrderMark: false 254 })), 255 emitSkipped: false, 256 diagnostics: emptyArray 257 }; 258 } 259 260 function expectedUsageEmitOutput(appendJsText?: string): EmitOutput { 261 return expectedEmitOutput(expectedUsageEmit(appendJsText)); 262 } 263 264 function noEmit(): SingleScenarioExpectedEmit { 265 return { 266 expectedEmitSuccess: false, 267 expectedFiles: emptyArray 268 }; 269 } 270 271 function noEmitOutput(): EmitOutput { 272 return { 273 emitSkipped: true, 274 outputFiles: [], 275 diagnostics: emptyArray 276 }; 277 } 278 279 function expectedDependencyEmit(appendJsText?: string, appendDtsText?: string): SingleScenarioExpectedEmit { 280 const appendJs = appendJsText ? `${appendJsText} 281` : ""; 282 const appendDts = appendDtsText ? `${appendDtsText} 283` : ""; 284 return { 285 expectedEmitSuccess: true, 286 expectedFiles: [ 287 { 288 path: `${dependecyLocation}/fns.js`, 289 content: `"use strict"; 290exports.__esModule = true; 291${appendJsText === changeJs ? "exports.fn3 = " : ""}exports.fn2 = exports.fn1 = void 0; 292function fn1() { } 293exports.fn1 = fn1; 294function fn2() { } 295exports.fn2 = fn2; 296${appendJs}` 297 }, 298 { 299 path: `${tscWatch.projectRoot}/decls/fns.d.ts`, 300 content: `export declare function fn1(): void; 301export declare function fn2(): void; 302${appendDts}` 303 } 304 ] 305 }; 306 } 307 308 function expectedDependencyEmitOutput(appendJsText?: string, appendDtsText?: string): EmitOutput { 309 return expectedEmitOutput(expectedDependencyEmit(appendJsText, appendDtsText)); 310 } 311 312 function scenarioDetailsOfUsage(isDependencyOpen?: boolean): ScenarioDetails[] { 313 return [ 314 { 315 scenarioName: "Of usageTs", 316 requestArgs: () => ({ file: usageTs.path, projectFileName: usageConfig.path }), 317 initial: () => initialUsageTs(), 318 // no change to usage so same as initial only usage file 319 localChangeToDependency: () => initialUsageTs(), 320 localChangeToUsage: () => initialUsageTs(localChange), 321 changeToDependency: () => initialUsageTs(), 322 changeToUsage: () => initialUsageTs(changeJs) 323 }, 324 { 325 scenarioName: "Of dependencyTs in usage project", 326 requestArgs: () => ({ file: dependencyTs.path, projectFileName: usageConfig.path }), 327 skipWithoutProject: !!isDependencyOpen, 328 initial: () => initialDependencyTs(), 329 localChangeToDependency: () => initialDependencyTs(/*noUsageFiles*/ true), 330 localChangeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true), 331 changeToDependency: () => initialDependencyTs(), 332 changeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true) 333 } 334 ]; 335 336 function initialUsageTs(jsText?: string) { 337 return { 338 expectedAffected: [ 339 expectedAffectedFiles(usageConfig, [usageTs]) 340 ], 341 expectedEmit: expectedUsageEmit(jsText), 342 expectedEmitOutput: expectedUsageEmitOutput(jsText) 343 }; 344 } 345 346 function initialDependencyTs(noUsageFiles?: true) { 347 return { 348 expectedAffected: [ 349 expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]) 350 ], 351 expectedEmit: noEmit(), 352 expectedEmitOutput: noEmitOutput() 353 }; 354 } 355 } 356 357 function scenarioDetailsOfDependencyWhenOpen(): ScenarioDetails { 358 return { 359 scenarioName: "Of dependencyTs", 360 requestArgs: () => ({ file: dependencyTs.path, projectFileName: dependencyConfig.path }), 361 initial, 362 localChangeToDependency: withProject => ({ 363 expectedAffected: withProject ? 364 [ 365 expectedAffectedFiles(dependencyConfig, [dependencyTs]) 366 ] : 367 [ 368 expectedAffectedFiles(usageConfig, []), 369 expectedAffectedFiles(dependencyConfig, [dependencyTs]) 370 ], 371 expectedEmit: expectedDependencyEmit(localChange), 372 expectedEmitOutput: expectedDependencyEmitOutput(localChange) 373 }), 374 localChangeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true), 375 changeToDependency: withProject => initial(withProject, /*noUsageFiles*/ undefined, changeJs, changeDts), 376 changeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true) 377 }; 378 379 function initial(withProject: boolean, noUsageFiles?: true, appendJs?: string, appendDts?: string): SingleScenarioResult { 380 return { 381 expectedAffected: withProject ? 382 [ 383 expectedAffectedFiles(dependencyConfig, [dependencyTs]) 384 ] : 385 [ 386 expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]), 387 expectedAffectedFiles(dependencyConfig, [dependencyTs]) 388 ], 389 expectedEmit: expectedDependencyEmit(appendJs, appendDts), 390 expectedEmitOutput: expectedDependencyEmitOutput(appendJs, appendDts) 391 }; 392 } 393 } 394 395 describe("when dependency project is not open", () => { 396 verifyScenario({ 397 openFiles: () => [usageTs], 398 scenarios: scenarioDetailsOfUsage() 399 }); 400 }); 401 402 describe("when the depedency file is open", () => { 403 verifyScenario({ 404 openFiles: () => [usageTs, dependencyTs], 405 scenarios: [ 406 ...scenarioDetailsOfUsage(/*isDependencyOpen*/ true), 407 scenarioDetailsOfDependencyWhenOpen(), 408 ] 409 }); 410 }); 411 }); 412 413 describe("unittests:: tsserver:: with project references and compile on save with external projects", () => { 414 it("compile on save emits same output as project build", () => { 415 const tsbaseJson: File = { 416 path: `${tscWatch.projectRoot}/tsbase.json`, 417 content: JSON.stringify({ 418 compileOnSave: true, 419 compilerOptions: { 420 module: "none", 421 composite: true 422 } 423 }) 424 }; 425 const buttonClass = `${tscWatch.projectRoot}/buttonClass`; 426 const buttonConfig: File = { 427 path: `${buttonClass}/tsconfig.json`, 428 content: JSON.stringify({ 429 extends: "../tsbase.json", 430 compilerOptions: { 431 outFile: "Source.js" 432 }, 433 files: ["Source.ts"] 434 }) 435 }; 436 const buttonSource: File = { 437 path: `${buttonClass}/Source.ts`, 438 content: `module Hmi { 439 export class Button { 440 public static myStaticFunction() { 441 } 442 } 443}` 444 }; 445 446 const siblingClass = `${tscWatch.projectRoot}/SiblingClass`; 447 const siblingConfig: File = { 448 path: `${siblingClass}/tsconfig.json`, 449 content: JSON.stringify({ 450 extends: "../tsbase.json", 451 references: [{ 452 path: "../buttonClass/" 453 }], 454 compilerOptions: { 455 outFile: "Source.js" 456 }, 457 files: ["Source.ts"] 458 }) 459 }; 460 const siblingSource: File = { 461 path: `${siblingClass}/Source.ts`, 462 content: `module Hmi { 463 export class Sibling { 464 public mySiblingFunction() { 465 } 466 } 467}` 468 }; 469 const host = createServerHost([libFile, tsbaseJson, buttonConfig, buttonSource, siblingConfig, siblingSource], { useCaseSensitiveFileNames: true }); 470 471 // ts build should succeed 472 tscWatch.ensureErrorFreeBuild(host, [siblingConfig.path]); 473 const sourceJs = changeExtension(siblingSource.path, ".js"); 474 const expectedSiblingJs = host.readFile(sourceJs); 475 476 const session = createSession(host); 477 openFilesForSession([siblingSource], session); 478 479 session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({ 480 command: protocol.CommandTypes.CompileOnSaveEmitFile, 481 arguments: { 482 file: siblingSource.path, 483 projectFileName: siblingConfig.path 484 } 485 }); 486 assert.equal(host.readFile(sourceJs), expectedSiblingJs); 487 }); 488 }); 489} 490