feat(testing): disable jest runtime by default when inferring targets (#29917)

This PR updates `@nx/jest/plugin` such that `disableJestRuntime` option
is true by default. Users will need to set it to false to bring in
`jest-config` and `jest-runtime` to compute atomized targets. We're
leaving it as an option if anyone runs into discrepancies between our
calculation and what jest-runtime calculates for test files within a
project.



## Current Behavior
Jest runtime is used by default and is potentially slow if you use many
transforms, presets, etc. in the jest config.

## Expected Behavior
Jest runtime is not used by default, and users have to option of
enabling it.

## 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 2025-02-07 13:32:24 -05:00 committed by GitHub
parent a7c8c1021e
commit 9bc63177df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 113 additions and 565 deletions

View File

@ -1,166 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@nx/jest/plugin should add package as externalDependencies to the inputs when specified as preset and containing a jest-preset.cjs file 1`] = `
[
[
"proj/jest.config.js",
{
"projects": {
"proj": {
"metadata": undefined,
"root": "proj",
"targets": {
"test": {
"cache": true,
"command": "jest",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
"some-package",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
},
},
},
],
]
`;
exports[`@nx/jest/plugin should add package as externalDependencies to the inputs when specified as preset and containing a jest-preset.js file 1`] = `
[
[
"proj/jest.config.js",
{
"projects": {
"proj": {
"metadata": undefined,
"root": "proj",
"targets": {
"test": {
"cache": true,
"command": "jest",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
"some-package",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
},
},
},
],
]
`;
exports[`@nx/jest/plugin should add package as externalDependencies to the inputs when specified as preset and containing a jest-preset.json file 1`] = `
[
[
"proj/jest.config.js",
{
"projects": {
"proj": {
"metadata": undefined,
"root": "proj",
"targets": {
"test": {
"cache": true,
"command": "jest",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
"some-package",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
},
},
},
],
]
`;

View File

@ -8,7 +8,7 @@ jest.mock('nx/src/utils/cache-directory', () => ({
workspaceDataDirectory: 'tmp/project-graph-cache',
}));
describe('@nx/jest/plugin', () => {
describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext;
let tempFs: TempFs;
@ -54,6 +54,7 @@ describe('@nx/jest/plugin', () => {
['proj/jest.config.js'],
{
targetName: 'test',
disableJestRuntime,
},
context
);
@ -127,6 +128,7 @@ describe('@nx/jest/plugin', () => {
{
targetName: 'test',
ciTargetName: 'test-ci',
disableJestRuntime,
},
context
);
@ -272,7 +274,7 @@ describe('@nx/jest/plugin', () => {
const results = await createNodesFunction(
['proj/jest.config.js'],
{ targetName: 'test' },
{ targetName: 'test', disableJestRuntime },
context
);
@ -352,354 +354,68 @@ describe('@nx/jest/plugin', () => {
const results = await createNodesFunction(
['proj/jest.config.js'],
{ targetName: 'test' },
{ targetName: 'test', disableJestRuntime },
context
);
expect(results).toMatchSnapshot();
const snapshot = `
[
[
"proj/jest.config.js",
{
"projects": {
"proj": {
"metadata": undefined,
"root": "proj",
"targets": {
"test": {
"cache": true,
"command": "jest",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
"some-package",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"help": {
"command": "npx jest --help",
"example": {
"options": {
"coverage": true,
},
},
},
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
},
},
},
],
]
`;
expect(results).toMatchInlineSnapshot(snapshot);
}
);
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": {
"TEST (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",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"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",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"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": {
"TEST (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",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"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",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
},
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
},
},
},
],
]
`);
}
);
});
describe('ciGroupName', () => {
it('should name atomized tasks group using provided group name', async () => {
mockJestConfig(
@ -715,6 +431,7 @@ describe('@nx/jest/plugin', () => {
{
ciTargetName: 'test-ci',
ciGroupName: 'MY ATOMIZED TEST TASKS (CI)',
disableJestRuntime,
},
context
);
@ -864,6 +581,7 @@ describe('@nx/jest/plugin', () => {
['proj/jest.config.js'],
{
ciTargetName: 'test-ci',
disableJestRuntime,
},
context
);
@ -1013,6 +731,7 @@ describe('@nx/jest/plugin', () => {
['proj/jest.config.js'],
{
ciTargetName: 'testci', // missing "-ci" suffix or similar construct, so group name cannot reliably be deducted from ci target name
disableJestRuntime,
},
context
);

View File

@ -27,11 +27,8 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { combineGlobPatterns } from 'nx/src/utils/globs';
import { dirname, isAbsolute, join, relative, resolve } from 'path';
import { getInstalledJestMajorVersion } from '../utils/version-utils';
import {
getFilesInDirectoryUsingContext,
globWithWorkspaceContext,
} from 'nx/src/utils/workspace-context';
import { normalize } from 'node:path';
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
import { normalize, sep } from 'node:path';
const pmc = getPackageManagerCommand();
@ -44,7 +41,7 @@ export interface JestPluginOptions {
*/
ciGroupName?: string;
/**
* Whether to use jest-config and jest-runtime to load Jest configuration and context.
* Whether to use jest-config and jest-runtime are used 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.
*/
@ -215,13 +212,16 @@ async function buildJestTargets(
},
});
// Not normalizing it here since also affects options for convert-to-inferred.
const disableJestRuntime = options.disableJestRuntime !== false;
const cache = (target.cache = true);
const inputs = (target.inputs = getInputs(
namedInputs,
rawConfig.preset,
projectRoot,
context.workspaceRoot,
options.disableJestRuntime
disableJestRuntime
));
let metadata: ProjectConfiguration['metadata'];
@ -229,7 +229,7 @@ async function buildJestTargets(
const groupName =
options?.ciGroupName ?? deductGroupNameFromTarget(options?.ciTargetName);
if (options.disableJestRuntime) {
if (disableJestRuntime) {
const outputs = (target.outputs = getOutputs(
projectRoot,
rawConfig.coverageDirectory
@ -326,15 +326,21 @@ async function buildJestTargets(
projectRoot,
context.workspaceRoot
);
const config = await readConfig(
{
_: [],
$0: undefined,
},
rawConfig,
undefined,
dirname(absConfigFilePath)
);
let config;
try {
config = await readConfig(
{
_: [],
$0: undefined,
},
rawConfig,
undefined,
dirname(absConfigFilePath)
);
} catch (e) {
console.error(e);
throw e;
}
const outputs = (target.outputs = getOutputs(
projectRoot,
@ -363,8 +369,7 @@ async function buildJestTargets(
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, config.projectConfig)
: await source.getTestPaths(config.globalConfig);
const testPaths = new Set(specs.tests.map(({ path }) => path));
@ -627,45 +632,35 @@ async function getTestPaths(
'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 = [
let paths = await globWithWorkspaceContext(
context.workspaceRoot,
(
testMatch || [
// Default copied from https://github.com/jestjs/jest/blob/d1a2ed7/packages/jest-config/src/Defaults.ts#L84
'**/__tests__/**/*.?([mc])[jt]s?(x)',
'**/?(*.)+(spec|test).?([mc])[jt]s?(x)',
];
return await globWithWorkspaceContext(
context.workspaceRoot,
defaultTestMatch.map((pattern) => join(projectRoot, pattern)),
[]
);
}
]
).map((pattern) => join(projectRoot, pattern)),
[]
);
const testRegex = await getJestOption<string[]>(
rawConfig,
absConfigFilePath,
'testRegex',
presetCache
);
if (testRegex) {
const testRegexes = Array.isArray(rawConfig.testRegex)
? rawConfig.testRegex.map((r: string) => new RegExp(r))
: [new RegExp(rawConfig.testRegex)];
paths = paths.filter((path: string) =>
testRegexes.some((r: RegExp) => r.test(path))
);
}
return paths;
}
async function getJestOption<T = any>(