feat(config): add read initial options helper (#13356)

This commit is contained in:
Nico Jansen 2022-10-03 14:50:47 +02:00 committed by GitHub
parent 8219820a87
commit d78baab693
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 352 additions and 48 deletions

View File

@ -2,6 +2,7 @@
### Features
- `[jest-config]` Add `readInitialConfig` utility function ([#13356](https://github.com/facebook/jest/pull/13356))
- `[jest-core]` Enable testResultsProcessor to be async ([#13343](https://github.com/facebook/jest/pull/13343))
- `[expect, @jest/expect-utils]` Allow `isA` utility to take a type argument ([#13355](https://github.com/facebook/jest/pull/13355))

View File

@ -0,0 +1,148 @@
/**
* 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 path = require('path');
import execa = require('execa');
import type {ReadJestConfigOptions, readInitialOptions} from 'jest-config';
function resolveFixture(...pathSegments: Array<string>) {
return path.resolve(__dirname, '..', 'read-initial-options', ...pathSegments);
}
interface ProxyReadJestConfigOptions extends ReadJestConfigOptions {
cwd?: string;
}
/**
* These e2e tests are running via a child process, because we're running in a VM and are not allowed to `import` directly
* It also represents a more real-world example of how to run.
*/
async function proxyReadInitialOptions(
configFile: string | undefined,
options: ProxyReadJestConfigOptions,
): ReturnType<typeof readInitialOptions> {
const {stdout} = await execa(
'node',
[
require.resolve('../read-initial-options/readOptions.js'),
configFile ?? '',
JSON.stringify(options),
],
{cwd: options?.cwd},
);
return JSON.parse(stdout);
}
describe('readInitialOptions', () => {
test('should read from the cwd by default', async () => {
const configFile = resolveFixture('js-config', 'jest.config.js');
const rootDir = resolveFixture('js-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'jest.config.js', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a jest.config.js file', async () => {
const configFile = resolveFixture('js-config', 'jest.config.js');
const rootDir = resolveFixture('js-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'jest.config.js', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a package.json file', async () => {
const configFile = resolveFixture('pkg-config', 'package.json');
const rootDir = resolveFixture('pkg-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'package.json', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a jest.config.ts file', async () => {
const configFile = resolveFixture('ts-config', 'jest.config.ts');
const rootDir = resolveFixture('ts-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'jest.config.ts', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a jest.config.mjs file', async () => {
const configFile = resolveFixture('mjs-config', 'jest.config.mjs');
const rootDir = resolveFixture('mjs-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'jest.config.mjs', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a jest.config.json file', async () => {
const configFile = resolveFixture('json-config', 'jest.config.json');
const rootDir = resolveFixture('json-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'jest.config.json', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a jest config exporting an async function', async () => {
const configFile = resolveFixture('async-config', 'jest.config.js');
const rootDir = resolveFixture('async-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'async-config', rootDir});
expect(configPath).toEqual(configFile);
});
test('should be able to skip config reading, instead read from cwd', async () => {
const expectedConfigFile = resolveFixture(
'json-config',
'jest.config.json',
);
const {config, configPath} = await proxyReadInitialOptions(
resolveFixture('js-config', 'jest.config.js'),
{
cwd: resolveFixture('json-config'),
readFromCwd: true,
},
);
expect(config).toEqual({
jestConfig: 'jest.config.json',
rootDir: path.dirname(expectedConfigFile),
});
expect(configPath).toEqual(expectedConfigFile);
});
test('should give an error when there are multiple config files', async () => {
const cwd = resolveFixture('multiple-config-files');
const error: Error = await proxyReadInitialOptions(undefined, {cwd}).catch(
error => error,
);
expect(error.message).toContain('Multiple configurations found');
expect(error.message).toContain('multiple-config-files/jest.config.js');
expect(error.message).toContain('multiple-config-files/jest.config.json');
});
test('should be able to ignore multiple config files error', async () => {
const cwd = resolveFixture('multiple-config-files');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd,
skipMultipleConfigError: true,
});
expect(config).toEqual({
jestConfig: 'jest.config.js',
rootDir: resolveFixture('multiple-config-files'),
});
expect(configPath).toEqual(
resolveFixture('multiple-config-files', 'jest.config.js'),
);
});
});

View File

@ -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 = async function () {
return {
jestConfig: 'async-config',
};
};

View File

@ -0,0 +1,9 @@
/**
* 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 = {
jestConfig: 'jest.config.js',
};

View File

@ -0,0 +1,3 @@
{
"jestConfig": "jest.config.json"
}

View File

@ -0,0 +1,9 @@
/**
* 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 default {
jestConfig: 'jest.config.mjs',
};

View File

@ -0,0 +1,9 @@
/**
* 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 = {
jestConfig: 'jest.config.js',
};

View File

@ -0,0 +1,3 @@
{
"jestConfig": "jest.config.json"
}

View File

@ -0,0 +1,5 @@
{
"jest": {
"jestConfig": "package.json"
}
}

View File

@ -0,0 +1,22 @@
/**
* 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.
*/
const {readInitialOptions} = require('jest-config');
async function readConfig() {
let config = process.argv[2];
let options = undefined;
if (config === '') {
config = undefined;
}
if (process.argv[3]) {
options = JSON.parse(process.argv[3]);
}
console.log(JSON.stringify(await readInitialOptions(config, options)));
}
readConfig().catch(err => {
console.error(err);
process.exitCode = 1;
});

View File

@ -0,0 +1,9 @@
/**
* 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 default {
jestConfig: 'jest.config.ts',
};

View File

@ -10,8 +10,7 @@ import {readConfig} from '../index';
test('readConfig() throws when an object is passed without a file path', async () => {
await expect(
readConfig(
// @ts-expect-error
null /* argv */,
{$0: '', _: []},
{} /* packageRootOrConfig */,
false /* skipArgvConfigOption */,
null /* parentConfigPath */,

View File

@ -0,0 +1,30 @@
/**
* 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 type {Config} from '@jest/types';
import {readInitialOptions} from '../index';
describe(readInitialOptions, () => {
test('should be able to use serialized jest config', async () => {
const inputConfig = {jestConfig: 'serialized'};
const {config, configPath} = await readInitialOptions(
JSON.stringify(inputConfig),
);
expect(config).toEqual({...inputConfig, rootDir: process.cwd()});
expect(configPath).toBeNull();
});
test('should allow deserialized options', async () => {
const inputConfig = {jestConfig: 'deserialized'};
const {config, configPath} = await readInitialOptions(undefined, {
packageRootOrConfig: inputConfig as Config.InitialOptions,
parentConfigDirname: process.cwd(),
});
expect(config).toEqual({...inputConfig, rootDir: process.cwd()});
expect(configPath).toBeNull();
});
// Note: actual file reading is tested in e2e test
});

View File

@ -43,56 +43,18 @@ export async function readConfig(
projectIndex = Infinity,
skipMultipleConfigError = false,
): Promise<ReadConfig> {
let rawOptions: Config.InitialOptions;
let configPath = null;
if (typeof packageRootOrConfig !== 'string') {
if (parentConfigDirname) {
rawOptions = packageRootOrConfig;
rawOptions.rootDir = rawOptions.rootDir
? replaceRootDirInPath(parentConfigDirname, rawOptions.rootDir)
: parentConfigDirname;
} else {
throw new Error(
'Jest: Cannot use configuration as an object without a file path.',
);
}
} else if (isJSONString(argv.config)) {
// A JSON string was passed to `--config` argument and we can parse it
// and use as is.
let config;
try {
config = JSON.parse(argv.config);
} catch {
throw new Error(
'There was an error while parsing the `--config` argument as a JSON string.',
);
}
// NOTE: we might need to resolve this dir to an absolute path in the future
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`, `jest.config.js` or `jest.config.ts`
} else if (!skipArgvConfigOption && typeof argv.config == 'string') {
configPath = resolveConfigPath(
argv.config,
process.cwd(),
skipMultipleConfigError,
);
rawOptions = await readConfigFileAndSetRootDir(configPath);
} else {
// Otherwise just try to find config in the current rootDir.
configPath = resolveConfigPath(
const {config: initialOptions, configPath} = await readInitialOptions(
argv.config,
{
packageRootOrConfig,
process.cwd(),
parentConfigDirname,
readFromCwd: skipArgvConfigOption,
skipMultipleConfigError,
);
rawOptions = await readConfigFileAndSetRootDir(configPath);
}
},
);
const {options, hasDeprecationWarnings} = await normalize(
rawOptions,
initialOptions,
argv,
configPath,
projectIndex,
@ -267,6 +229,90 @@ This usually means that your ${chalk.bold(
}
};
export interface ReadJestConfigOptions {
/**
* The package root or deserialized config (default is cwd)
*/
packageRootOrConfig?: string | Config.InitialOptions;
/**
* When the `packageRootOrConfig` contains config, this parameter should
* contain the dirname of the parent config
*/
parentConfigDirname?: null | string;
/**
* Indicates whether or not to read the specified config file from disk.
* When true, jest will read try to read config from the current working directory.
* (default is false)
*/
readFromCwd?: boolean;
/**
* Indicates whether or not to ignore the error of jest finding multiple config files.
* (default is false)
*/
skipMultipleConfigError?: boolean;
}
/**
* Reads the jest config, without validating them or filling it out with defaults.
* @param config The path to the file or serialized config.
* @param param1 Additional options
* @returns The raw initial config (not validated)
*/
export async function readInitialOptions(
config?: string,
{
packageRootOrConfig = process.cwd(),
parentConfigDirname = null,
readFromCwd = false,
skipMultipleConfigError = false,
}: ReadJestConfigOptions = {},
): Promise<{config: Config.InitialOptions; configPath: string | null}> {
if (typeof packageRootOrConfig !== 'string') {
if (parentConfigDirname) {
const rawOptions = packageRootOrConfig;
rawOptions.rootDir = rawOptions.rootDir
? replaceRootDirInPath(parentConfigDirname, rawOptions.rootDir)
: parentConfigDirname;
return {config: rawOptions, configPath: null};
} else {
throw new Error(
'Jest: Cannot use configuration as an object without a file path.',
);
}
}
if (isJSONString(config)) {
try {
// A JSON string was passed to `--config` argument and we can parse it
// and use as is.
const initialOptions = JSON.parse(config);
// NOTE: we might need to resolve this dir to an absolute path in the future
initialOptions.rootDir = initialOptions.rootDir || packageRootOrConfig;
return {config: initialOptions, configPath: null};
} catch {
throw new Error(
'There was an error while parsing the `--config` argument as a JSON string.',
);
}
}
if (!readFromCwd && typeof config == 'string') {
// A string passed to `--config`, which is either a direct path to the config
// or a path to directory containing `package.json`, `jest.config.js` or `jest.config.ts`
const configPath = resolveConfigPath(
config,
process.cwd(),
skipMultipleConfigError,
);
return {config: await readConfigFileAndSetRootDir(configPath), configPath};
}
// Otherwise just try to find config in the current rootDir.
const configPath = resolveConfigPath(
packageRootOrConfig,
process.cwd(),
skipMultipleConfigError,
);
return {config: await readConfigFileAndSetRootDir(configPath), configPath};
}
// Possible scenarios:
// 1. jest --config config.json
// 2. jest --projects p1 p2