• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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