Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for the jest.config.ts configuration file #10564

Merged
merged 39 commits into from Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
dcc71bb
feat: Add support for the `jest.config.ts` configuration file
Gamote Sep 29, 2020
e7b0573
chore: The `jest.config.ts` documentation was updated
Gamote Sep 29, 2020
a2c9fdc
chore: The `jest.config.ts` support was specified in the CHANGELOG.md
Gamote Sep 29, 2020
2b5d97a
chore: Documentation updated to encourage the usage of TS annotations
Gamote Sep 29, 2020
7f5a39d
Errors fixed for tests and prettier
Gamote Sep 29, 2020
0b68052
chore: Use 'ts-node' as peer dependency instead of 'typescript'
Gamote Sep 29, 2020
2ef46e2
chore: Documentation updated to specify the 'ts-node' peer dependency…
Gamote Sep 29, 2020
9bc4504
fix: Prettier errors
Gamote Sep 29, 2020
5674532
fix: Remove console logs to avoid them to be captured in snapshots
Gamote Sep 29, 2020
de5766b
fix: Remove console logs to avoid them to be captured in snapshots
Gamote Sep 29, 2020
c9069fd
fix: Added 'testEnvironment' in the `jest.config.ts` tests to fix the…
Gamote Sep 30, 2020
2cf2933
chore: `interopRequireDefault` was moved to save an import
Gamote Sep 30, 2020
144c341
chore: better comments for `readConfigFileAndSetRootDir`
Gamote Sep 30, 2020
8fa7b61
chore: indicate that `ts-node` is optional via 'peerDependenciesMeta'
Gamote Sep 30, 2020
d8724e6
chore: Updated documentation
Gamote Sep 30, 2020
a5033aa
chore: tests was added to be sure that the types are checked in the `…
Gamote Oct 2, 2020
4d49437
chore: Update the fixture for `jest.config.ts` to use 'default'
Gamote Oct 2, 2020
50623f4
Merge branch 'master' into feature/allow-ts-config-file
Gamote Oct 12, 2020
e0b5270
chore: (ts-node) allow for majors higher than 9
Gamote Oct 12, 2020
0d6f2ca
chore: revert changes in the 26.4 documentation
Gamote Oct 12, 2020
a291af2
chore: updated the return of 'loadTSConfigFile'
Gamote Oct 12, 2020
5d4065e
chore: replace local 'interopRequireDefault' with the one defined in …
Gamote Oct 12, 2020
ee9569b
chore: handle the case in which config is a function which imports mo…
Gamote Oct 12, 2020
91de77a
chore: let the consumers to deal with ts-node errors
Gamote Oct 12, 2020
2f16544
chore: docs comments were updated
Gamote Oct 12, 2020
b551b29
fix: tests were fixed after 'readConfigFileAndSetRootDir' updates
Gamote Oct 12, 2020
4ec1eb1
chore: 'jest-environment-jsdom-fifteen' was replaced with 'jest-envir…
Gamote Oct 12, 2020
7d6f815
chore: ask the user if he want's to use the Typescript configuration …
Gamote Oct 12, 2020
9bf4a0c
chore: use the local 'jest-environment-node'
Gamote Oct 12, 2020
a69c677
fix: use `jest-util` instead of `jest-util/build`
Gamote Oct 12, 2020
a3b7210
chore: add type annotation for the 'ts-node' register variable
Gamote Oct 12, 2020
8b678d9
chore: make Typescript override module
Gamote Oct 12, 2020
729f6dc
chore: 'jest-environment-node' was removed from the root `package.json`
Gamote Oct 12, 2020
7c02d1c
chore: added `ts-node` as dev dependency in the `jest-config`
Gamote Oct 12, 2020
9aa8d53
chores:
Gamote Oct 12, 2020
55169aa
chore: move `typescript` and `ts-node` from 'dependencies' to 'devDep…
Gamote Oct 12, 2020
b614995
fix: jest.config.ts test > user answered with "No"
Gamote Oct 12, 2020
3fb9994
chore: use multiline string instead of manual newlines
Gamote Oct 12, 2020
6336d36
chore: better naming for the 'configDocMessage': 'configHeaderMessage'
Gamote Oct 12, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest-cli, jest-config]` Add support for the `jest.config.ts` configuration file ([#10564](https://github.com/facebook/jest/pull/10564))
- `[jest-circus, jest-config, jest-runtime]` Add new `injectGlobals` config and CLI option to disable injecting global variables into the runtime ([#10484](https://github.com/facebook/jest/pull/10484))
- `[jest-each]` Fixes `.each` type to always be callable ([#10447](https://github.com/facebook/jest/pull/10447))
- `[jest-runner]` Add support for `moduleLoader`s with `default` exports ([#10541](https://github.com/facebook/jest/pull/10541))
Expand Down
24 changes: 23 additions & 1 deletion docs/Configuration.md
Expand Up @@ -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 <path/to/file.js|cjs|mjs|json>` 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`, or `jest.config.ts` file or through the `--config <path/to/file.js|ts|cjs|mjs|json>` 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
{
Expand Down Expand Up @@ -31,6 +31,28 @@ module.exports = async () => {
};
```

Or through TypeScript:

You can enable support for the TypeScript configuration file by installing the `ts-node` plugin in your project.
Gamote marked this conversation as resolved.
Show resolved Hide resolved

```ts
// jest.config.ts
import type {Config} from '@jest/types';

//Sync object
Gamote marked this conversation as resolved.
Show resolved Hide resolved
const config: Config.InitialOptions = {
verbose: true,
};
export default config;

//Or async function
Gamote marked this conversation as resolved.
Show resolved Hide resolved
export default async (): Promise<Config.InitialOptions> => {
return {
verbose: true,
};
};
```

Please keep in mind that the resulting configuration must be JSON-serializable.

When using the `--config` option, the JSON file must not contain a "jest" key:
Expand Down
36 changes: 36 additions & 0 deletions e2e/__tests__/__snapshots__/jest.config.ts.test.ts.snap
@@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`traverses directory tree up until it finds jest.config 1`] = `
console.log
<<REPLACED>>/jest.config.ts/some/nested/directory

at Object.log (__tests__/a-giraffe.js:3:27)

`;

exports[`traverses directory tree up until it finds jest.config 2`] = `
PASS ../../../__tests__/a-giraffe.js
✓ giraffe
✓ abc
`;

exports[`traverses directory tree up until it finds jest.config 3`] = `
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites.
`;

exports[`works with jest.config.ts 1`] = `
PASS __tests__/a-giraffe.js
✓ giraffe
`;

exports[`works with jest.config.ts 2`] = `
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites.
`;
73 changes: 73 additions & 0 deletions e2e/__tests__/jest.config.ts.test.ts
@@ -0,0 +1,73 @@
/**
Gamote marked this conversation as resolved.
Show resolved Hide resolved
* 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 * as path from 'path';
import {wrap} from 'jest-snapshot-serializer-raw';
import runJest from '../runJest';
import {cleanup, extractSummary, writeFiles} from '../Utils';

const DIR = path.resolve(__dirname, '../jest.config.ts');

beforeEach(() => cleanup(DIR));
afterAll(() => cleanup(DIR));

test('works with jest.config.ts', () => {
writeFiles(DIR, {
'__tests__/a-giraffe.js': `test('giraffe', () => expect(1).toBe(1));`,
'jest.config.ts': `export default {testEnvironment: 'jest-environment-jsdom-fifteen', testRegex: '.*-giraffe.js'};`,
'package.json': '{}',
});

const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
const {rest, summary} = extractSummary(stderr);
expect(exitCode).toBe(0);
expect(wrap(rest)).toMatchSnapshot();
expect(wrap(summary)).toMatchSnapshot();
});

test('traverses directory tree up until it finds jest.config', () => {
writeFiles(DIR, {
'__tests__/a-giraffe.js': `
const slash = require('slash');
test('giraffe', () => expect(1).toBe(1));
test('abc', () => console.log(slash(process.cwd())));
`,
'jest.config.ts': `export default {testEnvironment: 'jest-environment-jsdom-fifteen', testRegex: '.*-giraffe.js'};`,
'package.json': '{}',
'some/nested/directory/file.js': '// nothing special',
});

const {stderr, exitCode, stdout} = runJest(
path.join(DIR, 'some', 'nested', 'directory'),
['-w=1', '--ci=false'],
{skipPkgJsonCheck: true},
);

// Snapshot the console.loged `process.cwd()` and make sure it stays the same
expect(
wrap(stdout.replace(/^\W+(.*)e2e/gm, '<<REPLACED>>')),
).toMatchSnapshot();

const {rest, summary} = extractSummary(stderr);
expect(exitCode).toBe(0);
expect(wrap(rest)).toMatchSnapshot();
expect(wrap(summary)).toMatchSnapshot();
});

test('invalid JS in jest.config.ts', () => {
writeFiles(DIR, {
'__tests__/a-giraffe.js': `test('giraffe', () => expect(1).toBe(1));`,
'jest.config.ts': `export default i'll break this file yo`,
'package.json': '{}',
});

const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false']);
expect(stderr).toMatch(
'Error: Jest: Failed to parse the TypeScript config file ',
);
expect(exitCode).toBe(1);
});
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -55,6 +55,7 @@
"istanbul-lib-report": "^3.0.0",
"istanbul-reports": "^3.0.0",
"jest": "workspace:packages/jest",
"jest-environment-jsdom-fifteen": "^1.0.2",
Gamote marked this conversation as resolved.
Show resolved Hide resolved
"jest-junit": "^11.0.1",
"jest-runner-tsd": "^1.1.0",
"jest-silent-reporter": "^0.2.1",
Expand All @@ -77,6 +78,7 @@
"strip-ansi": "^6.0.0",
"tempy": "^0.6.0",
"throat": "^5.0.0",
"ts-node": "^9.0.0",
"type-fest": "^0.16.0",
"typescript": "^4.0.2",
"which": "^2.0.1"
Expand Down
4 changes: 2 additions & 2 deletions packages/jest-cli/src/__tests__/cli/args.test.ts
Expand Up @@ -82,13 +82,13 @@ describe('check', () => {
it('raises an exception if config is not a valid JSON string', () => {
const argv = {config: 'x:1'} as Config.Argv;
expect(() => check(argv)).toThrow(
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .mjs, .cjs, .json',
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json',
);
});

it('raises an exception if config is not a supported file type', () => {
const message =
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .mjs, .cjs, .json';
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json';

expect(() => check({config: 'jest.configjs'} as Config.Argv)).toThrow(
message,
Expand Down
Expand Up @@ -45,6 +45,15 @@ Object {
}
`;

exports[`init has-jest-config-file-ts 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://jestjs.io/docs/en/configuration.html
Expand Down
@@ -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.
*/

export = {};
@@ -0,0 +1 @@
{}
3 changes: 3 additions & 0 deletions packages/jest-config/package.json
Expand Up @@ -9,6 +9,9 @@
"license": "MIT",
"main": "build/index.js",
"types": "build/index.d.ts",
"peerDependencies": {
"ts-node": "^9.0.0"
Gamote marked this conversation as resolved.
Show resolved Hide resolved
Gamote marked this conversation as resolved.
Show resolved Hide resolved
},
"dependencies": {
"@babel/core": "^7.1.0",
"@jest/test-sequencer": "^26.4.2",
Expand Down
Expand Up @@ -81,7 +81,7 @@ describe.each(JEST_CONFIG_EXT_ORDER.slice(0))(

writeFiles(DIR, {[relativeJestConfigPath]: ''});

// jest.config.js takes presedence
// jest.config.js takes precedence

// absolute
expect(
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-config/src/constants.ts
Expand Up @@ -15,9 +15,11 @@ export const JEST_CONFIG_BASE_NAME = 'jest.config';
export const JEST_CONFIG_EXT_CJS = '.cjs';
export const JEST_CONFIG_EXT_MJS = '.mjs';
export const JEST_CONFIG_EXT_JS = '.js';
export const JEST_CONFIG_EXT_TS = '.ts';
export const JEST_CONFIG_EXT_JSON = '.json';
export const JEST_CONFIG_EXT_ORDER = Object.freeze([
JEST_CONFIG_EXT_JS,
JEST_CONFIG_EXT_TS,
JEST_CONFIG_EXT_MJS,
JEST_CONFIG_EXT_CJS,
JEST_CONFIG_EXT_JSON,
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-config/src/index.ts
Expand Up @@ -74,7 +74,7 @@ export async function readConfig(
config.rootDir = config.rootDir || packageRootOrConfig;
rawOptions = config;
// A string passed to `--config`, which is either a direct path to the config
// or a path to directory containing `package.json` or `jest.config.js`
// or a path to directory containing `package.json`, `jest.config.js` or `jest.config.ts`
} else if (!skipArgvConfigOption && typeof argv.config == 'string') {
configPath = resolveConfigPath(argv.config, process.cwd());
rawOptions = await readConfigFileAndSetRootDir(configPath);
Expand Down
50 changes: 47 additions & 3 deletions packages/jest-config/src/readConfigFileAndSetRootDir.ts
Expand Up @@ -9,22 +9,33 @@ import * as path from 'path';
import {pathToFileURL} from 'url';
import * as fs from 'graceful-fs';
import type {Config} from '@jest/types';
import {interopRequireDefault} from './utils';
// @ts-expect-error: vendored
import jsonlint from './vendor/jsonlint';
import {JEST_CONFIG_EXT_JSON, PACKAGE_JSON} from './constants';
import {
JEST_CONFIG_EXT_JSON,
JEST_CONFIG_EXT_TS,
PACKAGE_JSON,
} from './constants';

// Read the configuration and set its `rootDir`
// 1. If it's a `package.json` file, we look into its "jest" property
// 2. For any other file, we just require it. If we receive an 'ERR_REQUIRE_ESM'
// 2. If it's a `jest.config.ts` file, we convert it in JS and we parse it
Gamote marked this conversation as resolved.
Show resolved Hide resolved
// 3. For any other file, we just require it. If we receive an 'ERR_REQUIRE_ESM'
// from node, perform a dynamic import instead.
export default async function readConfigFileAndSetRootDir(
configPath: Config.Path,
): Promise<Config.InitialOptions> {
const isTS = configPath.endsWith(JEST_CONFIG_EXT_TS);
const isJSON = configPath.endsWith(JEST_CONFIG_EXT_JSON);
let configObject;

try {
configObject = require(configPath);
if (isTS) {
configObject = loadTSConfigFile(configPath);
} else {
configObject = require(configPath);
}
} catch (error) {
if (error.code === 'ERR_REQUIRE_ESM') {
try {
Expand Down Expand Up @@ -54,6 +65,11 @@ export default async function readConfigFileAndSetRootDir(
`Jest: Failed to parse config file ${configPath}\n` +
` ${jsonlint.errors(fs.readFileSync(configPath, 'utf8'))}`,
);
} else if (isTS) {
throw new Error(
`Jest: Failed to parse the TypeScript config file ${configPath}\n` +
` ${error}`,
);
} else {
throw error;
}
Expand Down Expand Up @@ -81,3 +97,31 @@ export default async function readConfigFileAndSetRootDir(

return configObject;
}

// Load the TypeScript configuration
const loadTSConfigFile = (configPath: Config.Path): Config.InitialOptions => {
Gamote marked this conversation as resolved.
Show resolved Hide resolved
let registerer;
Gamote marked this conversation as resolved.
Show resolved Hide resolved

// Register TypeScript compiler instance
try {
registerer = require('ts-node').register();
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
throw new Error(
`Jest: 'ts-node' is required for the TypeScript configuration files. Make sure it is installed\nError: ${e.message}`,
);
}

throw new Error(
`Jest: Could not register 'ts-node' to read the TypeScript file: ${configPath}`,
);
Gamote marked this conversation as resolved.
Show resolved Hide resolved
}

registerer.enabled(true);

const configObject = interopRequireDefault(require(configPath)).default;

registerer.enabled(false);
Gamote marked this conversation as resolved.
Show resolved Hide resolved

return configObject;
};
4 changes: 4 additions & 0 deletions packages/jest-config/src/utils.ts
Expand Up @@ -248,3 +248,7 @@ export const getSequencer = (
prefix: 'jest-sequencer-',
rootDir,
});

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const interopRequireDefault = (obj: any): {default: any} =>
Gamote marked this conversation as resolved.
Show resolved Hide resolved
obj && obj.__esModule ? obj : {default: obj};
24 changes: 23 additions & 1 deletion website/versioned_docs/version-26.4/Configuration.md
Expand Up @@ -4,7 +4,7 @@ title: Configuring Jest
original_id: configuration
---

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 <path/to/file.js|cjs|mjs|json>` 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`, or `jest.config.ts` file or through the `--config <path/to/file.js|ts|cjs|mjs|json>` 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:
Gamote marked this conversation as resolved.
Show resolved Hide resolved

```json
{
Expand Down Expand Up @@ -32,6 +32,28 @@ module.exports = async () => {
};
```

Or through TypeScript:

You can enable support for the TypeScript configuration file by installing the `ts-node` plugin in your project.

```ts
// jest.config.ts
import type {Config} from '@jest/types';

//Sync object
const config: Config.InitialOptions = {
verbose: true,
};
export default config;

//Or async function
export default async (): Promise<Config.InitialOptions> => {
return {
verbose: true,
};
};
```

Please keep in mind that the resulting configuration must be JSON-serializable.

When using the `--config` option, the JSON file must not contain a "jest" key:
Expand Down