diff --git a/cli.js b/cli.js
index d292428e45..3c3ce8156f 100755
--- a/cli.js
+++ b/cli.js
@@ -1,3 +1,3 @@
#!/usr/bin/env node
-require('./dist/cli')
+require('./dist/cli').processArgv()
diff --git a/docs/user/config.md b/docs/user/config.md
index 92a8633696..42e781b828 100644
--- a/docs/user/config.md
+++ b/docs/user/config.md
@@ -89,3 +89,19 @@ All options have default values which should fit most of the projects.
- [**`diagnostics`**: Diagnostics related configuration.](config/diagnostics)
- [**`babelConfig`**: Babel(Jest) related configuration.](config/babelConfig)
- [**`stringifyContentPathRegex`**: Configure which file(s) will become a module returning its content.](config/stringifyContentPathRegex)
+
+### Upgrading
+
+You can use the `config:migrate` tool of TSJest CLI if you're coming from an older version to help you migrate your Jest configuration.
+
+
+If you're using `jest.config.json`:
+```sh
+node ./node_modules/.bin/ts-jest config:migrate jest.config.js
+```
+
+If you're using `jest` config property of `package.json`:
+```sh
+node ./node_modules/.bin/ts-jest config:migrate package.json
+```
+
diff --git a/src/cli/cli.spec.ts b/src/cli/cli.spec.ts
new file mode 100644
index 0000000000..3847fd2816
--- /dev/null
+++ b/src/cli/cli.spec.ts
@@ -0,0 +1,284 @@
+import { testing } from 'bs-logger'
+import * as _fs from 'fs'
+import { normalize, resolve } from 'path'
+
+import { mocked } from '../__helpers__/mocks'
+import { rootLogger as _rootLogger } from '../util/logger'
+
+import { processArgv } from '.'
+
+// === helpers ================================================================
+jest.mock('../util/logger')
+jest.mock('fs')
+const fs = mocked(_fs)
+const rootLogger = _rootLogger as testing.LoggerMock
+
+const mockWriteStream = () => {
+ return {
+ written: [] as string[],
+ write(text: string) {
+ this.written.push(text)
+ },
+ clear() {
+ this.written = []
+ },
+ }
+}
+
+const mockObject = (obj: T, newProps: M): T & M & { mockRestore: () => T } => {
+ const backup: any = Object.create(null)
+
+ Object.keys(newProps).forEach(key => {
+ const desc = (backup[key] = Object.getOwnPropertyDescriptor(obj, key))
+ const newDesc: any = { ...desc }
+ if (newDesc.get) {
+ newDesc.get = () => (newProps as any)[key]
+ } else {
+ newDesc.value = (newProps as any)[key]
+ }
+ Object.defineProperty(obj, key, newDesc)
+ })
+ if ((obj as any).mockRestore) backup.mockRestore = Object.getOwnPropertyDescriptor(obj, 'mockRestore')
+ return Object.defineProperty(obj, 'mockRestore', {
+ value() {
+ Object.keys(backup).forEach(key => {
+ Object.defineProperty(obj, key, backup[key])
+ })
+ return obj
+ },
+ configurable: true,
+ })
+}
+
+let lastExitCode: number | undefined
+
+const runCli = async (
+ ...args: any[]
+): Promise<{ stdout: string; stderr: string; exitCode: number | undefined; log: string }> => {
+ mockedProcess.stderr.clear()
+ mockedProcess.stdout.clear()
+ rootLogger.target.clear()
+ mockedProcess.argv.splice(2, mockedProcess.argv.length - 2, ...args)
+ lastExitCode = undefined
+ await processArgv()
+ return {
+ exitCode: lastExitCode,
+ stdout: mockedProcess.stdout.written.join('\n'),
+ stderr: mockedProcess.stderr.written.join('\n'),
+ log: rootLogger.target.lines.join('\n'),
+ }
+}
+
+let mockedProcess: any
+const FAKE_CWD = normalize('/foo/bar')
+const FAKE_PKG = normalize(`${FAKE_CWD}/package.json`)
+fs.existsSync.mockImplementation(f => f === FAKE_PKG)
+fs.readFileSync.mockImplementation(f => {
+ if (f === FAKE_PKG) return JSON.stringify({ name: 'mock', version: '0.0.0-mock.0' })
+ throw new Error('ENOENT')
+})
+
+// === test ===================================================================
+
+beforeEach(() => {
+ // jest.resetModules()
+ lastExitCode = undefined
+ mockedProcess = mockObject(process, {
+ cwd: () => FAKE_CWD,
+ argv: ['node', resolve(__dirname, '..', '..', 'cli.js')],
+ stderr: mockWriteStream(),
+ stdout: mockWriteStream(),
+ exit: (exitCode = 0) => {
+ lastExitCode = exitCode
+ },
+ })
+ fs.writeFileSync.mockClear()
+ fs.existsSync.mockClear()
+ fs.readFileSync.mockClear()
+ rootLogger.target.clear()
+})
+afterEach(() => {
+ mockedProcess.mockRestore()
+ mockedProcess = undefined
+})
+
+describe('cli', async () => {
+ it('should output usage', async () => {
+ expect.assertions(2)
+ await expect(runCli()).resolves.toMatchInlineSnapshot(`
+Object {
+ "exitCode": 0,
+ "log": "",
+ "stderr": "",
+ "stdout": "
+Usage:
+ ts-jest command [options] [...args]
+
+Commands:
+ config:init Creates initial Jest configuration
+ config:migrate Migrates a given Jest configuration
+ help [command] Show this help, or help about a command
+
+Example:
+ ts-jest help config:migrate
+",
+}
+`)
+ await expect(runCli('hello:motto')).resolves.toMatchInlineSnapshot(`
+Object {
+ "exitCode": 0,
+ "log": "",
+ "stderr": "",
+ "stdout": "
+Usage:
+ ts-jest command [options] [...args]
+
+Commands:
+ config:init Creates initial Jest configuration
+ config:migrate Migrates a given Jest configuration
+ help [command] Show this help, or help about a command
+
+Example:
+ ts-jest help config:migrate
+",
+}
+`)
+ })
+})
+
+describe('config', async () => {
+ // briefly tested, see header comment in `config/init.ts`
+ describe('init', async () => {
+ const noOption = ['config:init']
+ const fullOptions = [
+ ...noOption,
+ '--babel',
+ '--tsconfig',
+ 'tsconfig.test.json',
+ '--jsdom',
+ '--no-jest-preset',
+ '--allow-js',
+ ]
+ it('should create a jest.config.json (without options)', async () => {
+ expect.assertions(2)
+ 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).toEqual([
+ [
+ normalize('/foo/bar/jest.config.js'),
+ `module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+};`,
+ ],
+ ])
+ })
+ it('should create a jest.config.foo.json (with all options set)', async () => {
+ expect.assertions(2)
+ 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).toEqual([
+ [
+ normalize('/foo/bar/jest.config.foo.js'),
+ `const tsJest = require('ts-jest').createJestPreset({ allowJs: true });
+
+module.exports = {
+ ...tsJest,
+ globals: {
+ 'ts-jest': {
+ tsconfig: 'tsconfig.test.json',
+ babelConfig: true,
+ },
+ },
+};`,
+ ],
+ ])
+ })
+ it('should update package.json (without options)', async () => {
+ 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('should update package.json (with all options set)', async () => {
+ expect.assertions(2)
+ const res = await runCli(...fullOptions, 'package.json')
+ expect(res).toEqual({
+ exitCode: 0,
+ log: `[level:20] creating jest presets handling JavaScript files
+`,
+ 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": {
+ "transform": {
+ "^.+\\\\.[tj]sx?$": "ts-jest"
+ },
+ "testMatch": [
+ "**/__tests__/**/*.js?(x)",
+ "**/?(*.)+(spec|test).js?(x)",
+ "**/__tests__/**/*.ts?(x)",
+ "**/?(*.)+(spec|test).ts?(x)"
+ ],
+ "moduleFileExtensions": [
+ "js",
+ "json",
+ "jsx",
+ "node",
+ "ts",
+ "tsx"
+ ],
+ "globals": {
+ "ts-jest": {
+ "tsconfig": "tsconfig.test.json",
+ "babelConfig": true
+ }
+ }
+ }
+}`,
+ ],
+ ])
+ })
+ })
+})
diff --git a/src/cli/config/init.ts b/src/cli/config/init.ts
new file mode 100644
index 0000000000..e0318a5d46
--- /dev/null
+++ b/src/cli/config/init.ts
@@ -0,0 +1,116 @@
+/**
+ * This has been written quickly. While trying to improve I realised it'd be better to have it in Jest...
+ * ...and I saw a merged PR with `jest --init` tool!
+ * TODO: see what's the best path for this
+ */
+
+import { existsSync, readFileSync, writeFileSync } from 'fs'
+import { stringify as stringifyJson5 } from 'json5'
+import { basename, join } from 'path'
+import { Arguments } from 'yargs'
+
+import { CliCommand } from '..'
+import { createJestPreset } from '../../config/create-jest-preset'
+
+export const run: CliCommand = async (args: Arguments /* , logger: Logger */) => {
+ const file = args._[0] || '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 { allowJs = false, jestPreset = true, tsconfig: askedTsconfig, babel = false, force, jsdom } = args
+ const tsconfig = askedTsconfig === 'tsconfig.json' ? undefined : askedTsconfig
+ const hasPresetVar = allowJs || !jestPreset
+ // read package
+ const pkgJson = hasPackage ? JSON.parse(readFileSync(pkgFile, 'utf8')) : {}
+
+ if (isPackage && !exists) {
+ throw new Error(`File ${file} does not exists.`)
+ } else if (!isPackage && exists && !force) {
+ throw new Error(`Configuration file ${file} already exists.`)
+ }
+ if (!isPackage && !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, ' '))
+ } 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 base: any = hasPresetVar ? createJestPreset({ allowJs }) : { preset: 'ts-jest' }
+ if (!jsdom) base.testEnvironment = 'node'
+ if (tsconfig || babel) {
+ const tsJestConf: any = {}
+ base.globals = { 'ts-jest': tsJestConf }
+ if (tsconfig) tsJestConf.tsconfig = tsconfig
+ if (babel) tsJestConf.babelConfig = true
+ }
+ body = JSON.stringify({ ...pkgJson, jest: base }, undefined, ' ')
+ } else {
+ // js config
+ const content = []
+ if (hasPresetVar) {
+ content.push(`const tsJest = require('ts-jest').createJestPreset({ allowJs: ${allowJs} });`, '')
+ }
+ content.push('module.exports = {')
+ if (hasPresetVar) {
+ content.push(` ...tsJest,`)
+ } else {
+ content.push(` preset: 'ts-jest',`)
+ }
+ if (!jsdom) content.push(` testEnvironment: 'node',`)
+
+ if (tsconfig || babel) {
+ content.push(` globals: {`)
+ content.push(` 'ts-jest': {`)
+ if (tsconfig) content.push(` tsconfig: ${stringifyJson5(tsconfig)},`)
+ if (babel) content.push(` babelConfig: true,`)
+ content.push(` },`)
+ content.push(` },`)
+ }
+
+ content.push('};')
+
+ // join all together
+ body = content.join('\n')
+ }
+
+ writeFileSync(filePath, body)
+
+ process.stderr.write(`
+Jest configuration written to "${filePath}".
+`)
+}
+
+export const help: CliCommand = async () => {
+ process.stdout.write(`
+Usage:
+ ts-jest config:init [options] []
+
+Arguments:
+ Can be a js or json Jest config file. If it is a
+ package.json file, the configuration will be read from
+ the "jest" property.
+ Defaul: jest.config.js
+
+Options:
+ --force Dicard any existing Jest config
+ --allow-js TSJest will be used to process JS files as well
+ --no-jest-preset Disable the use of Jest presets
+ --tsconfig Path to the tsconfig.json file
+ --babel Call BabelJest after TSJest
+ --jsdom Use jsdom as test environment instead of node
+`)
+}
diff --git a/src/cli/help.ts b/src/cli/help.ts
index af892ce3ab..de0d5465d7 100644
--- a/src/cli/help.ts
+++ b/src/cli/help.ts
@@ -6,8 +6,9 @@ Usage:
ts-jest command [options] [...args]
Commands:
- help [command] Show this help, or help about a command
+ config:init Creates initial Jest configuration
config:migrate Migrates a given Jest configuration
+ help [command] Show this help, or help about a command
Example:
ts-jest help config:migrate
diff --git a/src/cli/index.ts b/src/cli/index.ts
index 6c51aec9b8..a1f44f53a5 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -4,38 +4,41 @@ import yargsParser from 'yargs-parser'
import { rootLogger } from '../util/logger'
-const VALID_COMMANDS = ['help', 'config:migrate']
-
-// tslint:disable-next-line:prefer-const
-let [, , ...args] = process.argv
+const VALID_COMMANDS = ['help', 'config:migrate', 'config:init']
const logger = rootLogger.child({ [LogContexts.namespace]: 'cli', [LogContexts.application]: 'ts-jest' })
-const parsedArgv = yargsParser(args, {
- boolean: ['dryRun', 'jestPreset', 'allowJs', 'diff'],
- count: ['verbose'],
- alias: { verbose: ['v'] },
- default: { dryRun: false, jestPreset: true, allowJs: false, verbose: 0, diff: false },
-})
-let command = parsedArgv._.shift() as string
-const isHelp = command === 'help'
-if (isHelp) command = parsedArgv._.shift() as string
+export type CliCommand = (argv: Arguments, logger: Logger) => Promise
-if (!VALID_COMMANDS.includes(command)) command = 'help'
+async function cli(args: string[]): Promise {
+ const parsedArgv = yargsParser(args, {
+ boolean: ['dry-run', 'jest-preset', 'allow-js', 'diff', 'babel', 'force', 'jsdom'],
+ string: ['tsconfig'],
+ count: ['verbose'],
+ alias: { verbose: ['v'] },
+ default: { jestPreset: true, verbose: 0 },
+ })
-export type CliCommand = (argv: Arguments, logger: Logger) => Promise
+ let command = parsedArgv._.shift() as string
+ const isHelp = command === 'help'
+ if (isHelp) command = parsedArgv._.shift() as string
+
+ if (!VALID_COMMANDS.includes(command)) command = 'help'
+
+ // tslint:disable-next-line:no-var-requires
+ const { run, help }: { run: CliCommand; help: CliCommand } = require(`./${command.replace(/:/g, '/')}`)
-// tslint:disable-next-line:no-var-requires
-const { run, help }: { run: CliCommand; help: CliCommand } = require(`./${command.replace(/:/g, '/')}`)
+ const cmd = isHelp && command !== 'help' ? help : run
-const cmd = isHelp && command !== 'help' ? help : run
+ return cmd(parsedArgv, logger)
+}
-cmd(parsedArgv, logger).then(
- () => {
+export async function processArgv(): Promise {
+ try {
+ await cli(process.argv.slice(2))
process.exit(0)
- },
- (err: Error) => {
+ } catch (err) {
logger.fatal(err.message)
process.exit(1)
- },
-)
+ }
+}