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(config): add read initial options helper #13356

Merged
merged 24 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8d598dd
feat(config): add read initial options helper
nicojs Oct 2, 2022
aabb8de
Enable experimental-vm-modules to run jest.
nicojs Oct 2, 2022
e9b5c87
Update changelog
nicojs Oct 2, 2022
b6276fb
Add copyright headers
nicojs Oct 2, 2022
d93a12c
Update assert: also use forward slashes on windows.
nicojs Oct 2, 2022
eadd24e
Update packages/jest-config/src/index.ts
nicojs Oct 2, 2022
947bfaa
Update changelog sorting
nicojs Oct 2, 2022
7793ff2
Move integration tests to e2e
nicojs Oct 2, 2022
153af9b
Remove `--experimental-vm-modules`
nicojs Oct 2, 2022
4294610
Extract `ReadJestConfigOptions` interface
nicojs Oct 2, 2022
6d347a6
Finish rename `readFromCwdInstead` -> `readFromCwd`
nicojs Oct 2, 2022
c2dffe9
Import from `@jest/types` instead of `jest-runner`
nicojs Oct 2, 2022
b3a13cc
Update packages/jest-config/src/__tests__/readInitialOptions.test.ts
nicojs Oct 2, 2022
c41dbdf
Update packages/jest-config/src/__tests__/readInitialOptions.test.ts
nicojs Oct 2, 2022
8b479f5
Update packages/jest-config/src/__tests__/readInitialOptions.test.ts
nicojs Oct 2, 2022
9f01d80
Remove final `.name`
nicojs Oct 2, 2022
188b080
Read config by proxy for ts and mjs files
nicojs Oct 3, 2022
5c54a6a
Add copyright header
nicojs Oct 3, 2022
53b49ba
Read by proxy for every test
nicojs Oct 3, 2022
34e1a32
Update e2e/__tests__/readInitialOptions.test.ts
nicojs Oct 3, 2022
3cf1f21
Refactor if/else -> if
nicojs Oct 3, 2022
3d535ba
refactor: move some comments around
nicojs Oct 3, 2022
5ab7237
Update e2e/__tests__/readInitialOptions.test.ts
nicojs Oct 3, 2022
2dfe6ef
Require argv to be supplied
nicojs Oct 3, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
117 changes: 117 additions & 0 deletions e2e/__tests__/readInitialOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* 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 {readInitialOptions} from 'jest-config';

function resolveFixture(...pathSegments: Array<string>) {
return path.resolve(__dirname, '..', 'read-initial-options', ...pathSegments);
}

describe(readInitialOptions, () => {
test('should read from the cwd by default', async () => {
const configFile = resolveFixture('js-config', 'jest.config.js');
const rootDir = resolveFixture('js-config');
jest.spyOn(process, 'cwd').mockReturnValue(rootDir);
SimenB marked this conversation as resolved.
Show resolved Hide resolved
const {config, configPath} = await readInitialOptions();
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 readInitialOptions(configFile);
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 readInitialOptions(configFile);
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');
// Read by proxy, because we're running in a VM and are not allowed to import 'ts-node' directly
const {stdout} = await execa('node', ['../readOptions.js'], {
cwd: rootDir,
});
const {config, configPath} = JSON.parse(stdout);
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');
// Read by proxy, because we're running in a VM and are not allowed to `import` directly
const {stdout} = await execa('node', ['../readOptions.js'], {
cwd: rootDir,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding this approach should be used in all test in this file. It is based on real world implementation and that is how I like e2e tests to be written.

In contrary, importing readInitialOptions to a test file and mocking process.cwd still feels like a unit test. Of course, that just an approach or opinion. The same thing is tested either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, using execa is much better. Fixed with the latest push

const {config, configPath} = JSON.parse(stdout);
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 readInitialOptions(configFile);
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 readInitialOptions(configFile);
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',
);
jest.spyOn(process, 'cwd').mockReturnValue(resolveFixture('json-config'));
const {config, configPath} = await readInitialOptions(
resolveFixture('js-config', 'jest.config.js'),
{
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');
jest.spyOn(process, 'cwd').mockReturnValue(cwd);
const error: Error = await readInitialOptions().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');
jest.spyOn(process, 'cwd').mockReturnValue(cwd);
const {config, configPath} = await readInitialOptions(undefined, {
skipMultipleConfigError: true,
});
expect(config).toEqual({
jestConfig: 'jest.config.js',
rootDir: resolveFixture('multiple-config-files'),
});
expect(configPath).toEqual(
resolveFixture('multiple-config-files', 'jest.config.js'),
);
});
});
11 changes: 11 additions & 0 deletions e2e/read-initial-options/async-config/jest.config.js
Original file line number Diff line number Diff line change
@@ -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',
};
};
9 changes: 9 additions & 0 deletions e2e/read-initial-options/js-config/jest.config.js
Original file line number Diff line number Diff line change
@@ -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',
};
3 changes: 3 additions & 0 deletions e2e/read-initial-options/json-config/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"jestConfig": "jest.config.json"
}
9 changes: 9 additions & 0 deletions e2e/read-initial-options/mjs-config/jest.config.mjs
Original file line number Diff line number Diff line change
@@ -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',
};
9 changes: 9 additions & 0 deletions e2e/read-initial-options/multiple-config-files/jest.config.js
Original file line number Diff line number Diff line change
@@ -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',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"jestConfig": "jest.config.json"
}
5 changes: 5 additions & 0 deletions e2e/read-initial-options/pkg-config/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"jest": {
"jestConfig": "package.json"
}
}
11 changes: 11 additions & 0 deletions e2e/read-initial-options/readOptions.js
Original file line number Diff line number Diff line change
@@ -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.
*/
const {readInitialOptions} = require('jest-config');
async function readConfig() {
console.log(JSON.stringify(await readInitialOptions(process.argv[2])));
}
readConfig();
9 changes: 9 additions & 0 deletions e2e/read-initial-options/ts-config/jest.config.ts
Original file line number Diff line number Diff line change
@@ -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',
};
30 changes: 30 additions & 0 deletions packages/jest-config/src/__tests__/readInitialOptions.test.ts
Original file line number Diff line number Diff line change
@@ -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
});
139 changes: 93 additions & 46 deletions packages/jest-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
argv?.config,
argv.config,

Copy link
Contributor Author

@nicojs nicojs Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails the readConfig test. I assumed you would want to keep supporting it.

See packages/jest-config/src/tests/readConfig.test.ts

test('readConfig() throws when an object is passed without a file path', async () => {
  await expect(
    readConfig(
      // @ts-expect-error
      null /* argv */,
      {} /* packageRootOrConfig */,
      false /* skipArgvConfigOption */,
      null /* parentConfigPath */,
    ),
  ).rejects.toThrowError(
    'Jest: Cannot use configuration as an object without a file path',
  );
});

Copy link
Member

@SimenB SimenB Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a bug in the test - argv is always called according to the types

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, updated the tests and removed this ?

{
packageRootOrConfig,
process.cwd(),
parentConfigDirname,
readFromCwd: skipArgvConfigOption,
skipMultipleConfigError,
);
rawOptions = await readConfigFileAndSetRootDir(configPath);
}
},
);

const {options, hasDeprecationWarnings} = await normalize(
rawOptions,
initialOptions,
argv,
configPath,
projectIndex,
Expand Down Expand Up @@ -267,6 +229,91 @@ 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.',
);
}
} else if (isJSONString(config)) {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
// A JSON string was passed to `--config` argument and we can parse it
// and use as is.
let initialOptions;
try {
initialOptions = JSON.parse(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
initialOptions.rootDir = initialOptions.rootDir || packageRootOrConfig;
return {config: initialOptions, configPath: null};
// 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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should be moved two lines down. Or?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right! Fixed

} else if (!readFromCwd && typeof config == 'string') {
const configPath = resolveConfigPath(
config,
process.cwd(),
skipMultipleConfigError,
);
return {config: await readConfigFileAndSetRootDir(configPath), configPath};
} else {
// 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
Expand Down