Skip to content

Commit 714f5f0

Browse files
committed
feat(cli): adds a cli tool to migrate old config
TSJest migrates the config on the fly to stay compatible with older versions, but it generates warning. This adds a cli tool which users can call to migrate their configuraton easily without opening an issue :D
1 parent bb0c06e commit 714f5f0

12 files changed

Lines changed: 361 additions & 42 deletions

File tree

cli.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env node
2+
3+
require('./dist/cli')

package-lock.json

Lines changed: 25 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "23.10.0-beta.2",
44
"main": "dist/index.js",
55
"types": "dist/index.d.ts",
6+
"bin": "cli.js",
67
"description": "A preprocessor with sourcemap support to help use Typescript with Jest",
78
"scripts": {
89
"prebuild": "node scripts/clean-dist.js",
@@ -63,7 +64,8 @@
6364
"make-error": "^1.3.5",
6465
"mkdirp": "^0.5.1",
6566
"semver": "^5.5.1",
66-
"tslib": "^1.9.3"
67+
"tslib": "^1.9.3",
68+
"yargs-parser": "^10.1.0"
6769
},
6870
"peerDependencies": {
6971
"babel-jest": ">=22 <24",
@@ -86,6 +88,7 @@
8688
"@types/mkdirp": "^0.5.2",
8789
"@types/node": "^10.9.4",
8890
"@types/semver": "^5.5.0",
91+
"@types/yargs": "^11.1.1",
8992
"conventional-changelog-cli": "^2.0.5",
9093
"cross-spawn": "^6.0.5",
9194
"doctoc": "^1.3.1",

src/cli/config/migrate.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Logger } from 'bs-logger'
2+
import stringifyJson from 'fast-json-stable-stringify'
3+
import { existsSync } from 'fs'
4+
import { stringify as stringifyJson5 } from 'json5'
5+
import { basename, join } from 'path'
6+
import { Arguments } from 'yargs'
7+
8+
import { CliCommand } from '..'
9+
import { createJestPreset } from '../../config/create-jest-preset'
10+
import { backportJestConfig } from '../../util/backports'
11+
12+
export const run: CliCommand = async (args: Arguments, logger: Logger) => {
13+
const file = args._[0]
14+
const filePath = join(process.cwd(), file)
15+
if (!existsSync(filePath)) {
16+
throw new Error(`Configuration file ${file} does not exists.`)
17+
}
18+
const name = basename(file)
19+
const isPackage = name === 'package.json'
20+
if (!/\.(js|json)$/.test(name)) {
21+
throw new TypeError(`Configuration file ${file} must be a JavaScript or JSON file.`)
22+
}
23+
let actualConfig: jest.InitialOptions = require(filePath)
24+
if (isPackage) {
25+
actualConfig = (actualConfig as any).jest
26+
}
27+
if (!actualConfig) actualConfig = {}
28+
29+
// migrate
30+
// first we backport our options
31+
const migratedConfig = backportJestConfig(logger, actualConfig)
32+
// then we check if we can use `preset`
33+
if (!migratedConfig.preset && args.jestPreset) {
34+
migratedConfig.preset = 'ts-jest'
35+
} else if (!args.jestPreset && migratedConfig.preset === 'ts-jest') {
36+
delete migratedConfig.preset
37+
}
38+
const usesPreset = migratedConfig.preset === 'ts-jest'
39+
const presets = createJestPreset({ allowJs: args.allowJs })
40+
41+
// check the extensions
42+
if (migratedConfig.moduleFileExtensions && migratedConfig.moduleFileExtensions.length && usesPreset) {
43+
const presetValue = dedupSort(presets.moduleFileExtensions).join('::')
44+
const migratedValue = dedupSort(migratedConfig.moduleFileExtensions).join('::')
45+
if (presetValue === migratedValue) {
46+
delete migratedConfig.moduleFileExtensions
47+
}
48+
}
49+
// check the testMatch
50+
if (migratedConfig.testMatch && migratedConfig.testMatch.length && usesPreset) {
51+
const presetValue = dedupSort(presets.testMatch).join('::')
52+
const migratedValue = dedupSort(migratedConfig.testMatch).join('::')
53+
if (presetValue === migratedValue) {
54+
delete migratedConfig.testMatch
55+
}
56+
}
57+
58+
// migrate the transform
59+
if (migratedConfig.transform) {
60+
Object.keys(migratedConfig.transform).forEach(key => {
61+
const val = (migratedConfig.transform as any)[key]
62+
if (typeof val === 'string' && /\/?ts-jest(?:\/preprocessor\.js)?$/.test(val)) {
63+
// tslint:disable-next-line:semicolon
64+
;(migratedConfig.transform as any)[key] = 'ts-jest'
65+
}
66+
})
67+
}
68+
// check if it's the same as the preset's one
69+
if (
70+
usesPreset &&
71+
migratedConfig.transform &&
72+
stringifyJson(migratedConfig.transform) === stringifyJson(presets.transform)
73+
) {
74+
delete migratedConfig.transform
75+
}
76+
77+
// cleanup
78+
cleanupConfig(actualConfig)
79+
cleanupConfig(migratedConfig)
80+
const before = stringifyJson(actualConfig)
81+
const after = stringifyJson(migratedConfig)
82+
if (after === before) {
83+
process.stderr.write(`
84+
No migration needed for given Jest configuration
85+
`)
86+
return
87+
}
88+
89+
const stringify = /\.json$/.test(file) ? JSON.stringify : stringifyJson5
90+
const footNotes: string[] = []
91+
92+
// if we are using preset, inform the user that he might be able to remove some section(s)
93+
// we couldn't check for equality
94+
// if (usesPreset && migratedConfig.testMatch) {
95+
// footNotes.push(`
96+
// I couldn't check if your "testMatch" value is the same as mine which is: ${stringify(
97+
// presets.testMatch,
98+
// undefined,
99+
// ' ',
100+
// )}
101+
// If it is the case, you can safely remove the "testMatch" from what I've migrated.
102+
// `)
103+
// }
104+
if (usesPreset && migratedConfig.transform) {
105+
footNotes.push(`
106+
I couldn't check if your "transform" value is the same as mine which is: ${stringify(
107+
presets.transform,
108+
undefined,
109+
' ',
110+
)}
111+
If it is the case, you can safely remove the "transform" from what I've migrated.
112+
`)
113+
}
114+
115+
// output new config
116+
process.stderr.write(`
117+
Migrated Jest configuration:
118+
`)
119+
process.stdout.write(`${stringify(migratedConfig, undefined, ' ')}\n`)
120+
process.stderr.write(`
121+
${footNotes.join('\n')}
122+
`)
123+
}
124+
125+
function cleanupConfig(config: jest.InitialOptions): void {
126+
if (config.globals) {
127+
if ((config as any).globals['ts-jest'] && Object.keys((config as any).globals['ts-jest']).length === 0) {
128+
delete (config as any).globals['ts-jest']
129+
}
130+
if (Object.keys(config.globals).length === 0) {
131+
delete config.globals
132+
}
133+
}
134+
if (config.transform && Object.keys(config.transform).length === 0) {
135+
delete config.transform
136+
}
137+
if (config.moduleFileExtensions) {
138+
config.moduleFileExtensions = dedupSort(config.moduleFileExtensions)
139+
if (config.moduleFileExtensions.length === 0) delete config.moduleFileExtensions
140+
}
141+
if (config.testMatch) {
142+
config.testMatch = dedupSort(config.testMatch)
143+
if (config.testMatch.length === 0) delete config.testMatch
144+
}
145+
}
146+
147+
function dedupSort(arr: any[]) {
148+
return arr
149+
.filter((s, i, a) => a.findIndex(e => s.toString() === e.toString()) === i)
150+
.sort((a, b) => (a.toString() > b.toString() ? 1 : a.toString() < b.toString() ? -1 : 0))
151+
}
152+
153+
export const help: CliCommand = async () => {
154+
process.stdout.write(`
155+
Usage:
156+
ts-jest config:migrate [options] <config-file>
157+
158+
Arguments:
159+
<config-file> Can be a js or json Jest config file. If it is a
160+
package.json file, the configuration will be read from
161+
the "jest" property.
162+
163+
Options:
164+
--allow-js TSJest will be used to process JS files as well
165+
--no-jest-preset Disable the use of Jest presets
166+
`)
167+
}

src/cli/help.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Arguments } from 'yargs'
2+
3+
export const run = async (_: Arguments) => {
4+
process.stdout.write(`
5+
Usage:
6+
ts-jest command [options] [...args]
7+
8+
Commands:
9+
help [command] Show this help, or help about a command
10+
config:migrate Migrates a given Jest configuration
11+
12+
Example:
13+
ts-jest help config:migrate
14+
`)
15+
}
16+
17+
export { run as help }

src/cli/index.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { LogContexts, Logger } from 'bs-logger'
2+
import { Arguments } from 'yargs'
3+
import yargsParser from 'yargs-parser'
4+
5+
import { rootLogger } from '../util/logger'
6+
7+
const VALID_COMMANDS = ['help', 'config:migrate']
8+
9+
// tslint:disable-next-line:prefer-const
10+
let [, , ...args] = process.argv
11+
12+
const logger = rootLogger.child({ [LogContexts.namespace]: 'cli', [LogContexts.application]: 'ts-jest' })
13+
14+
const parsedArgv = yargsParser(args, {
15+
boolean: ['dryRun', 'jestPreset', 'allowJs', 'diff'],
16+
count: ['verbose'],
17+
alias: { verbose: ['v'] },
18+
default: { dryRun: false, jestPreset: true, allowJs: false, verbose: 0, diff: false },
19+
})
20+
let command = parsedArgv._.shift() as string
21+
const isHelp = command === 'help'
22+
if (isHelp) command = parsedArgv._.shift() as string
23+
24+
if (!VALID_COMMANDS.includes(command)) command = 'help'
25+
26+
export type CliCommand = (argv: Arguments, logger: Logger) => Promise<void>
27+
28+
// tslint:disable-next-line:no-var-requires
29+
const { run, help }: { run: CliCommand; help: CliCommand } = require(`./${command.replace(/:/g, '/')}`)
30+
31+
const cmd = isHelp && command !== 'help' ? help : run
32+
33+
cmd(parsedArgv, logger).then(
34+
() => {
35+
process.exit(0)
36+
},
37+
(err: Error) => {
38+
logger.fatal(err.message)
39+
process.exit(1)
40+
},
41+
)

src/config/create-jest-preset.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@ const defaults = jestConfig.defaults || {
1212
moduleFileExtensions: ['js', 'json', 'jsx', 'node'],
1313
}
1414

15-
// TODO: find out if tsconfig that we'll use contains `allowJs`
16-
// and change the transform so that it also uses ts-jest for js files
17-
18-
export function createJestPreset({ allowJs = false }: CreateJestPresetOptions = {}) {
15+
export function createJestPreset(
16+
{ allowJs = false }: CreateJestPresetOptions = {},
17+
from: jest.InitialOptions = defaults,
18+
) {
1919
logger.debug({ allowJs }, 'creating jest presets', allowJs ? 'handling' : 'not handling', 'JavaScript files')
2020
return {
2121
transform: {
22-
...defaults.transform,
22+
...from.transform,
2323
[allowJs ? '^.+\\.[tj]sx?$' : '^.+\\.tsx?$']: 'ts-jest',
2424
},
25-
testMatch: [...defaults.testMatch, '**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
26-
moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
25+
testMatch: dedup([...from.testMatch, '**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)']),
26+
moduleFileExtensions: dedup([...from.moduleFileExtensions, 'ts', 'tsx']),
2727
}
2828
}
29+
30+
function dedup(array: string[]): string[] {
31+
return array.filter((e, i, a) => a.indexOf(e) === i)
32+
}

src/shims.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ declare module 'jest-config' {
1717
}
1818

1919
declare module 'babel-core/lib/transformation/file'
20+
21+
declare module 'yargs-parser' {
22+
import yargs from 'yargs'
23+
export = yargs.parse
24+
}

0 commit comments

Comments
 (0)