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