diff --git a/CHANGELOG.md b/CHANGELOG.md index 4160a719cd8d..053ebb3bcbe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `[jest-config]` Throw the full error message and stack when a Jest preset is missing a dependency ([#8924](https://github.com/facebook/jest/pull/8924)) - `[jest-config]` [**BREAKING**] Set default display name color based on runner ([#8689](https://github.com/facebook/jest/pull/8689)) - `[jest-config]` Merge preset globals with project globals ([#9027](https://github.com/facebook/jest/pull/9027)) +- `[jest-config]` Support `.cjs` config files ([#9291](https://github.com/facebook/jest/pull/9291)) - `[jest-core]` Support reporters as default exports ([#9161](https://github.com/facebook/jest/pull/9161)) - `[jest-diff]` Add options for colors and symbols ([#8841](https://github.com/facebook/jest/pull/8841)) - `[jest-diff]` [**BREAKING**] Export as ECMAScript module ([#8873](https://github.com/facebook/jest/pull/8873)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 709686c84c38..42d3082bb8a7 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -3,7 +3,7 @@ id: configuration title: Configuring Jest --- -Jest's configuration can be defined in the `package.json` file of your project, or through a `jest.config.js` file or through the `--config ` option. If you'd like to use your `package.json` to store Jest's config, the "jest" key should be used on the top level so Jest will know how to find your settings: +Jest's configuration can be defined in the `package.json` file of your project, or through a `jest.config.js` file or through the `--config ` option. If you'd like to use your `package.json` to store Jest's config, the "jest" key should be used on the top level so Jest will know how to find your settings: ```json { diff --git a/e2e/__tests__/cjsConfigFile.test.ts b/e2e/__tests__/cjsConfigFile.test.ts new file mode 100644 index 000000000000..9c6b1345ef4e --- /dev/null +++ b/e2e/__tests__/cjsConfigFile.test.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. 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 {json as runWithJson} from '../runJest'; + +test('reads config from cjs file', () => { + const {json, exitCode} = runWithJson('cjs-config', ['--show-config'], { + skipPkgJsonCheck: true, + }); + + expect(exitCode).toBe(0); + expect(json.configs).toHaveLength(1); + expect(json.configs[0].displayName).toEqual({ + color: 'white', + name: 'Config from cjs file', + }); +}); diff --git a/e2e/cjs-config/__tests__/test.js b/e2e/cjs-config/__tests__/test.js new file mode 100644 index 000000000000..2b4a7ced6f45 --- /dev/null +++ b/e2e/cjs-config/__tests__/test.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. 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. + */ + +test('dummy test', () => { + expect(1).toBe(1); +}); diff --git a/e2e/cjs-config/jest.config.cjs b/e2e/cjs-config/jest.config.cjs new file mode 100644 index 000000000000..5c04c7468d30 --- /dev/null +++ b/e2e/cjs-config/jest.config.cjs @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. 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. + */ + +module.exports = { + displayName: 'Config from cjs file', + testEnvironment: 'node', +}; diff --git a/e2e/cjs-config/package.json b/e2e/cjs-config/package.json new file mode 100644 index 000000000000..586d4ca6b75c --- /dev/null +++ b/e2e/cjs-config/package.json @@ -0,0 +1,3 @@ +{ + "jest": {} +} diff --git a/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap b/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap index 63b08ad015bb..1ca7d79a90dc 100644 --- a/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap +++ b/packages/jest-cli/src/init/__tests__/__snapshots__/init.test.js.snap @@ -9,7 +9,25 @@ Object { } `; -exports[`init has-jest-config-file ask the user whether to override config or not user answered with "Yes" 1`] = ` +exports[`init has-jest-config-file-cjs 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 has-jest-config-file-js 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 has-jest-config-file-json 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?", diff --git a/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file/jest.config.js b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_cjs/jest.config.cjs similarity index 100% rename from packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file/jest.config.js rename to packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_cjs/jest.config.cjs diff --git a/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file/package.json b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_cjs/package.json similarity index 100% rename from packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file/package.json rename to packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_cjs/package.json diff --git a/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_js/jest.config.js b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_js/jest.config.js new file mode 100644 index 000000000000..54e6c946a5f7 --- /dev/null +++ b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_js/jest.config.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. 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. + */ + +module.exports = {}; diff --git a/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_js/package.json b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_js/package.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_js/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_json/jest.config.json b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_json/jest.config.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_json/jest.config.json @@ -0,0 +1 @@ +{} diff --git a/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_json/package.json b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_json/package.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/jest-cli/src/init/__tests__/fixtures/has_jest_config_file_json/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/jest-cli/src/init/__tests__/init.test.js b/packages/jest-cli/src/init/__tests__/init.test.js index f7a047cf0e57..6a6768f83779 100644 --- a/packages/jest-cli/src/init/__tests__/init.test.js +++ b/packages/jest-cli/src/init/__tests__/init.test.js @@ -10,6 +10,7 @@ import * as fs from 'fs'; import * as path from 'path'; import prompts from 'prompts'; import init from '../'; +import {JEST_CONFIG_EXT_ORDER} from '../constants'; jest.mock('prompts'); jest.mock('../../../../jest-config/build/getCacheDirectory', () => () => @@ -125,29 +126,32 @@ describe('init', () => { }); }); - 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({}); + describe.each(JEST_CONFIG_EXT_ORDER.map(e => e.substring(1)))( + 'has-jest-config-file-%s', + extension => { + 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')); + await init(resolveFromFixture(`has_jest_config_file_${extension}`)); - expect(prompts.mock.calls[0][0]).toMatchSnapshot(); + expect(prompts.mock.calls[0][0]).toMatchSnapshot(); - const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; + const writtenJestConfig = fs.writeFileSync.mock.calls[0][1]; - expect(writtenJestConfig).toBeDefined(); - }); + expect(writtenJestConfig).toBeDefined(); + }); - it('user answered with "No"', async () => { - prompts.mockReturnValueOnce({continue: false}); + 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); + await init(resolveFromFixture(`has_jest_config_file_${extension}`)); + // 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 () => { diff --git a/packages/jest-cli/src/init/constants.ts b/packages/jest-cli/src/init/constants.ts index 8ebd1eecd72a..b7c3ed5e25ff 100644 --- a/packages/jest-cli/src/init/constants.ts +++ b/packages/jest-cli/src/init/constants.ts @@ -6,4 +6,12 @@ */ export const PACKAGE_JSON = 'package.json'; -export const JEST_CONFIG = 'jest.config.js'; +export const JEST_CONFIG_BASE_NAME = 'jest.config'; +export const JEST_CONFIG_EXT_CJS = '.cjs'; +export const JEST_CONFIG_EXT_JS = '.js'; +export const JEST_CONFIG_EXT_JSON = '.json'; +export const JEST_CONFIG_EXT_ORDER = Object.freeze([ + JEST_CONFIG_EXT_JS, + JEST_CONFIG_EXT_CJS, + JEST_CONFIG_EXT_JSON, +]); diff --git a/packages/jest-cli/src/init/index.ts b/packages/jest-cli/src/init/index.ts index 55022c1be9f0..ffc8e1a2e30a 100644 --- a/packages/jest-cli/src/init/index.ts +++ b/packages/jest-cli/src/init/index.ts @@ -12,7 +12,13 @@ import prompts = require('prompts'); import {sync as realpath} from 'realpath-native'; import defaultQuestions, {testScriptQuestion} from './questions'; import {MalformedPackageJsonError, NotFoundPackageJsonError} from './errors'; -import {JEST_CONFIG, PACKAGE_JSON} from './constants'; +import { + JEST_CONFIG_BASE_NAME, + JEST_CONFIG_EXT_CJS, + JEST_CONFIG_EXT_JS, + JEST_CONFIG_EXT_ORDER, + PACKAGE_JSON, +} from './constants'; import generateConfigFile from './generate_config_file'; import modifyPackageJson from './modify_package_json'; import {ProjectPackageJson} from './types'; @@ -24,10 +30,11 @@ type PromptsResults = { scripts: boolean; }; +const getConfigFilename = (ext: string) => JEST_CONFIG_BASE_NAME + ext; + export default async (rootDir: string = realpath(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); @@ -35,7 +42,6 @@ export default async (rootDir: string = realpath(process.cwd())) => { const questions = defaultQuestions.slice(0); let hasJestProperty: boolean = false; - let hasJestConfig: boolean = false; let projectPackageJson: ProjectPackageJson; try { @@ -50,11 +56,21 @@ export default async (rootDir: string = realpath(process.cwd())) => { hasJestProperty = true; } - if (fs.existsSync(jestConfigPath)) { - hasJestConfig = true; - } + const existingJestConfigPath = JEST_CONFIG_EXT_ORDER.find(ext => + fs.existsSync(path.join(rootDir, getConfigFilename(ext))), + ); + const jestConfigPath = + existingJestConfigPath || + path.join( + rootDir, + getConfigFilename( + projectPackageJson.type === 'module' + ? JEST_CONFIG_EXT_CJS + : JEST_CONFIG_EXT_JS, + ), + ); - if (hasJestProperty || hasJestConfig) { + if (hasJestProperty || existingJestConfigPath) { const result: {continue: boolean} = await prompts({ initial: true, message: diff --git a/packages/jest-cli/src/init/types.ts b/packages/jest-cli/src/init/types.ts index 90dfeed163ad..e846607dd5ac 100644 --- a/packages/jest-cli/src/init/types.ts +++ b/packages/jest-cli/src/init/types.ts @@ -10,4 +10,5 @@ import {Config} from '@jest/types'; export type ProjectPackageJson = { jest?: Partial; scripts?: Record; + type?: 'commonjs' | 'module'; }; diff --git a/packages/jest-config/src/__tests__/resolveConfigPath.test.ts b/packages/jest-config/src/__tests__/resolveConfigPath.test.ts index fd0cb53739ee..0fba06b58295 100644 --- a/packages/jest-config/src/__tests__/resolveConfigPath.test.ts +++ b/packages/jest-config/src/__tests__/resolveConfigPath.test.ts @@ -8,84 +8,97 @@ import {tmpdir} from 'os'; import * as path from 'path'; import resolveConfigPath from '../resolveConfigPath'; +import {JEST_CONFIG_EXT_ORDER} from '../constants'; const {cleanup, writeFiles} = require('../../../../e2e/Utils'); const DIR = path.resolve(tmpdir(), 'resolve_config_path_test'); const ERROR_PATTERN = /Could not find a config file based on provided values/; -const NO_ROOT_DIR_ERROR_PATTERN = /Can\'t find a root directory/; +const NO_ROOT_DIR_ERROR_PATTERN = /Can't find a root directory/; beforeEach(() => cleanup(DIR)); afterEach(() => cleanup(DIR)); -test('file path', () => { - const relativeConfigPath = 'a/b/c/my_config.js'; - const absoluteConfigPath = path.resolve(DIR, relativeConfigPath); - - writeFiles(DIR, {[relativeConfigPath]: ''}); - - // absolute - expect(resolveConfigPath(absoluteConfigPath, DIR)).toBe(absoluteConfigPath); - expect(() => resolveConfigPath('/does_not_exist', DIR)).toThrowError( - NO_ROOT_DIR_ERROR_PATTERN, - ); - - // relative - expect(resolveConfigPath(relativeConfigPath, DIR)).toBe(absoluteConfigPath); - expect(() => resolveConfigPath('does_not_exist', DIR)).toThrowError( - NO_ROOT_DIR_ERROR_PATTERN, - ); -}); - -test('directory path', () => { - const relativePackageJsonPath = 'a/b/c/package.json'; - const absolutePackageJsonPath = path.resolve(DIR, relativePackageJsonPath); - const relativeJestConfigPath = 'a/b/c/jest.config.js'; - const absoluteJestConfigPath = path.resolve(DIR, relativeJestConfigPath); - - writeFiles(DIR, {'a/b/c/some_random_file.js': ''}); - - // no configs yet. should throw - expect(() => - // absolute - resolveConfigPath(path.dirname(absoluteJestConfigPath), DIR), - ).toThrowError(ERROR_PATTERN); - - expect(() => - // relative - resolveConfigPath(path.dirname(relativeJestConfigPath), DIR), - ).toThrowError(ERROR_PATTERN); - - writeFiles(DIR, {[relativePackageJsonPath]: ''}); - - // absolute - expect(resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR)).toBe( - absolutePackageJsonPath, - ); - - // relative - expect(resolveConfigPath(path.dirname(relativePackageJsonPath), DIR)).toBe( - absolutePackageJsonPath, - ); - - writeFiles(DIR, {[relativeJestConfigPath]: ''}); - - // jest.config.js takes presedence - - // absolute - expect(resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR)).toBe( - absoluteJestConfigPath, - ); - - // relative - expect(resolveConfigPath(path.dirname(relativePackageJsonPath), DIR)).toBe( - absoluteJestConfigPath, - ); - - expect(() => { - resolveConfigPath( - path.join(path.dirname(relativePackageJsonPath), 'j/x/b/m/'), - DIR, - ); - }).toThrowError(NO_ROOT_DIR_ERROR_PATTERN); -}); +describe.each(JEST_CONFIG_EXT_ORDER.slice(0))( + 'Resolve config path %s', + extension => { + test(`file path with "${extension}"`, () => { + const relativeConfigPath = `a/b/c/my_config${extension}`; + const absoluteConfigPath = path.resolve(DIR, relativeConfigPath); + + writeFiles(DIR, {[relativeConfigPath]: ''}); + + // absolute + expect(resolveConfigPath(absoluteConfigPath, DIR)).toBe( + absoluteConfigPath, + ); + expect(() => resolveConfigPath('/does_not_exist', DIR)).toThrowError( + NO_ROOT_DIR_ERROR_PATTERN, + ); + + // relative + expect(resolveConfigPath(relativeConfigPath, DIR)).toBe( + absoluteConfigPath, + ); + expect(() => resolveConfigPath('does_not_exist', DIR)).toThrowError( + NO_ROOT_DIR_ERROR_PATTERN, + ); + }); + + test(`directory path with "${extension}"`, () => { + const relativePackageJsonPath = 'a/b/c/package.json'; + const absolutePackageJsonPath = path.resolve( + DIR, + relativePackageJsonPath, + ); + const relativeJestConfigPath = `a/b/c/jest.config${extension}`; + const absoluteJestConfigPath = path.resolve(DIR, relativeJestConfigPath); + + writeFiles(DIR, {[`a/b/c/some_random_file${extension}`]: ''}); + + // no configs yet. should throw + expect(() => + // absolute + resolveConfigPath(path.dirname(absoluteJestConfigPath), DIR), + ).toThrowError(ERROR_PATTERN); + + expect(() => + // relative + resolveConfigPath(path.dirname(relativeJestConfigPath), DIR), + ).toThrowError(ERROR_PATTERN); + + writeFiles(DIR, {[relativePackageJsonPath]: ''}); + + // absolute + expect( + resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR), + ).toBe(absolutePackageJsonPath); + + // relative + expect( + resolveConfigPath(path.dirname(relativePackageJsonPath), DIR), + ).toBe(absolutePackageJsonPath); + + writeFiles(DIR, {[relativeJestConfigPath]: ''}); + + // jest.config.js takes presedence + + // absolute + expect( + resolveConfigPath(path.dirname(absolutePackageJsonPath), DIR), + ).toBe(absoluteJestConfigPath); + + // relative + expect( + resolveConfigPath(path.dirname(relativePackageJsonPath), DIR), + ).toBe(absoluteJestConfigPath); + + expect(() => { + resolveConfigPath( + path.join(path.dirname(relativePackageJsonPath), 'j/x/b/m/'), + DIR, + ); + }).toThrowError(NO_ROOT_DIR_ERROR_PATTERN); + }); + }, +); diff --git a/packages/jest-config/src/constants.ts b/packages/jest-config/src/constants.ts index 8e038fdf09de..08c0128a534a 100644 --- a/packages/jest-config/src/constants.ts +++ b/packages/jest-config/src/constants.ts @@ -11,4 +11,12 @@ export const NODE_MODULES = path.sep + 'node_modules' + path.sep; export const DEFAULT_JS_PATTERN = '^.+\\.[jt]sx?$'; export const DEFAULT_REPORTER_LABEL = 'default'; export const PACKAGE_JSON = 'package.json'; -export const JEST_CONFIG = 'jest.config.js'; +export const JEST_CONFIG_BASE_NAME = 'jest.config'; +export const JEST_CONFIG_EXT_CJS = '.cjs'; +export const JEST_CONFIG_EXT_JS = '.js'; +export const JEST_CONFIG_EXT_JSON = '.json'; +export const JEST_CONFIG_EXT_ORDER = Object.freeze([ + JEST_CONFIG_EXT_JS, + JEST_CONFIG_EXT_CJS, + JEST_CONFIG_EXT_JSON, +]); diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index eb392317c02b..cf81796a9aaa 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -20,6 +20,7 @@ export {default as deprecationEntries} from './Deprecated'; export {replaceRootDirInPath} from './utils'; export {default as defaults} from './Defaults'; export {default as descriptions} from './Descriptions'; +import {JEST_CONFIG_EXT_ORDER} from './constants'; type ReadConfig = { configPath: Config.Path | null | undefined; @@ -303,8 +304,7 @@ export function readConfigs( typeof root === 'string' && fs.existsSync(root) && !fs.lstatSync(root).isDirectory() && - !root.endsWith('.js') && - !root.endsWith('.json') + !JEST_CONFIG_EXT_ORDER.some(ext => root.endsWith(ext)) ) { return false; } diff --git a/packages/jest-config/src/resolveConfigPath.ts b/packages/jest-config/src/resolveConfigPath.ts index d7ccef3783ec..d4773eef1e4e 100644 --- a/packages/jest-config/src/resolveConfigPath.ts +++ b/packages/jest-config/src/resolveConfigPath.ts @@ -8,11 +8,17 @@ import * as path from 'path'; import * as fs from 'fs'; import {Config} from '@jest/types'; -import {JEST_CONFIG, PACKAGE_JSON} from './constants'; +import { + JEST_CONFIG_BASE_NAME, + JEST_CONFIG_EXT_ORDER, + PACKAGE_JSON, +} from './constants'; const isFile = (filePath: Config.Path) => fs.existsSync(filePath) && !fs.lstatSync(filePath).isDirectory(); +const getConfigFilename = (ext: string) => JEST_CONFIG_BASE_NAME + ext; + export default (pathToResolve: Config.Path, cwd: Config.Path): Config.Path => { if (!path.isAbsolute(cwd)) { throw new Error(`"cwd" must be an absolute path. cwd: ${cwd}`); @@ -51,8 +57,10 @@ const resolveConfigPathByTraversing = ( initialPath: Config.Path, cwd: Config.Path, ): Config.Path => { - const jestConfig = path.resolve(pathToResolve, JEST_CONFIG); - if (isFile(jestConfig)) { + const jestConfig = JEST_CONFIG_EXT_ORDER.map(ext => + path.resolve(pathToResolve, getConfigFilename(ext)), + ).find(isFile); + if (jestConfig) { return jestConfig; } @@ -84,5 +92,6 @@ const makeResolutionErrorMessage = ( `cwd: "${cwd}"\n` + 'Config paths must be specified by either a direct path to a config\n' + 'file, or a path to a directory. If directory is given, Jest will try to\n' + - `traverse directory tree up, until it finds either "${JEST_CONFIG}" or\n` + - `"${PACKAGE_JSON}".`; + `traverse directory tree up, until it finds one of those files in exact order: ${JEST_CONFIG_EXT_ORDER.map( + ext => `"${getConfigFilename(ext)}"`, + ).join(' or ')}.`;