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:
parent
4014662986
commit
178d93d9c0
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user