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/**/*"],
|
||||
"options": {
|
||||
"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
|
||||
|
||||
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/**/*"],
|
||||
"options": {
|
||||
"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
|
||||
|
||||
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",
|
||||
"jest",
|
||||
"@jest/types",
|
||||
"jest-runtime",
|
||||
// require.resolve is used for these packages
|
||||
"identity-obj-proxy"
|
||||
]
|
||||
|
||||
@ -347,6 +347,334 @@ describe('@nx/jest/plugin', () => {
|
||||
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) {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { Config } from '@jest/types';
|
||||
import {
|
||||
CreateNodes,
|
||||
CreateNodesContext,
|
||||
@ -28,12 +27,23 @@ 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';
|
||||
|
||||
const pmc = getPackageManagerCommand();
|
||||
|
||||
export interface JestPluginOptions {
|
||||
targetName?: 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>>;
|
||||
@ -57,11 +67,19 @@ export const createNodesV2: CreateNodesV2<JestPluginOptions> = [
|
||||
const optionsHash = hashObject(options);
|
||||
const cachePath = join(workspaceDataDirectory, `jest-${optionsHash}.hash`);
|
||||
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 {
|
||||
return await createNodesFromFiles(
|
||||
(configFile, options, context) =>
|
||||
createNodesInternal(configFile, options, context, targetsCache),
|
||||
createNodesInternal(
|
||||
configFile,
|
||||
options,
|
||||
context,
|
||||
targetsCache,
|
||||
presetCache
|
||||
),
|
||||
configFiles,
|
||||
options,
|
||||
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.'
|
||||
);
|
||||
|
||||
return createNodesInternal(...args, {});
|
||||
return createNodesInternal(...args, {}, {});
|
||||
},
|
||||
];
|
||||
|
||||
async function createNodesInternal(
|
||||
configFilePath,
|
||||
options,
|
||||
context,
|
||||
targetsCache: Record<string, JestTargets>
|
||||
configFilePath: string,
|
||||
options: JestPluginOptions,
|
||||
context: CreateNodesContext,
|
||||
targetsCache: Record<string, JestTargets>,
|
||||
presetCache: Record<string, unknown>
|
||||
) {
|
||||
const projectRoot = dirname(configFilePath);
|
||||
|
||||
@ -137,7 +156,8 @@ async function createNodesInternal(
|
||||
configFilePath,
|
||||
projectRoot,
|
||||
options,
|
||||
context
|
||||
context,
|
||||
presetCache
|
||||
);
|
||||
|
||||
const { targets, metadata } = targetsCache[hash];
|
||||
@ -157,34 +177,16 @@ async function buildJestTargets(
|
||||
configFilePath: string,
|
||||
projectRoot: string,
|
||||
options: JestPluginOptions,
|
||||
context: CreateNodesContext
|
||||
context: CreateNodesContext,
|
||||
presetCache: Record<string, unknown>
|
||||
): Promise<Pick<ProjectConfiguration, 'targets' | 'metadata'>> {
|
||||
const absConfigFilePath = resolve(context.workspaceRoot, configFilePath);
|
||||
|
||||
if (require.cache[absConfigFilePath]) {
|
||||
clearRequireCache();
|
||||
}
|
||||
|
||||
if (require.cache[absConfigFilePath]) clearRequireCache();
|
||||
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 namedInputs = getNamedInputs(projectRoot, context);
|
||||
|
||||
const target: TargetConfiguration = (targets[options.targetName] = {
|
||||
command: 'jest',
|
||||
@ -208,77 +210,56 @@ async function buildJestTargets(
|
||||
const cache = (target.cache = true);
|
||||
const inputs = (target.inputs = getInputs(
|
||||
namedInputs,
|
||||
rawConfig,
|
||||
rawConfig.preset,
|
||||
projectRoot,
|
||||
context.workspaceRoot
|
||||
context.workspaceRoot,
|
||||
options.disableJestRuntime
|
||||
));
|
||||
const outputs = (target.outputs = getOutputs(projectRoot, config, context));
|
||||
|
||||
let metadata: ProjectConfiguration['metadata'];
|
||||
if (options?.ciTargetName) {
|
||||
// nx-ignore-next-line
|
||||
const { default: Runtime } = requireJestUtil<typeof import('jest-runtime')>(
|
||||
'jest-runtime',
|
||||
|
||||
const groupName = 'E2E (CI)';
|
||||
|
||||
if (options.disableJestRuntime) {
|
||||
const outputs = (target.outputs = getOutputs(
|
||||
projectRoot,
|
||||
context.workspaceRoot
|
||||
);
|
||||
rawConfig.coverageDirectory
|
||||
? join(context.workspaceRoot, projectRoot, rawConfig.coverageDirectory)
|
||||
: undefined,
|
||||
undefined,
|
||||
context
|
||||
));
|
||||
|
||||
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 groupName = 'E2E (CI)';
|
||||
if (options?.ciTargetName) {
|
||||
const testPaths = await getTestPaths(
|
||||
projectRoot,
|
||||
rawConfig,
|
||||
absConfigFilePath,
|
||||
context,
|
||||
presetCache
|
||||
);
|
||||
const targetGroup = [];
|
||||
const dependsOn: string[] = [];
|
||||
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);
|
||||
const specIgnoreRegexes: undefined | RegExp[] =
|
||||
rawConfig.testPathIgnorePatterns?.map(
|
||||
(p: string) => new RegExp(replaceRootDirInPath(projectRoot, p))
|
||||
);
|
||||
|
||||
for (const testPath of testPaths) {
|
||||
const relativePath = normalizePath(
|
||||
relative(join(context.workspaceRoot, projectRoot), testPath)
|
||||
);
|
||||
|
||||
if (specIgnoreRegexes?.some((regex) => regex.test(relativePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetName = `${options.ciTargetName}--${relativePath}`;
|
||||
dependsOn.push(targetName);
|
||||
targets[targetName] = {
|
||||
@ -304,6 +285,141 @@ async function buildJestTargets(
|
||||
};
|
||||
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(
|
||||
namedInputs: NxJsonConfiguration['namedInputs'],
|
||||
jestConfig: { preset?: string },
|
||||
preset: string,
|
||||
projectRoot: string,
|
||||
workspaceRoot: string
|
||||
workspaceRoot: string,
|
||||
disableJestRuntime?: boolean
|
||||
): TargetConfiguration['inputs'] {
|
||||
const inputs: TargetConfiguration['inputs'] = [
|
||||
...('production' in namedInputs
|
||||
@ -323,11 +440,9 @@ function getInputs(
|
||||
];
|
||||
|
||||
const externalDependencies = ['jest'];
|
||||
const presetInput = resolvePresetInput(
|
||||
jestConfig.preset,
|
||||
projectRoot,
|
||||
workspaceRoot
|
||||
);
|
||||
const presetInput = disableJestRuntime
|
||||
? resolvePresetInputWithoutJestResolver(preset, projectRoot, workspaceRoot)
|
||||
: resolvePresetInputWithJestResolver(preset, projectRoot, workspaceRoot);
|
||||
if (presetInput) {
|
||||
if (
|
||||
typeof presetInput !== 'string' &&
|
||||
@ -344,20 +459,38 @@ function getInputs(
|
||||
return inputs;
|
||||
}
|
||||
|
||||
// preset resolution adapted from:
|
||||
// https://github.com/jestjs/jest/blob/c54bccd657fb4cf060898717c09f633b4da3eec4/packages/jest-config/src/normalize.ts#L122
|
||||
function resolvePresetInput(
|
||||
function resolvePresetInputWithoutJestResolver(
|
||||
presetValue: string | undefined,
|
||||
projectRoot: string,
|
||||
workspaceRoot: string
|
||||
): TargetConfiguration['inputs'][number] | null {
|
||||
if (!presetValue) {
|
||||
return null;
|
||||
if (!presetValue) return null;
|
||||
|
||||
const presetPath = replaceRootDirInPath(projectRoot, presetValue);
|
||||
const isNpmPackage = !presetValue.startsWith('.') && !isAbsolute(presetPath);
|
||||
|
||||
if (isNpmPackage) {
|
||||
return { externalDependencies: [presetValue] };
|
||||
}
|
||||
|
||||
const { replaceRootDirInPath } = requireJestUtil<
|
||||
typeof import('jest-config')
|
||||
>('jest-config', projectRoot, workspaceRoot);
|
||||
if (presetPath.startsWith('..')) {
|
||||
const relativePath = relative(workspaceRoot, join(projectRoot, presetPath));
|
||||
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);
|
||||
const isNpmPackage = !presetValue.startsWith('.') && !isAbsolute(presetPath);
|
||||
presetPath = presetPath.startsWith('.')
|
||||
@ -385,9 +518,18 @@ function resolvePresetInput(
|
||||
: 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(
|
||||
projectRoot: string,
|
||||
{ globalConfig }: { globalConfig: Config.GlobalConfig },
|
||||
coverageDirectory: string | undefined,
|
||||
outputFile: string | undefined,
|
||||
context: CreateNodesContext
|
||||
): string[] {
|
||||
function getOutput(path: string): string {
|
||||
@ -404,10 +546,7 @@ function getOutputs(
|
||||
|
||||
const outputs = [];
|
||||
|
||||
for (const outputOption of [
|
||||
globalConfig.coverageDirectory,
|
||||
globalConfig.outputFile,
|
||||
]) {
|
||||
for (const outputOption of [coverageDirectory, outputFile]) {
|
||||
if (outputOption) {
|
||||
outputs.push(getOutput(outputOption));
|
||||
}
|
||||
@ -423,6 +562,7 @@ function normalizeOptions(options: JestPluginOptions): JestPluginOptions {
|
||||
}
|
||||
|
||||
let resolvedJestPaths: Record<string, string>;
|
||||
|
||||
function resolveJestPath(projectRoot: string, workspaceRoot: string): string {
|
||||
resolvedJestPaths ??= {};
|
||||
if (resolvedJestPaths[projectRoot]) {
|
||||
@ -437,6 +577,7 @@ function resolveJestPath(projectRoot: string, workspaceRoot: string): string {
|
||||
}
|
||||
|
||||
let resolvedJestCorePaths: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Resolves a jest util package version that `jest` is using.
|
||||
*/
|
||||
@ -459,3 +600,86 @@ function requireJestUtil<T>(
|
||||
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