diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b2a46da94f..7f5c2dc5ad30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## master +### Features + +- `[jest-cli]` Add `jest --init` option that generates a basic configuration file with a short description for each option ([#6442](https://github.com/facebook/jest/pull/6442)) + ### Fixes - `[jest-config]` Add missing options to the `defaults` object ([#6428](https://github.com/facebook/jest/pull/6428)) diff --git a/docs/CLI.md b/docs/CLI.md index 631074ba59d7..a6dbd10c2bf1 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -169,6 +169,10 @@ Force Jest to exit after all tests have completed running. This is useful when r Show the help information, similar to this page. +### `--init` + +Generate a basic configuration file. Based on your project, Jest will ask you a few questions that will help to generate a `jest.config.js` file with a short description for each option. + ### `--json` Prints the test results in JSON. This mode will send all other test output and user messages to stderr. diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index c95e09a5ff04..fb8d2a6c5092 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -69,6 +69,14 @@ If you'd like to learn more about running `jest` through the command line, take ## Additional Configuration +### Generate a basic configuration file + +Based on your project, Jest will ask you a few questions and will create a basic configuration file with a short description for each option: + +```bash +jest --init +``` + ### Using Babel To use [Babel](http://babeljs.io/), install the `babel-jest` and `regenerator-runtime` packages: diff --git a/package.json b/package.json index 4002e87d51ff..781083ebf3f8 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "/packages/.*/src/__tests__/expect_util.js", "/packages/jest-cli/src/__tests__/test_root", "/packages/jest-cli/src/__tests__/__fixtures__/", + "/packages/jest-cli/src/lib/__tests__/fixtures/", "/packages/jest-haste-map/src/__tests__/haste_impl.js", "/packages/jest-resolve-dependencies/src/__tests__/__fixtures__/", "/packages/jest-runtime/src/__tests__/defaultResolver.js", diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index 1e9c92fef45f..a89b9e4a8b18 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -32,6 +32,7 @@ "jest-worker": "^23.0.1", "micromatch": "^2.3.11", "node-notifier": "^5.2.1", + "prompts": "^0.1.9", "realpath-native": "^1.0.0", "rimraf": "^2.5.4", "slash": "^1.0.0", diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index edc17bda56a3..6ecb7597daa3 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -294,6 +294,10 @@ export const options = { 'A JSON string with map of variables for the haste module system', type: 'string', }, + init: { + description: 'Generate a basic configuration file', + type: 'boolean', + }, json: { default: undefined, description: diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 37c59b3ea2a6..ad44745032cb 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -32,10 +32,17 @@ import pluralize from '../pluralize'; import yargs from 'yargs'; import rimraf from 'rimraf'; import {sync as realpath} from 'realpath-native'; +import init from '../lib/init'; export async function run(maybeArgv?: Argv, project?: Path) { try { const argv: Argv = buildArgv(maybeArgv, project); + + if (argv.init) { + await init(); + return; + } + const projects = getProjectListFromCLIArgs(argv, project); const {results, globalConfig} = await runCLI(argv, projects); diff --git a/packages/jest-cli/src/constants.js b/packages/jest-cli/src/constants.js index bdf2e5efdf85..67418d211d72 100644 --- a/packages/jest-cli/src/constants.js +++ b/packages/jest-cli/src/constants.js @@ -16,3 +16,5 @@ export const ICONS = { pending: '\u25CB', success: isWindows ? '\u221A' : '\u2713', }; +export const PACKAGE_JSON = 'package.json'; +export const JEST_CONFIG = 'jest.config.js'; diff --git a/packages/jest-cli/src/lib/__tests__/__snapshots__/init.test.js.snap b/packages/jest-cli/src/lib/__tests__/__snapshots__/init.test.js.snap new file mode 100644 index 000000000000..9b76d05c3803 --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/__snapshots__/init.test.js.snap @@ -0,0 +1,252 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`init has jest config in package.json should ask the user whether to override config or not 1`] = ` +Object { + "initial": true, + "message": "It seems that you already have a jest configuration, do you want to override it?", + "name": "continue", + "type": "confirm", +} +`; + +exports[`init has-jest-config-file ask the user whether to override config or not user answered with "Yes" 1`] = ` +Object { + "initial": true, + "message": "It seems that you already have a jest configuration, do you want to override it?", + "name": "continue", + "type": "confirm", +} +`; + +exports[`init project with package.json and no jest config all questions answered with answer: "No" should return the default configuration (an empty config) 1`] = ` +"// For a detailed explanation regarding each configuration property, visit: +// https://facebook.github.io/jest/docs/en/configuration.html + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after the first failure + // bail: false, + + // Respect \\"browser\\" field in package.json when resolving modules + // browser: false, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: \\"/tmp/jest\\", + + // Automatically clear mock calls and instances between every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: null, + + // The directory where Jest should output its coverage files + // coverageDirectory: null, + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // \\"/node_modules/\\" + // ], + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // \\"json\\", + // \\"text\\", + // \\"lcov\\", + // \\"clover\\" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: null, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files usin a array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: null, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: null, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // \\"node_modules\\" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // \\"js\\", + // \\"json\\", + // \\"jsx\\", + // \\"node\\" + // ], + + // A map from regular expressions to module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: \\"always\\", + + // A preset that is used as a base for Jest's configuration + // preset: null, + + // Run tests from one or more projects + // projects: null, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: null, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: null, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // \\"\\" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: \\"jest-runner\\", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // The path to a module that runs some code to configure or set up the testing framework before each test + // setupTestFrameworkScriptFile: null, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: \\"jest-environment-jsdom\\", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // \\"**/__tests__/**/*.js?(x)\\", + // \\"**/?(*.)+(spec|test).js?(x)\\" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // \\"/node_modules/\\" + // ], + + // The regexp pattern Jest uses to detect test files + // testRegex: \\"\\", + + // This option allows the use of a custom results processor + // testResultsProcessor: null, + + // This option allows use of a custom test runner + // testRunner: \\"jasmine2\\", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: \\"about:blank\\", + + // Setting this value to \\"fake\\" allows the use of fake timers for functions such as \\"setTimeout\\" + // timers: \\"real\\", + + // A map from regular expressions to paths to transformers + // transform: null, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // \\"/node_modules/\\" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: null, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; +" +`; + +exports[`init project with package.json and no jest config some questions answered with answer: "Yes" should create package.json with configured test command when {scripts: true} 1`] = ` +"{ + \\"name\\": \\"only_package_json\\", + \\"scripts\\": { + \\"test\\": \\"jest\\" + } +} +" +`; + +exports[`init typescript project should ask "typescript question" when has typescript in dependencies 1`] = ` +Object { + "initial": true, + "message": "Typescript detected, would you like to setup Jest for Typescript?", + "name": "typescript", + "type": "confirm", +} +`; + +exports[`init typescript project should ask "typescript question" when has typescript in devDependencies 1`] = ` +Object { + "initial": true, + "message": "Typescript detected, would you like to setup Jest for Typescript?", + "name": "typescript", + "type": "confirm", +} +`; + +exports[`init typescript project should create configuration for {typescript: true} 1`] = ` +Object { + "globals": Object { + "ts-jest": Object { + "tsConfigFile": "tsconfig.json", + }, + }, + "moduleFileExtensions": Array [ + "ts", + "tsx", + "js", + ], + "testMatch": Array [ + "**/__tests__/*.+(ts|tsx|js)", + ], + "transform": Object { + "^.+\\\\.(ts|tsx)$": "ts-jest", + }, +} +`; diff --git a/packages/jest-cli/src/lib/__tests__/__snapshots__/modify_package_json.test.js.snap b/packages/jest-cli/src/lib/__tests__/__snapshots__/modify_package_json.test.js.snap new file mode 100644 index 000000000000..da4b30f7f0a6 --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/__snapshots__/modify_package_json.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should add test script when there are no scripts 1`] = ` +"{ + \\"scripts\\": { + \\"test\\": \\"jest\\" + } +} +" +`; + +exports[`should add test script when there are scripts 1`] = ` +"{ + \\"scripts\\": { + \\"lint\\": \\"eslint .\\", + \\"test\\": \\"jest\\" + } +} +" +`; + +exports[`should not add test script when { shouldModifyScripts: false } 1`] = ` +"{} +" +`; + +exports[`should remove jest config if exists 1`] = ` +"{ + \\"scripts\\": { + \\"test\\": \\"jest\\" + } +} +" +`; diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_file/jest.config.js b/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_file/jest.config.js new file mode 100644 index 000000000000..f053ebf7976e --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_file/jest.config.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_file/package.json b/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_file/package.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_file/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_in_package_json/package.json b/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_in_package_json/package.json new file mode 100644 index 000000000000..4889b662753e --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/fixtures/has_jest_config_in_package_json/package.json @@ -0,0 +1,6 @@ +{ + "name": "has_jest_config_in_package_json", + "jest": { + "coverage": true + } +} diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/no_package_json/index.js b/packages/jest-cli/src/lib/__tests__/fixtures/no_package_json/index.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/only_package_json/package.json b/packages/jest-cli/src/lib/__tests__/fixtures/only_package_json/package.json new file mode 100644 index 000000000000..a58853b347e6 --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/fixtures/only_package_json/package.json @@ -0,0 +1,6 @@ +{ + "name": "only_package_json", + "scripts": { + "test": "different-test-runner" + } +} diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/test_script_configured/package.json b/packages/jest-cli/src/lib/__tests__/fixtures/test_script_configured/package.json new file mode 100644 index 000000000000..a89bb316e7df --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/fixtures/test_script_configured/package.json @@ -0,0 +1,6 @@ +{ + "name": "test_script_configured", + "scripts": { + "test": "jest" + } +} diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/typescript_in_dependencies/package.json b/packages/jest-cli/src/lib/__tests__/fixtures/typescript_in_dependencies/package.json new file mode 100644 index 000000000000..100738a1126c --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/fixtures/typescript_in_dependencies/package.json @@ -0,0 +1,6 @@ +{ + "name": "typescript_in_dev_dependencies", + "devDependencies": { + "typescript": "*" + } +} diff --git a/packages/jest-cli/src/lib/__tests__/fixtures/typescript_in_dev_dependencies/package.json b/packages/jest-cli/src/lib/__tests__/fixtures/typescript_in_dev_dependencies/package.json new file mode 100644 index 000000000000..c6578ac6d5e9 --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/fixtures/typescript_in_dev_dependencies/package.json @@ -0,0 +1,6 @@ +{ + "name": "only-package-json", + "dependencies": { + "typescript": "*" + } +} diff --git a/packages/jest-cli/src/lib/__tests__/init.test.js b/packages/jest-cli/src/lib/__tests__/init.test.js new file mode 100644 index 000000000000..36fdf85610a4 --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/init.test.js @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +/* eslint-disable no-eval */ +import fs from 'fs'; +import path from 'path'; +import prompts from 'prompts'; +import init from '../init'; +import getCacheDirectory from '../../../../jest-config/build/get_cache_directory'; + +jest.mock('prompts'); +jest.mock('../../../../jest-config/build/get_cache_directory'); + +// mocked to get the same snapshot on every machine +getCacheDirectory.mockReturnValue('/tmp/jest'); + +const resolveFromFixture = relativePath => + path.resolve(__dirname, 'fixtures', relativePath); + +const writeFileSync = fs.writeFileSync; +const sep = path.sep; + +describe('init', () => { + beforeEach(() => { + fs.writeFileSync = jest.fn(); + path.sep = '/'; + }); + + afterEach(() => { + jest.clearAllMocks(); + fs.writeFileSync = writeFileSync; + path.sep = sep; + }); + + describe('project with package.json and no jest config', () => { + describe('all questions answered with answer: "No"', () => { + it('should return the default configuration (an empty config)', async () => { + prompts.mockReturnValueOnce({}); + + await init(resolveFromFixture('only_package_json')); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + + expect(writtenJestConfig).toMatchSnapshot(); + + const evaluatedConfig = eval(writtenJestConfig); + + expect(evaluatedConfig).toEqual({}); + }); + }); + + describe('some questions answered with answer: "Yes"', () => { + it('should create configuration for {clearMocks: true}', async () => { + prompts.mockReturnValueOnce({clearMocks: true}); + + await init(resolveFromFixture('only_package_json')); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + const evaluatedConfig = eval(writtenJestConfig); + + expect(evaluatedConfig).toEqual({clearMocks: true}); + }); + + it('should create configuration for {coverage: true}', async () => { + prompts.mockReturnValueOnce({coverage: true}); + + await init(resolveFromFixture('only_package_json')); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + const evaluatedConfig = eval(writtenJestConfig); + + expect(evaluatedConfig).toEqual({coverageDirectory: 'coverage'}); + }); + + it('should create configuration for {environment: "jsdom"}', async () => { + prompts.mockReturnValueOnce({environment: 'jsdom'}); + + await init(resolveFromFixture('only_package_json')); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + const evaluatedConfig = eval(writtenJestConfig); + // should modify when the default environment will be changed to "node" + expect(evaluatedConfig).toEqual({}); + }); + + it('should create configuration for {environment: "node"}', async () => { + prompts.mockReturnValueOnce({environment: 'node'}); + + await init(resolveFromFixture('only_package_json')); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + const evaluatedConfig = eval(writtenJestConfig); + // should modify when the default environment will be changed to "node" + expect(evaluatedConfig).toEqual({testEnvironment: 'node'}); + }); + + it('should create package.json with configured test command when {scripts: true}', async () => { + prompts.mockReturnValueOnce({scripts: true}); + + await init(resolveFromFixture('only_package_json')); + + const writtenPackageJson = fs.writeFileSync.mock.calls[0][1]; + + expect(writtenPackageJson).toMatchSnapshot(); + expect(JSON.parse(writtenPackageJson).scripts.test).toEqual('jest'); + }); + }); + }); + + describe('no package json', () => { + it('should throw an error if there is no package.json file', async () => { + expect.assertions(1); + + try { + await init(resolveFromFixture('no_package_json')); + } catch (error) { + expect(error.message).toMatch( + 'Could not find a "package.json" file in', + ); + } + }); + }); + + describe('has-jest-config-file', () => { + describe('ask the user whether to override config or not', () => { + it('user answered with "Yes"', async () => { + prompts.mockReturnValueOnce({continue: true}).mockReturnValueOnce({}); + + await init(resolveFromFixture('has_jest_config_file')); + + expect(prompts.mock.calls[0][0]).toMatchSnapshot(); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + + expect(writtenJestConfig).toBeDefined(); + }); + + it('user answered with "No"', async () => { + prompts.mockReturnValueOnce({continue: false}); + + await init(resolveFromFixture('has_jest_config_file')); + // return after first prompt + expect(prompts).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('has jest config in package.json', () => { + it('should ask the user whether to override config or not', async () => { + prompts.mockReturnValueOnce({continue: true}).mockReturnValueOnce({}); + + await init(resolveFromFixture('has_jest_config_in_package_json')); + + expect(prompts.mock.calls[0][0]).toMatchSnapshot(); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + + expect(writtenJestConfig).toBeDefined(); + }); + }); + + describe('already has "jest" in packageJson.scripts.test', () => { + it('should not ask "test script question"', async () => { + prompts.mockReturnValueOnce({}); + + await init(resolveFromFixture('test_script_configured')); + + const questionsNames = prompts.mock.calls[0][0].map( + question => question.name, + ); + + expect(questionsNames).not.toContain('scripts'); + }); + }); + + describe('typescript project', () => { + it('should ask "typescript question" when has typescript in dependencies', async () => { + prompts.mockReturnValueOnce({}); + + await init(resolveFromFixture('typescript_in_dependencies')); + + const typescriptQuestion = prompts.mock.calls[0][0][0]; + + expect(typescriptQuestion).toMatchSnapshot(); + }); + + it('should ask "typescript question" when has typescript in devDependencies', async () => { + prompts.mockReturnValueOnce({}); + + await init(resolveFromFixture('typescript_in_dev_dependencies')); + + const typescriptQuestion = prompts.mock.calls[0][0][0]; + + expect(typescriptQuestion).toMatchSnapshot(); + }); + + it('should create configuration for {typescript: true}', async () => { + prompts.mockReturnValueOnce({typescript: true}); + + await init(resolveFromFixture('typescript_in_dev_dependencies')); + + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + const evaluatedConfig = eval(writtenJestConfig); + + expect(evaluatedConfig).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/jest-cli/src/lib/__tests__/modify_package_json.test.js b/packages/jest-cli/src/lib/__tests__/modify_package_json.test.js new file mode 100644 index 000000000000..d10df4463fc2 --- /dev/null +++ b/packages/jest-cli/src/lib/__tests__/modify_package_json.test.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import modifyPackageJson from '../init/modify_package_json'; + +test('should remove jest config if exists', () => { + expect( + modifyPackageJson({ + projectPackageJson: { + jest: { + coverage: true, + }, + }, + shouldModifyScripts: true, + }), + ).toMatchSnapshot(); +}); + +test('should add test script when there are no scripts', () => { + expect( + modifyPackageJson({ + projectPackageJson: {}, + shouldModifyScripts: true, + }), + ).toMatchSnapshot(); +}); + +test('should add test script when there are scripts', () => { + expect( + modifyPackageJson({ + projectPackageJson: { + scripts: { + lint: 'eslint .', + test: 'jasmine', + }, + }, + shouldModifyScripts: true, + }), + ).toMatchSnapshot(); +}); + +test('should not add test script when { shouldModifyScripts: false }', () => { + expect( + modifyPackageJson({ + projectPackageJson: {}, + shouldModifyScripts: false, + }), + ).toMatchSnapshot(); +}); diff --git a/packages/jest-cli/src/lib/init/errors.js b/packages/jest-cli/src/lib/init/errors.js new file mode 100644 index 000000000000..163cb7290f7d --- /dev/null +++ b/packages/jest-cli/src/lib/init/errors.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export class NotFoundPackageJsonError extends Error { + name: string; + message: string; + + constructor(rootDir: string) { + super(); + this.name = ''; + this.message = `Could not find a "package.json" file in ${rootDir}`; + Error.captureStackTrace(this, () => {}); + } +} + +export class MalformedPackageJsonError extends Error { + name: string; + message: string; + + constructor(packageJsonPath: string) { + super(); + this.name = ''; + this.message = + `There is malformed json in ${packageJsonPath}\n` + + 'Fix it, and then run "jest --init"'; + Error.captureStackTrace(this, () => {}); + } +} diff --git a/packages/jest-cli/src/lib/init/generate_config_file.js b/packages/jest-cli/src/lib/init/generate_config_file.js new file mode 100644 index 000000000000..f232095fff23 --- /dev/null +++ b/packages/jest-cli/src/lib/init/generate_config_file.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {defaults, descriptions} from 'jest-config'; + +const stringifyOption = ( + option: string, + map: Object, + linePrefix: string = '', +): string => { + const optionDescription = ` // ${descriptions[option]}`; + const stringifiedObject = `${option}: ${JSON.stringify( + map[option], + null, + 2, + )}`; + + return ( + optionDescription + + '\n' + + stringifiedObject + .split('\n') + .map(line => ' ' + linePrefix + line) + .join('\n') + + ',\n' + ); +}; + +const generateConfigFile = (results: {[string]: boolean}): string => { + const {typescript, coverage, clearMocks, environment} = results; + + const overrides: Object = {}; + + if (typescript) { + Object.assign(overrides, { + globals: { + 'ts-jest': { + tsConfigFile: 'tsconfig.json', + }, + }, + moduleFileExtensions: ['ts', 'tsx', 'js'], + testMatch: ['**/__tests__/*.+(ts|tsx|js)'], + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + }, + }); + } + + if (coverage) { + Object.assign(overrides, { + coverageDirectory: 'coverage', + }); + } + + if (environment === 'node') { + Object.assign(overrides, { + testEnvironment: 'node', + }); + } + + if (clearMocks) { + Object.assign(overrides, { + clearMocks: true, + }); + } + + const overrideKeys: Array = Object.keys(overrides); + + const properties: Array = []; + + for (const option in descriptions) { + if (overrideKeys.includes(option)) { + properties.push(stringifyOption(option, overrides)); + } else { + properties.push(stringifyOption(option, defaults, '// ')); + } + } + + return ( + '// For a detailed explanation regarding each configuration property, visit:\n' + + '// https://facebook.github.io/jest/docs/en/configuration.html\n\n' + + 'module.exports = {\n' + + properties.join('\n') + + '};\n' + ); +}; + +export default generateConfigFile; diff --git a/packages/jest-cli/src/lib/init/index.js b/packages/jest-cli/src/lib/init/index.js new file mode 100644 index 000000000000..69dce1f32421 --- /dev/null +++ b/packages/jest-cli/src/lib/init/index.js @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +import prompts from 'prompts'; +import defaultQuestions, { + typescriptQuestion, + testScriptQuestion, +} from './questions'; +import {NotFoundPackageJsonError, MalformedPackageJsonError} from './errors'; +import {PACKAGE_JSON, JEST_CONFIG} from '../../constants'; +import generateConfigFile from './generate_config_file'; +import modifyPackageJson from './modify_package_json'; + +type PromptsResults = { + clearMocks: boolean, + coverage: boolean, + environment: boolean, + scripts: boolean, + typescript: boolean, +}; + +export default async (rootDir: string = process.cwd()) => { + // prerequisite checks + const projectPackageJsonPath: string = path.join(rootDir, PACKAGE_JSON); + const jestConfigPath: string = path.join(rootDir, JEST_CONFIG); + + if (!fs.existsSync(projectPackageJsonPath)) { + throw new NotFoundPackageJsonError(rootDir); + } + + const questions = defaultQuestions.slice(0); + let hasJestProperty: boolean = false; + let hasJestConfig: boolean = false; + let projectPackageJson: ?Object; + + try { + projectPackageJson = JSON.parse( + fs.readFileSync(projectPackageJsonPath, 'utf-8'), + ); + } catch (error) { + throw new MalformedPackageJsonError(projectPackageJsonPath); + } + + if (projectPackageJson.jest) { + hasJestProperty = true; + } + + if (fs.existsSync(jestConfigPath)) { + hasJestConfig = true; + } + + if (hasJestProperty || hasJestConfig) { + const result: {continue: boolean} = await prompts({ + initial: true, + message: + 'It seems that you already have a jest configuration, do you want to override it?', + name: 'continue', + type: 'confirm', + }); + + if (!result.continue) { + console.log(); + console.log('Aborting...'); + return; + } + } + + // Add test script installation only if needed + if ( + !projectPackageJson.scripts || + projectPackageJson.scripts.test !== 'jest' + ) { + questions.unshift(testScriptQuestion); + } + + // Try to detect typescript and add a question if needed + const deps: Object = {}; + + Object.assign( + deps, + projectPackageJson.dependencies, + projectPackageJson.devDependencies, + ); + + if (Object.keys(deps).includes('typescript')) { + questions.unshift(typescriptQuestion); + } + + // Start the init process + console.log(); + console.log( + chalk.underline( + `The following questions will help Jest to create a suitable configuration for your project\n`, + ), + ); + + let promptAborted: boolean = false; + + const results: PromptsResults = await prompts(questions, { + onCancel: () => { + promptAborted = true; + }, + }); + + if (promptAborted) { + console.log(); + console.log('Aborting...'); + return; + } + + const shouldModifyScripts = results.scripts; + + if (shouldModifyScripts || hasJestProperty) { + const modifiedPackageJson = modifyPackageJson({ + projectPackageJson, + shouldModifyScripts, + }); + + fs.writeFileSync(projectPackageJsonPath, modifiedPackageJson); + + console.log(''); + console.log(`✏️ Modified ${chalk.cyan(projectPackageJsonPath)}`); + } + + const generatedConfig = generateConfigFile(results); + + fs.writeFileSync(jestConfigPath, generatedConfig); + + console.log(''); + console.log( + `📝 Configuration file created at ${chalk.cyan(jestConfigPath)}`, + ); +}; diff --git a/packages/jest-cli/src/lib/init/modify_package_json.js b/packages/jest-cli/src/lib/init/modify_package_json.js new file mode 100644 index 000000000000..fdc2b503aea3 --- /dev/null +++ b/packages/jest-cli/src/lib/init/modify_package_json.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const modifyPackageJson = ({ + projectPackageJson, + shouldModifyScripts, + hasJestProperty, +}: { + projectPackageJson: Object, + shouldModifyScripts: boolean, +}): string => { + if (shouldModifyScripts) { + projectPackageJson.scripts + ? (projectPackageJson.scripts.test = 'jest') + : (projectPackageJson.scripts = {test: 'jest'}); + } + + delete projectPackageJson.jest; + + return JSON.stringify(projectPackageJson, null, 2) + '\n'; +}; + +export default modifyPackageJson; diff --git a/packages/jest-cli/src/lib/init/questions.js b/packages/jest-cli/src/lib/init/questions.js new file mode 100644 index 000000000000..74183a8dd1fd --- /dev/null +++ b/packages/jest-cli/src/lib/init/questions.js @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +type Question = { + intial?: boolean | number, + message: string, + name: string, + type: string, + choices?: Array<{title: string, value: string}>, +}; + +const defaultQuestions: Array = [ + { + choices: [ + {title: 'node', value: 'node'}, + {title: 'jsdom (browser-like)', value: 'jsdom'}, + ], + initial: 0, + message: 'Choose the test environment that will be used for testing', + name: 'environment', + type: 'select', + }, + { + initial: false, + message: 'Do you want Jest to add coverage reports?', + name: 'coverage', + type: 'confirm', + }, + { + initial: false, + message: 'Automatically clear mock calls and instances between every test?', + name: 'clearMocks', + type: 'confirm', + }, +]; + +export default defaultQuestions; + +export const testScriptQuestion: Question = { + initial: true, + message: + 'Would you like to use Jest when running "test" script in "package.json"?', + name: 'scripts', + type: 'confirm', +}; + +export const typescriptQuestion: Question = { + initial: true, + message: 'Typescript detected, would you like to setup Jest for Typescript?', + name: 'typescript', + type: 'confirm', +}; diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index 8710f5de56bd..8fc32221d1ff 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -9,30 +9,18 @@ import type {DefaultOptions} from 'types/Config'; -import os from 'os'; -import path from 'path'; import {replacePathSepForRegex} from 'jest-regex-util'; import {NODE_MODULES} from './constants'; +import getCacheDirectory from './get_cache_directory'; const NODE_MODULES_REGEXP = replacePathSepForRegex(NODE_MODULES); -const cacheDirectory = (() => { - const {getuid} = process; - if (getuid == null) { - return path.join(os.tmpdir(), 'jest'); - } - // On some platforms tmpdir() is `/tmp`, causing conflicts between different - // users and permission issues. Adding an additional subdivision by UID can - // help. - return path.join(os.tmpdir(), 'jest_' + getuid.call(process).toString(36)); -})(); - export default ({ automock: false, bail: false, browser: false, cache: true, - cacheDirectory, + cacheDirectory: getCacheDirectory(), changedFilesWithAncestor: false, clearMocks: false, collectCoverage: false, diff --git a/packages/jest-config/src/descriptions.js b/packages/jest-config/src/descriptions.js new file mode 100644 index 000000000000..fc8cafb8e785 --- /dev/null +++ b/packages/jest-config/src/descriptions.js @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export default ({ + automock: 'All imported modules in your tests should be mocked automatically', + bail: 'Stop running tests after the first failure', + browser: 'Respect "browser" field in package.json when resolving modules', + cacheDirectory: + 'The directory where Jest should store its cached dependency information', + clearMocks: 'Automatically clear mock calls and instances between every test', + collectCoverage: + 'Indicates whether the coverage information should be collected while executing the test', + collectCoverageFrom: + 'An array of glob patterns indicating a set of files for which coverage information should be collected', + coverageDirectory: + 'The directory where Jest should output its coverage files', + coveragePathIgnorePatterns: + 'An array of regexp pattern strings used to skip coverage collection', + coverageReporters: + 'A list of reporter names that Jest uses when writing coverage reports', + coverageThreshold: + 'An object that configures minimum threshold enforcement for coverage results', + errorOnDeprecated: + 'Make calling deprecated APIs throw helpful error messages', + forceCoverageMatch: + 'Force coverage collection from ignored files usin a array of glob patterns', + globalSetup: + 'A path to a module which exports an async function that is triggered once before all test suites', + globalTeardown: + 'A path to a module which exports an async function that is triggered once after all test suites', + globals: + 'A set of global variables that need to be available in all test environments', + moduleDirectories: + "An array of directory names to be searched recursively up from the requiring module's location", + moduleFileExtensions: 'An array of file extensions your modules use', + moduleNameMapper: + 'A map from regular expressions to module names that allow to stub out resources with a single module', + modulePathIgnorePatterns: + "An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader", + notify: 'Activates notifications for test results', + notifyMode: + 'An enum that specifies notification mode. Requires { notify: true }', + preset: "A preset that is used as a base for Jest's configuration", + projects: 'Run tests from one or more projects', + reporters: 'Use this configuration option to add custom reporters to Jest', + resetMocks: 'Automatically reset mock state between every test', + resetModules: 'Reset the module registry before running each individual test', + resolver: 'A path to a custom resolver', + restoreMocks: 'Automatically restore mock state between every test', + rootDir: + 'The root directory that Jest should scan for tests and modules within', + roots: + 'A list of paths to directories that Jest should use to search for files in', + runner: + "Allows you to use a custom runner instead of Jest's default test runner", + setupFiles: + 'The paths to modules that run some code to configure or set up the testing environment before each test', + setupTestFrameworkScriptFile: + 'The path to a module that runs some code to configure or set up the testing framework before each test', + snapshotSerializers: + 'A list of paths to snapshot serializer modules Jest should use for snapshot testing', + testEnvironment: 'The test environment that will be used for testing', + testEnvironmentOptions: 'Options that will be passed to the testEnvironment', + testLocationInResults: 'Adds a location field to test results', + testMatch: 'The glob patterns Jest uses to detect test files', + testPathIgnorePatterns: + 'An array of regexp pattern strings that are matched against all test paths, matched tests are skipped', + testRegex: 'The regexp pattern Jest uses to detect test files', + testResultsProcessor: + 'This option allows the use of a custom results processor', + testRunner: 'This option allows use of a custom test runner', + testURL: + 'This option sets the URL for the jsdom environment. It is reflected in properties such as location.href', + timers: + 'Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"', + transform: 'A map from regular expressions to paths to transformers', + transformIgnorePatterns: + 'An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation', + unmockedModulePathPatterns: + 'An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them', + verbose: + 'Indicates whether each individual test should be reported during the run', + watchPathIgnorePatterns: + 'An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode', + watchman: 'Whether to use watchman for file crawling', +}: {[string]: string}); diff --git a/packages/jest-config/src/get_cache_directory.js b/packages/jest-config/src/get_cache_directory.js new file mode 100644 index 000000000000..6f3a11d21489 --- /dev/null +++ b/packages/jest-config/src/get_cache_directory.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const path = require('path'); +const os = require('os'); + +const getCacheDirectory = () => { + const {getuid} = process; + if (getuid == null) { + return path.join(os.tmpdir(), 'jest'); + } + // On some platforms tmpdir() is `/tmp`, causing conflicts between different + // users and permission issues. Adding an additional subdivision by UID can + // help. + return path.join(os.tmpdir(), 'jest_' + getuid.call(process).toString(36)); +}; + +export default getCacheDirectory; diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index b5392bb2b97e..00c31d7a908e 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -26,6 +26,7 @@ export {default as normalize} from './normalize'; export {default as deprecationEntries} from './deprecated'; export {replaceRootDirInPath} from './utils'; export {default as defaults} from './defaults'; +export {default as descriptions} from './descriptions'; export function readConfig( argv: Argv, diff --git a/types/Argv.js b/types/Argv.js index 81926b868cc7..ef157f155dfd 100644 --- a/types/Argv.js +++ b/types/Argv.js @@ -42,6 +42,7 @@ export type Argv = {| h: boolean, haste: string, help: boolean, + init: boolean, json: boolean, lastCommit: boolean, logHeapUsage: boolean, diff --git a/yarn.lock b/yarn.lock index 6e68f073a1b1..b73edfdb0188 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1963,6 +1963,10 @@ clone@~0.1.9: version "0.1.19" resolved "https://registry.yarnpkg.com/clone/-/clone-0.1.19.tgz#613fb68639b26a494ac53253e15b1a6bd88ada85" +clorox@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clorox/-/clorox-1.0.3.tgz#6fa63653f280c33d69f548fb14d239ddcfa1590d" + cmd-shim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" @@ -7546,6 +7550,13 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prompts@^0.1.9: + version "0.1.9" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.9.tgz#be109f62f794f138c6bbd35c25370c5c526d9d78" + dependencies: + clorox "^1.0.1" + sisteransi "^0.1.0" + prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" @@ -8740,6 +8751,10 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sisteransi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.0.tgz#2e6706ac427019b84e60f751d588d87920484f25" + sitemap@^1.13.0: version "1.13.0" resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-1.13.0.tgz#569cbe2180202926a62a266cd3de09c9ceb43f83"