1namespace ts { 2 function createFileSystem(ignoreCase: boolean, cwd: string, root: string) { 3 return new vfs.FileSystem(ignoreCase, { 4 cwd, 5 files: { 6 [root]: { 7 "dev/node_modules/config-box/package.json": JSON.stringify({ 8 name: "config-box", 9 version: "1.0.0", 10 tsconfig: "./strict.json" 11 }), 12 "dev/node_modules/config-box/strict.json": JSON.stringify({ 13 compilerOptions: { 14 strict: true, 15 } 16 }), 17 "dev/node_modules/config-box/unstrict.json": JSON.stringify({ 18 compilerOptions: { 19 strict: false, 20 } 21 }), 22 "dev/tsconfig.extendsBox.json": JSON.stringify({ 23 extends: "config-box", 24 files: [ 25 "main.ts", 26 ] 27 }), 28 "dev/tsconfig.extendsStrict.json": JSON.stringify({ 29 extends: "config-box/strict", 30 files: [ 31 "main.ts", 32 ] 33 }), 34 "dev/tsconfig.extendsUnStrict.json": JSON.stringify({ 35 extends: "config-box/unstrict", 36 files: [ 37 "main.ts", 38 ] 39 }), 40 "dev/tsconfig.extendsStrictExtension.json": JSON.stringify({ 41 extends: "config-box/strict.json", 42 files: [ 43 "main.ts", 44 ] 45 }), 46 "dev/node_modules/config-box-implied/package.json": JSON.stringify({ 47 name: "config-box-implied", 48 version: "1.0.0", 49 }), 50 "dev/node_modules/config-box-implied/tsconfig.json": JSON.stringify({ 51 compilerOptions: { 52 strict: true, 53 } 54 }), 55 "dev/node_modules/config-box-implied/unstrict/tsconfig.json": JSON.stringify({ 56 compilerOptions: { 57 strict: false, 58 } 59 }), 60 "dev/tsconfig.extendsBoxImplied.json": JSON.stringify({ 61 extends: "config-box-implied", 62 files: [ 63 "main.ts", 64 ] 65 }), 66 "dev/tsconfig.extendsBoxImpliedUnstrict.json": JSON.stringify({ 67 extends: "config-box-implied/unstrict", 68 files: [ 69 "main.ts", 70 ] 71 }), 72 "dev/tsconfig.extendsBoxImpliedUnstrictExtension.json": JSON.stringify({ 73 extends: "config-box-implied/unstrict/tsconfig", 74 files: [ 75 "main.ts", 76 ] 77 }), 78 "dev/tsconfig.extendsBoxImpliedPath.json": JSON.stringify({ 79 extends: "config-box-implied/tsconfig.json", 80 files: [ 81 "main.ts", 82 ] 83 }), 84 "dev/tsconfig.json": JSON.stringify({ 85 extends: "./configs/base", 86 files: [ 87 "main.ts", 88 "supplemental.ts" 89 ] 90 }), 91 "dev/tsconfig.nostrictnull.json": JSON.stringify({ 92 extends: "./tsconfig", 93 compilerOptions: { 94 strictNullChecks: false 95 } 96 }), 97 "dev/configs/base.json": JSON.stringify({ 98 compilerOptions: { 99 allowJs: true, 100 noImplicitAny: true, 101 strictNullChecks: true 102 } 103 }), 104 "dev/configs/tests.json": JSON.stringify({ 105 compilerOptions: { 106 preserveConstEnums: true, 107 removeComments: false, 108 sourceMap: true 109 }, 110 exclude: [ 111 "../tests/baselines", 112 "../tests/scenarios" 113 ], 114 include: [ 115 "../tests/**/*.ts" 116 ] 117 }), 118 "dev/circular.json": JSON.stringify({ 119 extends: "./circular2", 120 compilerOptions: { 121 module: "amd" 122 } 123 }), 124 "dev/circular2.json": JSON.stringify({ 125 extends: "./circular", 126 compilerOptions: { 127 module: "commonjs" 128 } 129 }), 130 "dev/missing.json": JSON.stringify({ 131 extends: "./missing2", 132 compilerOptions: { 133 types: [] 134 } 135 }), 136 "dev/failure.json": JSON.stringify({ 137 extends: "./failure2.json", 138 compilerOptions: { 139 typeRoots: [] 140 } 141 }), 142 "dev/failure2.json": JSON.stringify({ 143 excludes: ["*.js"] 144 }), 145 "dev/configs/first.json": JSON.stringify({ 146 extends: "./base", 147 compilerOptions: { 148 module: "commonjs" 149 }, 150 files: ["../main.ts"] 151 }), 152 "dev/configs/second.json": JSON.stringify({ 153 extends: "./base", 154 compilerOptions: { 155 module: "amd" 156 }, 157 include: ["../supplemental.*"] 158 }), 159 "dev/configs/third.json": JSON.stringify({ 160 extends: "./second", 161 compilerOptions: { 162 module: null // eslint-disable-line no-null/no-null 163 }, 164 include: ["../supplemental.*"] 165 }), 166 "dev/configs/fourth.json": JSON.stringify({ 167 extends: "./third", 168 compilerOptions: { 169 module: "system" 170 }, 171 include: null, // eslint-disable-line no-null/no-null 172 files: ["../main.ts"] 173 }), 174 "dev/configs/fifth.json": JSON.stringify({ 175 extends: "./fourth", 176 include: ["../tests/utils.ts"], 177 files: [] 178 }), 179 "dev/extends.json": JSON.stringify({ extends: 42 }), 180 "dev/extends2.json": JSON.stringify({ extends: "configs/base" }), 181 "dev/main.ts": "", 182 "dev/supplemental.ts": "", 183 "dev/tests/unit/spec.ts": "", 184 "dev/tests/utils.ts": "", 185 "dev/tests/scenarios/first.json": "", 186 "dev/tests/baselines/first/output.ts": "" 187 } 188 } 189 }); 190 } 191 192 const caseInsensitiveBasePath = "c:/dev/"; 193 const caseInsensitiveHost = new fakes.ParseConfigHost(createFileSystem(/*ignoreCase*/ true, caseInsensitiveBasePath, "c:/")); 194 195 const caseSensitiveBasePath = "/dev/"; 196 const caseSensitiveHost = new fakes.ParseConfigHost(createFileSystem(/*ignoreCase*/ false, caseSensitiveBasePath, "/")); 197 198 function verifyDiagnostics(actual: Diagnostic[], expected: { code: number; messageText: string; }[]) { 199 assert.isTrue(expected.length === actual.length, `Expected error: ${JSON.stringify(expected)}. Actual error: ${JSON.stringify(actual)}.`); 200 for (let i = 0; i < actual.length; i++) { 201 const actualError = actual[i]; 202 const expectedError = expected[i]; 203 assert.equal(actualError.code, expectedError.code, "Error code mismatch"); 204 assert.equal(actualError.category, DiagnosticCategory.Error, "Category mismatch"); // Should always be error 205 assert.equal(flattenDiagnosticMessageText(actualError.messageText, "\n"), expectedError.messageText); 206 } 207 } 208 209 describe("unittests:: config:: configurationExtension", () => { 210 forEach<[string, string, fakes.ParseConfigHost], void>([ 211 ["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost], 212 ["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost] 213 ], ([testName, basePath, host]) => { 214 function getParseCommandLine(entry: string) { 215 const {config, error} = readConfigFile(entry, name => host.readFile(name)); 216 assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); 217 return parseJsonConfigFileContent(config, host, basePath, {}, entry); 218 } 219 220 function getParseCommandLineJsonSourceFile(entry: string) { 221 const jsonSourceFile = readJsonConfigFile(entry, name => host.readFile(name)); 222 assert(jsonSourceFile.endOfFileToken && !jsonSourceFile.parseDiagnostics.length, flattenDiagnosticMessageText(jsonSourceFile.parseDiagnostics[0] && jsonSourceFile.parseDiagnostics[0].messageText, "\n")); 223 return { 224 jsonSourceFile, 225 parsed: parseJsonSourceFileConfigFileContent(jsonSourceFile, host, basePath, {}, entry) 226 }; 227 } 228 229 function testSuccess(name: string, entry: string, expected: CompilerOptions, expectedFiles: string[]) { 230 expected.configFilePath = entry; 231 it(name, () => { 232 const parsed = getParseCommandLine(entry); 233 assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); 234 assert.deepEqual(parsed.options, expected); 235 assert.deepEqual(parsed.fileNames, expectedFiles); 236 }); 237 238 it(name + " with jsonSourceFile", () => { 239 const { parsed, jsonSourceFile } = getParseCommandLineJsonSourceFile(entry); 240 assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); 241 assert.deepEqual(parsed.options, expected); 242 assert.equal(parsed.options.configFile, jsonSourceFile); 243 assert.deepEqual(parsed.fileNames, expectedFiles); 244 }); 245 } 246 247 function testFailure(name: string, entry: string, expectedDiagnostics: { code: number; messageText: string; }[]) { 248 it(name, () => { 249 const parsed = getParseCommandLine(entry); 250 verifyDiagnostics(parsed.errors, expectedDiagnostics); 251 }); 252 253 it(name + " with jsonSourceFile", () => { 254 const { parsed } = getParseCommandLineJsonSourceFile(entry); 255 verifyDiagnostics(parsed.errors, expectedDiagnostics); 256 }); 257 } 258 259 describe(testName, () => { 260 testSuccess("can resolve an extension with a base extension", "tsconfig.json", { 261 allowJs: true, 262 noImplicitAny: true, 263 strictNullChecks: true, 264 }, [ 265 combinePaths(basePath, "main.ts"), 266 combinePaths(basePath, "supplemental.ts"), 267 ]); 268 269 testSuccess("can resolve an extension with a base extension that overrides options", "tsconfig.nostrictnull.json", { 270 allowJs: true, 271 noImplicitAny: true, 272 strictNullChecks: false, 273 }, [ 274 combinePaths(basePath, "main.ts"), 275 combinePaths(basePath, "supplemental.ts"), 276 ]); 277 278 testFailure("can report errors on circular imports", "circular.json", [ 279 { 280 code: 18000, 281 messageText: `Circularity detected while resolving configuration: ${[combinePaths(basePath, "circular.json"), combinePaths(basePath, "circular2.json"), combinePaths(basePath, "circular.json")].join(" -> ")}` 282 } 283 ]); 284 285 testFailure("can report missing configurations", "missing.json", [{ 286 code: 6053, 287 messageText: `File './missing2' not found.` 288 }]); 289 290 testFailure("can report errors in extended configs", "failure.json", [{ 291 code: 6114, 292 messageText: `Unknown option 'excludes'. Did you mean 'exclude'?` 293 }]); 294 295 testFailure("can error when 'extends' is not a string", "extends.json", [{ 296 code: 5024, 297 messageText: `Compiler option 'extends' requires a value of type string.` 298 }]); 299 300 testSuccess("can overwrite compiler options using extended 'null'", "configs/third.json", { 301 allowJs: true, 302 noImplicitAny: true, 303 strictNullChecks: true, 304 module: undefined // Technically, this is distinct from the key never being set; but within the compiler we don't make the distinction 305 }, [ 306 combinePaths(basePath, "supplemental.ts") 307 ]); 308 309 testSuccess("can overwrite top-level options using extended 'null'", "configs/fourth.json", { 310 allowJs: true, 311 noImplicitAny: true, 312 strictNullChecks: true, 313 module: ModuleKind.System 314 }, [ 315 combinePaths(basePath, "main.ts") 316 ]); 317 318 testSuccess("can overwrite top-level files using extended []", "configs/fifth.json", { 319 allowJs: true, 320 noImplicitAny: true, 321 strictNullChecks: true, 322 module: ModuleKind.System 323 }, [ 324 combinePaths(basePath, "tests/utils.ts") 325 ]); 326 327 describe("finding extended configs from node_modules", () => { 328 testSuccess("can lookup via tsconfig field", "tsconfig.extendsBox.json", { strict: true }, [combinePaths(basePath, "main.ts")]); 329 testSuccess("can lookup via package-relative path", "tsconfig.extendsStrict.json", { strict: true }, [combinePaths(basePath, "main.ts")]); 330 testSuccess("can lookup via non-redirected-to package-relative path", "tsconfig.extendsUnStrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); 331 testSuccess("can lookup via package-relative path with extension", "tsconfig.extendsStrictExtension.json", { strict: true }, [combinePaths(basePath, "main.ts")]); 332 testSuccess("can lookup via an implicit tsconfig", "tsconfig.extendsBoxImplied.json", { strict: true }, [combinePaths(basePath, "main.ts")]); 333 testSuccess("can lookup via an implicit tsconfig in a package-relative directory", "tsconfig.extendsBoxImpliedUnstrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); 334 testSuccess("can lookup via an implicit tsconfig in a package-relative directory with name", "tsconfig.extendsBoxImpliedUnstrictExtension.json", { strict: false }, [combinePaths(basePath, "main.ts")]); 335 testSuccess("can lookup via an implicit tsconfig in a package-relative directory with extension", "tsconfig.extendsBoxImpliedPath.json", { strict: true }, [combinePaths(basePath, "main.ts")]); 336 }); 337 338 it("adds extendedSourceFiles only once", () => { 339 const sourceFile = readJsonConfigFile("configs/fourth.json", (path) => host.readFile(path)); 340 const dir = combinePaths(basePath, "configs"); 341 const expected = [ 342 combinePaths(dir, "third.json"), 343 combinePaths(dir, "second.json"), 344 combinePaths(dir, "base.json"), 345 ]; 346 parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); 347 assert.deepEqual(sourceFile.extendedSourceFiles, expected); 348 parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); 349 assert.deepEqual(sourceFile.extendedSourceFiles, expected); 350 }); 351 }); 352 }); 353 }); 354} 355