From 9f46acefceb1fa71ee2e8b3b3c172ceb0544b4c4 Mon Sep 17 00:00:00 2001 From: Ahn Date: Mon, 26 Oct 2020 15:07:43 +0100 Subject: [PATCH] feat(config): allow to override resolve tsconfig behavior (#2063) This change is needed because some jest transformers which want to reuse/extend our `ConfigSet` but want to change the default behavior when reading and resolving tsconfig. This PR introduces: - Change `_readTsConfig` to `_resolveTsConfig` and set `_resolveTsConfig` to `protected` - Allow to override `_resolveTsConfig` via overload signature - Expose `_transformCfgStr`, `_configSet`, `_overriddenCompilerOptions` --- src/compiler/language-service.ts | 4 +- .../__snapshots__/config-set.spec.ts.snap | 104 +++++++++--------- src/config/config-set.spec.ts | 2 +- src/config/config-set.ts | 22 ++-- src/constants.ts | 4 +- src/ts-jest-transformer.ts | 40 +++---- 6 files changed, 84 insertions(+), 92 deletions(-) diff --git a/src/compiler/language-service.ts b/src/compiler/language-service.ts index 7dfacebe25..e0f9dd5d72 100644 --- a/src/compiler/language-service.ts +++ b/src/compiler/language-service.ts @@ -75,8 +75,8 @@ export const initializeLanguageServiceInstance = (configs: ConfigSet, logger: Lo } // Initialize memory cache for typescript compiler configs.parsedTsConfig.fileNames - .filter((fileName) => !configs.isTestFile(fileName)) - .forEach((fileName) => { + .filter((fileName: string) => !configs.isTestFile(fileName)) + .forEach((fileName: string) => { memoryCache.files.set(fileName, { version: 0, }) diff --git a/src/config/__snapshots__/config-set.spec.ts.snap b/src/config/__snapshots__/config-set.spec.ts.snap index 488f51d338..b675ef0992 100644 --- a/src/config/__snapshots__/config-set.spec.ts.snap +++ b/src/config/__snapshots__/config-set.spec.ts.snap @@ -1,5 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`_resolveTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and allowSyntheticDefaultImports is false in tsConfig should use correct paths when searching 1`] = ` +Array [ + Object { + "category": 3, + "code": 151001, + "file": undefined, + "length": undefined, + "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", + "start": undefined, + }, +] +`; + +exports[`_resolveTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and allowSyntheticDefaultImports is false in tsConfig should use given tsconfig path 1`] = ` +Array [ + Object { + "category": 3, + "code": 151001, + "file": undefined, + "length": undefined, + "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", + "start": undefined, + }, +] +`; + +exports[`_resolveTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and esModuleInterop is not in tsConfig should use correct paths when searching 1`] = ` +Array [ + Object { + "category": 3, + "code": 151001, + "file": undefined, + "length": undefined, + "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", + "start": undefined, + }, +] +`; + +exports[`_resolveTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and esModuleInterop is not in tsConfig should use given tsconfig path 1`] = ` +Array [ + Object { + "category": 3, + "code": 151001, + "file": undefined, + "length": undefined, + "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", + "start": undefined, + }, +] +`; + exports[`customTransformers should return an object containing all resolved transformers 1`] = ` Object { "before": Array [ @@ -55,55 +107,3 @@ exports[`isTestFile should return a boolean value whether the file matches test exports[`isTestFile should return a boolean value whether the file matches test pattern 3`] = `true`; exports[`isTestFile should return a boolean value whether the file matches test pattern 4`] = `true`; - -exports[`readTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and allowSyntheticDefaultImports is false in tsConfig should use correct paths when searching 1`] = ` -Array [ - Object { - "category": 3, - "code": 151001, - "file": undefined, - "length": undefined, - "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", - "start": undefined, - }, -] -`; - -exports[`readTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and allowSyntheticDefaultImports is false in tsConfig should use given tsconfig path 1`] = ` -Array [ - Object { - "category": 3, - "code": 151001, - "file": undefined, - "length": undefined, - "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", - "start": undefined, - }, -] -`; - -exports[`readTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and esModuleInterop is not in tsConfig should use correct paths when searching 1`] = ` -Array [ - Object { - "category": 3, - "code": 151001, - "file": undefined, - "length": undefined, - "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", - "start": undefined, - }, -] -`; - -exports[`readTsConfig resolve configFileName normally module in tsConfig is not the same as forced module and esModuleInterop is not in tsConfig should use given tsconfig path 1`] = ` -Array [ - Object { - "category": 3, - "code": 151001, - "file": undefined, - "length": undefined, - "messageText": "If you have issues related to imports, you should consider setting \`esModuleInterop\` to \`true\` in your TypeScript configuration file (usually \`tsconfig.json\`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.", - "start": undefined, - }, -] -`; diff --git a/src/config/config-set.spec.ts b/src/config/config-set.spec.ts index a174f39601..95b0b50dd1 100644 --- a/src/config/config-set.spec.ts +++ b/src/config/config-set.spec.ts @@ -653,7 +653,7 @@ describe('resolvePath', () => { }) }) // resolvePath -describe('readTsConfig', () => { +describe('_resolveTsConfig', () => { let findConfig!: jest.SpyInstance let readConfig!: jest.SpyInstance<{ config?: any; error?: ts.Diagnostic }> let parseConfig!: jest.SpyInstance diff --git a/src/config/config-set.ts b/src/config/config-set.ts index a21bcc792f..ae736bdc89 100644 --- a/src/config/config-set.ts +++ b/src/config/config-set.ts @@ -116,7 +116,7 @@ export class ConfigSet { readonly isolatedModules: boolean readonly cwd: string tsCacheDir: string | undefined - parsedTsConfig!: ParsedCommandLine + parsedTsConfig!: ParsedCommandLine | Record customTransformers: CustomTransformers = Object.create(null) readonly rootDir: string /** @@ -139,10 +139,7 @@ export class ConfigSet { * @internal */ private _stringifyContentRegExp: RegExp | undefined - /** - * @internal - */ - private _overriddenCompilerOptions: Partial = { + protected _overriddenCompilerOptions: Partial = { // we handle sourcemaps this way and not another sourceMap: true, inlineSourceMap: false, @@ -282,7 +279,10 @@ export class ConfigSet { } const tsconfigOpt = options.tsConfig ?? options.tsconfig const configFilePath = typeof tsconfigOpt === 'string' ? this.resolvePath(tsconfigOpt) : undefined - this.parsedTsConfig = this._readTsConfig(typeof tsconfigOpt === 'object' ? tsconfigOpt : undefined, configFilePath) + this.parsedTsConfig = this._resolveTsConfig( + typeof tsconfigOpt === 'object' ? tsconfigOpt : undefined, + configFilePath, + ) // throw errors if any matching wanted diagnostics this.raiseDiagnostics(this.parsedTsConfig.errors, configFilePath) @@ -389,10 +389,10 @@ export class ConfigSet { /** * Load TypeScript configuration. Returns the parsed TypeScript config and * any `tsConfig` options specified in ts-jest tsConfig - * - * @internal */ - private _readTsConfig(compilerOptions?: CompilerOptions, resolvedConfigFile?: string): ParsedCommandLine { + protected _resolveTsConfig(compilerOptions?: CompilerOptions, resolvedConfigFile?: string): Record + // eslint-disable-next-line no-dupe-class-members + protected _resolveTsConfig(compilerOptions?: CompilerOptions, resolvedConfigFile?: string): ParsedCommandLine { let config = { compilerOptions: Object.create(null) } let basePath = normalizeSlashes(this.rootDir) const ts = this.compilerModule @@ -402,6 +402,7 @@ export class ConfigSet { : ts.findConfigFile(normalizeSlashes(this.rootDir), ts.sys.fileExists) if (configFileName) { this.logger.debug({ tsConfigFileName: configFileName }, 'readTsConfig(): reading', configFileName) + const result = ts.readConfigFile(configFileName, ts.sys.readFile) // Return diagnostics. if (result.error) { @@ -419,10 +420,8 @@ export class ConfigSet { // parse json, merge config extending others, ... const result = ts.parseJsonConfigFileContent(config, ts.sys, basePath, undefined, configFileName) - const { _overriddenCompilerOptions: forcedOptions } = this const finalOptions = result.options - // Target ES5 output by default (instead of ES3). if (finalOptions.target === undefined) { finalOptions.target = ts.ScriptTarget.ES5 @@ -484,6 +483,7 @@ export class ConfigSet { nodeJsVer: process.version, compilationTarget: config.compilerOptions.target ?? TARGET_TO_VERSION_MAPPING[compilationTarget], }) + this.logger.warn(message) } diff --git a/src/constants.ts b/src/constants.ts index bab221e10f..1c19aeb993 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,9 +1,7 @@ -/** - * @internal - */ export const LINE_FEED = '\n' export const TS_TSX_REGEX = /\.tsx?$/ export const JS_JSX_REGEX = /\.jsx?$/ +export const DECLARATION_TYPE_EXT = '.d.ts' /** * @internal * See https://jestjs.io/docs/en/configuration#testmatch-arraystring diff --git a/src/ts-jest-transformer.ts b/src/ts-jest-transformer.ts index e60e1ffca3..54c582a60f 100644 --- a/src/ts-jest-transformer.ts +++ b/src/ts-jest-transformer.ts @@ -4,7 +4,7 @@ import type { Config } from '@jest/types' import type { Logger } from 'bs-logger' import { ConfigSet } from './config/config-set' -import { JS_JSX_REGEX, TS_TSX_REGEX } from './constants' +import { DECLARATION_TYPE_EXT, JS_JSX_REGEX, TS_TSX_REGEX } from './constants' import { stringify } from './utils/json' import { JsonableValue } from './utils/jsonable-value' import { rootLogger } from './utils/logger' @@ -24,14 +24,8 @@ export class TsJestTransformer implements Transformer { */ private static readonly _cachedConfigSets: CachedConfigSet[] = [] protected readonly logger: Logger - /** - * @internal - */ - private _transformCfgStr!: string - /** - * @internal - */ - private _tsJestCfgSet!: ConfigSet + protected _transformCfgStr!: string + protected _configSet!: ConfigSet constructor() { this.logger = rootLogger.child({ namespace: 'ts-jest-transformer' }) @@ -49,10 +43,10 @@ export class TsJestTransformer implements Transformer { let result: string | TransformedSource const source: string = input - const { hooks } = this._tsJestCfgSet - const shouldStringifyContent = this._tsJestCfgSet.shouldStringifyContent(filePath) - const babelJest = shouldStringifyContent ? undefined : this._tsJestCfgSet.babelJestTransformer - const isDefinitionFile = filePath.endsWith('.d.ts') + const { hooks } = this._configSet + const shouldStringifyContent = this._configSet.shouldStringifyContent(filePath) + const babelJest = shouldStringifyContent ? undefined : this._configSet.babelJestTransformer + const isDefinitionFile = filePath.endsWith(DECLARATION_TYPE_EXT) const isJsFile = JS_JSX_REGEX.test(filePath) const isTsFile = !isDefinitionFile && TS_TSX_REGEX.test(filePath) if (shouldStringifyContent) { @@ -61,7 +55,7 @@ export class TsJestTransformer implements Transformer { } else if (isDefinitionFile) { // do not try to compile declaration files result = '' - } else if (!this._tsJestCfgSet.parsedTsConfig.options.allowJs && isJsFile) { + } else if (!this._configSet.parsedTsConfig.options.allowJs && isJsFile) { // we've got a '.js' but the compiler option `allowJs` is not set or set to false this.logger.warn({ fileName: filePath }, interpolate(Errors.GotJsFileButAllowJsFalse, { path: filePath })) @@ -69,7 +63,7 @@ export class TsJestTransformer implements Transformer { } else if (isJsFile || isTsFile) { // transpile TS code (source maps are included) /* istanbul ignore if */ - result = this._tsJestCfgSet.tsCompiler.compile(source, filePath) + result = this._configSet.tsCompiler.compile(source, filePath) } else { // we should not get called for files with other extension than js[x], ts[x] and d.ts, // TypeScript will bail if we try to compile, and if it was to call babel, users can @@ -131,7 +125,7 @@ export class TsJestTransformer implements Transformer { ) if (ccs) { this._transformCfgStr = ccs.transformerCfgStr - this._tsJestCfgSet = ccs.configSet + this._configSet = ccs.configSet } else { // try to look-it up by stringified version const serializedJestCfg = stringify(jestConfig) @@ -144,24 +138,24 @@ export class TsJestTransformer implements Transformer { // the config, and then it calls the transformer with the proper object serializedCcs.jestConfig.value = jestConfig this._transformCfgStr = serializedCcs.transformerCfgStr - this._tsJestCfgSet = serializedCcs.configSet + this._configSet = serializedCcs.configSet } else { // create the new record in the index this.logger.info('no matching config-set found, creating a new one') - this._tsJestCfgSet = new ConfigSet(jestConfig) + this._configSet = new ConfigSet(jestConfig) this._transformCfgStr = new JsonableValue({ - digest: this._tsJestCfgSet.tsJestDigest, - babel: this._tsJestCfgSet.babelConfig, + digest: this._configSet.tsJestDigest, + babel: this._configSet.babelConfig, ...jestConfig, tsconfig: { - options: this._tsJestCfgSet.parsedTsConfig.options, - raw: this._tsJestCfgSet.parsedTsConfig.raw, + options: this._configSet.parsedTsConfig.options, + raw: this._configSet.parsedTsConfig.raw, }, }).serialized TsJestTransformer._cachedConfigSets.push({ jestConfig: new JsonableValue(jestConfig), - configSet: this._tsJestCfgSet, + configSet: this._configSet, transformerCfgStr: this._transformCfgStr, }) }