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