diff --git a/package-lock.json b/package-lock.json index c9616f3541..54648a8ce8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@jest/types": "^29.6.3", "@types/babel__core": "7.20.5", "@types/cross-spawn": "latest", + "@types/ejs": "^3.1.5", "@types/fs-extra": "latest", "@types/js-yaml": "latest", "@types/lodash.camelcase": "4.3.9", @@ -44,6 +45,7 @@ "babel-jest": "^29.7.0", "conventional-changelog-cli": "^5.0.0", "cross-spawn": "latest", + "ejs": "^3.1.10", "esbuild": "~0.21.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -2230,6 +2232,12 @@ "@types/node": "*" } }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -2939,6 +2947,12 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4030,6 +4044,21 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.774", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz", @@ -4918,6 +4947,36 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6308,6 +6367,24 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -11553,6 +11630,12 @@ "@types/node": "*" } }, + "@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -12088,6 +12171,12 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -12875,6 +12964,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, "electron-to-chromium": { "version": "1.4.774", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz", @@ -13570,6 +13668,35 @@ "flat-cache": "^3.0.4" } }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -14524,6 +14651,18 @@ "cliui": "^7.0.4" } }, + "jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + } + }, "jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", diff --git a/package.json b/package.json index 71d6cf4ac9..57fe28e90d 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@jest/types": "^29.6.3", "@types/babel__core": "7.20.5", "@types/cross-spawn": "latest", + "@types/ejs": "^3.1.5", "@types/fs-extra": "latest", "@types/js-yaml": "latest", "@types/lodash.camelcase": "4.3.9", @@ -115,6 +116,7 @@ "babel-jest": "^29.7.0", "conventional-changelog-cli": "^5.0.0", "cross-spawn": "latest", + "ejs": "^3.1.10", "esbuild": "~0.21.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", diff --git a/src/cli/__snapshots__/cli.spec.ts.snap b/src/cli/__snapshots__/cli.spec.ts.snap index 6046989920..5bb669e967 100644 --- a/src/cli/__snapshots__/cli.spec.ts.snap +++ b/src/cli/__snapshots__/cli.spec.ts.snap @@ -1,5 +1,119 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`config init should create a jest config file with cli options for config type default 1`] = ` +"/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + }, +};" +`; + +exports[`config init should create a jest config file with cli options for config type default and type "module" package.json 1`] = ` +"/** @type {import('ts-jest').JestConfigWithTsJest} **/ +export default { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + }, +};" +`; + +exports[`config init should create a jest config file with cli options for config type js-with-babel-full-options 1`] = ` +"/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "jsdom", + transform: { + "^.+.jsx?$": ["babel-jest",{}], + }, +};" +`; + +exports[`config init should create a jest config file with cli options for config type js-with-babel-full-options and type "module" package.json 1`] = ` +"/** @type {import('ts-jest').JestConfigWithTsJest} **/ +export default { + testEnvironment: "jsdom", + transform: { + "^.+.jsx?$": ["babel-jest",{}], + }, +};" +`; + +exports[`config init should create a jest config file with cli options for config type js-with-ts-full-options 1`] = ` +"/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "jsdom", + transform: { + "^.+.[tj]sx?$": ["ts-jest",{tsconfig:"tsconfig.test.json"}], + }, +};" +`; + +exports[`config init should create a jest config file with cli options for config type js-with-ts-full-options and type "module" package.json 1`] = ` +"/** @type {import('ts-jest').JestConfigWithTsJest} **/ +export default { + testEnvironment: "jsdom", + transform: { + "^.+.[tj]sx?$": ["ts-jest",{tsconfig:"tsconfig.test.json"}], + }, +};" +`; + +exports[`config init should update package.json for config type default when user defines jest config via package.json 1`] = ` +"{ + "name": "mock", + "version": "0.0.0-mock.0", + "jest": { + "transform": { + "^.+.tsx?$": [ + "ts-jest", + {} + ] + } + } +}" +`; + +exports[`config init should update package.json for config type js-with-babel-full-options when user defines jest config via package.json 1`] = ` +"{ + "name": "mock", + "version": "0.0.0-mock.0", + "jest": { + "transform": { + "^.+.jsx?$": [ + "babel-jest", + {} + ], + "^.+.tsx?$": [ + "ts-jest", + { + "tsconfig": "tsconfig.test.json", + "babelConfig": true + } + ] + } + } +}" +`; + +exports[`config init should update package.json for config type js-with-ts-full-options when user defines jest config via package.json 1`] = ` +"{ + "name": "mock", + "version": "0.0.0-mock.0", + "jest": { + "transform": { + "^.+.[tj]sx?$": [ + "ts-jest", + { + "tsconfig": "tsconfig.test.json" + } + ] + } + } +}" +`; + exports[`config migrate should migrate globals ts-jest config to transformer config 1`] = ` ""jest": { "transform": { diff --git a/src/cli/cli.spec.ts b/src/cli/cli.spec.ts index a8b29a2d5c..56d009466a 100644 --- a/src/cli/cli.spec.ts +++ b/src/cli/cli.spec.ts @@ -112,179 +112,73 @@ describe('config', () => { // briefly tested, see header comment in `config/init.ts` describe('init', () => { const noOption = ['config:init'] - const fullOptions = [ - ...noOption, - '--tsconfig', - 'tsconfig.test.json', - '--jsdom', - '--jest-preset', - '--js', - 'ts', - '--babel', + const cliOptionCases = [ + { + cliOptions: [...noOption], + configType: 'default', + }, + { + cliOptions: [...noOption, '--tsconfig', 'tsconfig.test.json', '--jsdom', '--js', 'ts'], + configType: 'js-with-ts-full-options', + }, + { + cliOptions: [...noOption, '--tsconfig', 'tsconfig.test.json', '--jsdom', '--js', 'babel'], + configType: 'js-with-babel-full-options', + }, ] - it('should create a jest.config.js (without options)', async () => { - fs.existsSync.mockImplementation((f) => f === FAKE_PKG) - fs.readFileSync - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementationOnce((f): any => { - if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0' }) - throw new Error('ENOENT') - }) - expect.assertions(3) - const res = await runCli(...noOption) - - expect(res).toEqual({ - exitCode: 0, - log: '', - stderr: ` -Jest configuration written to "${normalize('/foo/bar/jest.config.js')}". -`, - stdout: '', - }) - expect(fs.writeFileSync.mock.calls[0][0]).toBe(normalize('/foo/bar/jest.config.js')) - expect(fs.writeFileSync.mock.calls[0][1]).toMatchInlineSnapshot(` - "/** @type {import('ts-jest').JestConfigWithTsJest} **/ - module.exports = { - testEnvironment: 'node', - transform: { - '^.+.tsx?$': 'ts-jest', - }, - };" - `) - }) - - it('should create a jest.config.foo.js (with all options set)', async () => { - fs.existsSync.mockImplementation((f) => f === FAKE_PKG) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fs.readFileSync.mockImplementationOnce((f): any => { - if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0' }) - throw new Error('ENOENT') - }) - expect.assertions(3) - const res = await runCli(...fullOptions, 'jest.config.foo.js') - - expect(res).toEqual({ - exitCode: 0, - log: '', - stderr: ` -Jest configuration written to "${normalize('/foo/bar/jest.config.foo.js')}". -`, - stdout: '', - }) - expect(fs.writeFileSync.mock.calls[0][0]).toBe(normalize('/foo/bar/jest.config.foo.js')) - expect(fs.writeFileSync.mock.calls[0][1]).toMatchInlineSnapshot(` - "/** @type {import('ts-jest').JestConfigWithTsJest} **/ - module.exports = { - testEnvironment: 'jsdom', - transform: { - '^.+.[tj]sx?$': - [ - 'ts-jest', - { - tsconfig: 'tsconfig.test.json' - } - ] - , - }, - };" - `) - }) - - it('should create jest config with type "module" package.json', async () => { - fs.existsSync.mockImplementation((f) => f === FAKE_PKG) - fs.readFileSync - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementationOnce((f): any => { - if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0', type: 'module' }) - throw new Error('ENOENT') - }) - expect.assertions(3) - const res = await runCli(...noOption) - - expect(res).toEqual({ - exitCode: 0, - log: '', - stderr: ` -Jest configuration written to "${normalize('/foo/bar/jest.config.js')}". -`, - stdout: '', - }) - expect(fs.writeFileSync.mock.calls[0][0]).toBe(normalize('/foo/bar/jest.config.js')) - expect(fs.writeFileSync.mock.calls[0][1]).toMatchInlineSnapshot(` - "/** @type {import('ts-jest').JestConfigWithTsJest} **/ - export default { - testEnvironment: 'node', - transform: { - '^.+.tsx?$': 'ts-jest', - }, - };" - `) - }) - - it('should update package.json (without options)', async () => { - fs.existsSync.mockImplementation((f) => f === FAKE_PKG) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fs.readFileSync.mockImplementationOnce((f): any => { - if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0' }) - throw new Error('ENOENT') - }) - expect.assertions(2) - const res = await runCli(...noOption, 'package.json') - - expect(res).toEqual({ - exitCode: 0, - log: '', - stderr: ` -Jest configuration written to "${normalize('/foo/bar/package.json')}". -`, - stdout: '', - }) - expect(fs.writeFileSync.mock.calls).toEqual([ - [ - normalize('/foo/bar/package.json'), - `{ - "name": "mock", - "version": "0.0.0-mock.0", - "jest": { - "preset": "ts-jest", - "testEnvironment": "node" - } -}`, - ], - ]) - }) + it.each(cliOptionCases)( + 'should create a jest config file with cli options for config type $configType', + async ({ cliOptions }) => { + fs.existsSync.mockImplementation((f) => f === FAKE_PKG) + fs.readFileSync + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementationOnce((f): any => { + if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0' }) + throw new Error('ENOENT') + }) + expect.assertions(2) + await runCli(...cliOptions) + + expect(fs.writeFileSync.mock.calls[0][0]).toBe(normalize('/foo/bar/jest.config.js')) + expect(fs.writeFileSync.mock.calls[0][1]).toMatchSnapshot() + }, + ) + + it.each(cliOptionCases)( + 'should create a jest config file with cli options for config type $configType and type "module" package.json', + async ({ cliOptions }) => { + fs.existsSync.mockImplementation((f) => f === FAKE_PKG) + fs.readFileSync + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementationOnce((f): any => { + if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0', type: 'module' }) + throw new Error('ENOENT') + }) + expect.assertions(2) + await runCli(...cliOptions) + + expect(fs.writeFileSync.mock.calls[0][0]).toBe(normalize('/foo/bar/jest.config.js')) + expect(fs.writeFileSync.mock.calls[0][1]).toMatchSnapshot() + }, + ) - it('should update package.json (with all options set)', async () => { - fs.existsSync.mockImplementation((f) => f === FAKE_PKG) - fs.readFileSync + it.each(cliOptionCases)( + 'should update package.json for config type $configType when user defines jest config via package.json', + async ({ cliOptions }) => { + fs.existsSync.mockImplementation((f) => f === FAKE_PKG) // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementationOnce((f): any => { + fs.readFileSync.mockImplementationOnce((f): any => { if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0' }) throw new Error('ENOENT') }) - expect.assertions(3) - const res = await runCli(...fullOptions, 'package.json') - - expect(res).toEqual({ - exitCode: 0, - log: '', - stderr: ` -Jest configuration written to "${normalize('/foo/bar/package.json')}". -`, - stdout: '', - }) - expect(fs.writeFileSync.mock.calls[0][0]).toBe(normalize('/foo/bar/package.json')) - expect(fs.writeFileSync.mock.calls[0][1]).toMatchInlineSnapshot(` - "{ - "name": "mock", - "version": "0.0.0-mock.0", - "jest": { - "preset": "ts-jest/presets/js-with-ts" - } - }" - `) - }) + expect.assertions(2) + await runCli(...cliOptions, 'package.json') + + expect(fs.writeFileSync.mock.calls[0][0]).toBe(normalize('/foo/bar/package.json')) + expect(fs.writeFileSync.mock.calls[0][1]).toMatchSnapshot() + }, + ) it('should output help', async () => { fs.existsSync.mockImplementation((f) => f === FAKE_PKG) @@ -314,7 +208,7 @@ Jest configuration written to "${normalize('/foo/bar/package.json')}". --force Discard any existing Jest config --js ts|babel Process '.js' files with ts-jest if 'ts' or with babel-jest if 'babel' - --jest-preset Toggle using preset + --no-jest-preset Disable the use of Jest presets --tsconfig Path to the tsconfig.json file --babel Enable using Babel to process 'js' resulted content from 'ts-jest' processing --jsdom Use 'jsdom' as test environment instead of 'node' @@ -687,4 +581,4 @@ Jest configuration written to "${normalize('/foo/bar/package.json')}". expect(res.stdout).toMatchSnapshot() }) }) // migrate -}) // config +}) diff --git a/src/cli/config/init.ts b/src/cli/config/init.ts index 29ab84837b..882ad682b6 100644 --- a/src/cli/config/init.ts +++ b/src/cli/config/init.ts @@ -11,135 +11,82 @@ import ejs from 'ejs' import { stringify as stringifyJson5 } from 'json5' import type { CliCommand, CliCommandArgs } from '..' -import { JEST_CONFIG_EJS_TEMPLATE, TS_JS_TRANSFORM_PATTERN, TS_TRANSFORM_PATTERN } from '../../constants' -import type { JestConfigWithTsJest, TsJestTransformerOptions } from '../../types' -import { type TsJestPresetDescriptor, defaults, jsWIthBabel, jsWithTs, JestPresetNames } from '../helpers/presets' +import { JEST_CONFIG_EJS_TEMPLATE } from '../../constants' +import { + createLegacyDefaultPreset, + createLegacyJsWithTsPreset, + createLegacyWithBabelPreset, +} from '../../presets/create-jest-preset' +import type { DefaultPreset, JsWithBabelPreset, JsWithTsPreset, TsJestTransformerOptions } from '../../types' + +const ensureOnlyUsingDoubleQuotes = (str: string): string => { + return str + .replace(/"'(.*?)'"/g, '"$1"') + .replace(/'ts-jest'/g, '"ts-jest"') + .replace(/'babel-jest'/g, '"babel-jest"') +} /** * @internal */ export const run: CliCommand = async (args: CliCommandArgs /* , logger: Logger */) => { + const { tsconfig: askedTsconfig, force, jsdom, js: jsFilesProcessor, babel: shouldPostProcessWithBabel } = args const file = args._[0]?.toString() ?? 'jest.config.js' const filePath = join(process.cwd(), file) const name = basename(file) - const isPackage = name === 'package.json' - const exists = existsSync(filePath) - const pkgFile = isPackage ? filePath : join(process.cwd(), 'package.json') - const hasPackage = isPackage || existsSync(pkgFile) - // read config - const { jestPreset = true, tsconfig: askedTsconfig, force, jsdom } = args + const isPackageJsonConfig = name === 'package.json' + const isJestConfigFileExisted = existsSync(filePath) + const pkgFile = isPackageJsonConfig ? filePath : join(process.cwd(), 'package.json') + const isPackageJsonExisted = isPackageJsonConfig || existsSync(pkgFile) const tsconfig = askedTsconfig === 'tsconfig.json' ? undefined : (askedTsconfig as TsJestTransformerOptions['tsconfig']) - // read package - const pkgJson = hasPackage ? JSON.parse(readFileSync(pkgFile, 'utf8')) : {} - - // auto js/babel - let { js: jsFilesProcessor, babel: shouldPostProcessWithBabel } = args - // set defaults for missing options - if (jsFilesProcessor == null) { - // set default js files processor depending on whether the user wants to post-process with babel - jsFilesProcessor = shouldPostProcessWithBabel ? 'babel' : undefined - } else if (shouldPostProcessWithBabel == null) { - // auto enables babel post-processing if the user wants babel to process js files - shouldPostProcessWithBabel = jsFilesProcessor === 'babel' - } - - // preset - let preset: TsJestPresetDescriptor | undefined - if (jsFilesProcessor === 'babel') { - preset = jsWIthBabel - } else if (jsFilesProcessor === 'ts') { - preset = jsWithTs - } else { - preset = defaults - } + const pkgJsonContent = isPackageJsonExisted ? JSON.parse(readFileSync(pkgFile, 'utf8')) : {} - if (isPackage && !exists) { + if (isPackageJsonConfig && !isJestConfigFileExisted) { throw new Error(`File ${file} does not exists.`) - } else if (!isPackage && exists && !force) { + } else if (!isPackageJsonConfig && isJestConfigFileExisted && !force) { throw new Error(`Configuration file ${file} already exists.`) } - if (!isPackage && !name.endsWith('.js')) { + if (!isPackageJsonConfig && !name.endsWith('.js')) { throw new TypeError(`Configuration file ${file} must be a .js file or the package.json.`) } - if (hasPackage && pkgJson.jest) { - if (force && !isPackage) { - delete pkgJson.jest - writeFileSync(pkgFile, JSON.stringify(pkgJson, undefined, ' ')) + if (isPackageJsonExisted && pkgJsonContent.jest) { + if (force && !isPackageJsonConfig) { + delete pkgJsonContent.jest + writeFileSync(pkgFile, JSON.stringify(pkgJsonContent, undefined, ' ')) } else if (!force) { throw new Error(`A Jest configuration is already set in ${pkgFile}.`) } } - // build configuration let body: string - - if (isPackage) { - // package.json config - const jestConfig: JestConfigWithTsJest = jestPreset ? { preset: preset.name } : { ...preset.value } - if (!jsdom) jestConfig.testEnvironment = 'node' - const transformerConfig = Object.entries(jestConfig.transform ?? {}).reduce( - (acc, [fileRegex, transformerConfig]) => { - if (tsconfig || shouldPostProcessWithBabel) { - const tsJestConf: TsJestTransformerOptions = {} - if (tsconfig) tsJestConf.tsconfig = tsconfig - if (shouldPostProcessWithBabel) tsJestConf.babelConfig = true - - return { - ...acc, - [fileRegex]: - typeof transformerConfig === 'string' - ? [transformerConfig, tsJestConf] - : [transformerConfig[0], { ...transformerConfig[1], ...tsJestConf }], - } - } - - return { - ...acc, - [fileRegex]: transformerConfig, - } - }, - {}, + const resolvedTsconfigOption = tsconfig ? { tsconfig: `${stringifyJson5(tsconfig)}` } : undefined + let transformConfig: DefaultPreset | JsWithTsPreset | JsWithBabelPreset + if (jsFilesProcessor === 'babel' || shouldPostProcessWithBabel) { + transformConfig = createLegacyWithBabelPreset(resolvedTsconfigOption) + } else if (jsFilesProcessor === 'ts') { + transformConfig = createLegacyJsWithTsPreset(resolvedTsconfigOption) + } else { + transformConfig = createLegacyDefaultPreset(resolvedTsconfigOption) + } + if (isPackageJsonConfig) { + body = ensureOnlyUsingDoubleQuotes( + JSON.stringify( + { + ...pkgJsonContent, + jest: transformConfig, + }, + undefined, + ' ', + ), ) - if (Object.keys(transformerConfig).length) { - jestConfig.transform = { - ...jestConfig.transform, - ...transformerConfig, - } - } - body = JSON.stringify({ ...pkgJson, jest: jestConfig }, undefined, ' ') } else { - let transformPattern = TS_TRANSFORM_PATTERN - let transformValue = !tsconfig - ? `'ts-jest'` - : ` - [ - 'ts-jest', - { - tsconfig: ${stringifyJson5(tsconfig)} - } - ] - ` - if (preset.name === JestPresetNames.jsWithTs) { - transformPattern = TS_JS_TRANSFORM_PATTERN - } else if (preset.name === JestPresetNames.jsWIthBabel) { - transformValue = !tsconfig - ? `'ts-jest'` - : ` - [ - 'ts-jest', - { - babelConfig: true, - tsconfig: ${stringifyJson5(tsconfig)} - } - ] - ` - } + const [transformPattern, transformValue] = Object.entries(transformConfig.transform)[0] body = ejs.render(JEST_CONFIG_EJS_TEMPLATE, { - exportKind: pkgJson.type === 'module' ? 'export default' : 'module.exports =', + exportKind: pkgJsonContent.type === 'module' ? 'export default' : 'module.exports =', testEnvironment: jsdom ? 'jsdom' : 'node', transformPattern, - transformValue, + transformValue: ensureOnlyUsingDoubleQuotes(stringifyJson5(transformValue)), }) } @@ -168,7 +115,7 @@ Options: --force Discard any existing Jest config --js ts|babel Process '.js' files with ts-jest if 'ts' or with babel-jest if 'babel' - --jest-preset Toggle using preset + --no-jest-preset Disable the use of Jest presets --tsconfig Path to the tsconfig.json file --babel Enable using Babel to process 'js' resulted content from 'ts-jest' processing --jsdom Use 'jsdom' as test environment instead of 'node' diff --git a/src/constants.ts b/src/constants.ts index 18c85f92c2..cd79cfdf2d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -22,8 +22,8 @@ export const DEFAULT_JEST_TEST_MATCH = ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.) */ export const JEST_CONFIG_EJS_TEMPLATE = `/** @type {import('ts-jest').JestConfigWithTsJest} **/ <%= exportKind %> { - testEnvironment: '<%= testEnvironment %>', + testEnvironment: "<%= testEnvironment %>", transform: { - '<%= transformPattern %>': <%- transformValue %>, + "<%= transformPattern %>": <%- transformValue %>, }, };` diff --git a/src/legacy/config/config-set.spec.ts b/src/legacy/config/config-set.spec.ts index 95d1a18c4b..d167817c16 100644 --- a/src/legacy/config/config-set.spec.ts +++ b/src/legacy/config/config-set.spec.ts @@ -557,7 +557,7 @@ describe('raiseDiagnostics', () => { code = 9999, category = ts.DiagnosticCategory.Warning, }: // eslint-disable-next-line @typescript-eslint/no-explicit-any - Partial = {}): ts.Diagnostic => ({ messageText, code, category }) as any + Partial = {}): ts.Diagnostic => ({ messageText, code, category } as any) it('should throw when warnOnly is false', () => { const cs = createConfigSet({ filterDiagnostics, logger, tsJestConfig: { diagnostics: { pretty: false } } }) @@ -604,7 +604,7 @@ describe('raiseDiagnostics', () => { code = 9999, category = ts.DiagnosticCategory.Warning, }: // eslint-disable-next-line @typescript-eslint/no-explicit-any - Partial = {}): ts.Diagnostic => ({ messageText, code, category }) as any + Partial = {}): ts.Diagnostic => ({ messageText, code, category } as any) test('should not throw when diagnostics contains file path and exclude config matches file path', () => { const cs = createConfigSet({ logger, @@ -642,7 +642,7 @@ describe('raiseDiagnostics', () => { category = ts.DiagnosticCategory.Warning, file = program.getSourceFiles().find((sourceFile) => sourceFile.fileName === 'src/__mocks__/index.ts'), }: // eslint-disable-next-line @typescript-eslint/no-explicit-any - Partial = {}): ts.Diagnostic => ({ messageText, code, category, file }) as any + Partial = {}): ts.Diagnostic => ({ messageText, code, category, file } as any) test(`should throw when exclude config doesn't match source file path`, () => { const cs = createConfigSet({ diff --git a/src/legacy/config/config-set.ts b/src/legacy/config/config-set.ts index e7bd4da1be..10165f315d 100644 --- a/src/legacy/config/config-set.ts +++ b/src/legacy/config/config-set.ts @@ -186,10 +186,7 @@ export class ConfigSet { tsBuildInfoFile: undefined, } - constructor( - jestConfig: TsJestTransformOptions['config'] | undefined, - readonly parentLogger?: Logger, - ) { + constructor(jestConfig: TsJestTransformOptions['config'] | undefined, readonly parentLogger?: Logger) { this.logger = this.parentLogger ? this.parentLogger.child({ [LogContexts.namespace]: 'config' }) : rootLogger.child({ namespace: 'config' })