diff --git a/src/cli/__snapshots__/cli.spec.ts.snap b/src/cli/__snapshots__/cli.spec.ts.snap index e4454982bc..8756b87501 100644 --- a/src/cli/__snapshots__/cli.spec.ts.snap +++ b/src/cli/__snapshots__/cli.spec.ts.snap @@ -110,44 +110,80 @@ exports[`config init should update package.json for config type js-with-ts-full- }" `; -exports[`config migrate should migrate globals ts-jest config to transformer config 1`] = ` -""jest": { - "transform": { - "^.+\\\\.tsx?$": [ - "ts-jest", - { - "tsconfig": "./tsconfig.json" - } - ] +exports[`config migrate should generate transform config with existing transform options for js-with-babel 1`] = ` +"module.exports = { + transform: { + '^.+.jsx?$': 'babel-jest', + '^.+.tsx?$': [ + 'ts-jest', + {}, + ], + }, +} +" +`; + +exports[`config migrate should generate transform config with existing transform options for js-with-ts 1`] = ` +"module.exports = { + transform: { + '^.+.[tj]sx?$': [ + 'ts-jest', + {}, + ], + '^.+.tsx?$': [ + 'ts-jest', + {}, + ], }, - "preset": "ts-jest/presets/js-with-babel" } " `; exports[`config migrate should migrate preset if valid preset value is used 1`] = ` +""jest": { + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } +} " -No migration needed for given Jest configuration - " `; exports[`config migrate should migrate preset if valid preset value is used 2`] = ` ""jest": { - "preset": "ts-jest" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } } " `; exports[`config migrate should migrate preset if valid preset value is used 3`] = ` +""jest": { + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } +} " -No migration needed for given Jest configuration - " `; exports[`config migrate should reset testMatch if testRegex is used 1`] = ` ""jest": { "testRegex": "foo-pattern", - "preset": "ts-jest" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } } " `; @@ -157,7 +193,12 @@ exports[`config migrate should reset testMatch if testRegex is used 2`] = ` "testRegex": [ "foo-pattern" ], - "preset": "ts-jest" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } } " `; @@ -168,7 +209,12 @@ exports[`config migrate should reset testMatch if testRegex is used 3`] = ` "testMatch": [ "**/__tests__/**/*.(spec|test).[tj]s?(x)" ], - "preset": "ts-jest" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } } " `; @@ -178,7 +224,12 @@ exports[`config migrate should reset testMatch if testRegex is used 4`] = ` "testMatch": [ "**/__tests__/**/*.(spec|test).[tj]s?(x)" ], - "preset": "ts-jest" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } } " `; @@ -186,7 +237,12 @@ exports[`config migrate should reset testMatch if testRegex is used 4`] = ` exports[`config migrate should reset testMatch if testRegex is used 5`] = ` ""jest": { "testRegex": "foo-pattern", - "preset": "ts-jest" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } } " `; diff --git a/src/cli/cli.spec.ts b/src/cli/cli.spec.ts index 56d009466a..071b0df75f 100644 --- a/src/cli/cli.spec.ts +++ b/src/cli/cli.spec.ts @@ -2,6 +2,7 @@ import * as _fs from 'fs' import { normalize, resolve } from 'path' import { logTargetMock, mockObject, mockWriteStream } from '../__helpers__/mocks' +import { JS_TRANSFORM_PATTERN, TS_JS_TRANSFORM_PATTERN, TS_TRANSFORM_PATTERN } from '../constants' import { processArgv } from '.' @@ -261,32 +262,38 @@ describe('config', () => { jest.mock( pkgPaths.next, () => ({ - jest: { globals: { __TS_CONFIG__: { target: 'es6' } } }, + jest: { + globals: { + 'ts-jest': { + tsconfig: { target: 'es6' }, + }, + }, + }, }), { virtual: true }, ) + const res = await runCli(...noOption, pkgPaths.current) + expect(res).toMatchInlineSnapshot(` { "exitCode": 0, - "log": "", + "log": "[level:20] creating default CJS Jest preset + ", "stderr": " Migrated Jest configuration: - - - Detected preset 'default' as the best matching preset for your configuration. - Visit https://kulshekhar.github.io/ts-jest/user/config/#jest-preset for more information about presets. - ", "stdout": ""jest": { - "globals": { - "ts-jest": { - "tsconfig": { - "target": "es6" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + { + "tsconfig": { + "target": "es6" + } } - } - }, - "preset": "ts-jest" + ] + } } ", } @@ -300,25 +307,37 @@ describe('config', () => { jest.mock( pkgPaths.next, () => ({ - jest: { globals: { __TS_CONFIG__: { target: 'es6' } } }, + jest: { + globals: { + 'ts-jest': { + tsconfig: { target: 'es6' }, + }, + }, + }, }), { virtual: true }, ) + const res = await runCli(...fullOptions, pkgPaths.current) + expect(res).toMatchInlineSnapshot(` { "exitCode": 0, - "log": "", + "log": "[level:20] creating Js with Ts CJS Jest preset + ", "stderr": " Migrated Jest configuration: ", "stdout": ""jest": { - "globals": { - "ts-jest": { - "tsconfig": { - "target": "es6" + "transform": { + "^.+.[tj]sx?$": [ + "ts-jest", + { + "tsconfig": { + "target": "es6" + } } - } + ] } } ", @@ -327,14 +346,18 @@ describe('config', () => { expect(fs.writeFileSync).not.toHaveBeenCalled() }) - it('should detect same option values', async () => { + it('should generate transform options while keeping other jest config options', async () => { expect.assertions(1) fs.existsSync.mockImplementation(() => true) jest.mock( pkgPaths.next, () => ({ jest: { - globals: { __TS_CONFIG__: { target: 'es6' } }, + globals: { + 'ts-jest': { + tsconfig: { target: 'es6' }, + }, + }, moduleFileExtensions: ['ts', 'tsx', 'js'], testMatch: [ '**/__tests__/**/*.js?(x)', @@ -346,16 +369,11 @@ describe('config', () => { }), { virtual: true }, ) + const res = await runCli(...noOption, pkgPaths.current) + expect(res.stdout).toMatchInlineSnapshot(` ""jest": { - "globals": { - "ts-jest": { - "tsconfig": { - "target": "es6" - } - } - }, "moduleFileExtensions": [ "js", "ts", @@ -367,13 +385,22 @@ describe('config', () => { "**/__tests__/**/*.js?(x)", "**/__tests__/**/*.ts?(x)" ], - "preset": "ts-jest" + "transform": { + "^.+.tsx?$": [ + "ts-jest", + { + "tsconfig": { + "target": "es6" + } + } + ] + } } " `) }) - test.each([ + it.each([ { jest: { preset: 'ts-jest', @@ -399,7 +426,7 @@ describe('config', () => { expect(res.stdout ? res.stdout : res.stderr).toMatchSnapshot() }) - test.each([ + it.each([ { jest: { testRegex: 'foo-pattern', @@ -438,67 +465,102 @@ describe('config', () => { expect(res.stdout).toMatchSnapshot() }) - it('should detect best preset', async () => { - expect.assertions(5) + it('should generate transform config with default CLI options', async () => { fs.existsSync.mockImplementation(() => true) jest.mock(pkgPaths.next, () => ({}), { virtual: true }) - - // defaults jest.doMock(pkgPaths.nextCfg, () => ({}), { virtual: true }) - let res = await runCli(...noOption, pkgPaths.currentCfg) + + const res = await runCli(...noOption, pkgPaths.currentCfg) + expect(res.stdout).toMatchInlineSnapshot(` "module.exports = { - preset: 'ts-jest', + transform: { + '^.+.tsx?$': [ + 'ts-jest', + {}, + ], + }, } " `) + }) - // js-with-ts from args + it('should generate transform config with allow-js in CLI options', async () => { + fs.existsSync.mockImplementation(() => true) + jest.mock(pkgPaths.next, () => ({}), { virtual: true }) jest.doMock(pkgPaths.nextCfg, () => ({}), { virtual: true }) - res = await runCli(...noOption, '--allow-js', pkgPaths.currentCfg) - expect(res.stdout).toMatchInlineSnapshot(` - "module.exports = { - preset: 'ts-jest/presets/js-with-ts', - } - " - `) - // js-with-ts from previous transform - jest.doMock(pkgPaths.nextCfg, () => ({ transform: { '^.+\\.[tj]sx?$': 'ts-jest' } }), { virtual: true }) - res = await runCli(...noOption, pkgPaths.currentCfg) - expect(res.stdout).toMatchInlineSnapshot(` - "module.exports = { - preset: 'ts-jest/presets/js-with-ts', - } - " - `) + const res = await runCli(...noOption, '--allow-js', pkgPaths.currentCfg) - // js-with-babel from previous transform - jest.doMock(pkgPaths.nextCfg, () => ({ transform: { '^.+\\.jsx?$': 'babel-jest', '^.+\\.tsx?$': 'ts-jest' } }), { - virtual: true, - }) - res = await runCli(...noOption, pkgPaths.currentCfg) expect(res.stdout).toMatchInlineSnapshot(` "module.exports = { - preset: 'ts-jest/presets/js-with-babel', + transform: { + '^.+.[tj]sx?$': [ + 'ts-jest', + {}, + ], + }, } " `) + }) - // defaults when previous transform is ambiguous + it.each([ + { + name: 'js-with-babel', + transform: { + [JS_TRANSFORM_PATTERN]: 'babel-jest', + [TS_TRANSFORM_PATTERN]: 'ts-jest', + }, + }, + { + name: 'js-with-ts', + transform: { + [TS_JS_TRANSFORM_PATTERN]: 'ts-jest', + }, + }, + ])('should generate transform config with existing transform options for $name', async ({ transform }) => { + fs.existsSync.mockImplementation(() => true) + jest.mock(pkgPaths.next, () => ({}), { virtual: true }) + jest.doMock( + pkgPaths.nextCfg, + () => ({ + transform, + }), + { + virtual: true, + }, + ) + + const res = await runCli(...noOption, pkgPaths.currentCfg) + + expect(res.stdout).toMatchSnapshot() + }) + + it('should generate transform config by merging existing transform options with default transform options', async () => { + fs.existsSync.mockImplementation(() => true) + jest.mock(pkgPaths.next, () => ({}), { virtual: true }) jest.doMock( pkgPaths.nextCfg, () => ({ transform: { '^src/js/.+\\.jsx?$': 'babel-jest', '^src/ts/.+\\.tsx?$': 'ts-jest' } }), { virtual: true }, ) - res = await runCli(...noOption, pkgPaths.currentCfg) + + const res = await runCli(...noOption, pkgPaths.currentCfg) + expect(res.stdout).toMatchInlineSnapshot(` "module.exports = { transform: { '^src/js/.+\\\\.jsx?$': 'babel-jest', - '^src/ts/.+\\\\.tsx?$': 'ts-jest', + '^src/ts/.+\\\\.tsx?$': [ + 'ts-jest', + {}, + ], + '^.+.tsx?$': [ + 'ts-jest', + {}, + ], }, - preset: 'ts-jest', } " `) @@ -520,15 +582,29 @@ describe('config', () => { }), { virtual: true }, ) + const res = await runCli(...noOption, pkgPaths.current) + expect(res.stdout).toMatchInlineSnapshot(` ""jest": { "transform": { - "/src/.+\\\\.[jt]s$": "ts-jest", - "foo\\\\.ts": "ts-jest", - "bar\\\\.ts": "ts-jest" - }, - "preset": "ts-jest" + "/src/.+\\\\.[jt]s$": [ + "ts-jest", + {} + ], + "foo\\\\.ts": [ + "ts-jest", + {} + ], + "bar\\\\.ts": [ + "ts-jest", + {} + ], + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } } " `) @@ -536,6 +612,7 @@ describe('config', () => { it('should output help', async () => { const res = await runCli('help', noOption[0]) + expect(res).toMatchInlineSnapshot(` { "exitCode": 0, @@ -558,27 +635,5 @@ describe('config', () => { } `) }) - - it('should migrate globals ts-jest config to transformer config', async () => { - fs.existsSync.mockImplementation(() => true) - jest.mock( - pkgPaths.next, - () => ({ - jest: { - globals: { - 'ts-jest': { - tsconfig: './tsconfig.json', - }, - }, - transform: { '^.+\\.jsx?$': 'babel-jest', '^.+\\.tsx?$': 'ts-jest' }, - }, - }), - { virtual: true }, - ) - - const res = await runCli(...noOption, pkgPaths.current) - - expect(res.stdout).toMatchSnapshot() - }) }) // migrate }) diff --git a/src/cli/config/migrate.ts b/src/cli/config/migrate.ts index 04e178d3b3..3c99218a75 100644 --- a/src/cli/config/migrate.ts +++ b/src/cli/config/migrate.ts @@ -7,8 +7,56 @@ import stableStringify from 'fast-json-stable-stringify' import { stringify as stringifyJson5 } from 'json5' import type { CliCommand, CliCommandArgs } from '..' +import { createDefaultPreset, createJsWithBabelPreset, createJsWithTsPreset } from '../../presets/create-jest-preset' +import type { TsJestTransformerOptions } from '../../types' import { backportJestConfig } from '../../utils/backports' -import { JestPresetNames, TsJestPresetDescriptor, allPresets, defaults } from '../helpers/presets' +import { JestPresetNames, type TsJestPresetDescriptor, allPresets } from '../helpers/presets' + +const migrateGlobalConfigToTransformConfig = ( + transformConfig: Config.InitialOptions['transform'], + globalsTsJestConfig: TsJestTransformerOptions | undefined, +) => { + if (transformConfig) { + return Object.entries(transformConfig).reduce((previousValue, currentValue) => { + const [key, transformOptions] = currentValue + if (typeof transformOptions === 'string' && transformOptions.includes('ts-jest')) { + return { + ...previousValue, + [key]: globalsTsJestConfig ? ['ts-jest', globalsTsJestConfig] : 'ts-jest', + } + } + + return { + ...previousValue, + [key]: transformOptions, + } + }, {}) + } + + return {} +} + +const migratePresetToTransformConfig = ( + transformConfig: Config.InitialOptions['transform'], + preset: TsJestPresetDescriptor | undefined, + globalsTsJestConfig: TsJestTransformerOptions | undefined, +) => { + if (preset) { + const transformConfigFromPreset = + preset.name === JestPresetNames.jsWithTs + ? createJsWithTsPreset(globalsTsJestConfig) + : preset.name === JestPresetNames.jsWIthBabel + ? createJsWithBabelPreset(globalsTsJestConfig) + : createDefaultPreset(globalsTsJestConfig) + + return { + ...transformConfig, + ...transformConfigFromPreset.transform, + } + } + + return transformConfig +} /** * @internal @@ -17,7 +65,6 @@ export const run: CliCommand = async (args: CliCommandArgs /* , logger: Logger*/ const nullLogger = createLogger({ targets: [] }) const file = args._[0]?.toString() const filePath = resolve(process.cwd(), file) - const footNotes: string[] = [] if (!existsSync(filePath)) { throw new Error(`Configuration file ${file} does not exists.`) } @@ -36,59 +83,18 @@ export const run: CliCommand = async (args: CliCommandArgs /* , logger: Logger*/ // migrate // first we backport our options const migratedConfig = backportJestConfig(nullLogger, actualConfig) - let presetName: JestPresetNames | undefined let preset: TsJestPresetDescriptor | undefined - // then we check if we can use `preset` - if (!migratedConfig.preset && args.jestPreset) { - // find the best preset + if (migratedConfig.preset) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + preset = (allPresets as any)[migratedConfig.preset] ?? allPresets[JestPresetNames.default] + } else { if (args.js) { - presetName = args.js === 'babel' ? JestPresetNames.jsWIthBabel : JestPresetNames.jsWithTs - } else { - // try to detect what transformer the js extensions would target - const jsTransformers = Object.keys(migratedConfig.transform || {}).reduce((list, pattern) => { - if (RegExp(pattern.replace(/^\/?/, '/dummy-project/')).test('/dummy-project/src/foo.js')) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let transformer: string = (migratedConfig.transform as any)[pattern] - if (/\bbabel-jest\b/.test(transformer)) transformer = 'babel-jest' - else if (/\ts-jest\b/.test(transformer)) transformer = 'ts-jest' - - return [...list, transformer] - } - - return list - }, [] as string[]) - // depending on the transformer found, we use one or the other preset - const jsWithTs = jsTransformers.includes('ts-jest') - const jsWithBabel = jsTransformers.includes('babel-jest') - if (jsWithBabel && !jsWithTs) { - presetName = JestPresetNames.jsWIthBabel - } else if (jsWithTs && !jsWithBabel) { - presetName = JestPresetNames.jsWithTs - } else { - // sounds like js files are NOT handled, or handled with a unknown transformer, so we do not need to handle it - presetName = JestPresetNames.default - } - } - // ensure we are using a preset - presetName = presetName ?? JestPresetNames.default - preset = allPresets[presetName] - footNotes.push( - `Detected preset '${preset.label}' as the best matching preset for your configuration. -Visit https://kulshekhar.github.io/ts-jest/user/config/#jest-preset for more information about presets. -`, - ) - } else if (migratedConfig.preset?.startsWith('ts-jest')) { - if (args.jestPreset === false) { - delete migratedConfig.preset + preset = args.js === 'babel' ? allPresets[JestPresetNames.jsWIthBabel] : allPresets[JestPresetNames.jsWithTs] } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - preset = (allPresets as any)[migratedConfig.preset] ?? defaults + preset = allPresets[JestPresetNames.default] } } - // enforce the correct name - if (preset) migratedConfig.preset = preset.name - // check the extensions if (migratedConfig.moduleFileExtensions?.length && preset) { const presetValue = dedupSort(preset.value.moduleFileExtensions ?? []).join('::') @@ -98,7 +104,7 @@ Visit https://kulshekhar.github.io/ts-jest/user/config/#jest-preset for more inf } } // there is a testRegex, remove our testMatch - if ((typeof migratedConfig.testRegex === 'string' || migratedConfig.testRegex?.length) && preset) { + if (typeof migratedConfig.testRegex === 'string' || migratedConfig.testRegex?.length) { delete migratedConfig.testMatch } // check the testMatch @@ -110,64 +116,10 @@ Visit https://kulshekhar.github.io/ts-jest/user/config/#jest-preset for more inf } } - // migrate the transform - if (migratedConfig.transform) { - Object.keys(migratedConfig.transform).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const val = (migratedConfig.transform as any)[key] - if (typeof val === 'string' && /\/?ts-jest?(?:\/preprocessor\.js)?$/.test(val)) { - // eslint-disable-next-line - ;(migratedConfig.transform as any)[key] = 'ts-jest' - } - }) - } - - // migrate globals config to transformer config const globalsTsJestConfig = migratedConfig.globals?.['ts-jest'] - if (globalsTsJestConfig && migratedConfig.transform) { - Object.keys(migratedConfig.transform).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const val = (migratedConfig.transform as any)[key] - if (typeof val === 'string' && val.includes('ts-jest')) { - // eslint-disable-next-line - ;(migratedConfig.transform as any)[key] = Object.keys(globalsTsJestConfig).length ? [val, globalsTsJestConfig] : val - } - }) - delete (migratedConfig.globals ?? Object.create(null))['ts-jest'] - } + migratedConfig.transform = migrateGlobalConfigToTransformConfig(migratedConfig.transform, globalsTsJestConfig) + migratedConfig.transform = migratePresetToTransformConfig(migratedConfig.transform, preset, globalsTsJestConfig) - // check if it's the same as the preset's one - if (preset && migratedConfig.transform) { - if (stableStringify(migratedConfig.transform) === stableStringify(preset.value.transform)) { - delete migratedConfig.transform - } else { - const migratedConfigTransform = migratedConfig.transform - const presetValueTransform = preset.value.transform - if (migratedConfigTransform && presetValueTransform) { - migratedConfig.transform = Object.entries(migratedConfigTransform).reduce( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (acc: undefined | Record, [fileRegex, transformerConfig]) => { - const presetValueTransformerConfig = presetValueTransform[fileRegex] - const shouldRemoveDuplicatedConfig = - (presetValueTransformerConfig && - Array.isArray(presetValueTransformerConfig) && - transformerConfig === presetValueTransformerConfig[0] && - !Object.keys(presetValueTransformerConfig[1]).length) || - transformerConfig === presetValueTransformerConfig - - return shouldRemoveDuplicatedConfig - ? acc - : acc - ? { ...acc, [fileRegex]: transformerConfig } - : { [fileRegex]: transformerConfig } - }, - undefined, - ) - } - } - } - - // cleanup cleanupConfig(actualConfig) cleanupConfig(migratedConfig) const before = stableStringify(actualConfig) @@ -183,48 +135,16 @@ No migration needed for given Jest configuration const stringify = file.endsWith('.json') ? JSON.stringify : stringifyJson5 const prefix = file.endsWith('.json') ? '"jest": ' : 'module.exports = ' - // if we are using preset, inform the user that he might be able to remove some section(s) - // we couldn't check for equality - // if (usesPreset && migratedConfig.testMatch) { - // footNotes.push(` - // I couldn't check if your "testMatch" value is the same as mine which is: ${stringify( - // presets.testMatch, - // undefined, - // ' ', - // )} - // If it is the case, you can safely remove the "testMatch" from what I've migrated. - // `) - // } - if (preset && migratedConfig.transform) { - footNotes.push(` -I couldn't check if your "transform" value is the same as mine which is: ${stringify( - preset.value.transform, - undefined, - ' ', - )} -If it is the case, you can safely remove the "transform" from what I've migrated. -`) - } - // output new config process.stderr.write(` Migrated Jest configuration: `) process.stdout.write(`${prefix}${stringify(migratedConfig, undefined, ' ')}\n`) - if (footNotes.length) { - process.stderr.write(` -${footNotes.join('\n')} -`) - } } function cleanupConfig(config: Config.InitialOptions): void { if (config.globals) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((config as any).globals['ts-jest'] && Object.keys((config as any).globals['ts-jest']).length === 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (config as any).globals['ts-jest'] - } + delete config.globals['ts-jest'] if (!Object.keys(config.globals).length) { delete config.globals } @@ -240,7 +160,7 @@ function cleanupConfig(config: Config.InitialOptions): void { config.testMatch = dedupSort(config.testMatch) if (!config.testMatch.length) delete config.testMatch } - if (config.preset === JestPresetNames.default) config.preset = defaults.name + delete config.preset } // eslint-disable-next-line @typescript-eslint/no-explicit-any