• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts {
2    interface TestProjectSpecification {
3        configFileName?: string;
4        references?: readonly (string | ProjectReference)[];
5        files: { [fileName: string]: string };
6        outputFiles?: { [fileName: string]: string };
7        config?: object;
8        options?: Partial<CompilerOptions>;
9    }
10    interface TestSpecification {
11        [path: string]: TestProjectSpecification;
12    }
13
14    function assertHasError(message: string, errors: readonly Diagnostic[], diag: DiagnosticMessage) {
15        if (!errors.some(e => e.code === diag.code)) {
16            const errorString = errors.map(e => `    ${e.file ? e.file.fileName : "[global]"}: ${e.messageText}`).join("\r\n");
17            assert(false, `${message}: Did not find any diagnostic for ${diag.message} in:\r\n${errorString}`);
18        }
19    }
20
21    function assertNoErrors(message: string, errors: readonly Diagnostic[]) {
22        if (errors && errors.length > 0) {
23            assert(false, `${message}: Expected no errors, but found:\r\n${errors.map(e => `    ${e.messageText}`).join("\r\n")}`);
24        }
25    }
26
27    function combineAllPaths(...paths: string[]) {
28        let result = paths[0];
29        for (let i = 1; i < paths.length; i++) {
30            result = combinePaths(result, paths[i]);
31        }
32        return result;
33    }
34
35    const emptyModule = "export { };";
36
37    /**
38     * Produces the text of a source file which imports all of the
39     * specified module names
40     */
41    function moduleImporting(...names: string[]) {
42        return names.map((n, i) => `import * as mod_${i} from ${n}`).join("\r\n");
43    }
44
45    function testProjectReferences(spec: TestSpecification, entryPointConfigFileName: string, checkResult: (prog: Program, host: fakes.CompilerHost) => void) {
46        const files = new Map<string, string>();
47        for (const key in spec) {
48            const sp = spec[key];
49            const configFileName = combineAllPaths("/", key, sp.configFileName || "tsconfig.json");
50            const options = {
51                compilerOptions: {
52                    composite: true,
53                    outDir: "bin",
54                    ...sp.options
55                },
56                references: (sp.references || []).map(r => {
57                    if (typeof r === "string") {
58                        return { path: r };
59                    }
60                    return r;
61                }),
62                ...sp.config
63            };
64            const configContent = JSON.stringify(options);
65            const outDir = options.compilerOptions.outDir;
66            files.set(configFileName, configContent);
67            for (const sourceFile of Object.keys(sp.files)) {
68                files.set(sourceFile, sp.files[sourceFile]);
69            }
70            if (sp.outputFiles) {
71                for (const outFile of Object.keys(sp.outputFiles)) {
72                    files.set(combineAllPaths("/", key, outDir, outFile), sp.outputFiles[outFile]);
73                }
74            }
75        }
76
77        const vfsys = new vfs.FileSystem(false, { files: { "/lib.d.ts": TestFSWithWatch.libFile.content } });
78        files.forEach((v, k) => {
79            vfsys.mkdirpSync(getDirectoryPath(k));
80            vfsys.writeFileSync(k, v);
81        });
82        const host = new fakes.CompilerHost(new fakes.System(vfsys));
83
84        const { config, error } = readConfigFile(entryPointConfigFileName, name => host.readFile(name));
85
86        // We shouldn't have any errors about invalid tsconfig files in these tests
87        assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n"));
88        const file = parseJsonConfigFileContent(config, parseConfigHostFromCompilerHostLike(host), getDirectoryPath(entryPointConfigFileName), {}, entryPointConfigFileName);
89        file.options.configFilePath = entryPointConfigFileName;
90        const prog = createProgram({
91            rootNames: file.fileNames,
92            options: file.options,
93            host,
94            projectReferences: file.projectReferences
95        });
96        checkResult(prog, host);
97    }
98
99    describe("unittests:: config:: project-references meta check", () => {
100        it("default setup was created correctly", () => {
101            const spec: TestSpecification = {
102                "/primary": {
103                    files: { "/primary/a.ts": emptyModule },
104                    references: []
105                },
106                "/reference": {
107                    files: { "/secondary/b.ts": moduleImporting("../primary/a") },
108                    references: ["../primary"]
109                }
110            };
111            testProjectReferences(spec, "/primary/tsconfig.json", prog => {
112                assert.isTrue(!!prog, "Program should exist");
113                assertNoErrors("Sanity check should not produce errors", prog.getOptionsDiagnostics());
114            });
115        });
116    });
117
118    /**
119     * Validate that we enforce the basic settings constraints for referenced projects
120     */
121    describe("unittests:: config:: project-references constraint checking for settings", () => {
122        it("errors when declaration = false", () => {
123            const spec: TestSpecification = {
124                "/primary": {
125                    files: { "/primary/a.ts": emptyModule },
126                    references: [],
127                    options: {
128                        declaration: false
129                    }
130                }
131            };
132
133            testProjectReferences(spec, "/primary/tsconfig.json", program => {
134                const errs = program.getOptionsDiagnostics();
135                assertHasError("Reports an error about the wrong decl setting", errs, Diagnostics.Composite_projects_may_not_disable_declaration_emit);
136            });
137        });
138
139        it("errors when the referenced project doesn't have composite:true", () => {
140            const spec: TestSpecification = {
141                "/primary": {
142                    files: { "/primary/a.ts": emptyModule },
143                    references: [],
144                    options: {
145                        composite: false
146                    }
147                },
148                "/reference": {
149                    files: { "/secondary/b.ts": moduleImporting("../primary/a") },
150                    references: ["../primary"],
151                    config: {
152                        files: ["b.ts"]
153                    }
154                }
155            };
156            testProjectReferences(spec, "/reference/tsconfig.json", program => {
157                const errs = program.getOptionsDiagnostics();
158                assertHasError("Reports an error about 'composite' not being set", errs, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true);
159            });
160        });
161
162        it("does not error when the referenced project doesn't have composite:true if its a container project", () => {
163            const spec: TestSpecification = {
164                "/primary": {
165                    files: { "/primary/a.ts": emptyModule },
166                    references: [],
167                    options: {
168                        composite: false
169                    }
170                },
171                "/reference": {
172                    files: { "/secondary/b.ts": moduleImporting("../primary/a") },
173                    references: ["../primary"],
174                }
175            };
176            testProjectReferences(spec, "/reference/tsconfig.json", program => {
177                const errs = program.getOptionsDiagnostics();
178                assertNoErrors("Reports an error about 'composite' not being set", errs);
179            });
180        });
181
182        it("errors when the file list is not exhaustive", () => {
183            const spec: TestSpecification = {
184                "/primary": {
185                    files: {
186                        "/primary/a.ts": "import * as b from './b'",
187                        "/primary/b.ts": "export {}"
188                    },
189                    config: {
190                        files: ["a.ts"]
191                    }
192                }
193            };
194
195            testProjectReferences(spec, "/primary/tsconfig.json", program => {
196                const errs = program.getSemanticDiagnostics(program.getSourceFile("/primary/a.ts"));
197                assertHasError("Reports an error about b.ts not being in the list", errs, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern);
198            });
199        });
200
201        it("errors when the referenced project doesn't exist", () => {
202            const spec: TestSpecification = {
203                "/primary": {
204                    files: { "/primary/a.ts": emptyModule },
205                    references: ["../foo"]
206                }
207            };
208            testProjectReferences(spec, "/primary/tsconfig.json", program => {
209                const errs = program.getOptionsDiagnostics();
210                assertHasError("Reports an error about a missing file", errs, Diagnostics.File_0_not_found);
211            });
212        });
213
214        it("errors when a prepended project reference doesn't set outFile", () => {
215            const spec: TestSpecification = {
216                "/primary": {
217                    files: { "/primary/a.ts": emptyModule },
218                    references: [{ path: "../someProj", prepend: true }]
219                },
220                "/someProj": {
221                    files: { "/someProj/b.ts": "const x = 100;" }
222                }
223            };
224            testProjectReferences(spec, "/primary/tsconfig.json", program => {
225                const errs = program.getOptionsDiagnostics();
226                assertHasError("Reports an error about outFile not being set", errs, Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set);
227            });
228        });
229
230        it("errors when a prepended project reference output doesn't exist", () => {
231            const spec: TestSpecification = {
232                "/primary": {
233                    files: { "/primary/a.ts": "const y = x;" },
234                    references: [{ path: "../someProj", prepend: true }]
235                },
236                "/someProj": {
237                    files: { "/someProj/b.ts": "const x = 100;" },
238                    options: { outFile: "foo.js" }
239                }
240            };
241            testProjectReferences(spec, "/primary/tsconfig.json", program => {
242                const errs = program.getOptionsDiagnostics();
243                assertHasError("Reports an error about outFile being missing", errs, Diagnostics.Output_file_0_from_project_1_does_not_exist);
244            });
245        });
246    });
247
248    /**
249     * Path mapping behavior
250     */
251    describe("unittests:: config:: project-references path mapping", () => {
252        it("redirects to the output .d.ts file", () => {
253            const spec: TestSpecification = {
254                "/alpha": {
255                    files: { "/alpha/a.ts": "export const m: number = 3;" },
256                    references: [],
257                    outputFiles: { "a.d.ts": emptyModule }
258                },
259                "/beta": {
260                    files: { "/beta/b.ts": "import { m } from '../alpha/a'" },
261                    references: ["../alpha"]
262                }
263            };
264            testProjectReferences(spec, "/beta/tsconfig.json", program => {
265                assertNoErrors("File setup should be correct", program.getOptionsDiagnostics());
266                assertHasError("Found a type error", program.getSemanticDiagnostics(), Diagnostics.Module_0_has_no_exported_member_1);
267            });
268        });
269    });
270
271    describe("unittests:: config:: project-references nice-behavior", () => {
272        it("issues a nice error when the input file is missing", () => {
273            const spec: TestSpecification = {
274                "/alpha": {
275                    files: { "/alpha/a.ts": "export const m: number = 3;" },
276                    references: []
277                },
278                "/beta": {
279                    files: { "/beta/b.ts": "import { m } from '../alpha/a'" },
280                    references: ["../alpha"]
281                }
282            };
283            testProjectReferences(spec, "/beta/tsconfig.json", program => {
284                assertHasError("Issues a useful error", program.getSemanticDiagnostics(), Diagnostics.Output_file_0_has_not_been_built_from_source_file_1);
285            });
286        });
287
288        it("issues a nice error when the input file is missing when module reference is not relative", () => {
289            const spec: TestSpecification = {
290                "/alpha": {
291                    files: { "/alpha/a.ts": "export const m: number = 3;" },
292                    references: []
293                },
294                "/beta": {
295                    files: { "/beta/b.ts": "import { m } from '@alpha/a'" },
296                    references: ["../alpha"],
297                    options: {
298                        baseUrl: "./",
299                        paths: {
300                            "@alpha/*": ["/alpha/*"]
301                        }
302                    }
303                }
304            };
305            testProjectReferences(spec, "/beta/tsconfig.json", program => {
306                assertHasError("Issues a useful error", program.getSemanticDiagnostics(), Diagnostics.Output_file_0_has_not_been_built_from_source_file_1);
307            });
308        });
309    });
310
311    /**
312     * 'composite' behavior
313     */
314    describe("unittests:: config:: project-references behavior changes under composite: true", () => {
315        it("doesn't infer the rootDir from source paths", () => {
316            const spec: TestSpecification = {
317                "/alpha": {
318                    files: { "/alpha/src/a.ts": "export const m: number = 3;" },
319                    options: {
320                        declaration: true,
321                        outDir: "bin"
322                    },
323                    references: []
324                }
325            };
326            testProjectReferences(spec, "/alpha/tsconfig.json", (program, host) => {
327                program.emit();
328                assert.deepEqual(host.outputs.map(e => e.file).sort(), ["/alpha/bin/src/a.d.ts", "/alpha/bin/src/a.js", "/alpha/bin/tsconfig.tsbuildinfo"]);
329            });
330        });
331    });
332
333    describe("unittests:: config:: project-references errors when a file in a composite project occurs outside the root", () => {
334        it("Errors when a file is outside the rootdir", () => {
335            const spec: TestSpecification = {
336                "/alpha": {
337                    files: { "/alpha/src/a.ts": "import * from '../../beta/b'", "/beta/b.ts": "export { }" },
338                    options: {
339                        declaration: true,
340                        outDir: "bin"
341                    },
342                    references: []
343                }
344            };
345            testProjectReferences(spec, "/alpha/tsconfig.json", (program) => {
346                const semanticDiagnostics = program.getSemanticDiagnostics(program.getSourceFile("/alpha/src/a.ts"));
347                assertHasError("Issues an error about the rootDir", semanticDiagnostics, Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files);
348                assertHasError("Issues an error about the fileList", semanticDiagnostics, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern);
349            });
350        });
351    });
352}
353