in packages/jest-config/src/normalize.ts [551:1232]
export default async function normalize(
initialOptions: Config.InitialOptions,
argv: Config.Argv,
configPath?: string | null,
projectIndex = Infinity,
): Promise<{
hasDeprecationWarnings: boolean;
options: AllOptions;
}> {
const {hasDeprecationWarnings} = validate(initialOptions, {
comment: DOCUMENTATION_NOTE,
deprecatedConfig: DEPRECATED_CONFIG,
exampleConfig: VALID_CONFIG,
recursiveDenylist: [
'collectCoverageOnlyFrom',
// 'coverageThreshold' allows to use 'global' and glob strings on the same
// level, there's currently no way we can deal with such config
'coverageThreshold',
'globals',
'moduleNameMapper',
'testEnvironmentOptions',
'transform',
],
});
let options = normalizePreprocessor(
normalizeReporters(
normalizeMissingOptions(
normalizeRootDir(setFromArgv(initialOptions, argv)),
configPath,
projectIndex,
),
),
);
if (options.preset) {
options = await setupPreset(options, options.preset);
}
if (!options.setupFilesAfterEnv) {
options.setupFilesAfterEnv = [];
}
if (
options.setupTestFrameworkScriptFile &&
options.setupFilesAfterEnv.length > 0
) {
throw createConfigError(` Options: ${chalk.bold(
'setupTestFrameworkScriptFile',
)} and ${chalk.bold('setupFilesAfterEnv')} cannot be used together.
Please change your configuration to only use ${chalk.bold(
'setupFilesAfterEnv',
)}.`);
}
if (options.setupTestFrameworkScriptFile) {
options.setupFilesAfterEnv.push(options.setupTestFrameworkScriptFile);
}
options.testEnvironment = resolveTestEnvironment({
requireResolveFunction: require.resolve,
rootDir: options.rootDir,
testEnvironment:
options.testEnvironment ||
require.resolve(DEFAULT_CONFIG.testEnvironment),
});
if (!options.roots && options.testPathDirs) {
options.roots = options.testPathDirs;
delete options.testPathDirs;
}
if (!options.roots) {
options.roots = [options.rootDir];
}
if (
!options.testRunner ||
options.testRunner === 'circus' ||
options.testRunner === 'jest-circus' ||
options.testRunner === 'jest-circus/runner'
) {
options.testRunner = require.resolve('jest-circus/runner');
} else if (options.testRunner === 'jasmine2') {
try {
options.testRunner = require.resolve('jest-jasmine2');
} catch (error: any) {
if (error.code === 'MODULE_NOT_FOUND') {
createConfigError(
'jest-jasmine is no longer shipped by default with Jest, you need to install it explicitly or provide an absolute path to Jest',
);
}
throw error;
}
}
if (!options.coverageDirectory) {
options.coverageDirectory = path.resolve(options.rootDir, 'coverage');
}
setupBabelJest(options);
// TODO: Type this properly
const newOptions = {
...DEFAULT_CONFIG,
} as unknown as AllOptions;
if (options.resolver) {
newOptions.resolver = resolve(null, {
filePath: options.resolver,
key: 'resolver',
rootDir: options.rootDir,
});
}
validateExtensionsToTreatAsEsm(options.extensionsToTreatAsEsm);
if (options.watchman == null) {
options.watchman = DEFAULT_CONFIG.watchman;
}
const optionKeys = Object.keys(options) as Array<keyof Config.InitialOptions>;
optionKeys.reduce((newOptions, key: keyof Config.InitialOptions) => {
// The resolver has been resolved separately; skip it
if (key === 'resolver') {
return newOptions;
}
// This is cheating, because it claims that all keys of InitialOptions are Required.
// We only really know it's Required for oldOptions[key], not for oldOptions.someOtherKey,
// so oldOptions[key] is the only way it should be used.
const oldOptions = options as Config.InitialOptions &
Required<Pick<Config.InitialOptions, typeof key>>;
let value;
switch (key) {
case 'collectCoverageOnlyFrom':
value = normalizeCollectCoverageOnlyFrom(oldOptions, key);
break;
case 'setupFiles':
case 'setupFilesAfterEnv':
case 'snapshotSerializers':
{
const option = oldOptions[key];
value =
option &&
option.map(filePath =>
resolve(newOptions.resolver, {
filePath,
key,
rootDir: options.rootDir,
}),
);
}
break;
case 'modulePaths':
case 'roots':
{
const option = oldOptions[key];
value =
option &&
option.map(filePath =>
path.resolve(
options.rootDir,
replaceRootDirInPath(options.rootDir, filePath),
),
);
}
break;
case 'collectCoverageFrom':
value = normalizeCollectCoverageFrom(oldOptions, key);
break;
case 'cacheDirectory':
case 'coverageDirectory':
{
const option = oldOptions[key];
value =
option &&
path.resolve(
options.rootDir,
replaceRootDirInPath(options.rootDir, option),
);
}
break;
case 'dependencyExtractor':
case 'globalSetup':
case 'globalTeardown':
case 'runtime':
case 'snapshotResolver':
case 'testResultsProcessor':
case 'testRunner':
case 'filter':
{
const option = oldOptions[key];
value =
option &&
resolve(newOptions.resolver, {
filePath: option,
key,
rootDir: options.rootDir,
});
}
break;
case 'runner':
{
const option = oldOptions[key];
value =
option &&
resolveRunner(newOptions.resolver, {
filePath: option,
requireResolveFunction: require.resolve,
rootDir: options.rootDir,
});
}
break;
case 'prettierPath':
{
// We only want this to throw if "prettierPath" is explicitly passed
// from config or CLI, and the requested path isn't found. Otherwise we
// set it to null and throw an error lazily when it is used.
const option = oldOptions[key];
value =
option &&
resolve(newOptions.resolver, {
filePath: option,
key,
optional: option === DEFAULT_CONFIG[key],
rootDir: options.rootDir,
});
}
break;
case 'moduleNameMapper':
const moduleNameMapper = oldOptions[key];
value =
moduleNameMapper &&
Object.keys(moduleNameMapper).map(regex => {
const item = moduleNameMapper && moduleNameMapper[regex];
return item && [regex, _replaceRootDirTags(options.rootDir, item)];
});
break;
case 'transform':
const transform = oldOptions[key];
value =
transform &&
Object.keys(transform).map(regex => {
const transformElement = transform[regex];
return [
regex,
resolve(newOptions.resolver, {
filePath: Array.isArray(transformElement)
? transformElement[0]
: transformElement,
key,
rootDir: options.rootDir,
}),
Array.isArray(transformElement) ? transformElement[1] : {},
];
});
break;
case 'coveragePathIgnorePatterns':
case 'modulePathIgnorePatterns':
case 'testPathIgnorePatterns':
case 'transformIgnorePatterns':
case 'watchPathIgnorePatterns':
case 'unmockedModulePathPatterns':
value = normalizeUnmockedModulePathPatterns(oldOptions, key);
break;
case 'haste':
value = {...oldOptions[key]};
if (value.hasteImplModulePath != null) {
const resolvedHasteImpl = resolve(newOptions.resolver, {
filePath: replaceRootDirInPath(
options.rootDir,
value.hasteImplModulePath,
),
key: 'haste.hasteImplModulePath',
rootDir: options.rootDir,
});
value.hasteImplModulePath = resolvedHasteImpl || undefined;
}
break;
case 'projects':
value = (oldOptions[key] || [])
.map(project =>
typeof project === 'string'
? _replaceRootDirTags(options.rootDir, project)
: project,
)
.reduce<Array<string | Config.InitialProjectOptions>>(
(projects, project) => {
// Project can be specified as globs. If a glob matches any files,
// We expand it to these paths. If not, we keep the original path
// for the future resolution.
const globMatches =
typeof project === 'string' ? glob(project) : [];
return projects.concat(
globMatches.length ? globMatches : project,
);
},
[],
);
break;
case 'moduleDirectories':
case 'testMatch':
{
const replacedRootDirTags = _replaceRootDirTags(
escapeGlobCharacters(options.rootDir),
oldOptions[key],
);
if (replacedRootDirTags) {
value = Array.isArray(replacedRootDirTags)
? replacedRootDirTags.map(replacePathSepForGlob)
: replacePathSepForGlob(replacedRootDirTags);
} else {
value = replacedRootDirTags;
}
}
break;
case 'testRegex':
{
const option = oldOptions[key];
value = option
? (Array.isArray(option) ? option : [option]).map(
replacePathSepForRegex,
)
: [];
}
break;
case 'moduleFileExtensions': {
value = oldOptions[key];
if (
Array.isArray(value) && // If it's the wrong type, it can throw at a later time
(options.runner === undefined ||
options.runner === DEFAULT_CONFIG.runner) && // Only require 'js' for the default jest-runner
!value.includes('js')
) {
const errorMessage =
" moduleFileExtensions must include 'js':\n" +
' but instead received:\n' +
` ${chalk.bold.red(JSON.stringify(value))}`;
// If `js` is not included, any dependency Jest itself injects into
// the environment, like jasmine or sourcemap-support, will need to
// `require` its modules with a file extension. This is not plausible
// in the long run, so it's way easier to just fail hard early.
// We might consider throwing if `json` is missing as well, as it's a
// fair assumption from modules that they can do
// `require('some-package/package') without the trailing `.json` as it
// works in Node normally.
throw createConfigError(
`${errorMessage}\n Please change your configuration to include 'js'.`,
);
}
break;
}
case 'bail': {
const bail = oldOptions[key];
if (typeof bail === 'boolean') {
value = bail ? 1 : 0;
} else if (typeof bail === 'string') {
value = 1;
// If Jest is invoked as `jest --bail someTestPattern` then need to
// move the pattern from the `bail` configuration and into `argv._`
// to be processed as an extra parameter
argv._.push(bail);
} else {
value = oldOptions[key];
}
break;
}
case 'displayName': {
const displayName = oldOptions[key] as Config.DisplayName;
/**
* Ensuring that displayName shape is correct here so that the
* reporters can trust the shape of the data
*/
if (typeof displayName === 'object') {
const {name, color} = displayName;
if (
!name ||
!color ||
typeof name !== 'string' ||
typeof color !== 'string'
) {
const errorMessage =
` Option "${chalk.bold('displayName')}" must be of type:\n\n` +
' {\n' +
' name: string;\n' +
' color: string;\n' +
' }\n';
throw createConfigError(errorMessage);
}
value = oldOptions[key];
} else {
value = {
color: getDisplayNameColor(options.runner),
name: displayName,
};
}
break;
}
case 'testTimeout': {
if (oldOptions[key] < 0) {
throw createConfigError(
` Option "${chalk.bold('testTimeout')}" must be a natural number.`,
);
}
value = oldOptions[key];
break;
}
case 'automock':
case 'cache':
case 'changedSince':
case 'changedFilesWithAncestor':
case 'clearMocks':
case 'collectCoverage':
case 'coverageProvider':
case 'coverageReporters':
case 'coverageThreshold':
case 'detectLeaks':
case 'detectOpenHandles':
case 'errorOnDeprecated':
case 'expand':
case 'extensionsToTreatAsEsm':
case 'globals':
case 'findRelatedTests':
case 'forceCoverageMatch':
case 'forceExit':
case 'injectGlobals':
case 'lastCommit':
case 'listTests':
case 'logHeapUsage':
case 'maxConcurrency':
case 'name':
case 'noStackTrace':
case 'notify':
case 'notifyMode':
case 'onlyChanged':
case 'onlyFailures':
case 'outputFile':
case 'passWithNoTests':
case 'replname':
case 'reporters':
case 'resetMocks':
case 'resetModules':
case 'restoreMocks':
case 'rootDir':
case 'runTestsByPath':
case 'sandboxInjectedGlobals':
case 'silent':
case 'skipFilter':
case 'skipNodeResolution':
case 'slowTestThreshold':
case 'snapshotFormat':
case 'testEnvironment':
case 'testEnvironmentOptions':
case 'testFailureExitCode':
case 'testLocationInResults':
case 'testNamePattern':
case 'timers':
case 'useStderr':
case 'verbose':
case 'watch':
case 'watchAll':
case 'watchman':
value = oldOptions[key];
break;
case 'watchPlugins':
value = (oldOptions[key] || []).map(watchPlugin => {
if (typeof watchPlugin === 'string') {
return {
config: {},
path: resolveWatchPlugin(newOptions.resolver, {
filePath: watchPlugin,
requireResolveFunction: require.resolve,
rootDir: options.rootDir,
}),
};
} else {
return {
config: watchPlugin[1] || {},
path: resolveWatchPlugin(newOptions.resolver, {
filePath: watchPlugin[0],
requireResolveFunction: require.resolve,
rootDir: options.rootDir,
}),
};
}
});
break;
}
// @ts-expect-error: automock is missing in GlobalConfig, so what
newOptions[key] = value;
return newOptions;
}, newOptions);
if (options.watchman && options.haste?.enableSymlinks) {
throw new ValidationError(
'Validation Error',
'haste.enableSymlinks is incompatible with watchman',
'Either set haste.enableSymlinks to false or do not use watchman',
);
}
newOptions.roots.forEach((root, i) => {
verifyDirectoryExists(root, `roots[${i}]`);
});
try {
// try to resolve windows short paths, ignoring errors (permission errors, mostly)
newOptions.cwd = tryRealpath(process.cwd());
} catch {
// ignored
}
newOptions.testSequencer = resolveSequencer(newOptions.resolver, {
filePath:
options.testSequencer || require.resolve(DEFAULT_CONFIG.testSequencer),
requireResolveFunction: require.resolve,
rootDir: options.rootDir,
});
if (newOptions.runner === DEFAULT_CONFIG.runner) {
newOptions.runner = require.resolve(newOptions.runner);
}
newOptions.nonFlagArgs = argv._?.map(arg => `${arg}`);
newOptions.testPathPattern = buildTestPathPattern(argv);
newOptions.json = !!argv.json;
newOptions.testFailureExitCode = parseInt(
newOptions.testFailureExitCode as unknown as string,
10,
);
if (
newOptions.lastCommit ||
newOptions.changedFilesWithAncestor ||
newOptions.changedSince
) {
newOptions.onlyChanged = true;
}
if (argv.all) {
newOptions.onlyChanged = false;
newOptions.onlyFailures = false;
} else if (newOptions.testPathPattern) {
// When passing a test path pattern we don't want to only monitor changed
// files unless `--watch` is also passed.
newOptions.onlyChanged = newOptions.watch;
}
if (!newOptions.onlyChanged) {
newOptions.onlyChanged = false;
}
if (!newOptions.lastCommit) {
newOptions.lastCommit = false;
}
if (!newOptions.onlyFailures) {
newOptions.onlyFailures = false;
}
if (!newOptions.watchAll) {
newOptions.watchAll = false;
}
// as unknown since it can happen. We really need to fix the types here
if (
newOptions.moduleNameMapper === (DEFAULT_CONFIG.moduleNameMapper as unknown)
) {
newOptions.moduleNameMapper = [];
}
if (argv.ci != null) {
newOptions.ci = argv.ci;
}
newOptions.updateSnapshot =
newOptions.ci && !argv.updateSnapshot
? 'none'
: argv.updateSnapshot
? 'all'
: 'new';
newOptions.maxConcurrency = parseInt(
newOptions.maxConcurrency as unknown as string,
10,
);
newOptions.maxWorkers = getMaxWorkers(argv, options);
if (newOptions.testRegex!.length && options.testMatch) {
throw createConfigError(
` Configuration options ${chalk.bold('testMatch')} and` +
` ${chalk.bold('testRegex')} cannot be used together.`,
);
}
if (newOptions.testRegex!.length && !options.testMatch) {
// Prevent the default testMatch conflicting with any explicitly
// configured `testRegex` value
newOptions.testMatch = [];
}
// If argv.json is set, coverageReporters shouldn't print a text report.
if (argv.json) {
newOptions.coverageReporters = (newOptions.coverageReporters || []).filter(
reporter => reporter !== 'text',
);
}
// If collectCoverage is enabled while using --findRelatedTests we need to
// avoid having false negatives in the generated coverage report.
// The following: `--findRelatedTests '/rootDir/file1.js' --coverage`
// Is transformed to: `--findRelatedTests '/rootDir/file1.js' --coverage --collectCoverageFrom 'file1.js'`
// where arguments to `--collectCoverageFrom` should be globs (or relative
// paths to the rootDir)
if (newOptions.collectCoverage && argv.findRelatedTests) {
let collectCoverageFrom = newOptions.nonFlagArgs.map(filename => {
filename = replaceRootDirInPath(options.rootDir, filename);
return path.isAbsolute(filename)
? path.relative(options.rootDir, filename)
: filename;
});
// Don't override existing collectCoverageFrom options
if (newOptions.collectCoverageFrom) {
collectCoverageFrom = collectCoverageFrom.reduce((patterns, filename) => {
if (
micromatch(
[replacePathSepForGlob(path.relative(options.rootDir, filename))],
newOptions.collectCoverageFrom!,
).length === 0
) {
return patterns;
}
return [...patterns, filename];
}, newOptions.collectCoverageFrom);
}
newOptions.collectCoverageFrom = collectCoverageFrom;
} else if (!newOptions.collectCoverageFrom) {
newOptions.collectCoverageFrom = [];
}
if (!newOptions.findRelatedTests) {
newOptions.findRelatedTests = false;
}
if (!newOptions.projects) {
newOptions.projects = [];
}
if (!newOptions.sandboxInjectedGlobals) {
newOptions.sandboxInjectedGlobals = [];
}
if (!newOptions.forceExit) {
newOptions.forceExit = false;
}
if (!newOptions.logHeapUsage) {
newOptions.logHeapUsage = false;
}
if (argv.shard) {
newOptions.shard = parseShardPair(argv.shard);
}
return {
hasDeprecationWarnings,
options: newOptions,
};
}