diff --git a/e2e/__tests__/__snapshots__/logger.test.ts.snap b/e2e/__tests__/__snapshots__/logger.test.ts.snap index 77a4803f96..b3c4251ede 100644 --- a/e2e/__tests__/__snapshots__/logger.test.ts.snap +++ b/e2e/__tests__/__snapshots__/logger.test.ts.snap @@ -8,19 +8,21 @@ Array [ "[level:20] checking version of jest: OK", "[level:20] created new transformer", "[level:30] no matching config-set found, creating a new one", - "[level:20] backporting config", - "[level:20] normalized jest config", - "[level:20] normalized ts-jest config", - "[level:20] babel is disabled", "[level:20] loaded module typescript", "[level:20] patching typescript", "[level:20] checking version of typescript: OK", + "[level:20] normalized compiler module config via ts-jest option", + "[level:20] backporting config", + "[level:20] normalized jest config", + "[level:20] babel is disabled", + "[level:20] normalized diagnostics config via ts-jest option", "[level:20] readTsConfig(): reading /tsconfig.json", - "[level:20] normalized typescript config", + "[level:20] normalized typescript config via ts-jest option", + "[level:20] normalized custom AST transformers via ts-jest option", + "[level:20] file caching disabled", "[level:20] computing cache key for /Hello.spec.ts", "[level:20] processing /Hello.spec.ts", "[level:20] initializeLanguageServiceInstance(): create typescript compiler", - "[level:20] file caching disabled", "[level:20] initializeLanguageServiceInstance(): creating language service", "[level:20] compileAndUpdateOutput(): get compile output", "[level:20] compileFn(): compiling using language service", @@ -45,23 +47,25 @@ Array [ "[level:20] checking version of jest: OK", "[level:20] created new transformer", "[level:30] no matching config-set found, creating a new one", - "[level:20] backporting config", - "[level:20] normalized jest config", - "[level:20] normalized ts-jest config", - "[level:20] normalized babel config via ts-jest option", "[level:20] loaded module typescript", "[level:20] patching typescript", "[level:20] checking version of typescript: OK", - "[level:20] readTsConfig(): reading /tsconfig.json", - "[level:20] normalized typescript config", - "[level:20] computing cache key for /Hello.spec.ts", - "[level:20] processing /Hello.spec.ts", - "[level:20] creating babel-jest transformer", + "[level:20] normalized compiler module config via ts-jest option", + "[level:20] backporting config", + "[level:20] normalized jest config", + "[level:20] normalized babel config via ts-jest option", "[level:20] loaded module babel-jest", "[level:20] patching babel-jest", "[level:20] checking version of babel-jest: OK", - "[level:20] initializeLanguageServiceInstance(): create typescript compiler", + "[level:20] created babel-jest transformer", + "[level:20] normalized diagnostics config via ts-jest option", + "[level:20] readTsConfig(): reading /tsconfig.json", + "[level:20] normalized typescript config via ts-jest option", + "[level:20] normalized custom AST transformers via ts-jest option", "[level:20] file caching disabled", + "[level:20] computing cache key for /Hello.spec.ts", + "[level:20] processing /Hello.spec.ts", + "[level:20] initializeLanguageServiceInstance(): create typescript compiler", "[level:20] initializeLanguageServiceInstance(): creating language service", "[level:20] compileAndUpdateOutput(): get compile output", "[level:20] compileFn(): compiling using language service", @@ -88,24 +92,26 @@ Array [ "[level:20] checking version of jest: OK", "[level:20] created new transformer", "[level:30] no matching config-set found, creating a new one", + "[level:20] loaded module typescript", + "[level:20] patching typescript", + "[level:20] checking version of typescript: OK", + "[level:20] normalized compiler module config via ts-jest option", "[level:20] backporting config", "[level:20] normalized jest config", "[level:20] resolved path from babel.config.js to /babel.config.js", - "[level:20] normalized ts-jest config", "[level:20] normalized babel config via ts-jest option", - "[level:20] loaded module typescript", - "[level:20] patching typescript", - "[level:20] checking version of typescript: OK", - "[level:20] readTsConfig(): reading /tsconfig.json", - "[level:20] normalized typescript config", - "[level:20] computing cache key for /Hello.spec.ts", - "[level:20] processing /Hello.spec.ts", - "[level:20] creating babel-jest transformer", "[level:20] loaded module babel-jest", "[level:20] patching babel-jest", "[level:20] checking version of babel-jest: OK", - "[level:20] initializeLanguageServiceInstance(): create typescript compiler", + "[level:20] created babel-jest transformer", + "[level:20] normalized diagnostics config via ts-jest option", + "[level:20] readTsConfig(): reading /tsconfig.json", + "[level:20] normalized typescript config via ts-jest option", + "[level:20] normalized custom AST transformers via ts-jest option", "[level:20] file caching disabled", + "[level:20] computing cache key for /Hello.spec.ts", + "[level:20] processing /Hello.spec.ts", + "[level:20] initializeLanguageServiceInstance(): create typescript compiler", "[level:20] initializeLanguageServiceInstance(): creating language service", "[level:20] compileAndUpdateOutput(): get compile output", "[level:20] compileFn(): compiling using language service", @@ -124,12 +130,13 @@ Array [ ] `; -exports[`ts-jest logging deprecation warning with astTransformers config as an object should pass using template "default" 1`] = ` +exports[`ts-jest logging deprecation warning with astTransformers config as string array should pass using template "default" 1`] = ` √ jest ↳ exit code: 0 ===[ STDOUT ]=================================================================== ===[ STDERR ]=================================================================== + ts-jest[config] (WARN) The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers PASS ./Hello.spec.ts Hello Class √ should create a new Hello @@ -142,12 +149,13 @@ exports[`ts-jest logging deprecation warning with astTransformers config as an o ================================================================================ `; -exports[`ts-jest logging deprecation warning with astTransformers config as an object should pass using template "with-babel-7" 1`] = ` +exports[`ts-jest logging deprecation warning with astTransformers config as string array should pass using template "with-babel-7" 1`] = ` √ jest ↳ exit code: 0 ===[ STDOUT ]=================================================================== ===[ STDERR ]=================================================================== + ts-jest[config] (WARN) The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers PASS ./Hello.spec.ts Hello Class √ should create a new Hello @@ -160,12 +168,13 @@ exports[`ts-jest logging deprecation warning with astTransformers config as an o ================================================================================ `; -exports[`ts-jest logging deprecation warning with astTransformers config as an object should pass using template "with-babel-7-string-config" 1`] = ` +exports[`ts-jest logging deprecation warning with astTransformers config as string array should pass using template "with-babel-7-string-config" 1`] = ` √ jest ↳ exit code: 0 ===[ STDOUT ]=================================================================== ===[ STDERR ]=================================================================== + ts-jest[config] (WARN) The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers PASS ./Hello.spec.ts Hello Class √ should create a new Hello @@ -178,13 +187,13 @@ exports[`ts-jest logging deprecation warning with astTransformers config as an o ================================================================================ `; -exports[`ts-jest logging deprecation warning with astTransformers config as string array should pass using template "default" 1`] = ` +exports[`ts-jest logging deprecation warning with packageJson config should pass using template "default" 1`] = ` √ jest ↳ exit code: 0 ===[ STDOUT ]=================================================================== ===[ STDERR ]=================================================================== - ts-jest[config] (WARN) The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers + ts-jest[config] (WARN) The option \`packageJson\` is deprecated and will be removed in ts-jest 27. This option is not used by internal \`ts-jest\` PASS ./Hello.spec.ts Hello Class √ should create a new Hello @@ -197,13 +206,13 @@ exports[`ts-jest logging deprecation warning with astTransformers config as stri ================================================================================ `; -exports[`ts-jest logging deprecation warning with astTransformers config as string array should pass using template "with-babel-7" 1`] = ` +exports[`ts-jest logging deprecation warning with packageJson config should pass using template "with-babel-7" 1`] = ` √ jest ↳ exit code: 0 ===[ STDOUT ]=================================================================== ===[ STDERR ]=================================================================== - ts-jest[config] (WARN) The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers + ts-jest[config] (WARN) The option \`packageJson\` is deprecated and will be removed in ts-jest 27. This option is not used by internal \`ts-jest\` PASS ./Hello.spec.ts Hello Class √ should create a new Hello @@ -216,13 +225,70 @@ exports[`ts-jest logging deprecation warning with astTransformers config as stri ================================================================================ `; -exports[`ts-jest logging deprecation warning with astTransformers config as string array should pass using template "with-babel-7-string-config" 1`] = ` +exports[`ts-jest logging deprecation warning with packageJson config should pass using template "with-babel-7-string-config" 1`] = ` √ jest ↳ exit code: 0 ===[ STDOUT ]=================================================================== ===[ STDERR ]=================================================================== - ts-jest[config] (WARN) The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers + ts-jest[config] (WARN) The option \`packageJson\` is deprecated and will be removed in ts-jest 27. This option is not used by internal \`ts-jest\` + PASS ./Hello.spec.ts + Hello Class + √ should create a new Hello + + Test Suites: 1 passed, 1 total + Tests: 1 passed, 1 total + Snapshots: 0 total + Time: XXs + Ran all test suites. + ================================================================================ +`; + +exports[`ts-jest logging deprecation warning with tsConfig config should pass using template "default" 1`] = ` + √ jest + ↳ exit code: 0 + ===[ STDOUT ]=================================================================== + + ===[ STDERR ]=================================================================== + ts-jest[config] (WARN) The option \`tsConfig\` is deprecated and will be removed in ts-jest 27, use \`tsconfig\` instead + PASS ./Hello.spec.ts + Hello Class + √ should create a new Hello + + Test Suites: 1 passed, 1 total + Tests: 1 passed, 1 total + Snapshots: 0 total + Time: XXs + Ran all test suites. + ================================================================================ +`; + +exports[`ts-jest logging deprecation warning with tsConfig config should pass using template "with-babel-7" 1`] = ` + √ jest + ↳ exit code: 0 + ===[ STDOUT ]=================================================================== + + ===[ STDERR ]=================================================================== + ts-jest[config] (WARN) The option \`tsConfig\` is deprecated and will be removed in ts-jest 27, use \`tsconfig\` instead + PASS ./Hello.spec.ts + Hello Class + √ should create a new Hello + + Test Suites: 1 passed, 1 total + Tests: 1 passed, 1 total + Snapshots: 0 total + Time: XXs + Ran all test suites. + ================================================================================ +`; + +exports[`ts-jest logging deprecation warning with tsConfig config should pass using template "with-babel-7-string-config" 1`] = ` + √ jest + ↳ exit code: 0 + ===[ STDOUT ]=================================================================== + + ===[ STDERR ]=================================================================== + ts-jest[config] (WARN) The option \`tsConfig\` is deprecated and will be removed in ts-jest 27, use \`tsconfig\` instead PASS ./Hello.spec.ts Hello Class √ should create a new Hello diff --git a/e2e/__tests__/logger.test.ts b/e2e/__tests__/logger.test.ts index 82079bcf48..8c191f7b7a 100644 --- a/e2e/__tests__/logger.test.ts +++ b/e2e/__tests__/logger.test.ts @@ -1,7 +1,7 @@ import { LogContexts, LogLevels } from 'bs-logger' import { existsSync } from 'fs' -import { PackageSets, allValidPackageSets } from '../__helpers__/templates' +import { PackageSets, allValidPackageSets, allPackageSetsWithPreset } from '../__helpers__/templates' import { configureTestCase } from '../__helpers__/test-case' describe('ts-jest logging', () => { @@ -108,14 +108,30 @@ describe('ts-jest logging', () => { }) }) - describe('with astTransformers config as an object', () => { + describe('with packageJson config', () => { const testCase = configureTestCase('simple', { tsJestConfig: { - astTransformers: {} + packageJson: true, } }) - testCase.runWithTemplates(allValidPackageSets, 0, (runTest, { testLabel }) => { + testCase.runWithTemplates(allPackageSetsWithPreset, 0, (runTest, { testLabel }) => { + it(testLabel, () => { + const result = runTest() + expect(result.status).toBe(0) + expect(result).toMatchSnapshot() + }) + }) + }) + + describe('with tsConfig config', () => { + const testCase = configureTestCase('simple', { + tsJestConfig: { + tsConfig: true, + } + }) + + testCase.runWithTemplates(allPackageSetsWithPreset, 0, (runTest, { testLabel }) => { it(testLabel, () => { const result = runTest() expect(result.status).toBe(0) diff --git a/src/compiler/instance.ts b/src/compiler/instance.ts index 96a716ab26..233f3b6257 100644 --- a/src/compiler/instance.ts +++ b/src/compiler/instance.ts @@ -62,7 +62,6 @@ export const createCompilerInstance = (configs: ConfigSet): TsCompiler => { const logger = configs.logger.child({ namespace: 'ts-compiler' }) const { parsedTsConfig: { options: compilerOptions }, - tsJest, } = configs const extensions = ['.ts', '.tsx'] // Enable `allowJs` when flag is set. @@ -70,7 +69,7 @@ export const createCompilerInstance = (configs: ConfigSet): TsCompiler => { extensions.push('.js') extensions.push('.jsx') } - const compilerInstance: CompilerInstance = !tsJest.isolatedModules + const compilerInstance: CompilerInstance = !configs.isolatedModules ? initializeLanguageServiceInstance(configs, logger) // Use language services by default : initializeTranspilerInstance(configs, logger) const compile = compileAndUpdateOutput(compilerInstance.compileFn, logger) diff --git a/src/compiler/language-service.ts b/src/compiler/language-service.ts index 323d23a467..d26f95f3ac 100644 --- a/src/compiler/language-service.ts +++ b/src/compiler/language-service.ts @@ -34,7 +34,7 @@ function doTypeChecking( service: _ts.LanguageService, logger: Logger, ): void { - if (configs.shouldReportDiagnostic(fileName)) { + if (configs.shouldReportDiagnostics(fileName)) { // Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`. const diagnostics = service.getSemanticDiagnostics(fileName).concat(service.getSyntacticDiagnostics(fileName)) diagnosedFiles.push(fileName) @@ -200,7 +200,7 @@ export const initializeLanguageServiceInstance = (configs: ConfigSet, logger: Lo getCurrentDirectory: () => cwd, getCompilationSettings: () => options, getDefaultLibFileName: () => ts.getDefaultLibFilePath(options), - getCustomTransformers: () => configs.tsCustomTransformers, + getCustomTransformers: () => configs.customTransformers, resolveModuleNames, } diff --git a/src/compiler/transpiler.ts b/src/compiler/transpiler.ts index fa954025fd..edf6f49ab0 100644 --- a/src/compiler/transpiler.ts +++ b/src/compiler/transpiler.ts @@ -19,11 +19,11 @@ export const initializeTranspilerInstance = (configs: ConfigSet, logger: Logger) const result: _ts.TranspileOutput = ts.transpileModule(code, { fileName, - transformers: configs.tsCustomTransformers, + transformers: configs.customTransformers, compilerOptions: options, - reportDiagnostics: configs.shouldReportDiagnostic(fileName), + reportDiagnostics: configs.shouldReportDiagnostics(fileName), }) - if (result.diagnostics && configs.shouldReportDiagnostic(fileName)) { + if (result.diagnostics && configs.shouldReportDiagnostics(fileName)) { configs.raiseDiagnostics(result.diagnostics, fileName, logger) } diff --git a/src/config/__snapshots__/config-set.spec.ts.snap b/src/config/__snapshots__/config-set.spec.ts.snap index 52869a2d13..488f51d338 100644 --- a/src/config/__snapshots__/config-set.spec.ts.snap +++ b/src/config/__snapshots__/config-set.spec.ts.snap @@ -1,5 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`customTransformers should return an object containing all resolved transformers 1`] = ` +Object { + "before": Array [ + [Function], + ], +} +`; + +exports[`customTransformers should return an object containing all resolved transformers 2`] = ` +Object { + "before": Array [ + [Function], + [Function], + ], +} +`; + +exports[`customTransformers should return an object containing all resolved transformers 3`] = ` +Object { + "after": Array [ + [Function], + ], + "before": Array [ + [Function], + ], +} +`; + +exports[`customTransformers should return an object containing all resolved transformers 4`] = ` +Object { + "afterDeclarations": Array [ + [Function], + ], + "before": Array [ + [Function], + ], +} +`; + +exports[`customTransformers should return an object containing all resolved transformers 5`] = ` +Object { + "before": Array [ + [Function], + [Function], + ], +} +`; + exports[`isTestFile should return a boolean value whether the file matches test pattern 1`] = `true`; exports[`isTestFile should return a boolean value whether the file matches test pattern 2`] = `true`; @@ -59,233 +107,3 @@ Array [ }, ] `; - -exports[`tsCustomTransformers should return an object containing all resolved transformers 1`] = ` -Object { - "before": Array [ - [Function], - ], -} -`; - -exports[`tsCustomTransformers should return an object containing all resolved transformers 2`] = ` -Object { - "before": Array [ - [Function], - [Function], - ], -} -`; - -exports[`tsCustomTransformers should return an object containing all resolved transformers 3`] = ` -Object { - "after": Array [ - [Function], - ], - "before": Array [ - [Function], - ], -} -`; - -exports[`tsCustomTransformers should return an object containing all resolved transformers 4`] = ` -Object { - "afterDeclarations": Array [ - [Function], - ], - "before": Array [ - [Function], - ], -} -`; - -exports[`tsJest diagnostics should be correct for various kinds of ignoreCodes 1`] = ` -Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - 10, - ], - "pathRegex": undefined, - "pretty": true, - "throws": true, -} -`; - -exports[`tsJest diagnostics should be correct for various kinds of ignoreCodes 2`] = ` -Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - 10, - ], - "pathRegex": undefined, - "pretty": true, - "throws": true, -} -`; - -exports[`tsJest diagnostics should be correct for various kinds of ignoreCodes 3`] = ` -Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - 2571, - ], - "pathRegex": undefined, - "pretty": true, - "throws": true, -} -`; - -exports[`tsJest diagnostics should be correct for various kinds of ignoreCodes 4`] = ` -Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - 1009, - 2571, - 4072, - ], - "pathRegex": undefined, - "pretty": true, - "throws": true, -} -`; - -exports[`tsJest diagnostics should be correct for various kinds of ignoreCodes 5`] = ` -Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - 1009, - 2571, - 6031, - 10, - ], - "pathRegex": undefined, - "pretty": true, - "throws": true, -} -`; - -exports[`tsJest diagnostics should be correct for various kinds of ignoreCodes 6`] = ` -Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - ], - "pathRegex": undefined, - "pretty": true, - "throws": true, -} -`; - -exports[`tsJest diagnostics should be correct for various kinds of ignoreCodes 7`] = ` -Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - ], - "pathRegex": undefined, - "pretty": true, - "throws": true, -} -`; - -exports[`tsJest jest should merge parent config if any with globals is an empty object 1`] = ` -Object { - "babelConfig": undefined, - "compiler": "typescript", - "diagnostics": Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - ], - "pretty": true, - "throws": true, - }, - "isolatedModules": false, - "stringifyContentPathRegex": undefined, - "transformers": Object {}, - "tsConfig": Object { - "kind": "file", - "value": undefined, - }, -} -`; - -exports[`tsJest jest should merge parent config if any with globals is undefined 1`] = ` -Object { - "babelConfig": undefined, - "compiler": "typescript", - "diagnostics": Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - ], - "pretty": true, - "throws": true, - }, - "isolatedModules": false, - "stringifyContentPathRegex": undefined, - "transformers": Object {}, - "tsConfig": Object { - "kind": "file", - "value": undefined, - }, -} -`; - -exports[`tsJest jest should return correct config and go thru backports 1`] = ` -Object { - "babelConfig": undefined, - "compiler": "typescript", - "diagnostics": Object { - "ignoreCodes": Array [ - 6059, - 18002, - 18003, - ], - "pretty": true, - "throws": true, - }, - "isolatedModules": false, - "stringifyContentPathRegex": undefined, - "transformers": Object {}, - "tsConfig": Object { - "kind": "file", - "value": undefined, - }, -} -`; - -exports[`tsJest transformers should display deprecation warning message when config transformers is string array 1`] = ` -"[level:40] The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers -" -`; - -exports[`tsJest transformers should support transformers with options 1`] = ` -Array [ - Object { - "options": Object { - "foo": 1, - }, - "path": Any, - }, -] -`; - -exports[`tsJest tsconfig should show warning message with tsConfig option 1`] = ` -"[level:40] The option \`tsConfig\` is deprecated and will be removed in ts-jest 27, use \`tsconfig\` instead -" -`; diff --git a/src/config/config-set.spec.ts b/src/config/config-set.spec.ts index 5dcc28ecbb..77646bab11 100644 --- a/src/config/config-set.spec.ts +++ b/src/config/config-set.spec.ts @@ -1,21 +1,18 @@ /* eslint-disable jest/no-mocks-import */ import type { Transformer } from '@jest/transform' -import { LogLevels, testing } from 'bs-logger' +import { testing } from 'bs-logger' import { join, resolve } from 'path' import ts from 'typescript' import { logTargetMock } from '../__helpers__/mocks' import { createConfigSet } from '../__helpers__/fakers' -import type { TsJestGlobalOptions, TsJestConfig } from '../types' +import type { TsJestGlobalOptions } from '../types' import * as _backports from '../utils/backports' import { getPackageVersion } from '../utils/get-package-version' import { normalizeSlashes } from '../utils/normalize-slashes' import { mocked } from '../utils/testing' -import { IGNORE_DIAGNOSTIC_CODES, MY_DIGEST, TS_JEST_OUT_DIR } from './config-set' -// eslint-disable-next-line no-duplicate-imports -import type { ConfigSet } from './config-set' -import { Deprecations } from '../utils/messages' +import { ConfigSet, MY_DIGEST, TS_JEST_OUT_DIR } from './config-set' jest.mock('../utils/backports') jest.mock('../index') @@ -32,348 +29,24 @@ beforeEach(() => { jest.clearAllMocks() }) -describe('isTestFile', () => { - it.each([ - { - jestConfig: { - testRegex: [{}], - testMatch: [], - } as any, - }, - { - jestConfig: { - testMatch: [], - testRegex: [/.*\.(spec|test)\.[jt]sx?$/], - } as any, - }, - { - jestConfig: { - testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], - testRegex: [], - } as any, - }, - { - jestConfig: { - testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], - testRegex: ['**/?(*.)+(foo|bar).[tj]s?(x)'], - } as any, - }, - ])('should return a boolean value whether the file matches test pattern', (config) => { - expect(createConfigSet(config).isTestFile('foo.spec.ts')).toMatchSnapshot() - }) -}) - -describe('tsJest', () => { - const getConfigSet = (tsJest?: TsJestGlobalOptions) => createConfigSet({ tsJestConfig: tsJest }) - const getTsJest = (tsJest?: TsJestGlobalOptions) => getConfigSet(tsJest).tsJest - - describe('jest', () => { - it('should return correct config and go thru backports', () => { - expect(createConfigSet().tsJest).toMatchSnapshot() - expect(backports.backportJestConfig).toHaveBeenCalledTimes(1) - }) - - it('should merge parent config if any with globals is an empty object', () => { - expect( - createConfigSet({ - jestConfig: { - globals: Object.create(null), - }, - }).tsJest, - ).toMatchSnapshot() - }) - - it('should merge parent config if any with globals is undefined', () => { - expect( +describe('packageJson', () => { + it('should not contain packageJson in final tsJest config', () => { + expect( + Object.keys( createConfigSet({ jestConfig: { - globals: undefined, - } as any, - }).tsJest, - ).toMatchSnapshot() - }) - }) - - describe('tsconfig', () => { - const logger = testing.createLoggerMock() - - beforeEach(() => { - logger.target.clear() - }) - - it('should show warning message with tsConfig option', () => { - const cs = createConfigSet({ - jestConfig: { - globals: { - 'ts-jest': { - tsConfig: { - resolveJsonModule: true, - }, - }, - }, - } as any, - logger, - resolve: null, - }) - - expect(cs.tsJest.tsConfig).toEqual({ - kind: 'inline', - value: { - resolveJsonModule: true, - }, - }) - expect(logger.target.lines[0]).toMatchSnapshot() - }) - - it('should not show warning message with tsconfig option', () => { - const cs = createConfigSet({ - jestConfig: { - globals: { - 'ts-jest': { - tsconfig: { - resolveJsonModule: true, + globals: { + 'ts-jest': { + packageJson: true, }, }, - }, - } as any, - logger, - resolve: null, - }) - - expect(cs.tsJest.tsConfig).toEqual({ - kind: 'inline', - value: { - resolveJsonModule: true, - }, - }) - expect(logger.target.lines[0]).not.toContain(Deprecations.TsConfig) - }) - }) - - describe('packageJson', () => { - const logger = testing.createLoggerMock() - let tsJestCfg: TsJestConfig - - beforeEach(() => { - logger.target.clear() - tsJestCfg = createConfigSet({ - jestConfig: { - globals: { - 'ts-jest': { - packageJson: true, - }, - }, - } as any, - logger, - resolve: null, - }).tsJest - }) - - it('should not contain packageJson in final tsJest config', () => { - expect(Object.keys(tsJestCfg)).not.toContain('packageJson') - }) - - it('should show warning message when packageJson is provided', () => { - expect(logger.target.filteredLines(LogLevels.warn)[0]).toMatchInlineSnapshot(` - "[level:40] The option \`packageJson\` is deprecated and will be removed in ts-jest 27. This option is not used by internal \`ts-jest\` - " - `) - }) + } as any, + resolve: null, + }), + ), + ).not.toContain('packageJson') }) - - describe('transformers', () => { - const logger = testing.createLoggerMock() - - it('should display deprecation warning message when config transformers is string array', () => { - const cs = createConfigSet({ - jestConfig: { - rootDir: 'src', - cwd: 'src', - globals: { - 'ts-jest': { - astTransformers: ['dummy-transformer'], - }, - }, - } as any, - logger, - resolve: null, - }) - logger.target.clear() - - expect(Object.keys(cs.tsJest.transformers)).toHaveLength(1) - expect(logger.target.lines[0]).toMatchSnapshot() - }) - - it('should support transformers with options', () => { - const cs = createConfigSet({ - jestConfig: { - rootDir: 'src', - cwd: 'src', - globals: { - 'ts-jest': { - astTransformers: { - before: [ - { - path: 'dummy-transformer', - options: { - foo: 1, - }, - }, - ], - }, - }, - }, - } as any, - logger, - resolve: null, - }) - - expect(cs.tsJest.transformers.before).toMatchSnapshot([ - { - path: expect.any(String), - }, - ]) - }) - - it.each([ - {}, - { - before: ['dummy-transformer'], - }, - { - after: ['dummy-transformer'], - }, - { - afterDeclarations: ['dummy-transformer'], - }, - ])('should not display deprecation warning message when config transformers is an object', (data) => { - const cs = createConfigSet({ - jestConfig: { - rootDir: 'src', - cwd: 'src', - globals: { - 'ts-jest': { - astTransformers: data, - }, - }, - } as any, - logger, - resolve: null, - }) - logger.target.clear() - - expect(Object.keys(cs.tsJest.transformers)).toHaveLength(Object.keys(data).length) - expect(logger.target.lines[0]).not.toContain(Deprecations.AstTransformerArrayConfig) - }) - }) // custom AST transformers - - describe('diagnostics', () => { - it('should be correct for default value', () => { - const EXPECTED = { - ignoreCodes: IGNORE_DIAGNOSTIC_CODES, - pretty: true, - throws: true, - } - expect(getTsJest().diagnostics).toEqual(EXPECTED) - expect(getTsJest({ diagnostics: true }).diagnostics).toEqual(EXPECTED) - }) - - it('should be correct for false', () => { - const EXPECTED = { - ignoreCodes: [], - pretty: true, - throws: false, - } - expect(getTsJest({ diagnostics: false }).diagnostics).toEqual(EXPECTED) - }) - - it('should be correct for inline config', () => { - const EXPECTED = { - ignoreCodes: [...IGNORE_DIAGNOSTIC_CODES, 10, 25], - pretty: false, - pathRegex: '\\.test\\.ts', - throws: true, - } - expect( - getTsJest({ - diagnostics: { - ignoreCodes: '10, 25', - pathRegex: EXPECTED.pathRegex, - pretty: false, - }, - }).diagnostics, - ).toEqual(EXPECTED) - expect( - getTsJest({ - diagnostics: { - ignoreCodes: ['10', 25], - pretty: false, - pathRegex: RegExp(EXPECTED.pathRegex), - }, - }).diagnostics, - ).toEqual(EXPECTED) - }) - - it('should have correct throws value', () => { - const EXPECTED = { - ignoreCodes: IGNORE_DIAGNOSTIC_CODES, - pretty: true, - } - expect(getTsJest({ diagnostics: { warnOnly: true } }).diagnostics).toEqual({ ...EXPECTED, throws: false }) - expect(getTsJest({ diagnostics: { warnOnly: false } }).diagnostics).toEqual({ ...EXPECTED, throws: true }) - }) - - it.each([ - '10', - 10, - 'TS2571', - '1009, TS2571, 4072', - [1009, 'TS2571', '6031', 'TS6031, 10', NaN, 'undefined', 'null', ''], - '', - NaN, - ])('should be correct for various kinds of ignoreCodes', (ignoreCodes) => { - expect( - getTsJest({ - diagnostics: { - ignoreCodes, - }, - }).diagnostics, - ).toMatchSnapshot() - }) - }) // diagnostics - - describe('stringifyContentPathRegex', () => { - it('should be correct for default value', () => { - expect(getTsJest().stringifyContentPathRegex).toBeUndefined() - expect(getTsJest({ stringifyContentPathRegex: null as any }).stringifyContentPathRegex).toBeUndefined() - }) - it('should be normalized to a string', () => { - expect(getTsJest({ stringifyContentPathRegex: /abc/ }).stringifyContentPathRegex).toBe('abc') - expect(getTsJest({ stringifyContentPathRegex: 'abc' }).stringifyContentPathRegex).toBe('abc') - }) - }) // stringifyContentPathRegex - - describe('isolatedModules', () => { - it('should be correct for default value', () => { - expect(getTsJest().isolatedModules).toBe(false) - expect(getTsJest({ isolatedModules: false }).isolatedModules).toBe(false) - }) - it('should be normalized to a boolean', () => { - expect(getTsJest({ isolatedModules: 'yes' as any }).isolatedModules).toBe(true) - expect(getTsJest({ isolatedModules: 1 as any }).isolatedModules).toBe(true) - }) - }) // isolatedModules - - describe('compiler', () => { - it('should be correct for default value', () => { - expect(getTsJest().compiler).toBe('typescript') - expect(getTsJest({ compiler: 'typescript' }).compiler).toBe('typescript') - }) - it('should be given non-default value', () => { - expect(getTsJest({ compiler: 'ttypescript' }).compiler).toBe('ttypescript') - }) - }) // compiler -}) // tsJest +}) // packageJson describe('parsedTsConfig', () => { const get = (tsJest?: TsJestGlobalOptions) => createConfigSet({ tsJestConfig: tsJest }).parsedTsConfig @@ -452,82 +125,579 @@ describe('parsedTsConfig', () => { esModuleInterop: false, }) expect(cs.parsedTsConfig.options.allowSyntheticDefaultImports).toBeFalsy() - expect(target.lines.warn).toHaveLength(0) }) }) // parsedTsConfig -describe('resolvePath', () => { - it('should resolve paths', () => { - const cs = createConfigSet({ jestConfig: { rootDir: '/root', cwd: '/cwd' } as any, resolve: null }) - const doResolve = (path: string) => cs.resolvePath(path, { throwIfMissing: false }) - expect(doResolve('bar.js')).toBe(resolve('/cwd/bar.js')) - expect(doResolve('./bar.js')).toBe(resolve('/cwd/./bar.js')) - expect(doResolve('bar.js')).toBe(resolve('/root/bar.js')) - expect(doResolve('/bar.js')).toBe(resolve('/root//bar.js')) - }) - it('should resolve node paths', () => { - const cs = createConfigSet({ jestConfig: { rootDir: '/root', cwd: '/cwd' } as any, resolve: null }) - const doResolve = (path: string) => cs.resolvePath(path, { throwIfMissing: false, nodeResolve: true }) - expect(doResolve('json5')).toBe(resolve(__dirname, '../../node_modules/json5', require('json5/package.json').main)) - expect(doResolve('./bar.js')).toBe(resolve('/cwd/bar.js')) - expect(doResolve('bar.js')).toBe(resolve('/root/bar.js')) - expect(doResolve('/bar.js')).toBe(resolve('/root//bar.js')) - }) - it('should throw for invalid paths', () => { - const cs = createConfigSet({ jestConfig: { rootDir: __dirname, cwd: __dirname } as any, resolve: null }) - const doResolve = (path: string) => cs.resolvePath(path) - expect(() => doResolve('bar.js')).toThrow() - expect(() => doResolve('./bar.js')).toThrow() - expect(() => doResolve('bar.js')).toThrow() - expect(() => doResolve('/bar.js')).toThrow() - }) -}) // resolvePath - -describe('readTsConfig', () => { - let findConfig!: jest.SpyInstance - let readConfig!: jest.SpyInstance<{ config?: any; error?: ts.Diagnostic }> - let parseConfig!: jest.SpyInstance - let cs!: ConfigSet +describe('compilerModule', () => { + test('should return typescript compiler module when no custom compiler module is specified', () => { + const compilerModule = createConfigSet().compilerModule - beforeAll(() => { - findConfig = jest.spyOn(ts, 'findConfigFile') - readConfig = jest.spyOn(ts, 'readConfigFile') - parseConfig = jest.spyOn(ts, 'parseJsonConfigFileContent') + expect(compilerModule).toBeDefined() + expect(typeof compilerModule).toBe('object') }) - afterAll(() => { - findConfig.mockRestore() - readConfig.mockRestore() - parseConfig.mockRestore() + test('should return custom compiler module when specifying via config', () => { + expect(() => + createConfigSet({ + tsJestConfig: { + compiler: 'ttypescript', + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Unable to load the module \\"ttypescript\\". Using \\"ts-jest\\" requires this package to be installed. To fix it: + ↳ install \\"ttypescript\\": \`npm i -D ttypescript\` (or \`yarn add --dev ttypescript\`)" + `) }) +}) // compilerModule - describe('cannot resolve configFileName', () => { - beforeEach(() => { - cs = createConfigSet({ jestConfig: { rootDir: '/root', cwd: '/cwd' } as any }) - findConfig.mockReturnValue(undefined) - readConfig.mockReturnValue({ - error: { - code: 404, - } as any, - }) - }) +describe('customTransformers', () => { + const logger = testing.createLoggerMock() - afterEach(() => { - findConfig.mockClear() - readConfig.mockClear() - parseConfig.mockClear() + it.each([ + {}, + { + before: ['dummy-transformer'], + }, + { + after: ['dummy-transformer'], + }, + { + afterDeclarations: ['dummy-transformer'], + }, + { + before: [ + { + path: 'dummy-transformer', + options: Object.create(null), + }, + ], + }, + ])('should return an object containing all resolved transformers', (data) => { + const cs = createConfigSet({ + jestConfig: { + rootDir: 'src', + cwd: 'src', + } as any, + tsJestConfig: { + astTransformers: data, + }, + resolve: null, }) - it('should use correct paths when searching', () => { - const conf = cs.readTsConfig() - expect(conf.options.configFilePath).toBeUndefined() - expect(readConfig).not.toHaveBeenCalled() + expect(cs.customTransformers).toMatchSnapshot() + }) + + it('should return an object containing all resolved transformers when astTransformers config is an array', () => { + expect( + createConfigSet({ + jestConfig: { + rootDir: 'src', + cwd: 'src', + } as any, + logger, + tsJestConfig: { + astTransformers: ['dummy-transformer'], + }, + resolve: null, + }).customTransformers, + ).toMatchInlineSnapshot(` + Object { + "before": Array [ + [Function], + [Function], + ], + } + `) + }) +}) + +describe('tsCompiler', () => { + it('should be a compiler object', () => { + const cs = createConfigSet({ + jestConfig: { + testRegex: [], + testMatch: [], + }, + tsJestConfig: { tsconfig: false } as any, + }) + const compiler = cs.tsCompiler + expect(compiler.cwd).toBe(cs.cwd) + expect(typeof compiler.compile).toBe('function') + }) +}) // tsCompiler + +describe('babelJestTransformer', () => { + it('should return babelJestTransformer without babelConfig option', () => { + const cs = createConfigSet({ + jestConfig: { rootDir: 'src', cwd: 'src' }, + resolve: null, + }) + const babelJest = cs.babelJestTransformer as Transformer + + expect(cs.babelConfig).toBeUndefined() + expect(babelJest).toBeUndefined() + }) + + it('should return babelJestTransformer with babalConfig is true', () => { + const cs = createConfigSet({ + jestConfig: { + rootDir: 'src', + cwd: 'src', + globals: { + 'ts-jest': { + babelConfig: true, + }, + }, + }, + resolve: null, + }) + const babelJest = cs.babelJestTransformer as Transformer + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const babelCfg = cs.babelConfig! + expect(babelCfg.cwd).toEqual(cs.cwd) + expect(babelJest.canInstrument).toBe(true) + expect(babelJest.createTransformer).toBeUndefined() + expect(typeof babelJest.getCacheKey).toBe('function') + expect(typeof babelJest.process).toBe('function') + }) + + it('should return babelJestTransformer with non javascript file path', () => { + const FILE = 'src/__mocks__/.babelrc-foo' + const cs = createConfigSet({ + jestConfig: { + globals: { + 'ts-jest': { + babelConfig: FILE, + }, + }, + }, + resolve: null, + }) + const babelJest = cs.babelJestTransformer as Transformer + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const babelCfg = cs.babelConfig! + expect(babelCfg.cwd).toEqual(cs.cwd) + expect(babelCfg.presets).toMatchInlineSnapshot(` + Array [ + "@babel/preset-env", + "@babel/preset-typescript", + "@babel/preset-react", + ] + `) + expect(babelJest.canInstrument).toBe(true) + expect(babelJest.createTransformer).toBeUndefined() + expect(typeof babelJest.getCacheKey).toBe('function') + expect(typeof babelJest.process).toBe('function') + }) + + it('should return babelJestTransformer with javascript file path', () => { + const FILE = 'src/__mocks__/babel-foo.config.js' + const cs = createConfigSet({ + jestConfig: { + globals: { + 'ts-jest': { + babelConfig: FILE, + }, + }, + }, + resolve: null, + }) + const babelJest = cs.babelJestTransformer as Transformer + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const babelCfg = cs.babelConfig! + expect(babelCfg.cwd).toEqual(cs.cwd) + expect(babelCfg.presets).toMatchInlineSnapshot(` + Array [ + "@babel/preset-env", + "@babel/preset-typescript", + "@babel/preset-react", + ] + `) + expect(babelJest.canInstrument).toBe(true) + expect(babelJest.createTransformer).toBeUndefined() + expect(typeof babelJest.getCacheKey).toBe('function') + expect(typeof babelJest.process).toBe('function') + }) + + it('should return babelJestTransformer with loaded config object', () => { + const babelConfig = require('../__mocks__/babel-foo.config') + const cs = createConfigSet({ + jestConfig: { + globals: { + 'ts-jest': { + babelConfig, + }, + }, + }, + resolve: null, + }) + const babelJest = cs.babelJestTransformer as Transformer + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const babelCfg = cs.babelConfig! + expect(babelCfg.cwd).toEqual(cs.cwd) + expect(babelCfg.presets).toMatchInlineSnapshot(` + Array [ + "@babel/preset-env", + "@babel/preset-typescript", + "@babel/preset-react", + ] + `) + expect(babelJest.canInstrument).toBe(true) + expect(babelJest.createTransformer).toBeUndefined() + expect(typeof babelJest.getCacheKey).toBe('function') + expect(typeof babelJest.process).toBe('function') + }) + + it('should return babelJestTransformer with inline config', () => { + const CONFIG = { comments: true } + const cs = createConfigSet({ + jestConfig: { + globals: { + 'ts-jest': { + babelConfig: CONFIG, + }, + }, + }, + resolve: null, + }) + const babelJest = cs.babelJestTransformer as Transformer + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const babelCfg = cs.babelConfig! + expect(babelCfg.cwd).toEqual(cs.cwd) + expect(babelCfg.comments).toEqual(true) + expect(babelJest.canInstrument).toBe(true) + expect(babelJest.createTransformer).toBeUndefined() + expect(typeof babelJest.getCacheKey).toBe('function') + expect(typeof babelJest.process).toBe('function') + }) +}) // babelJestTransformer + +describe('tsCacheDir', () => { + const cacheName = 'configSetTmp' + const cacheDir = join(process.cwd(), cacheName) + const partialTsJestCacheDir = join(cacheDir, 'ts-jest') + + it.each([undefined, Object.create(null)])( + 'should return value from which is the combination of ts jest config and jest config when running test with cache', + (data) => { + expect( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + createConfigSet({ + jestConfig: { + cache: true, + cacheDirectory: cacheDir, + globals: data, + }, + resolve: null, + }).tsCacheDir!.indexOf(partialTsJestCacheDir), + ).toEqual(0) + }, + ) + + it('should return undefined when running test without cache', () => { + expect(createConfigSet({ resolve: null }).tsCacheDir).toBeUndefined() + }) + + it('return value with the real version of dependencies in package.json when running test with cache', () => { + const pkg = { + optionalDependencies: { opt: '1.2.3' }, + peerDependencies: { peer: '1.2.4' }, + devDependencies: { dev: '1.2.5' }, + dependencies: { std: '1.2.6' }, + } + const realVersions: any = { + peer: '0.1.0', + dev: '4.3.2', + std: '9.10.2', + opt: '2.0.2', + } + const mock: jest.MockInstance = mocked(getPackageVersion).mockImplementation( + (moduleName: string) => realVersions[moduleName], + ) + const cs = createConfigSet({ + jestConfig: { + cache: true, + cacheDirectory: cacheDir, + globals: { + 'ts-jest': { tsconfig: false }, + }, + }, + projectPackageJson: pkg, + }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(cs.tsCacheDir!.indexOf(partialTsJestCacheDir)).toEqual(0) + + mock.mockRestore() + }) +}) + +describe('tsJestDigest', () => { + it('should be the package digest', () => { + expect(createConfigSet().tsJestDigest).toBe(MY_DIGEST) + }) +}) // tsJestDigest + +describe('hooks', () => { + it('should return empty object when environment variable TS_JEST_HOOKS is undefined', () => { + expect(createConfigSet().hooks).toEqual({}) + }) + + it('should return value when environment variable TS_JEST_HOOKS is defined', () => { + process.env.TS_JEST_HOOKS = './foo' + expect(createConfigSet().hooks).toBeDefined() + }) +}) // hooks + +describe('isTestFile', () => { + it.each([ + { + jestConfig: { + testRegex: [{}], + testMatch: [], + } as any, + }, + { + jestConfig: { + testMatch: [], + testRegex: [/.*\.(spec|test)\.[jt]sx?$/], + } as any, + }, + { + jestConfig: { + testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], + testRegex: [], + } as any, + }, + { + jestConfig: { + testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], + testRegex: ['**/?(*.)+(foo|bar).[tj]s?(x)'], + } as any, + }, + ])('should return a boolean value whether the file matches test pattern', (config) => { + expect(createConfigSet(config).isTestFile('foo.spec.ts')).toMatchSnapshot() + }) +}) // isTestFile + +describe('shouldStringifyContent', () => { + it('should return correct value is defined', () => { + const cs = createConfigSet({ tsJestConfig: { tsconfig: false, stringifyContentPathRegex: '\\.str$' } as any }) + expect(cs.shouldStringifyContent('/foo/bar.ts')).toBe(false) + expect(cs.shouldStringifyContent('/foo/bar.str')).toBe(true) + }) + + it('should return correct value when stringifyContentPathRegex is undefined', () => { + const cs = createConfigSet({ tsJestConfig: { tsconfig: false } as any }) + expect(cs.shouldStringifyContent('/foo/bar.ts')).toBe(false) + }) +}) // shouldStringifyContent + +describe('raiseDiagnostics', () => { + const logger = testing.createLoggerMock() + + describe('with warnOnly config', () => { + const filterDiagnostics = jest.fn((list) => list) + const makeDiagnostic = ({ + messageText = 'foo', + code = 9999, + category = ts.DiagnosticCategory.Warning, + }: Partial = {}): ts.Diagnostic => ({ messageText, code, category } as any) + + it('should throw when warnOnly is false', () => { + const cs = createConfigSet({ filterDiagnostics, logger, tsJestConfig: { diagnostics: { pretty: false } } }) + + expect(() => cs.raiseDiagnostics([])).not.toThrow() + expect(() => cs.raiseDiagnostics([makeDiagnostic()])).toThrowErrorMatchingInlineSnapshot(`"warning TS9999: foo"`) + expect(() => cs.raiseDiagnostics([makeDiagnostic({ category: ts.DiagnosticCategory.Message })])).not.toThrow() + }) + + it('should not throw when warnOnly is true', () => { + const cs = createConfigSet({ + filterDiagnostics, + logger, + tsJestConfig: { diagnostics: { warnOnly: true, pretty: false } }, + }) + + logger.target.clear() + expect(() => cs.raiseDiagnostics([])).not.toThrow() + expect(() => cs.raiseDiagnostics([makeDiagnostic()])).not.toThrow() + expect(logger.target.lines).toMatchInlineSnapshot(` + Array [ + "[level:40] warning TS9999: foo + ", + ] + `) + }) + }) + + describe("diagnostics don't contain source file", () => { + const makeDiagnostic = ({ + messageText = 'foo', + code = 9999, + category = ts.DiagnosticCategory.Warning, + }: Partial = {}): ts.Diagnostic => ({ messageText, code, category } as any) + it('should throw when diagnostics contains file path and pathRegex config matches file path', () => { + const cs = createConfigSet({ + logger, + tsJestConfig: { diagnostics: { pathRegex: 'src/__mocks__/index.ts', pretty: false } }, + }) + logger.target.clear() + + expect(() => + cs.raiseDiagnostics([makeDiagnostic()], 'src/__mocks__/index.ts', logger), + ).toThrowErrorMatchingInlineSnapshot(`"warning TS9999: foo"`) + }) + + it("should not throw when diagnostics contains file path and pathRegex config doesn't match file path", () => { + const cs = createConfigSet({ + logger, + tsJestConfig: { diagnostics: { warnOnly: true, pathRegex: '/bar/', pretty: false } }, + }) + logger.target.clear() + + expect(() => cs.raiseDiagnostics([makeDiagnostic()], 'src/__mocks__/index.ts', logger)).not.toThrow() + }) + }) + + describe('diagnostics contain source file', () => { + const program: ts.Program = ts.createProgram({ + options: { + module: ts.ModuleKind.CommonJS, + }, + rootNames: ['src/__mocks__/index.ts'], + }) + const makeDiagnostic = ({ + messageText = 'foo', + code = 9999, + category = ts.DiagnosticCategory.Warning, + file = program.getSourceFiles().find((sourceFile) => sourceFile.fileName === 'src/__mocks__/index.ts'), + }: Partial = {}): ts.Diagnostic => ({ messageText, code, category, file } as any) + + it("should not throw when pathRegex config doesn't match source file path", () => { + const cs = createConfigSet({ + logger, + tsJestConfig: { diagnostics: { pathRegex: '/foo/', pretty: false, ignoreCodes: [1111] } }, + }) + logger.target.clear() + + expect(() => cs.raiseDiagnostics([makeDiagnostic()])).not.toThrow() + }) + + it("should throw when pathRegex config doesn't match source file path", () => { + const cs = createConfigSet({ + logger, + tsJestConfig: { diagnostics: { pathRegex: 'src/__mocks__/index.ts', pretty: false } }, + }) + logger.target.clear() + + expect(() => cs.raiseDiagnostics([makeDiagnostic()])).toThrowErrorMatchingInlineSnapshot( + `"Debug Failure. False expression: position cannot precede the beginning of the file"`, + ) + }) + }) +}) // raiseDiagnostics + +describe('shouldReportDiagnostics', () => { + it('should return correct value', () => { + let cs = createConfigSet({ tsJestConfig: { tsconfig: false, diagnostics: { pathRegex: '/foo/' } } as any }) + expect(cs.shouldReportDiagnostics('/foo/index.ts')).toBe(true) + expect(cs.shouldReportDiagnostics('/bar/index.ts')).toBe(false) + cs = createConfigSet({ tsJestConfig: { tsconfig: false } as any }) + expect(cs.shouldReportDiagnostics('/foo/index.ts')).toBe(true) + expect(cs.shouldReportDiagnostics('/bar/index.ts')).toBe(true) + }) +}) // shouldReportDiagnostics + +describe('resolvePath', () => { + it('should resolve paths', () => { + const cs = createConfigSet({ jestConfig: { rootDir: '/root', cwd: '/cwd' } as any, resolve: null }) + const doResolve = (path: string) => cs.resolvePath(path, { throwIfMissing: false }) + expect(doResolve('bar.js')).toBe(resolve('/cwd/bar.js')) + expect(doResolve('./bar.js')).toBe(resolve('/cwd/./bar.js')) + expect(doResolve('bar.js')).toBe(resolve('/root/bar.js')) + expect(doResolve('/bar.js')).toBe(resolve('/root//bar.js')) + }) + it('should resolve node paths', () => { + const cs = createConfigSet({ jestConfig: { rootDir: '/root', cwd: '/cwd' } as any, resolve: null }) + const doResolve = (path: string) => cs.resolvePath(path, { throwIfMissing: false, nodeResolve: true }) + expect(doResolve('json5')).toBe(resolve(__dirname, '../../node_modules/json5', require('json5/package.json').main)) + expect(doResolve('./bar.js')).toBe(resolve('/cwd/bar.js')) + expect(doResolve('bar.js')).toBe(resolve('/root/bar.js')) + expect(doResolve('/bar.js')).toBe(resolve('/root//bar.js')) + }) + it('should throw for invalid paths', () => { + const cs = createConfigSet({ jestConfig: { rootDir: __dirname, cwd: __dirname } as any, resolve: null }) + const doResolve = (path: string) => cs.resolvePath(path) + expect(() => doResolve('bar.js')).toThrow() + expect(() => doResolve('./bar.js')).toThrow() + expect(() => doResolve('bar.js')).toThrow() + expect(() => doResolve('/bar.js')).toThrow() + }) +}) // resolvePath + +describe('readTsConfig', () => { + let findConfig!: jest.SpyInstance + let readConfig!: jest.SpyInstance<{ config?: any; error?: ts.Diagnostic }> + let parseConfig!: jest.SpyInstance + let cs!: ConfigSet + + beforeAll(() => { + findConfig = jest.spyOn(ts, 'findConfigFile') + readConfig = jest.spyOn(ts, 'readConfigFile') + parseConfig = jest.spyOn(ts, 'parseJsonConfigFileContent') + }) + + afterAll(() => { + findConfig.mockRestore() + readConfig.mockRestore() + parseConfig.mockRestore() + }) + + describe('cannot resolve configFileName', () => { + beforeEach(() => { + findConfig.mockReturnValue(undefined) + readConfig.mockReturnValue({ + error: { + code: 404, + } as any, + }) + }) + + afterEach(() => { + findConfig.mockClear() + readConfig.mockClear() + parseConfig.mockClear() + }) + + it('should use correct paths when searching', () => { + cs = createConfigSet({ jestConfig: { rootDir: '/root', cwd: '/cwd' } as any }) + + const conf = cs.parsedTsConfig + expect(conf.options.configFilePath).toBeUndefined() + expect(readConfig).not.toHaveBeenCalled() expect(parseConfig.mock.calls[0][2]).toBe('/root') expect(parseConfig.mock.calls[0][4]).toBeUndefined() }) it('should use given tsconfig path', () => { - const conf = cs.readTsConfig(undefined, '/foo/tsconfig.bar.json') + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce('/foo/tsconfig.bar.json') + jest.spyOn(ConfigSet.prototype, 'raiseDiagnostics').mockImplementationOnce(() => {}) + + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.bar.json' } }, + } as any, + }) + + const conf = cs.parsedTsConfig expect(conf.options.configFilePath).toBeUndefined() expect(findConfig).not.toBeCalled() expect(readConfig.mock.calls[0][0]).toBe('/foo/tsconfig.bar.json') @@ -537,7 +707,6 @@ describe('readTsConfig', () => { describe('resolve configFileName normally', () => { beforeEach(() => { - cs = createConfigSet({ jestConfig: { rootDir: '/root', cwd: '/cwd' } as any }) findConfig.mockImplementation((p: string) => `${p}/tsconfig.json`) readConfig.mockImplementation((p) => ({ config: { path: p, compilerOptions: {} } })) }) @@ -564,112 +733,54 @@ describe('readTsConfig', () => { }) it('should use correct paths when searching', () => { - const conf = cs.readTsConfig() - expect(conf.options.path).toBe('/root/tsconfig.json') - expect(findConfig.mock.calls[0][0]).toBe('/root') - expect(readConfig.mock.calls[0][0]).toBe('/root/tsconfig.json') - expect(parseConfig.mock.calls[0][2]).toBe('/root') - expect(parseConfig.mock.calls[0][4]).toBe('/root/tsconfig.json') - expect(conf.options.allowSyntheticDefaultImports).toEqual(true) - expect(conf.errors).toMatchSnapshot() - }) - - it('should use given tsconfig path', () => { - const conf = cs.readTsConfig(undefined, '/foo/tsconfig.bar.json') - expect(conf.options.path).toBe('/foo/tsconfig.bar.json') - expect(findConfig).not.toBeCalled() - expect(readConfig.mock.calls[0][0]).toBe('/foo/tsconfig.bar.json') - expect(parseConfig.mock.calls[0][2]).toBe('/foo') - expect(parseConfig.mock.calls[0][4]).toBe('/foo/tsconfig.bar.json') - expect(conf.errors).toMatchSnapshot() - }) - }) - - describe('module in tsConfig is not the same as forced module and allowSyntheticDefaultImports is false in tsConfig', () => { - beforeEach(() => { - parseConfig.mockImplementation((conf: any) => ({ - options: { - ...conf, - module: ts.ModuleKind.AMD, - allowSyntheticDefaultImports: false, - }, - fileNames: [], - errors: [], - })) - }) + const tscfgPathStub = '/root/tsconfig.json' - afterEach(() => { - parseConfig.mockClear() - }) + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + } as any, + }) - it('should use correct paths when searching', () => { - const conf = cs.readTsConfig() - expect(conf.options.path).toBe('/root/tsconfig.json') + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) expect(findConfig.mock.calls[0][0]).toBe('/root') - expect(readConfig.mock.calls[0][0]).toBe('/root/tsconfig.json') + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) expect(parseConfig.mock.calls[0][2]).toBe('/root') - expect(parseConfig.mock.calls[0][4]).toBe('/root/tsconfig.json') + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) expect(conf.options.allowSyntheticDefaultImports).toEqual(true) expect(conf.errors).toMatchSnapshot() }) it('should use given tsconfig path', () => { - const conf = cs.readTsConfig(undefined, '/foo/tsconfig.bar.json') - expect(conf.options.path).toBe('/foo/tsconfig.bar.json') - expect(findConfig).not.toBeCalled() - expect(readConfig.mock.calls[0][0]).toBe('/foo/tsconfig.bar.json') - expect(parseConfig.mock.calls[0][2]).toBe('/foo') - expect(parseConfig.mock.calls[0][4]).toBe('/foo/tsconfig.bar.json') - expect(conf.errors).toMatchSnapshot() - }) - }) - - describe('module in tsConfig is the same as forced module and esModuleInterop true is in tsConfig', () => { - beforeEach(() => { - parseConfig.mockImplementation((conf: any) => ({ - options: { - ...conf, - module: ts.ModuleKind.ESNext, - esModuleInterop: true, - }, - fileNames: [], - errors: [], - })) - }) - - afterEach(() => { - parseConfig.mockClear() - }) + const tscfgPathStub = '/foo/tsconfig.bar.json' + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce(tscfgPathStub) - it('should use correct paths when searching', () => { - const conf = cs.readTsConfig() - expect(conf.options.path).toBe('/root/tsconfig.json') - expect(findConfig.mock.calls[0][0]).toBe('/root') - expect(readConfig.mock.calls[0][0]).toBe('/root/tsconfig.json') - expect(parseConfig.mock.calls[0][2]).toBe('/root') - expect(parseConfig.mock.calls[0][4]).toBe('/root/tsconfig.json') - expect(conf.options.allowSyntheticDefaultImports).toBeUndefined() - expect(conf.errors).toEqual([]) - }) + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.bar.json' } }, + } as any, + }) - it('should use given tsconfig path', () => { - const conf = cs.readTsConfig(undefined, '/foo/tsconfig.bar.json') - expect(conf.options.path).toBe('/foo/tsconfig.bar.json') + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) expect(findConfig).not.toBeCalled() - expect(readConfig.mock.calls[0][0]).toBe('/foo/tsconfig.bar.json') + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) expect(parseConfig.mock.calls[0][2]).toBe('/foo') - expect(parseConfig.mock.calls[0][4]).toBe('/foo/tsconfig.bar.json') - expect(conf.errors).toEqual([]) + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) + expect(conf.errors).toMatchSnapshot() }) }) - describe('module in tsConfig is the same as forced module and allowSyntheticDefaultImports true is in tsConfig', () => { + describe('module in tsConfig is not the same as forced module and allowSyntheticDefaultImports is false in tsConfig', () => { beforeEach(() => { parseConfig.mockImplementation((conf: any) => ({ options: { ...conf, - module: ts.ModuleKind.ESNext, - allowSyntheticDefaultImports: true, + module: ts.ModuleKind.AMD, + allowSyntheticDefaultImports: false, }, fileNames: [], errors: [], @@ -680,451 +791,208 @@ describe('readTsConfig', () => { parseConfig.mockClear() }) - it('should use correct paths when searching', () => { - const conf = cs.readTsConfig() - expect(conf.options.path).toBe('/root/tsconfig.json') - expect(findConfig.mock.calls[0][0]).toBe('/root') - expect(readConfig.mock.calls[0][0]).toBe('/root/tsconfig.json') - expect(parseConfig.mock.calls[0][2]).toBe('/root') - expect(parseConfig.mock.calls[0][4]).toBe('/root/tsconfig.json') - expect(conf.errors).toEqual([]) - expect(conf.options.allowSyntheticDefaultImports).toEqual(true) - }) - - it('should use given tsconfig path', () => { - const conf = cs.readTsConfig(undefined, '/foo/tsconfig.bar.json') - expect(conf.options.path).toBe('/foo/tsconfig.bar.json') - expect(findConfig).not.toBeCalled() - expect(readConfig.mock.calls[0][0]).toBe('/foo/tsconfig.bar.json') - expect(parseConfig.mock.calls[0][2]).toBe('/foo') - expect(parseConfig.mock.calls[0][4]).toBe('/foo/tsconfig.bar.json') - expect(conf.errors).toEqual([]) - expect(conf.options.allowSyntheticDefaultImports).toEqual(true) - }) - }) - }) -}) // readTsConfig - -describe('tsJestDigest', () => { - it('should be the package digest', () => { - expect(createConfigSet().tsJestDigest).toBe(MY_DIGEST) - }) -}) // tsJestDigest - -describe('shouldStringifyContent', () => { - it('should return correct value is defined', () => { - const cs = createConfigSet({ tsJestConfig: { tsconfig: false, stringifyContentPathRegex: '\\.str$' } as any }) - expect(cs.shouldStringifyContent('/foo/bar.ts')).toBe(false) - expect(cs.shouldStringifyContent('/foo/bar.str')).toBe(true) - }) - - it('should return correct value when stringifyContentPathRegex is undefined', () => { - const cs = createConfigSet({ tsJestConfig: { tsconfig: false } as any }) - expect(cs.shouldStringifyContent('/foo/bar.ts')).toBe(false) - }) -}) // shouldStringifyContent - -describe('tsCacheDir', () => { - const cacheName = 'configSetTmp' - const cacheDir = join(process.cwd(), cacheName) - const partialTsJestCacheDir = join(cacheDir, 'ts-jest') - - it.each([undefined, Object.create(null)])( - 'should return value from which is the combination of ts jest config and jest config when running test with cache', - (data) => { - expect( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - createConfigSet({ - jestConfig: { - cache: true, - cacheDirectory: cacheDir, - globals: data, - }, - resolve: null, - }).tsCacheDir!.indexOf(partialTsJestCacheDir), - ).toEqual(0) - }, - ) - - it('should return undefined when running test without cache', () => { - expect(createConfigSet({ resolve: null }).tsCacheDir).toBeUndefined() - }) - - it('return value with the real version of dependencies in package.json when running test with cache', () => { - const pkg = { - optionalDependencies: { opt: '1.2.3' }, - peerDependencies: { peer: '1.2.4' }, - devDependencies: { dev: '1.2.5' }, - dependencies: { std: '1.2.6' }, - } - const realVersions: any = { - peer: '0.1.0', - dev: '4.3.2', - std: '9.10.2', - opt: '2.0.2', - } - const mock: jest.MockInstance = mocked(getPackageVersion).mockImplementation( - (moduleName: string) => realVersions[moduleName], - ) - const cs = createConfigSet({ - jestConfig: { - cache: true, - cacheDirectory: cacheDir, - globals: { - 'ts-jest': { tsconfig: false }, - }, - }, - projectPackageJson: pkg, - }) - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(cs.tsCacheDir!.indexOf(partialTsJestCacheDir)).toEqual(0) - - mock.mockRestore() - }) -}) - -describe('shouldReportDiagnostic', () => { - it('should return correct value', () => { - let cs = createConfigSet({ tsJestConfig: { tsconfig: false, diagnostics: { pathRegex: '/foo/' } } as any }) - expect(cs.shouldReportDiagnostic('/foo/index.ts')).toBe(true) - expect(cs.shouldReportDiagnostic('/bar/index.ts')).toBe(false) - cs = createConfigSet({ tsJestConfig: { tsconfig: false } as any }) - expect(cs.shouldReportDiagnostic('/foo/index.ts')).toBe(true) - expect(cs.shouldReportDiagnostic('/bar/index.ts')).toBe(true) - }) -}) // shouldReportDiagnostic - -describe('tsCompiler', () => { - it('should be a compiler object', () => { - const cs = createConfigSet({ - jestConfig: { - testRegex: [], - testMatch: [], - }, - tsJestConfig: { tsconfig: false } as any, - }) - const compiler = cs.tsCompiler - expect(compiler.cwd).toBe(cs.cwd) - expect(typeof compiler.compile).toBe('function') - }) -}) // tsCompiler - -describe('tsCustomTransformers', () => { - it.each([ - {}, - { - before: ['dummy-transformer'], - }, - { - after: ['dummy-transformer'], - }, - { - afterDeclarations: ['dummy-transformer'], - }, - ])('should return an object containing all resolved transformers', (data) => { - const cs = createConfigSet({ - jestConfig: { - rootDir: 'src', - cwd: 'src', - globals: { - 'ts-jest': { - astTransformers: data, - }, - }, - } as any, - resolve: null, - }) - - expect(cs.tsCustomTransformers).toMatchSnapshot() - }) -}) - -describe('hooks', () => { - it('should return empty object when environment variable TS_JEST_HOOKS is undefined', () => { - expect(createConfigSet().hooks).toEqual({}) - }) - - it('should return value when environment variable TS_JEST_HOOKS is defined', () => { - process.env.TS_JEST_HOOKS = './foo' - expect(createConfigSet().hooks).toBeDefined() - }) -}) // hooks - -describe('babelJestTransformer', () => { - const logger = testing.createLoggerMock() + it('should use correct paths when searching', () => { + const tscfgPathStub = '/root/tsconfig.json' + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce('') - it('should return babelJestTransformer without babelConfig option', () => { - const cs = createConfigSet({ - jestConfig: { rootDir: 'src', cwd: 'src' }, - logger, - resolve: null, - }) - logger.target.clear() - const babelJest = cs.babelJestTransformer as Transformer + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.json' } }, + } as any, + }) - expect(cs.tsJest.babelConfig).toBeUndefined() - expect(logger.target.lines[1]).toMatchInlineSnapshot(` - "[level:20] babel is disabled - " - `) - expect(babelJest).toBeUndefined() - }) + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) + expect(findConfig.mock.calls[0][0]).toBe('/root') + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) + expect(parseConfig.mock.calls[0][2]).toBe('/root') + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) + expect(conf.options.allowSyntheticDefaultImports).toEqual(true) + expect(conf.errors).toMatchSnapshot() + }) - it('should return babelJestTransformer with babalConfig is true', () => { - const cs = createConfigSet({ - jestConfig: { - rootDir: 'src', - cwd: 'src', - globals: { - 'ts-jest': { - babelConfig: true, - }, - }, - }, - logger, - resolve: null, - }) - logger.target.clear() - const babelJest = cs.babelJestTransformer as Transformer + it('should use given tsconfig path', () => { + const tscfgPathStub = '/foo/tsconfig.bar.json' + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce(tscfgPathStub) - expect(cs.tsJest.babelConfig?.kind).toEqual('file') - expect(cs.tsJest.babelConfig?.value).toBeUndefined() - expect(logger.target.lines[1]).toMatchInlineSnapshot(` - "[level:20] normalized babel config via ts-jest option - " - `) - expect(babelJest.canInstrument).toBe(true) - expect(babelJest.createTransformer).toBeUndefined() - expect(typeof babelJest.getCacheKey).toBe('function') - expect(typeof babelJest.process).toBe('function') - }) + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.bar.json' } }, + } as any, + }) - it('should return babelJestTransformer with non javascript file path', () => { - const FILE = 'src/__mocks__/.babelrc-foo' - const cs = createConfigSet({ - jestConfig: { - globals: { - 'ts-jest': { - babelConfig: FILE, - }, - }, - }, - logger, - resolve: null, + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) + expect(findConfig).not.toBeCalled() + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) + expect(parseConfig.mock.calls[0][2]).toBe('/foo') + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) + expect(conf.errors).toMatchSnapshot() + }) }) - logger.target.clear() - const babelJest = cs.babelJestTransformer as Transformer - - expect(cs.tsJest.babelConfig?.kind).toEqual('file') - expect(cs.tsJest.babelConfig?.value).toEqual(join(process.cwd(), FILE)) - expect(logger.target.lines[2]).toMatchInlineSnapshot(` - "[level:20] normalized babel config via ts-jest option - " - `) - expect(babelJest.canInstrument).toBe(true) - expect(babelJest.createTransformer).toBeUndefined() - expect(typeof babelJest.getCacheKey).toBe('function') - expect(typeof babelJest.process).toBe('function') - }) - it('should return babelJestTransformer with javascript file path', () => { - const FILE = 'src/__mocks__/babel-foo.config.js' - const cs = createConfigSet({ - jestConfig: { - globals: { - 'ts-jest': { - babelConfig: FILE, + describe('module in tsConfig is the same as forced module and esModuleInterop true is in tsConfig', () => { + beforeEach(() => { + parseConfig.mockImplementation((conf: any) => ({ + options: { + ...conf, + module: ts.ModuleKind.ESNext, + esModuleInterop: true, }, - }, - }, - logger, - resolve: null, - }) - logger.target.clear() - const babelJest = cs.babelJestTransformer as Transformer - - expect(cs.tsJest.babelConfig?.kind).toEqual('file') - expect(cs.tsJest.babelConfig?.value).toEqual(join(process.cwd(), FILE)) - expect(logger.target.lines[2]).toMatchInlineSnapshot(` - "[level:20] normalized babel config via ts-jest option - " - `) - expect(babelJest.canInstrument).toBe(true) - expect(babelJest.createTransformer).toBeUndefined() - expect(typeof babelJest.getCacheKey).toBe('function') - expect(typeof babelJest.process).toBe('function') - }) + fileNames: [], + errors: [], + })) + }) - it('should return babelJestTransformer with loaded config object', () => { - const babelConfig = require('../__mocks__/babel-foo.config') - const cs = createConfigSet({ - jestConfig: { - globals: { - 'ts-jest': { - babelConfig, - }, - }, - }, - logger, - resolve: null, - }) - logger.target.clear() - const babelJest = cs.babelJestTransformer as Transformer + afterEach(() => { + parseConfig.mockClear() + }) - expect(cs.tsJest.babelConfig?.kind).toEqual('inline') - expect(cs.tsJest.babelConfig?.value).toMatchInlineSnapshot(` - Object { - "presets": Array [ - "@babel/preset-env", - "@babel/preset-typescript", - "@babel/preset-react", - ], - } - `) - expect(logger.target.lines[1]).toMatchInlineSnapshot(` - "[level:20] normalized babel config via ts-jest option - " - `) - expect(babelJest.canInstrument).toBe(true) - expect(babelJest.createTransformer).toBeUndefined() - expect(typeof babelJest.getCacheKey).toBe('function') - expect(typeof babelJest.process).toBe('function') - }) + it('should use correct paths when searching', () => { + const tscfgPathStub = '/root/tsconfig.json' + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce('') - it('should return babelJestTransformer with inline config', () => { - const CONFIG = { comments: true } - const cs = createConfigSet({ - jestConfig: { - globals: { - 'ts-jest': { - babelConfig: CONFIG, - }, - }, - }, - resolve: null, - logger, - }) - logger.target.clear() - const babelJest = cs.babelJestTransformer as Transformer + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.json' } }, + } as any, + }) - expect(cs.tsJest.babelConfig?.kind).toEqual('inline') - expect(cs.tsJest.babelConfig?.value).toEqual(CONFIG) - expect(logger.target.lines[1]).toMatchInlineSnapshot(` - "[level:20] normalized babel config via ts-jest option - " - `) - expect(babelJest.canInstrument).toBe(true) - expect(babelJest.createTransformer).toBeUndefined() - expect(typeof babelJest.getCacheKey).toBe('function') - expect(typeof babelJest.process).toBe('function') - }) -}) // babelJestTransformer + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) + expect(findConfig.mock.calls[0][0]).toBe('/root') + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) + expect(parseConfig.mock.calls[0][2]).toBe('/root') + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) + expect(conf.options.allowSyntheticDefaultImports).toBeUndefined() + expect(conf.errors).toEqual([]) + }) -describe('raiseDiagnostics', () => { - const createTsError = jest.fn( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - (list: ts.Diagnostic[]) => new Error(list.map((d) => `[TS${d.code}] ${d.messageText}`).join('\n')), - ) - const logger = testing.createLoggerMock() - describe('with warnOnly config', () => { - const filterDiagnostics = jest.fn((list) => list) - const makeDiagnostic = ({ - messageText = 'foo', - code = 9999, - category = ts.DiagnosticCategory.Warning, - }: Partial = {}): ts.Diagnostic => ({ messageText, code, category } as any) + it('should use given tsconfig path', () => { + const tscfgPathStub = '/foo/tsconfig.bar.json' + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce(tscfgPathStub) - it('should throw when warnOnly is false', () => { - const { raiseDiagnostics } = createConfigSet({ createTsError, filterDiagnostics }) - expect(() => raiseDiagnostics([])).not.toThrow() - expect(() => raiseDiagnostics([makeDiagnostic()])).toThrowErrorMatchingInlineSnapshot('"[TS9999] foo"') - expect(() => raiseDiagnostics([makeDiagnostic({ category: ts.DiagnosticCategory.Message })])).not.toThrow() - }) + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.bar.json' } }, + } as any, + }) - it('should not throw when warnOnly is true', () => { - const { raiseDiagnostics } = createConfigSet({ - createTsError, - filterDiagnostics, - logger, - tsJestConfig: { diagnostics: { warnOnly: true } }, + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) + expect(findConfig).not.toBeCalled() + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) + expect(parseConfig.mock.calls[0][2]).toBe('/foo') + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) + expect(conf.errors).toEqual([]) }) - logger.target.clear() - expect(() => raiseDiagnostics([])).not.toThrow() - expect(() => raiseDiagnostics([makeDiagnostic()])).not.toThrow() - expect(logger.target.lines).toMatchInlineSnapshot(` - Array [ - "[level:40] [TS9999] foo - ", - ] - `) }) - }) - describe("diagnostics don't contain source file", () => { - const makeDiagnostic = ({ - messageText = 'foo', - code = 9999, - category = ts.DiagnosticCategory.Warning, - }: Partial = {}): ts.Diagnostic => ({ messageText, code, category } as any) - it('should throw when diagnostics contains file path and pathRegex config matches file path', () => { - const { raiseDiagnostics } = createConfigSet({ - createTsError, - logger, - tsJestConfig: { diagnostics: { pathRegex: 'src/__mocks__/index.ts' } }, + describe('module in tsConfig is the same as forced module and allowSyntheticDefaultImports true is in tsConfig', () => { + beforeEach(() => { + parseConfig.mockImplementation((conf: any) => ({ + options: { + ...conf, + module: ts.ModuleKind.ESNext, + allowSyntheticDefaultImports: true, + }, + fileNames: [], + errors: [], + })) }) - logger.target.clear() - expect(() => raiseDiagnostics([makeDiagnostic()], 'src/__mocks__/index.ts')).toThrowErrorMatchingInlineSnapshot( - '"[TS9999] foo"', - ) - expect(logger.target.lines).toMatchInlineSnapshot('Array []') - }) - it("should not throw when diagnostics contains file path and pathRegex config doesn't match file path", () => { - const { raiseDiagnostics } = createConfigSet({ - createTsError, - logger, - tsJestConfig: { diagnostics: { warnOnly: true, pathRegex: '/bar/' } }, + afterEach(() => { + parseConfig.mockClear() }) - logger.target.clear() - expect(() => raiseDiagnostics([makeDiagnostic()], 'src/__mocks__/index.ts')).not.toThrow() - expect(logger.target.lines).toMatchInlineSnapshot('Array []') - }) - }) - describe('diagnostics contain source file', () => { - const program: ts.Program = ts.createProgram({ - options: { - module: ts.ModuleKind.CommonJS, - }, - rootNames: ['src/__mocks__/index.ts'], - }) - const makeDiagnostic = ({ - messageText = 'foo', - code = 9999, - category = ts.DiagnosticCategory.Warning, - file = program.getSourceFiles().find((sourceFile) => sourceFile.fileName === 'src/__mocks__/index.ts'), - }: Partial = {}): ts.Diagnostic => ({ messageText, code, category, file } as any) - it("should not throw when pathRegex config doesn't match source file path", () => { - const { raiseDiagnostics } = createConfigSet({ - createTsError, - logger, - tsJestConfig: { diagnostics: { pathRegex: '/foo/' } }, + it('should use correct paths when searching', () => { + const tscfgPathStub = '/root/tsconfig.json' + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce('') + + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.json' } }, + } as any, + }) + + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) + expect(findConfig.mock.calls[0][0]).toBe('/root') + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) + expect(parseConfig.mock.calls[0][2]).toBe('/root') + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) + expect(conf.errors).toEqual([]) + expect(conf.options.allowSyntheticDefaultImports).toEqual(true) }) - logger.target.clear() - expect(() => raiseDiagnostics([makeDiagnostic()])).not.toThrow() - expect(logger.target.lines).toMatchInlineSnapshot('Array []') - }) - it("should throw when pathRegex config doesn't match source file path", () => { - const { raiseDiagnostics } = createConfigSet({ - createTsError, - logger, - tsJestConfig: { diagnostics: { pathRegex: 'src/__mocks__/index.ts' } }, + it('should use given tsconfig path', () => { + const tscfgPathStub = '/foo/tsconfig.bar.json' + jest.spyOn(ConfigSet.prototype, 'resolvePath').mockReturnValueOnce(tscfgPathStub) + + cs = createConfigSet({ + jestConfig: { + rootDir: '/root', + cwd: '/cwd', + globals: { 'ts-jest': { tsconfig: 'tsconfig.bar.json' } }, + } as any, + }) + + const conf = cs.parsedTsConfig + expect(conf.options.path).toBe(tscfgPathStub) + expect(findConfig).not.toBeCalled() + expect(readConfig.mock.calls[0][0]).toBe(tscfgPathStub) + expect(parseConfig.mock.calls[0][2]).toBe('/foo') + expect(parseConfig.mock.calls[0][4]).toBe(tscfgPathStub) + expect(conf.errors).toEqual([]) + expect(conf.options.allowSyntheticDefaultImports).toEqual(true) }) - logger.target.clear() - expect(() => raiseDiagnostics([makeDiagnostic()])).toThrowErrorMatchingInlineSnapshot('"[TS9999] foo"') - expect(logger.target.lines).toMatchInlineSnapshot('Array []') }) }) -}) // raiseDiagnostics +}) // readTsConfig + +describe('diagnostics', () => { + it.each([ + { diagnostics: true }, + { diagnostics: false }, + { + diagnostics: { + ignoreCodes: '10, 25', + pathRegex: '\\.test\\.ts', + pretty: false, + }, + }, + { + diagnostics: { + ignoreCodes: ['10', 25], + pretty: false, + pathRegex: RegExp('\\.test\\.ts'), + }, + }, + { diagnostics: { warnOnly: true } }, + { diagnostics: { warnOnly: false } }, + ])('should handle different diagnostics config', (data) => { + expect(createConfigSet({ tsJestConfig: data })).toBeDefined() + }) + + it.each([ + '10', + 10, + 'TS2571', + '1009, TS2571, 4072', + [1009, 'TS2571', '6031', 'TS6031, 10', NaN, 'undefined', 'null', ''], + '', + NaN, + ])('should handle various kinds of ignoreCodes', (ignoreCodes) => { + expect(createConfigSet({ tsJestConfig: { diagnostics: { ignoreCodes } } })).toBeDefined() + }) +}) // diagnostics diff --git a/src/config/config-set.ts b/src/config/config-set.ts index dcc6407de8..44014489b7 100644 --- a/src/config/config-set.ts +++ b/src/config/config-set.ts @@ -16,28 +16,23 @@ import { globsToMatcher } from 'jest-util' import json5 = require('json5') import { dirname, extname, isAbsolute, join, normalize, resolve } from 'path' import { - Bundle, CompilerOptions, CustomTransformers, Diagnostic, - DiagnosticCategory, FormatDiagnosticsHost, ParsedCommandLine, ScriptTarget, - SourceFile, - TransformerFactory, } from 'typescript' import { createCompilerInstance } from '../compiler/instance' import { DEFAULT_JEST_TEST_MATCH } from '../constants' -import { internals as internalAstTransformers } from '../transformers' +import { factory as hoisting } from '../transformers/hoist-jest' import type { - AstTransformerDesc, + AstTransformer, BabelConfig, BabelJestTransformer, - ConfigCustomTransformer, TsCompiler, - TsJestConfig, + TsJestDiagnosticsCfg, TsJestGlobalOptions, TTypeScript, } from '../types' @@ -56,15 +51,6 @@ import { TSError } from '../utils/ts-error' */ export const MY_DIGEST: string = readFileSync(resolve(__dirname, '..', '..', '.ts-jest-digest'), 'utf8') -interface AstTransformerObj> { - transformModule: AstTransformerDesc - options?: T -} -interface AstTransformer { - before: AstTransformerObj[] - after?: AstTransformerObj[] - afterDeclarations?: AstTransformerObj[] -} interface TsJestHooksMap { afterProcess?(args: any[], result: string | TransformedSource): string | TransformedSource | void } @@ -126,29 +112,55 @@ const toDiagnosticCodeList = (items: (string | number)[], into: number[] = []): export class ConfigSet { readonly logger: Logger - /** - * @internal - */ private readonly _cwd: string - /** - * @internal - */ private readonly _rootDir: string - /** - * @internal - */ private _jestCfg!: Config.ProjectConfig + private _isolatedModules!: boolean + private _parsedTsConfig!: ParsedCommandLine + private _customTransformers: CustomTransformers = Object.create(null) + private _babelConfig: BabelConfig | undefined + private _babelJestTransformers: BabelJestTransformer | undefined + private _diagnostics!: TsJestDiagnosticsCfg + private _stringifyContentRegExp: RegExp | undefined + private readonly _compilerModule!: TTypeScript + private _tsCacheDir: string | undefined + private _overriddenCompilerOptions: Partial = { + // we handle sourcemaps this way and not another + sourceMap: true, + inlineSourceMap: false, + inlineSources: true, + // we don't want to create declaration files + declaration: false, + noEmit: false, // set to true will make compiler API not emit any compiled results. + // else istanbul related will be dropped + removeComments: false, + // to clear out else it's buggy + out: undefined, + outFile: undefined, + composite: undefined, // see https://github.com/TypeStrong/ts-node/pull/657/files + declarationDir: undefined, + declarationMap: undefined, + emitDeclarationOnly: undefined, + sourceRoot: undefined, + tsBuildInfoFile: undefined, + } constructor(private readonly jestConfig: Config.ProjectConfig) { this.logger = rootLogger.child({ [LogContexts.namespace]: 'config' }) this._cwd = normalize(this.jestConfig.cwd ?? process.cwd()) this._rootDir = normalize(this.jestConfig.rootDir ?? this._cwd) + const tsJestCfg = this.jestConfig.globals && this.jestConfig.globals['ts-jest'] + const options: TsJestGlobalOptions = tsJestCfg ?? Object.create(null) + // compiler module + this._compilerModule = importer.typescript(ImportReasons.TsJest, options.compiler ?? 'typescript') + + this.logger.debug({ compilerModule: this._compilerModule }, 'normalized compiler module config via ts-jest option') + this._backportJestCfg() + this._setupTsJestCfg(options) + this._resolveTsCacheDir() } - /** - * @internal - */ private _backportJestCfg(): void { const config = backportJestConfig(this.logger, this.jestConfig) @@ -157,115 +169,53 @@ export class ConfigSet { this._jestCfg = config } - /** - * @internal - */ - @Memoize() - get isTestFile(): (fileName: string) => boolean { - const matchablePatterns = [...this._jestCfg.testMatch, ...this._jestCfg.testRegex].filter( - (pattern) => - /** - * jest config testRegex doesn't always deliver the correct RegExp object - * See https://github.com/facebook/jest/issues/9778 - */ - pattern instanceof RegExp || typeof pattern === 'string', - ) - if (!matchablePatterns.length) { - matchablePatterns.push(...DEFAULT_JEST_TEST_MATCH) - } - const stringPatterns = matchablePatterns.filter((pattern: any) => typeof pattern === 'string') as string[] - const isMatch = globsToMatcher(stringPatterns) - - return (fileName: string) => - matchablePatterns.some((pattern) => (typeof pattern === 'string' ? isMatch(fileName) : pattern.test(fileName))) - } - - /** - * @internal - */ - @Memoize() - get tsJest(): TsJestConfig { - const parsedConfig = this._jestCfg - const { globals = {} } = parsedConfig as any - const options: TsJestGlobalOptions = { ...globals['ts-jest'] } + private _setupTsJestCfg(options: TsJestGlobalOptions): void { + // isolatedModules + this._isolatedModules = options.isolatedModules ?? false - // tsconfig - if (options.tsConfig) { - this.logger.warn(Deprecations.TsConfig) + if (options.packageJson) { + this.logger.warn(Deprecations.PackageJson) } - const tsConfig: TsJestConfig['tsConfig'] = this.getInlineOrFileConfigOpt( - options.tsConfig ?? options.tsconfig ?? true, - ) - - // transformers - let transformers: ConfigCustomTransformer = Object.create(null) - const { astTransformers } = options - if (astTransformers) { - if (Array.isArray(astTransformers)) { - this.logger.warn(Deprecations.AstTransformerArrayConfig) - transformers = { - before: astTransformers.map((transformerPath) => ({ - path: this.resolvePath(transformerPath, { nodeResolve: true }), - })), - } - } else { - if (astTransformers.before) { - transformers = { - before: astTransformers.before.map((transformer) => - typeof transformer === 'string' - ? { - path: this.resolvePath(transformer, { nodeResolve: true }), - } - : { - ...transformer, - path: this.resolvePath(transformer.path, { nodeResolve: true }), - }, - ), + // babel config (for babel-jest) default is undefined so we don't need to have fallback like tsConfig + if (!options.babelConfig) { + this.logger.debug('babel is disabled') + } else { + const baseBabelCfg = { cwd: this._cwd } + if (typeof options.babelConfig === 'string') { + if (extname(options.babelConfig) === '.js') { + this._babelConfig = { + ...baseBabelCfg, + ...require(this.resolvePath(options.babelConfig, { nodeResolve: true })), } - } - if (astTransformers.after) { - transformers = { - ...transformers, - after: astTransformers.after.map((transformer) => - typeof transformer === 'string' - ? { - path: this.resolvePath(transformer, { nodeResolve: true }), - } - : { - ...transformer, - path: this.resolvePath(transformer.path, { nodeResolve: true }), - }, - ), + } else { + this._babelConfig = { + ...baseBabelCfg, + ...json5.parse(readFileSync(options.babelConfig, 'utf-8')), } } - if (astTransformers.afterDeclarations) { - transformers = { - ...transformers, - afterDeclarations: astTransformers.afterDeclarations.map((transformer) => - typeof transformer === 'string' - ? { - path: this.resolvePath(transformer, { nodeResolve: true }), - } - : { - ...transformer, - path: this.resolvePath(transformer.path, { nodeResolve: true }), - }, - ), - } + } else if (typeof options.babelConfig === 'object') { + this._babelConfig = { + ...baseBabelCfg, + ...options.babelConfig, } + } else { + this._babelConfig = baseBabelCfg } - } - if (options.packageJson) { - this.logger.warn(Deprecations.PackageJson) + this.logger.debug({ babelConfig: this._babelConfig }, 'normalized babel config via ts-jest option') } + if (!this._babelConfig) { + this._overriddenCompilerOptions.module = this.compilerModule.ModuleKind.CommonJS + } else { + this._babelJestTransformers = importer + .babelJest(ImportReasons.BabelJest) + .createTransformer(this._babelConfig) as BabelJestTransformer - // babel config (for babel-jest) default is undefined so we don't need to have fallback like tsConfig or packageJson - const babelConfig: TsJestConfig['babelConfig'] = this.getInlineOrFileConfigOpt(options.babelConfig) + this.logger.debug('created babel-jest transformer') + } // diagnostics - let diagnostics: TsJestConfig['diagnostics'] const diagnosticsOpt = options.diagnostics ?? true const ignoreList: (string | number)[] = [...IGNORE_DIAGNOSTIC_CODES] if (typeof diagnosticsOpt === 'object') { @@ -273,354 +223,107 @@ export class ConfigSet { if (ignoreCodes) { Array.isArray(ignoreCodes) ? ignoreList.push(...ignoreCodes) : ignoreList.push(ignoreCodes) } - diagnostics = { + this._diagnostics = { pretty: diagnosticsOpt.pretty ?? true, ignoreCodes: toDiagnosticCodeList(ignoreList), pathRegex: normalizeRegex(diagnosticsOpt.pathRegex), throws: !diagnosticsOpt.warnOnly, } } else { - diagnostics = { + this._diagnostics = { ignoreCodes: diagnosticsOpt ? toDiagnosticCodeList(ignoreList) : [], pretty: true, throws: diagnosticsOpt, } } - // stringifyContentPathRegex option - const stringifyContentPathRegex = normalizeRegex(options.stringifyContentPathRegex) - // parsed options - const res: TsJestConfig = { - tsConfig, - babelConfig, - diagnostics, - isolatedModules: !!options.isolatedModules, - compiler: options.compiler ?? 'typescript', - transformers, - stringifyContentPathRegex, - } - - this.logger.debug({ tsJestConfig: res }, 'normalized ts-jest config') - - return res - } - /** - * @internal - */ - get parsedTsConfig(): ParsedCommandLine { - return this._parsedTsConfig - } + this.logger.debug({ diagnostics: this._diagnostics }, 'normalized diagnostics config via ts-jest option') - /** - * @internal - */ - @Memoize() - private get _parsedTsConfig(): ParsedCommandLine { - const { - tsJest: { tsConfig }, - } = this - const configFilePath = tsConfig?.kind === 'file' ? tsConfig.value : undefined - const result = this.readTsConfig( - tsConfig?.kind === 'inline' ? tsConfig.value : undefined, - configFilePath, - tsConfig == null, - ) + // tsconfig + if (options.tsConfig) { + this.logger.warn(Deprecations.TsConfig) + } + 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) // throw errors if any matching wanted diagnostics - this.raiseDiagnostics(result.errors, configFilePath) - - this.logger.debug({ tsconfig: result }, 'normalized typescript config') + this.raiseDiagnostics(this._parsedTsConfig.errors, configFilePath) - return result - } + this.logger.debug({ tsconfig: this._parsedTsConfig }, 'normalized typescript config via ts-jest option') - /** - * @internal - */ - @Memoize() - get raiseDiagnostics(): (diagnostics: Diagnostic[], filePath?: string, logger?: Logger) => void | never { - const { - createTsError, - filterDiagnostics, - tsJest: { - diagnostics: { throws }, - }, - compilerModule: { DiagnosticCategory }, - } = this - - return (diagnostics: Diagnostic[], filePath?: string, logger: Logger = this.logger): void | never => { - const filteredDiagnostics = filterDiagnostics(diagnostics, filePath) - if (!filteredDiagnostics.length) return - const error = createTsError(filteredDiagnostics) - // only throw if `warnOnly` and it is a warning or error - const importantCategories = [DiagnosticCategory.Warning, DiagnosticCategory.Error] - if (throws && filteredDiagnostics.some((d) => importantCategories.includes(d.category))) { - throw error - } - logger.warn({ error }, error.message) + // transformers + const { astTransformers } = options + this._customTransformers = { + before: [hoisting(this)], } - } + if (astTransformers) { + if (Array.isArray(astTransformers)) { + this.logger.warn(Deprecations.AstTransformerArrayConfig) - /** - * @internal - */ - @Memoize() - get babel(): BabelConfig | undefined { - const { - tsJest: { babelConfig }, - } = this - if (babelConfig == null) { - this.logger.debug('babel is disabled') + this._customTransformers = { + before: [ + ...this._customTransformers.before, + ...astTransformers.map((transformer) => { + const transformerPath = this.resolvePath(transformer, { nodeResolve: true }) - return undefined - } - let base: BabelConfig = { cwd: this.cwd } - if (babelConfig.kind === 'file') { - if (babelConfig.value) { - if (extname(babelConfig.value) === '.js') { - base = { - ...base, - ...require(babelConfig.value), + return require(transformerPath).factory(this) + }), + ], + } + } else { + const resolveTransformers = (transformers: (string | AstTransformer)[]) => + transformers.map((transformer) => { + let transformerPath: string + if (typeof transformer === 'string') { + transformerPath = this.resolvePath(transformer, { nodeResolve: true }) + + return require(transformerPath).factory(this) + } else { + transformerPath = this.resolvePath(transformer.path, { nodeResolve: true }) + + return require(transformerPath).factory(this, transformer.options) + } + }) + if (astTransformers.before) { + this._customTransformers = { + before: [...this._customTransformers.before, ...resolveTransformers(astTransformers.before)], } - } else { - base = { - ...base, - ...json5.parse(readFileSync(babelConfig.value, 'utf8')), + } + if (astTransformers.after) { + this._customTransformers = { + ...this._customTransformers, + after: resolveTransformers(astTransformers.after), } } - } - } else if (babelConfig.kind === 'inline') { - base = { ...base, ...babelConfig.value } - } - - this.logger.debug({ babelConfig: base }, 'normalized babel config via ts-jest option') - - return base - } - - /** - * This API can be used by custom transformers - */ - @Memoize() - get compilerModule(): TTypeScript { - return importer.typescript(ImportReasons.TsJest, this.tsJest.compiler) - } - - /** - * @internal - */ - @Memoize() - get babelJestTransformer(): BabelJestTransformer | undefined { - const { babel } = this - if (!babel) return undefined - - this.logger.debug('creating babel-jest transformer') - - return importer.babelJest(ImportReasons.BabelJest).createTransformer(babel) as BabelJestTransformer - } - - @Memoize() - get tsCompiler(): TsCompiler { - return createCompilerInstance(this) - } - - /** - * @internal - */ - @Memoize() - private get astTransformers(): AstTransformer { - let astTransformers: AstTransformer = { - before: [ - ...internalAstTransformers.map((transformer) => ({ - transformModule: transformer, - })), - ], - } - const { transformers } = this.tsJest - if (transformers.before) { - astTransformers = { - before: [ - ...astTransformers.before, - ...transformers.before.map((transformer) => - typeof transformer === 'string' - ? { - transformModule: require(transformer), - } - : { - transformModule: require(transformer.path), - options: transformer.options, - }, - ), - ], - } - } - if (transformers.after) { - astTransformers = { - ...astTransformers, - after: transformers.after.map((transformer) => - typeof transformer === 'string' - ? { - transformModule: require(transformer), - } - : { - transformModule: require(transformer.path), - options: transformer.options, - }, - ), - } - } - if (transformers.afterDeclarations) { - astTransformers = { - ...astTransformers, - afterDeclarations: transformers.afterDeclarations.map((transformer) => - typeof transformer === 'string' - ? { - transformModule: require(transformer), - } - : { - transformModule: require(transformer.path), - options: transformer.options, - }, - ), - } - } - - return astTransformers - } - - /** - * @internal - */ - @Memoize() - get tsCustomTransformers(): CustomTransformers { - let customTransformers: CustomTransformers = { - before: this.astTransformers.before.map((t) => t.transformModule.factory(this, t.options)) as TransformerFactory< - SourceFile - >[], - } - if (this.astTransformers.after) { - customTransformers = { - ...customTransformers, - after: this.astTransformers.after.map((t) => t.transformModule.factory(this, t.options)) as TransformerFactory< - SourceFile - >[], - } - } - if (this.astTransformers.afterDeclarations) { - customTransformers = { - ...customTransformers, - afterDeclarations: this.astTransformers.afterDeclarations.map((t) => - t.transformModule.factory(this, t.options), - ) as TransformerFactory[], - } - } - - return customTransformers - } - - /** - * @internal - */ - @Memoize() - get hooks(): TsJestHooksMap { - let hooksFile = process.env.TS_JEST_HOOKS - if (hooksFile) { - hooksFile = resolve(this.cwd, hooksFile) - - return importer.tryTheseOr(hooksFile, {}) - } - - return {} - } - - /** - * @internal - */ - @Memoize() - private get filterDiagnostics(): (diagnostics: Diagnostic[], filePath?: string) => Diagnostic[] { - const { - tsJest: { - diagnostics: { ignoreCodes }, - }, - shouldReportDiagnostic, - } = this - - return (diagnostics: Diagnostic[], filePath?: string): Diagnostic[] => { - if (filePath && !shouldReportDiagnostic(filePath)) return [] - - return diagnostics.filter((diagnostic) => { - if (diagnostic.file?.fileName && !shouldReportDiagnostic(diagnostic.file.fileName)) { - return false + if (astTransformers.afterDeclarations) { + this._customTransformers = { + ...this._customTransformers, + afterDeclarations: resolveTransformers(astTransformers.afterDeclarations), + } } - - return !ignoreCodes.includes(diagnostic.code) - }) - } - } - - /** - * @internal - */ - @Memoize() - get shouldReportDiagnostic(): (filePath: string) => boolean { - const { - diagnostics: { pathRegex }, - } = this.tsJest - if (pathRegex) { - const regex = new RegExp(pathRegex) - - return (file: string): boolean => regex.test(file) - } else { - return (): true => true - } - } - - /** - * @internal - */ - @Memoize() - get shouldStringifyContent(): (filePath: string) => boolean { - const { stringifyContentPathRegex } = this.tsJest - if (stringifyContentPathRegex) { - const regex = new RegExp(stringifyContentPathRegex) - - return (file: string): boolean => regex.test(file) - } else { - return (): false => false + } } - } - - /** - * @internal - */ - @Memoize() - private get createTsError(): (diagnostics: readonly Diagnostic[]) => TSError { - const { - diagnostics: { pretty }, - } = this.tsJest - - const formatDiagnostics = pretty - ? this.compilerModule.formatDiagnosticsWithColorAndContext - : this.compilerModule.formatDiagnostics - const diagnosticHost: FormatDiagnosticsHost = { - getNewLine: () => '\n', - getCurrentDirectory: () => this.cwd, - getCanonicalFileName: (path: string) => path, - } + this.logger.debug( + { customTransformers: this._customTransformers }, + 'normalized custom AST transformers via ts-jest option', + ) - return (diagnostics: readonly Diagnostic[]): TSError => { - const diagnosticText = formatDiagnostics(diagnostics, diagnosticHost) - const diagnosticCodes = diagnostics.map((x) => x.code) + // stringifyContentPathRegex + if (options.stringifyContentPathRegex) { + this._stringifyContentRegExp = + typeof options.stringifyContentPathRegex === 'string' + ? new RegExp(normalizeRegex(options.stringifyContentPathRegex)!) // eslint-disable-line @typescript-eslint/no-non-null-assertion + : options.stringifyContentPathRegex - return new TSError(diagnosticText, diagnosticCodes) + this.logger.debug( + { stringifyContentPathRegex: this._stringifyContentRegExp }, + 'normalized stringifyContentPathRegex config via ts-jest option', + ) } } - /** - * @internal - */ - @Memoize() - get tsCacheDir(): string | undefined { + private _resolveTsCacheDir(): void { if (!this._jestCfg.cache) { this.logger.debug('file caching disabled') @@ -628,107 +331,19 @@ export class ConfigSet { } const cacheSuffix = sha1( stringify({ - version: this.compilerModule.version, + version: this._compilerModule.version, digest: this.tsJestDigest, - compiler: this.tsJest.compiler, - compilerOptions: this.parsedTsConfig.options, - isolatedModules: this.tsJest.isolatedModules, - diagnostics: this.tsJest.diagnostics, + compilerModule: this._compilerModule, + compilerOptions: this._parsedTsConfig.options, + isolatedModules: this._isolatedModules, + diagnostics: this._diagnostics, }), ) const res = join(this._jestCfg.cacheDirectory, 'ts-jest', cacheSuffix.substr(0, 2), cacheSuffix.substr(2)) this.logger.debug({ cacheDirectory: res }, 'will use file caching') - return res - } - - /** - * @internal - */ - @Memoize() - private get overriddenCompilerOptions(): Partial { - const options: Partial = { - // we handle sourcemaps this way and not another - sourceMap: true, - inlineSourceMap: false, - inlineSources: true, - // we don't want to create declaration files - declaration: false, - noEmit: false, // set to true will make compiler API not emit any compiled results. - // else istanbul related will be dropped - removeComments: false, - // to clear out else it's buggy - out: undefined, - outFile: undefined, - composite: undefined, // see https://github.com/TypeStrong/ts-node/pull/657/files - declarationDir: undefined, - declarationMap: undefined, - emitDeclarationOnly: undefined, - sourceRoot: undefined, - tsBuildInfoFile: undefined, - } - // force the module kind if not piping babel-jest - if (!this.tsJest.babelConfig) { - // commonjs is required for jest - options.module = this.compilerModule.ModuleKind.CommonJS - } - - return options - } - - get cwd(): string { - return this._cwd - } - - /** - * Use by e2e, don't mark as internal - */ - @Memoize() - // eslint-disable-next-line class-methods-use-this - get tsJestDigest(): string { - return MY_DIGEST - } - - /** - * @internal - */ - private makeDiagnostic( - code: number, - messageText: string, - options: { category?: DiagnosticCategory; file?: SourceFile; start?: number; length?: number } = {}, - ): Diagnostic { - const { category = this.compilerModule.DiagnosticCategory.Warning, file, start, length } = options - - return { - code, - messageText, - category, - file, - start, - length, - } - } - - /** - * @internal - */ - private getInlineOrFileConfigOpt( - configOpt: string | boolean | Record | undefined, - ): { kind: 'inline' | 'file'; value: any } | undefined { - if (typeof configOpt === 'string' || configOpt === true) { - return { - kind: 'file', - value: typeof configOpt === 'string' ? this.resolvePath(configOpt) : undefined, - } - } else if (typeof configOpt === 'object') { - return { - kind: 'inline', - value: configOpt, - } - } - - return + this._tsCacheDir = res } /** @@ -737,34 +352,24 @@ export class ConfigSet { * * @internal */ - readTsConfig( - compilerOptions?: CompilerOptions, - resolvedConfigFile?: string | null, - noProject?: boolean | null, - ): ParsedCommandLine { + private _readTsConfig(compilerOptions?: CompilerOptions, resolvedConfigFile?: string): ParsedCommandLine { let config = { compilerOptions: Object.create(null) } let basePath = normalizeSlashes(this._rootDir) - let configFileName: string | undefined - const ts = this.compilerModule - - if (!noProject) { - // Read project configuration when available. - configFileName = resolvedConfigFile - ? normalizeSlashes(resolvedConfigFile) - : 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) { - return { errors: [result.error], fileNames: [], options: {} } - } - - config = result.config - basePath = normalizeSlashes(dirname(configFileName)) + const ts = this._compilerModule + // Read project configuration when available. + const configFileName: string | undefined = resolvedConfigFile + ? normalizeSlashes(resolvedConfigFile) + : 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) { + return { errors: [result.error], fileNames: [], options: {} } } + + config = result.config + basePath = normalizeSlashes(dirname(configFileName)) } // Override default configuration options `ts-jest` requires. config.compilerOptions = { @@ -775,7 +380,7 @@ export class ConfigSet { // parse json, merge config extending others, ... const result = ts.parseJsonConfigFileContent(config, ts.sys, basePath, undefined, configFileName) - const { overriddenCompilerOptions: forcedOptions } = this + const { _overriddenCompilerOptions: forcedOptions } = this const finalOptions = result.options // Target ES5 output by default (instead of ES3). @@ -795,11 +400,14 @@ export class ConfigSet { moduleValue !== forcedOptions.module && !(finalOptions.esModuleInterop || finalOptions.allowSyntheticDefaultImports) ) { - result.errors.push( - this.makeDiagnostic(DiagnosticCodes.ConfigModuleOption, Errors.ConfigNoModuleInterop, { - category: ts.DiagnosticCategory.Message, - }), - ) + result.errors.push({ + code: DiagnosticCodes.ConfigModuleOption, + messageText: Errors.ConfigNoModuleInterop, + category: ts.DiagnosticCategory.Message, + file: undefined, + start: undefined, + length: undefined, + }) // at least enable synthetic default imports (except if it's set in the input config) if (!('allowSyntheticDefaultImports' in config.compilerOptions)) { finalOptions.allowSyntheticDefaultImports = true @@ -828,7 +436,7 @@ export class ConfigSet { const compilationTarget = result.options.target! /* istanbul ignore next (cover by e2e) */ if ( - !this.tsJest.babelConfig && + !this._babelConfig && ((nodeJsVer.startsWith('v10') && compilationTarget > ScriptTarget.ES2018) || (nodeJsVer.startsWith('v12') && compilationTarget > ScriptTarget.ES2019)) ) { @@ -842,6 +450,157 @@ export class ConfigSet { return result } + get parsedTsConfig(): ParsedCommandLine { + return this._parsedTsConfig + } + + get isolatedModules(): boolean { + return this._isolatedModules + } + + /** + * This API can be used by custom transformers + */ + get compilerModule(): TTypeScript { + return this._compilerModule + } + + get customTransformers(): CustomTransformers { + return this._customTransformers + } + + @Memoize() + get tsCompiler(): TsCompiler { + return createCompilerInstance(this) + } + + /** + * @internal + */ + get babelConfig(): BabelConfig | undefined { + return this._babelConfig + } + + /** + * @internal + */ + get babelJestTransformer(): BabelJestTransformer | undefined { + return this._babelJestTransformers + } + + get cwd(): string { + return this._cwd + } + + get tsCacheDir(): string | undefined { + return this._tsCacheDir + } + + /** + * Use by e2e, don't mark as internal + */ + @Memoize() + // eslint-disable-next-line class-methods-use-this + get tsJestDigest(): string { + return MY_DIGEST + } + + /** + * @internal + */ + @Memoize() + get hooks(): TsJestHooksMap { + let hooksFile = process.env.TS_JEST_HOOKS + if (hooksFile) { + hooksFile = resolve(this._cwd, hooksFile) + + return importer.tryTheseOr(hooksFile, {}) + } + + return {} + } + + @Memoize() + get isTestFile(): (fileName: string) => boolean { + const matchablePatterns = [...this._jestCfg.testMatch, ...this._jestCfg.testRegex].filter( + (pattern) => + /** + * jest config testRegex doesn't always deliver the correct RegExp object + * See https://github.com/facebook/jest/issues/9778 + */ + pattern instanceof RegExp || typeof pattern === 'string', + ) + if (!matchablePatterns.length) { + matchablePatterns.push(...DEFAULT_JEST_TEST_MATCH) + } + const stringPatterns = matchablePatterns.filter((pattern: any) => typeof pattern === 'string') as string[] + const isMatch = globsToMatcher(stringPatterns) + + return (fileName: string) => + matchablePatterns.some((pattern) => (typeof pattern === 'string' ? isMatch(fileName) : pattern.test(fileName))) + } + + /** + * @internal + */ + shouldStringifyContent(filePath: string): boolean { + return this._stringifyContentRegExp ? this._stringifyContentRegExp.test(filePath) : false + } + + raiseDiagnostics(diagnostics: Diagnostic[], filePath?: string, logger?: Logger): void { + const { ignoreCodes } = this._diagnostics + const { DiagnosticCategory } = this._compilerModule + const filteredDiagnostics = + filePath && !this.shouldReportDiagnostics(filePath) + ? [] + : diagnostics.filter((diagnostic) => { + if (diagnostic.file?.fileName && !this.shouldReportDiagnostics(diagnostic.file.fileName)) { + return false + } + + return !ignoreCodes.includes(diagnostic.code) + }) + if (!filteredDiagnostics.length) return + const error = this._createTsError(filteredDiagnostics) + // only throw if `warnOnly` and it is a warning or error + const importantCategories = [DiagnosticCategory.Warning, DiagnosticCategory.Error] + if (this._diagnostics.throws && filteredDiagnostics.some((d) => importantCategories.includes(d.category))) { + throw error + } + /* istanbul ignore next (already covered) */ + logger ? logger.warn({ error }, error.message) : this.logger.warn({ error }, error.message) + } + + shouldReportDiagnostics(filePath: string): boolean { + const { pathRegex } = this._diagnostics + if (pathRegex) { + const regex = new RegExp(pathRegex) + + return regex.test(filePath) + } else { + return true + } + } + + /** + * @internal + */ + private _createTsError(diagnostics: readonly Diagnostic[]): TSError { + const formatDiagnostics = this._diagnostics.pretty + ? this.compilerModule.formatDiagnosticsWithColorAndContext + : this.compilerModule.formatDiagnostics + /* istanbul ignore next (not possible to cover) */ + const diagnosticHost: FormatDiagnosticsHost = { + getNewLine: () => '\n', + getCurrentDirectory: () => this.cwd, + getCanonicalFileName: (path: string) => path, + } + const diagnosticText = formatDiagnostics(diagnostics, diagnosticHost) + const diagnosticCodes = diagnostics.map((x) => x.code) + + return new TSError(diagnosticText, diagnosticCodes) + } + resolvePath( inputPath: string, { throwIfMissing = true, nodeResolve = false }: { throwIfMissing?: boolean; nodeResolve?: boolean } = {}, diff --git a/src/constants.ts b/src/constants.ts index 10c4fcf39e..bab221e10f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,22 +2,8 @@ * @internal */ export const LINE_FEED = '\n' -/** - * @internal - */ -export const EXTENSION_REGEX = /\.[^.]+$/ -/** - * @internal - */ export const TS_TSX_REGEX = /\.tsx?$/ -/** - * @internal - */ export const JS_JSX_REGEX = /\.jsx?$/ -/** - * @internal - */ -export const JSON_REGEX = /\.json$/i /** * @internal * See https://jestjs.io/docs/en/configuration#testmatch-arraystring diff --git a/src/transformers/index.spec.ts b/src/transformers/index.spec.ts new file mode 100644 index 0000000000..f0c3ad8d79 --- /dev/null +++ b/src/transformers/index.spec.ts @@ -0,0 +1,16 @@ +import { internals } from '.' + +describe('transformers', () => { + it('should return internal transformer', () => { + expect(internals).toHaveLength(1) + expect(internals).toMatchInlineSnapshot(` + Array [ + Object { + "factory": [Function], + "name": "hoisting-jest-mock", + "version": 4, + }, + ] + `) + }) +}) diff --git a/src/ts-jest-transformer.ts b/src/ts-jest-transformer.ts index 73cad756e6..4dca40139e 100644 --- a/src/ts-jest-transformer.ts +++ b/src/ts-jest-transformer.ts @@ -2,7 +2,6 @@ import createCacheKey from '@jest/create-cache-key-function' import type { CacheKeyOptions, TransformedSource, Transformer, TransformOptions } from '@jest/transform' import type { Config } from '@jest/types' import type { Logger } from 'bs-logger' -import type { CompilerOptions } from 'typescript' import { ConfigSet } from './config/config-set' import { JS_JSX_REGEX, TS_TSX_REGEX } from './constants' @@ -10,21 +9,11 @@ import { stringify } from './utils/json' import { JsonableValue } from './utils/jsonable-value' import { rootLogger } from './utils/logger' import { Errors, interpolate } from './utils/messages' -import type { BabelConfig } from './types' - -interface TransformerConfig extends Config.ProjectConfig { - digest: string - babel: BabelConfig | undefined - tsconfig: { - options: CompilerOptions - raw: Record - } -} interface CachedConfigSet { configSet: ConfigSet jestConfig: JsonableValue - transformerConfig: JsonableValue + transformerCfgStr: string } export class TsJestTransformer implements Transformer { @@ -133,7 +122,7 @@ export class TsJestTransformer implements Transformer { (cs) => cs.jestConfig.value === jestConfig, ) if (ccs) { - this._transformCfgStr = ccs.transformerConfig.serialized + this._transformCfgStr = ccs.transformerCfgStr this._tsJestCfgSet = ccs.configSet } else { // try to look-it up by stringified version @@ -146,29 +135,27 @@ export class TsJestTransformer implements Transformer { // this happens because jest first calls getCacheKey with stringified version of // the config, and then it calls the transformer with the proper object serializedCcs.jestConfig.value = jestConfig - this._transformCfgStr = serializedCcs.transformerConfig.serialized + this._transformCfgStr = serializedCcs.transformerCfgStr this._tsJestCfgSet = serializedCcs.configSet } else { // create the new record in the index this.logger.info('no matching config-set found, creating a new one') - const configSet = new ConfigSet(jestConfig) - const transformerCfg = new JsonableValue({ - digest: configSet.tsJestDigest, - babel: configSet.babel, + this._tsJestCfgSet = new ConfigSet(jestConfig) + this._transformCfgStr = new JsonableValue({ + digest: this._tsJestCfgSet.tsJestDigest, + babel: this._tsJestCfgSet.babelConfig, ...jestConfig, tsconfig: { - options: configSet.parsedTsConfig.options, - raw: configSet.parsedTsConfig.raw, + options: this._tsJestCfgSet.parsedTsConfig.options, + raw: this._tsJestCfgSet.parsedTsConfig.raw, }, - }) + }).serialized TsJestTransformer._cachedConfigSets.push({ jestConfig: new JsonableValue(jestConfig), - configSet, - transformerConfig: transformerCfg, + configSet: this._tsJestCfgSet, + transformerCfgStr: this._transformCfgStr, }) - this._transformCfgStr = transformerCfg.serialized - this._tsJestCfgSet = configSet } } } diff --git a/src/types.ts b/src/types.ts index 85bdf95d49..1e930234ec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -156,7 +156,7 @@ interface TsJestConfig$tsConfig$inline { value: _ts.CompilerOptions } type TsJestConfig$tsConfig = TsJestConfig$tsConfig$file | TsJestConfig$tsConfig$inline | undefined -interface TsJestConfig$diagnostics { +export interface TsJestDiagnosticsCfg { pretty: boolean ignoreCodes: number[] pathRegex?: string | undefined @@ -172,7 +172,6 @@ interface TsJestConfig$babelConfig$inline { value: BabelConfig } type TsJestConfig$babelConfig = TsJestConfig$babelConfig$file | TsJestConfig$babelConfig$inline | undefined -type TsJestConfig$stringifyContentPathRegex = string | undefined /** * @internal */ @@ -180,11 +179,11 @@ export interface TsJestConfig { tsConfig: TsJestConfig$tsConfig isolatedModules: boolean compiler: string - diagnostics: TsJestConfig$diagnostics + diagnostics: TsJestDiagnosticsCfg babelConfig: TsJestConfig$babelConfig transformers: ConfigCustomTransformer // to deprecate / deprecated === === === - stringifyContentPathRegex: TsJestConfig$stringifyContentPathRegex + stringifyContentPathRegex: string | undefined } export interface TsCompiler {