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