Support recursive extended tsconfigs Closes #265 PiperOrigin-RevId: 235554123
diff --git a/internal/tsc_wrapped/tsconfig.ts b/internal/tsc_wrapped/tsconfig.ts index 512b314..f8460c3 100644 --- a/internal/tsc_wrapped/tsconfig.ts +++ b/internal/tsc_wrapped/tsconfig.ts
@@ -278,12 +278,76 @@ // TypeScript expects an absolute path for the tsconfig.json file tsconfigFile = resolveNormalizedPath(tsconfigFile); - const {config, error} = ts.readConfigFile(tsconfigFile, host.readFile); + const isUndefined = (value: any): value is undefined => value === undefined; + + // Handle bazel specific options, but make sure not to crash when reading a + // vanilla tsconfig.json. + + const readExtendedConfigFile = + (configFile: string, existingConfig?: any): {config?: any, error?: ts.Diagnostic} => { + const {config, error} = ts.readConfigFile(configFile, host.readFile); + + if (error) { + return {error}; + } + + // Allow Bazel users to control some of the bazel options. + // Since TypeScript's "extends" mechanism applies only to "compilerOptions" + // we have to repeat some of their logic to get the user's bazelOptions. + const mergedConfig = existingConfig || config; + + if (existingConfig) { + const existingBazelOpts: BazelOptions = existingConfig.bazelOptions || {}; + const newBazelBazelOpts: BazelOptions = config.bazelOptions || {}; + + mergedConfig.bazelOptions = { + ...existingBazelOpts, + + disableStrictDeps: isUndefined(existingBazelOpts.disableStrictDeps) + ? newBazelBazelOpts.disableStrictDeps + : existingBazelOpts.disableStrictDeps, + + suppressTsconfigOverrideWarnings: isUndefined(existingBazelOpts.suppressTsconfigOverrideWarnings) + ? newBazelBazelOpts.suppressTsconfigOverrideWarnings + : existingBazelOpts.suppressTsconfigOverrideWarnings, + + tsickle: isUndefined(existingBazelOpts.tsickle) + ? newBazelBazelOpts.tsickle + : existingBazelOpts.tsickle, + + googmodule: isUndefined(existingBazelOpts.googmodule) + ? newBazelBazelOpts.googmodule + : existingBazelOpts.googmodule, + + devmodeTargetOverride: isUndefined(existingBazelOpts.devmodeTargetOverride) + ? newBazelBazelOpts.devmodeTargetOverride + : existingBazelOpts.devmodeTargetOverride, + } + + if (!mergedConfig.bazelOptions.suppressTsconfigOverrideWarnings) { + warnOnOverriddenOptions(config); + } + } + + if (config.extends) { + let extendedConfigPath = resolveNormalizedPath(path.dirname(configFile), config.extends); + if (!extendedConfigPath.endsWith('.json')) extendedConfigPath += '.json'; + + return readExtendedConfigFile(extendedConfigPath, mergedConfig); + } + + return {config: mergedConfig}; + }; + + const {config, error} = readExtendedConfigFile(tsconfigFile); if (error) { // target is in the config file we failed to load... return [null, [error], {target: ''}]; } + const {options, errors, fileNames} = + ts.parseJsonConfigFileContent(config, host, path.dirname(tsconfigFile)); + // Handle bazel specific options, but make sure not to crash when reading a // vanilla tsconfig.json. const bazelOpts: BazelOptions = config.bazelOptions || {}; @@ -292,37 +356,7 @@ bazelOpts.typeBlackListPaths = bazelOpts.typeBlackListPaths || []; bazelOpts.compilationTargetSrc = bazelOpts.compilationTargetSrc || []; - // Allow Bazel users to control some of the bazel options. - // Since TypeScript's "extends" mechanism applies only to "compilerOptions" - // we have to repeat some of their logic to get the user's bazelOptions. - if (config.extends) { - let userConfigFile = - resolveNormalizedPath(path.dirname(tsconfigFile), config.extends); - if (!userConfigFile.endsWith('.json')) userConfigFile += '.json'; - const {config: userConfig, error} = - ts.readConfigFile(userConfigFile, host.readFile); - if (error) { - return [null, [error], {target}]; - } - if (userConfig.bazelOptions) { - bazelOpts.disableStrictDeps = bazelOpts.disableStrictDeps || - userConfig.bazelOptions.disableStrictDeps; - bazelOpts.suppressTsconfigOverrideWarnings = - bazelOpts.suppressTsconfigOverrideWarnings || - userConfig.bazelOptions.suppressTsconfigOverrideWarnings; - bazelOpts.tsickle = bazelOpts.tsickle || userConfig.bazelOptions.tsickle; - bazelOpts.googmodule = - bazelOpts.googmodule || userConfig.bazelOptions.googmodule; - bazelOpts.devmodeTargetOverride = bazelOpts.devmodeTargetOverride || - userConfig.bazelOptions.devmodeTargetOverride; - } - if (!bazelOpts.suppressTsconfigOverrideWarnings) { - warnOnOverriddenOptions(userConfig); - } - } - const {options, errors, fileNames} = - ts.parseJsonConfigFileContent(config, host, path.dirname(tsconfigFile)); if (errors && errors.length) { return [null, errors, {target}]; }
diff --git a/internal/tsc_wrapped/tsconfig_test.ts b/internal/tsc_wrapped/tsconfig_test.ts index 63d3e7b..eac3ffe 100644 --- a/internal/tsc_wrapped/tsconfig_test.ts +++ b/internal/tsc_wrapped/tsconfig_test.ts
@@ -57,4 +57,60 @@ expect(bazelOpts.disableStrictDeps).toBeTruthy(); } }); + + it('honors bazelOptions in recursive extends', ()=> { + const tsconfigOne = { + extends: './tsconfig-level-b.json', + files: ['a.ts'], + bazelOptions: { + disableStrictDeps: false + } + }; + + const tsconfigTwo = { + extends: './tsconfig-level-c.json', + bazelOptions: { + suppressTsconfigOverrideWarnings: true + } + }; + + const tsconfigThree = { + bazelOptions: { + tsickle: true, + suppressTsconfigOverrideWarnings: false, + disableStrictDeps: true + } + }; + + const files = { + [resolveNormalizedPath('path/to/tsconfig-level-a.json')]: JSON.stringify(tsconfigOne), + [resolveNormalizedPath('path/to/tsconfig-level-b.json')]: JSON.stringify(tsconfigTwo), + [resolveNormalizedPath('path/to/tsconfig-level-c.json')]: JSON.stringify(tsconfigThree), + }; + + const host: ts.ParseConfigHost = { + useCaseSensitiveFileNames: true, + fileExists: (path: string) => !!files[path], + readFile: (path: string) => files[path], + readDirectory( + rootDir: string, extensions: ReadonlyArray<string>, + excludes: ReadonlyArray<string>, includes: ReadonlyArray<string>, + depth: number): string[] { + return []; + }, + }; + + const [parsed, diagnostics] = + parseTsconfig('path/to/tsconfig-level-a.json', host); + expect(diagnostics).toBeNull(); + + if (!parsed) { + fail('Expected parsed'); + } else { + const {bazelOpts} = parsed; + expect(bazelOpts.tsickle).toBeTruthy(); + expect(bazelOpts.suppressTsconfigOverrideWarnings).toBeTruthy(); + expect(bazelOpts.disableStrictDeps).toBeFalsy(); + } + }) });