feat(testing): add disableJestRuntime option to @nx/jest/plugin to speed up target inference (#28522)

This PR adds a new `disableJestRuntime` option to `@nx/jest/plugin`. By
setting this to `true`, the inference plugin will not use `jest-config`
and `jest-runtime` to retrieve `outputs`, nor matching spec files when
`ciTargetName` is used. Instead, we'll use our own config loader and
glob/matcher to get both values for the inferred targets.

This speeds up computation quite a bit. With a large monorepo (800
projects, 75K files), the computation goes from 2m 41s to 35s, or ~78%
time reduction.

NOTE: This has no effect on existing projects and needs to be opted
into.


<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Jack Hsu 2024-10-31 11:48:43 -04:00 committed by GitHub
parent 4014662986
commit 178d93d9c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 664 additions and 105 deletions

View File

@ -60,13 +60,16 @@ within the same workspace. In this case, you can configure the `@nx/jest/plugin`
"include": ["e2e/**/*"], "include": ["e2e/**/*"],
"options": { "options": {
"targetName": "e2e-local", "targetName": "e2e-local",
"ciTargetName": "e2e-ci" "ciTargetName": "e2e-ci",
"disableJestRuntime": false
} }
} }
] ]
} }
``` ```
If you experience slowness from `@nx/jest/plugin`, then set `disableJestRuntime` to `true` to skip creating the Jest runtime. By disabling the Jest runtime, Nx will use its own utilities to find `inputs`, `outputs`, and test files for [Atomized targets](/ci/features/split-e2e-tasks). This can reduce computation time by as much as 80%.
### Splitting E2E Tests ### Splitting E2E Tests
If Jest is used to run E2E tests, you can enable [splitting the tasks](/ci/features/split-e2e-tasks) by file to get If Jest is used to run E2E tests, you can enable [splitting the tasks](/ci/features/split-e2e-tasks) by file to get

View File

@ -60,13 +60,16 @@ within the same workspace. In this case, you can configure the `@nx/jest/plugin`
"include": ["e2e/**/*"], "include": ["e2e/**/*"],
"options": { "options": {
"targetName": "e2e-local", "targetName": "e2e-local",
"ciTargetName": "e2e-ci" "ciTargetName": "e2e-ci",
"disableJestRuntime": false
} }
} }
] ]
} }
``` ```
If you experience slowness from `@nx/jest/plugin`, then set `disableJestRuntime` to `true` to skip creating the Jest runtime. By disabling the Jest runtime, Nx will use its own utilities to find `inputs`, `outputs`, and test files for [Atomized targets](/ci/features/split-e2e-tasks). This can reduce computation time by as much as 80%.
### Splitting E2E Tests ### Splitting E2E Tests
If Jest is used to run E2E tests, you can enable [splitting the tasks](/ci/features/split-e2e-tasks) by file to get If Jest is used to run E2E tests, you can enable [splitting the tasks](/ci/features/split-e2e-tasks) by file to get

View File

@ -39,6 +39,7 @@
"prettier", "prettier",
"jest", "jest",
"@jest/types", "@jest/types",
"jest-runtime",
// require.resolve is used for these packages // require.resolve is used for these packages
"identity-obj-proxy" "identity-obj-proxy"
] ]

View File

@ -347,6 +347,334 @@ describe('@nx/jest/plugin', () => {
expect(results).toMatchSnapshot(); expect(results).toMatchSnapshot();
} }
); );
describe('disableJestRuntime', () => {
it('should create test and test-ci targets based on jest.config.ts', async () => {
mockJestConfig(
{
coverageDirectory: '../coverage',
testMatch: ['**/*.spec.ts'],
testPathIgnorePatterns: ['ignore.spec.ts'],
},
context
);
const results = await createNodesFunction(
['proj/jest.config.js'],
{
targetName: 'test',
ciTargetName: 'test-ci',
disableJestRuntime: true,
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"proj/jest.config.js",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"E2E (CI)": [
"test-ci",
"test-ci--src/unit.spec.ts",
],
},
},
"root": "proj",
"targets": {
"test": {
"cache": true,
"command": "jest",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
"test-ci": {
"cache": true,
"dependsOn": [
"test-ci--src/unit.spec.ts",
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests in CI",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"nonAtomizedTarget": "test",
"technologies": [
"jest",
],
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
"test-ci--src/unit.spec.ts": {
"cache": true,
"command": "jest src/unit.spec.ts",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests in src/unit.spec.ts",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
},
},
},
],
]
`);
});
it.each`
preset | expectedInput
${'<rootDir>/jest.preset.js'} | ${'{projectRoot}/jest.preset.js'}
${'../jest.preset.js'} | ${'{workspaceRoot}/jest.preset.js'}
`('should correct input from preset', async ({ preset, expectedInput }) => {
mockJestConfig(
{
preset,
coverageDirectory: '../coverage',
testMatch: ['**/*.spec.ts'],
testPathIgnorePatterns: ['ignore.spec.ts'],
},
context
);
const results = await createNodesFunction(
['proj/jest.config.js'],
{
targetName: 'test',
ciTargetName: 'test-ci',
disableJestRuntime: true,
},
context
);
expect(results[0][1].projects['proj'].targets['test'].inputs).toContain(
expectedInput
);
});
it.each`
testRegex
${'\\.*\\.spec\\.ts'}
${['\\.*\\.spec\\.ts']}
`(
'should create test-ci targets from testRegex config option',
async ({ testRegex }) => {
mockJestConfig(
{
coverageDirectory: '../coverage',
testRegex,
testPathIgnorePatterns: ['ignore.spec.ts'],
},
context
);
const results = await createNodesFunction(
['proj/jest.config.js'],
{
targetName: 'test',
ciTargetName: 'test-ci',
disableJestRuntime: true,
},
context
);
expect(results).toMatchInlineSnapshot(`
[
[
"proj/jest.config.js",
{
"projects": {
"proj": {
"metadata": {
"targetGroups": {
"E2E (CI)": [
"test-ci",
"test-ci--src/unit.spec.ts",
],
},
},
"root": "proj",
"targets": {
"test": {
"cache": true,
"command": "jest",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
"test-ci": {
"cache": true,
"dependsOn": [
"test-ci--src/unit.spec.ts",
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests in CI",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"nonAtomizedTarget": "test",
"technologies": [
"jest",
],
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
"test-ci--src/unit.spec.ts": {
"cache": true,
"command": "jest src/unit.spec.ts",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests in src/unit.spec.ts",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
},
},
},
],
]
`);
}
);
});
}); });
function mockJestConfig(config: any, context: CreateNodesContext) { function mockJestConfig(config: any, context: CreateNodesContext) {

View File

@ -1,4 +1,3 @@
import type { Config } from '@jest/types';
import { import {
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
@ -28,12 +27,23 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { combineGlobPatterns } from 'nx/src/utils/globs'; import { combineGlobPatterns } from 'nx/src/utils/globs';
import { dirname, isAbsolute, join, relative, resolve } from 'path'; import { dirname, isAbsolute, join, relative, resolve } from 'path';
import { getInstalledJestMajorVersion } from '../utils/version-utils'; import { getInstalledJestMajorVersion } from '../utils/version-utils';
import {
getFilesInDirectoryUsingContext,
globWithWorkspaceContext,
} from 'nx/src/utils/workspace-context';
import { normalize } from 'node:path';
const pmc = getPackageManagerCommand(); const pmc = getPackageManagerCommand();
export interface JestPluginOptions { export interface JestPluginOptions {
targetName?: string; targetName?: string;
ciTargetName?: string; ciTargetName?: string;
/**
* Whether to use jest-config and jest-runtime to load Jest configuration and context.
* Disabling this is much faster but could be less correct since we are using our own config loader
* and test matcher instead of Jest's.
*/
disableJestRuntime?: boolean;
} }
type JestTargets = Awaited<ReturnType<typeof buildJestTargets>>; type JestTargets = Awaited<ReturnType<typeof buildJestTargets>>;
@ -57,11 +67,19 @@ export const createNodesV2: CreateNodesV2<JestPluginOptions> = [
const optionsHash = hashObject(options); const optionsHash = hashObject(options);
const cachePath = join(workspaceDataDirectory, `jest-${optionsHash}.hash`); const cachePath = join(workspaceDataDirectory, `jest-${optionsHash}.hash`);
const targetsCache = readTargetsCache(cachePath); const targetsCache = readTargetsCache(cachePath);
// Cache jest preset(s) to avoid penalties of module load times. Most of jest configs will use the same preset.
const presetCache: Record<string, unknown> = {};
try { try {
return await createNodesFromFiles( return await createNodesFromFiles(
(configFile, options, context) => (configFile, options, context) =>
createNodesInternal(configFile, options, context, targetsCache), createNodesInternal(
configFile,
options,
context,
targetsCache,
presetCache
),
configFiles, configFiles,
options, options,
context context
@ -83,15 +101,16 @@ export const createNodes: CreateNodes<JestPluginOptions> = [
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
); );
return createNodesInternal(...args, {}); return createNodesInternal(...args, {}, {});
}, },
]; ];
async function createNodesInternal( async function createNodesInternal(
configFilePath, configFilePath: string,
options, options: JestPluginOptions,
context, context: CreateNodesContext,
targetsCache: Record<string, JestTargets> targetsCache: Record<string, JestTargets>,
presetCache: Record<string, unknown>
) { ) {
const projectRoot = dirname(configFilePath); const projectRoot = dirname(configFilePath);
@ -137,7 +156,8 @@ async function createNodesInternal(
configFilePath, configFilePath,
projectRoot, projectRoot,
options, options,
context context,
presetCache
); );
const { targets, metadata } = targetsCache[hash]; const { targets, metadata } = targetsCache[hash];
@ -157,34 +177,16 @@ async function buildJestTargets(
configFilePath: string, configFilePath: string,
projectRoot: string, projectRoot: string,
options: JestPluginOptions, options: JestPluginOptions,
context: CreateNodesContext context: CreateNodesContext,
presetCache: Record<string, unknown>
): Promise<Pick<ProjectConfiguration, 'targets' | 'metadata'>> { ): Promise<Pick<ProjectConfiguration, 'targets' | 'metadata'>> {
const absConfigFilePath = resolve(context.workspaceRoot, configFilePath); const absConfigFilePath = resolve(context.workspaceRoot, configFilePath);
if (require.cache[absConfigFilePath]) { if (require.cache[absConfigFilePath]) clearRequireCache();
clearRequireCache();
}
const rawConfig = await loadConfigFile(absConfigFilePath); const rawConfig = await loadConfigFile(absConfigFilePath);
const { readConfig } = requireJestUtil<typeof import('jest-config')>(
'jest-config',
projectRoot,
context.workspaceRoot
);
const config = await readConfig(
{
_: [],
$0: undefined,
},
rawConfig,
undefined,
dirname(absConfigFilePath)
);
const namedInputs = getNamedInputs(projectRoot, context);
const targets: Record<string, TargetConfiguration> = {}; const targets: Record<string, TargetConfiguration> = {};
const namedInputs = getNamedInputs(projectRoot, context);
const target: TargetConfiguration = (targets[options.targetName] = { const target: TargetConfiguration = (targets[options.targetName] = {
command: 'jest', command: 'jest',
@ -208,77 +210,56 @@ async function buildJestTargets(
const cache = (target.cache = true); const cache = (target.cache = true);
const inputs = (target.inputs = getInputs( const inputs = (target.inputs = getInputs(
namedInputs, namedInputs,
rawConfig, rawConfig.preset,
projectRoot, projectRoot,
context.workspaceRoot context.workspaceRoot,
options.disableJestRuntime
)); ));
const outputs = (target.outputs = getOutputs(projectRoot, config, context));
let metadata: ProjectConfiguration['metadata']; let metadata: ProjectConfiguration['metadata'];
if (options?.ciTargetName) {
// nx-ignore-next-line const groupName = 'E2E (CI)';
const { default: Runtime } = requireJestUtil<typeof import('jest-runtime')>(
'jest-runtime', if (options.disableJestRuntime) {
const outputs = (target.outputs = getOutputs(
projectRoot, projectRoot,
context.workspaceRoot rawConfig.coverageDirectory
); ? join(context.workspaceRoot, projectRoot, rawConfig.coverageDirectory)
: undefined,
undefined,
context
));
const jestContext = await Runtime.createContext(config.projectConfig, { if (options?.ciTargetName) {
maxWorkers: 1, const testPaths = await getTestPaths(
watchman: false, projectRoot,
}); rawConfig,
absConfigFilePath,
const jest = require(resolveJestPath( context,
projectRoot, presetCache
context.workspaceRoot );
)) as typeof import('jest');
const source = new jest.SearchSource(jestContext);
const jestVersion = getInstalledJestMajorVersion()!;
const specs =
jestVersion >= 30
? // @ts-expect-error Jest 30+ expects the project config as the second argument
await source.getTestPaths(config.globalConfig, config.projectConfig)
: await source.getTestPaths(config.globalConfig);
const testPaths = new Set(specs.tests.map(({ path }) => path));
if (testPaths.size > 0) {
const groupName = 'E2E (CI)';
const targetGroup = []; const targetGroup = [];
const dependsOn: string[] = [];
metadata = { metadata = {
targetGroups: { targetGroups: {
[groupName]: targetGroup, [groupName]: targetGroup,
}, },
}; };
const dependsOn: string[] = [];
targets[options.ciTargetName] = { const specIgnoreRegexes: undefined | RegExp[] =
executor: 'nx:noop', rawConfig.testPathIgnorePatterns?.map(
cache: true, (p: string) => new RegExp(replaceRootDirInPath(projectRoot, p))
inputs, );
outputs,
dependsOn,
metadata: {
technologies: ['jest'],
description: 'Run Jest Tests in CI',
nonAtomizedTarget: options.targetName,
help: {
command: `${pmc.exec} jest --help`,
example: {
options: {
coverage: true,
},
},
},
},
};
targetGroup.push(options.ciTargetName);
for (const testPath of testPaths) { for (const testPath of testPaths) {
const relativePath = normalizePath( const relativePath = normalizePath(
relative(join(context.workspaceRoot, projectRoot), testPath) relative(join(context.workspaceRoot, projectRoot), testPath)
); );
if (specIgnoreRegexes?.some((regex) => regex.test(relativePath))) {
continue;
}
const targetName = `${options.ciTargetName}--${relativePath}`; const targetName = `${options.ciTargetName}--${relativePath}`;
dependsOn.push(targetName); dependsOn.push(targetName);
targets[targetName] = { targets[targetName] = {
@ -304,6 +285,141 @@ async function buildJestTargets(
}; };
targetGroup.push(targetName); targetGroup.push(targetName);
} }
if (targetGroup.length > 0) {
targets[options.ciTargetName] = {
executor: 'nx:noop',
cache: true,
inputs,
outputs,
dependsOn,
metadata: {
technologies: ['jest'],
description: 'Run Jest Tests in CI',
nonAtomizedTarget: options.targetName,
help: {
command: `${pmc.exec} jest --help`,
example: {
options: {
coverage: true,
},
},
},
},
};
targetGroup.unshift(options.ciTargetName);
}
}
} else {
const { readConfig } = requireJestUtil<typeof import('jest-config')>(
'jest-config',
projectRoot,
context.workspaceRoot
);
const config = await readConfig(
{
_: [],
$0: undefined,
},
rawConfig,
undefined,
dirname(absConfigFilePath)
);
const outputs = (target.outputs = getOutputs(
projectRoot,
config.globalConfig?.coverageDirectory,
config.globalConfig?.outputFile,
context
));
if (options?.ciTargetName) {
// nx-ignore-next-line
const { default: Runtime } = requireJestUtil<
typeof import('jest-runtime')
>('jest-runtime', projectRoot, context.workspaceRoot);
const jestContext = await Runtime.createContext(config.projectConfig, {
maxWorkers: 1,
watchman: false,
});
const jest = require(resolveJestPath(
projectRoot,
context.workspaceRoot
)) as typeof import('jest');
const source = new jest.SearchSource(jestContext);
const jestVersion = getInstalledJestMajorVersion()!;
const specs =
jestVersion >= 30
? // @ts-expect-error Jest 30+ expects the project config as the second argument
await source.getTestPaths(config.globalConfig, config.projectConfig)
: await source.getTestPaths(config.globalConfig);
const testPaths = new Set(specs.tests.map(({ path }) => path));
if (testPaths.size > 0) {
const targetGroup = [];
metadata = {
targetGroups: {
[groupName]: targetGroup,
},
};
const dependsOn: string[] = [];
targets[options.ciTargetName] = {
executor: 'nx:noop',
cache: true,
inputs,
outputs,
dependsOn,
metadata: {
technologies: ['jest'],
description: 'Run Jest Tests in CI',
nonAtomizedTarget: options.targetName,
help: {
command: `${pmc.exec} jest --help`,
example: {
options: {
coverage: true,
},
},
},
},
};
targetGroup.push(options.ciTargetName);
for (const testPath of testPaths) {
const relativePath = normalizePath(
relative(join(context.workspaceRoot, projectRoot), testPath)
);
const targetName = `${options.ciTargetName}--${relativePath}`;
dependsOn.push(targetName);
targets[targetName] = {
command: `jest ${relativePath}`,
cache,
inputs,
outputs,
options: {
cwd: projectRoot,
},
metadata: {
technologies: ['jest'],
description: `Run Jest Tests in ${relativePath}`,
help: {
command: `${pmc.exec} jest --help`,
example: {
options: {
coverage: true,
},
},
},
},
};
targetGroup.push(targetName);
}
}
} }
} }
@ -312,9 +428,10 @@ async function buildJestTargets(
function getInputs( function getInputs(
namedInputs: NxJsonConfiguration['namedInputs'], namedInputs: NxJsonConfiguration['namedInputs'],
jestConfig: { preset?: string }, preset: string,
projectRoot: string, projectRoot: string,
workspaceRoot: string workspaceRoot: string,
disableJestRuntime?: boolean
): TargetConfiguration['inputs'] { ): TargetConfiguration['inputs'] {
const inputs: TargetConfiguration['inputs'] = [ const inputs: TargetConfiguration['inputs'] = [
...('production' in namedInputs ...('production' in namedInputs
@ -323,11 +440,9 @@ function getInputs(
]; ];
const externalDependencies = ['jest']; const externalDependencies = ['jest'];
const presetInput = resolvePresetInput( const presetInput = disableJestRuntime
jestConfig.preset, ? resolvePresetInputWithoutJestResolver(preset, projectRoot, workspaceRoot)
projectRoot, : resolvePresetInputWithJestResolver(preset, projectRoot, workspaceRoot);
workspaceRoot
);
if (presetInput) { if (presetInput) {
if ( if (
typeof presetInput !== 'string' && typeof presetInput !== 'string' &&
@ -344,20 +459,38 @@ function getInputs(
return inputs; return inputs;
} }
// preset resolution adapted from: function resolvePresetInputWithoutJestResolver(
// https://github.com/jestjs/jest/blob/c54bccd657fb4cf060898717c09f633b4da3eec4/packages/jest-config/src/normalize.ts#L122
function resolvePresetInput(
presetValue: string | undefined, presetValue: string | undefined,
projectRoot: string, projectRoot: string,
workspaceRoot: string workspaceRoot: string
): TargetConfiguration['inputs'][number] | null { ): TargetConfiguration['inputs'][number] | null {
if (!presetValue) { if (!presetValue) return null;
return null;
const presetPath = replaceRootDirInPath(projectRoot, presetValue);
const isNpmPackage = !presetValue.startsWith('.') && !isAbsolute(presetPath);
if (isNpmPackage) {
return { externalDependencies: [presetValue] };
} }
const { replaceRootDirInPath } = requireJestUtil< if (presetPath.startsWith('..')) {
typeof import('jest-config') const relativePath = relative(workspaceRoot, join(projectRoot, presetPath));
>('jest-config', projectRoot, workspaceRoot); return join('{workspaceRoot}', relativePath);
} else {
const relativePath = relative(projectRoot, presetPath);
return join('{projectRoot}', relativePath);
}
}
// preset resolution adapted from:
// https://github.com/jestjs/jest/blob/c54bccd657fb4cf060898717c09f633b4da3eec4/packages/jest-config/src/normalize.ts#L122
function resolvePresetInputWithJestResolver(
presetValue: string | undefined,
projectRoot: string,
workspaceRoot: string
): TargetConfiguration['inputs'][number] | null {
if (!presetValue) return null;
let presetPath = replaceRootDirInPath(projectRoot, presetValue); let presetPath = replaceRootDirInPath(projectRoot, presetValue);
const isNpmPackage = !presetValue.startsWith('.') && !isAbsolute(presetPath); const isNpmPackage = !presetValue.startsWith('.') && !isAbsolute(presetPath);
presetPath = presetPath.startsWith('.') presetPath = presetPath.startsWith('.')
@ -385,9 +518,18 @@ function resolvePresetInput(
: join('{projectRoot}', relativePath); : join('{projectRoot}', relativePath);
} }
// Adapted from here https://github.com/jestjs/jest/blob/c13bca3/packages/jest-config/src/utils.ts#L57-L69
function replaceRootDirInPath(rootDir: string, filePath: string): string {
if (!filePath.startsWith('<rootDir>')) {
return filePath;
}
return resolve(rootDir, normalize(`./${filePath.slice('<rootDir>'.length)}`));
}
function getOutputs( function getOutputs(
projectRoot: string, projectRoot: string,
{ globalConfig }: { globalConfig: Config.GlobalConfig }, coverageDirectory: string | undefined,
outputFile: string | undefined,
context: CreateNodesContext context: CreateNodesContext
): string[] { ): string[] {
function getOutput(path: string): string { function getOutput(path: string): string {
@ -404,10 +546,7 @@ function getOutputs(
const outputs = []; const outputs = [];
for (const outputOption of [ for (const outputOption of [coverageDirectory, outputFile]) {
globalConfig.coverageDirectory,
globalConfig.outputFile,
]) {
if (outputOption) { if (outputOption) {
outputs.push(getOutput(outputOption)); outputs.push(getOutput(outputOption));
} }
@ -423,6 +562,7 @@ function normalizeOptions(options: JestPluginOptions): JestPluginOptions {
} }
let resolvedJestPaths: Record<string, string>; let resolvedJestPaths: Record<string, string>;
function resolveJestPath(projectRoot: string, workspaceRoot: string): string { function resolveJestPath(projectRoot: string, workspaceRoot: string): string {
resolvedJestPaths ??= {}; resolvedJestPaths ??= {};
if (resolvedJestPaths[projectRoot]) { if (resolvedJestPaths[projectRoot]) {
@ -437,6 +577,7 @@ function resolveJestPath(projectRoot: string, workspaceRoot: string): string {
} }
let resolvedJestCorePaths: Record<string, string>; let resolvedJestCorePaths: Record<string, string>;
/** /**
* Resolves a jest util package version that `jest` is using. * Resolves a jest util package version that `jest` is using.
*/ */
@ -459,3 +600,86 @@ function requireJestUtil<T>(
paths: [dirname(resolvedJestCorePaths[jestPath])], paths: [dirname(resolvedJestCorePaths[jestPath])],
})); }));
} }
async function getTestPaths(
projectRoot: string,
rawConfig: any,
absConfigFilePath: string,
context: CreateNodesContext,
presetCache: Record<string, unknown>
): Promise<string[]> {
const testMatch = await getJestOption<string[]>(
rawConfig,
absConfigFilePath,
'testMatch',
presetCache
);
if (testMatch) {
return await globWithWorkspaceContext(
context.workspaceRoot,
testMatch.map((pattern) => join(projectRoot, pattern)),
[]
);
} else {
const testRegex = await getJestOption<string[]>(
rawConfig,
absConfigFilePath,
'testRegex',
presetCache
);
if (testRegex) {
const files: string[] = [];
const testRegexes = Array.isArray(rawConfig.testRegex)
? rawConfig.testRegex.map((r: string) => new RegExp(r))
: [new RegExp(rawConfig.testRegex)];
const projectFiles = await getFilesInDirectoryUsingContext(
context.workspaceRoot,
projectRoot
);
for (const file of projectFiles) {
if (testRegexes.some((r: RegExp) => r.test(file))) files.push(file);
}
return files;
} else {
// Default copied from https://github.com/jestjs/jest/blob/d1a2ed7/packages/jest-config/src/Defaults.ts#L84
const defaultTestMatch = [
'**/__tests__/**/*.?([mc])[jt]s?(x)',
'**/?(*.)+(spec|test).?([mc])[jt]s?(x)',
];
return await globWithWorkspaceContext(
context.workspaceRoot,
defaultTestMatch.map((pattern) => join(projectRoot, pattern)),
[]
);
}
}
}
async function getJestOption<T = any>(
rawConfig: any,
absConfigFilePath: string,
optionName: string,
presetCache: Record<string, unknown>
): Promise<T> {
if (rawConfig[optionName]) return rawConfig[optionName];
if (rawConfig.preset) {
const dir = dirname(absConfigFilePath);
const presetPath = resolve(dir, rawConfig.preset);
try {
let preset = presetCache[presetPath];
if (!preset) {
preset = await loadConfigFile(presetPath);
presetCache[presetPath] = preset;
}
if (preset[optionName]) return preset[optionName];
} catch {
// If preset fails to load, ignore the error and continue.
// This is safe and less jarring for users. They will need to fix the
// preset for Jest to run, and at that point we can read in the correct
// value.
}
}
return undefined;
}