feat(testing): use createNodesV2 for jest (#26292)

<!-- 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` -->

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

`@nx/jest/plugin` uses CreateNodesV1. Multiple Jest plugins are not able
to be run in parallel and in isolation.

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

`@nx/jest/plugin` uses CreateNodesV2. Multiple jest plugins are able to
run in parallel and in isolation.

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

Fixes #
This commit is contained in:
Jason Jean 2024-05-31 15:43:01 -04:00 committed by GitHub
parent 1f7c0bc51d
commit b558f56c69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 263 additions and 212 deletions

View File

@ -10,12 +10,12 @@
#### Parameters #### Parameters
| Name | Type | | Name | Type |
| :------------ | :-------------------------------------------------------------------- | | :------------ | :------------------------------------------------------------------------- |
| `createNodes` | [`CreateNodesFunction`](../../devkit/documents/CreateNodesFunction) | | `createNodes` | [`CreateNodesFunction`](../../devkit/documents/CreateNodesFunction)\<`T`\> |
| `configFiles` | readonly `string`[] | | `configFiles` | readonly `string`[] |
| `options` | `T` | | `options` | `T` |
| `context` | [`CreateNodesContextV2`](../../devkit/documents/CreateNodesContextV2) | | `context` | [`CreateNodesContextV2`](../../devkit/documents/CreateNodesContextV2) |
#### Returns #### Returns

View File

@ -123,13 +123,14 @@ export const makeCreateNodes =
}; };
/** /**
* @deprecated `{@link createNodesV2} is replacing this. Update your plugin to export its own `createNodesV2` function that wraps this one instead.` @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead.
This function will change to the v2 function in Nx 20.
*/ */
export const createNodes: CreateNodes<GradlePluginOptions> = [ export const createNodes: CreateNodes<GradlePluginOptions> = [
gradleConfigGlob, gradleConfigGlob,
(configFile, options, context) => { (configFile, options, context) => {
logger.warn( logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will error.' '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
); );
populateGradleReport(context.workspaceRoot); populateGradleReport(context.workspaceRoot);
const gradleReport = getCurrentGradleReport(); const gradleReport = getCurrentGradleReport();

View File

@ -1,5 +1,5 @@
export { export {
createNodes, createNodes,
createDependencies, createNodesV2,
JestPluginOptions, JestPluginOptions,
} from './src/plugins/plugin'; } from './src/plugins/plugin';

View File

@ -9,8 +9,8 @@ import {
type GeneratorCallback, type GeneratorCallback,
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin'; import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../plugins/plugin'; import { createNodesV2 } from '../../plugins/plugin';
import { import {
getPresetExt, getPresetExt,
type JestPresetExtension, type JestPresetExtension,
@ -104,11 +104,11 @@ export async function jestInitGeneratorInternal(
if (!tree.exists(`jest.preset.${presetExt}`)) { if (!tree.exists(`jest.preset.${presetExt}`)) {
updateProductionFileSet(tree); updateProductionFileSet(tree);
if (options.addPlugin) { if (options.addPlugin) {
await addPluginV1( await addPlugin(
tree, tree,
await createProjectGraphAsync(), await createProjectGraphAsync(),
'@nx/jest/plugin', '@nx/jest/plugin',
createNodes, createNodesV2,
{ {
targetName: ['test', 'jest:test', 'jest-test'], targetName: ['test', 'jest:test', 'jest-test'],
}, },

View File

@ -1,11 +1,11 @@
import { CreateNodesContext } from '@nx/devkit'; import { CreateNodesContext } from '@nx/devkit';
import { join } from 'path'; import { join } from 'path';
import { createNodes } from './plugin'; import { createNodesV2 } from './plugin';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
describe('@nx/jest/plugin', () => { describe('@nx/jest/plugin', () => {
let createNodesFunction = createNodes[1]; let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext; let context: CreateNodesContext;
let tempFs: TempFs; let tempFs: TempFs;
let cwd: string; let cwd: string;
@ -45,46 +45,55 @@ describe('@nx/jest/plugin', () => {
}, },
context context
); );
const nodes = await createNodesFunction( const results = await createNodesFunction(
'proj/jest.config.js', ['proj/jest.config.js'],
{ {
targetName: 'test', targetName: 'test',
}, },
context context
); );
expect(nodes.projects.proj).toMatchInlineSnapshot(` expect(results).toMatchInlineSnapshot(`
{ [
"metadata": undefined, [
"root": "proj", "proj/jest.config.js",
"targets": { {
"test": { "projects": {
"cache": true, "proj": {
"command": "jest", "metadata": undefined,
"inputs": [ "root": "proj",
"default", "targets": {
"^production", "test": {
{ "cache": true,
"externalDependencies": [ "command": "jest",
"jest", "inputs": [
], "default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
}, },
],
"metadata": {
"description": "Run Jest Tests",
"technologies": [
"jest",
],
}, },
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
}, },
}, ],
} ]
`); `);
}); });
@ -97,8 +106,8 @@ describe('@nx/jest/plugin', () => {
}, },
context context
); );
const nodes = await createNodesFunction( const results = await createNodesFunction(
'proj/jest.config.js', ['proj/jest.config.js'],
{ {
targetName: 'test', targetName: 'test',
ciTargetName: 'test-ci', ciTargetName: 'test-ci',
@ -106,95 +115,104 @@ describe('@nx/jest/plugin', () => {
context context
); );
expect(nodes.projects.proj).toMatchInlineSnapshot(` expect(results).toMatchInlineSnapshot(`
{ [
"metadata": { [
"targetGroups": { "proj/jest.config.js",
"E2E (CI)": [ {
"test-ci", "projects": {
"test-ci--src/unit.spec.ts", "proj": {
], "metadata": {
}, "targetGroups": {
}, "E2E (CI)": [
"root": "proj", "test-ci",
"targets": { "test-ci--src/unit.spec.ts",
"test": { ],
"cache": true, },
"command": "jest", },
"inputs": [ "root": "proj",
"default", "targets": {
"^production", "test": {
{ "cache": true,
"externalDependencies": [ "command": "jest",
"jest", "inputs": [
], "default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"metadata": {
"description": "Run Jest Tests",
"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",
"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",
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
}, },
],
"metadata": {
"description": "Run Jest Tests",
"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",
"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",
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
}
`); `);
}); });
}); });

View File

@ -1,8 +1,10 @@
import { import {
CreateDependencies,
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
createNodesFromFiles,
CreateNodesV2,
joinPathFragments, joinPathFragments,
logger,
normalizePath, normalizePath,
NxJsonConfiguration, NxJsonConfiguration,
ProjectConfiguration, ProjectConfiguration,
@ -10,7 +12,7 @@ import {
TargetConfiguration, TargetConfiguration,
writeJsonFile, writeJsonFile,
} from '@nx/devkit'; } from '@nx/devkit';
import { dirname, join, normalize, relative, resolve } from 'path'; import { dirname, join, relative, resolve } from 'path';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { existsSync, readdirSync, readFileSync } from 'fs'; import { existsSync, readdirSync, readFileSync } from 'fs';
@ -21,101 +23,131 @@ import { clearRequireCache } from '@nx/devkit/src/utils/config-utils';
import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/src/plugins/package-json-workspaces'; import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/src/plugins/package-json-workspaces';
import { combineGlobPatterns } from 'nx/src/utils/globs'; import { combineGlobPatterns } from 'nx/src/utils/globs';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { hashObject } from 'nx/src/devkit-internals';
export interface JestPluginOptions { export interface JestPluginOptions {
targetName?: string; targetName?: string;
ciTargetName?: string; ciTargetName?: string;
} }
const cachePath = join(projectGraphCacheDirectory, 'jest.hash');
const targetsCache = readTargetsCache();
type JestTargets = Awaited<ReturnType<typeof buildJestTargets>>; type JestTargets = Awaited<ReturnType<typeof buildJestTargets>>;
function readTargetsCache(): Record<string, JestTargets> { function readTargetsCache(cachePath: string): Record<string, JestTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; return existsSync(cachePath) ? readJsonFile(cachePath) : {};
} }
function writeTargetsToCache() { function writeTargetsToCache(
const cache = readTargetsCache(); cachePath: string,
writeJsonFile(cachePath, { results: Record<string, JestTargets>
...cache, ) {
...targetsCache, writeJsonFile(cachePath, results);
});
} }
export const createDependencies: CreateDependencies = () => { const jestConfigGlob = '**/jest.config.{cjs,mjs,js,cts,mts,ts}';
writeTargetsToCache();
return [];
};
export const createNodes: CreateNodes<JestPluginOptions> = [ export const createNodesV2: CreateNodesV2<JestPluginOptions> = [
'**/jest.config.{cjs,mjs,js,cts,mts,ts}', jestConfigGlob,
async (configFilePath, options, context) => { async (configFiles, options, context) => {
const projectRoot = dirname(configFilePath); const optionsHash = hashObject(options);
const cachePath = join(
const packageManagerWorkspacesGlob = combineGlobPatterns( projectGraphCacheDirectory,
getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot) `jest-${optionsHash}.hash`
); );
const targetsCache = readTargetsCache(cachePath);
// Do not create a project if package.json and project.json isn't there. try {
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); return await createNodesFromFiles(
if ( (configFile, options, context) =>
!siblingFiles.includes('package.json') && createNodesInternal(configFile, options, context, targetsCache),
!siblingFiles.includes('project.json') configFiles,
) { options,
return {}; context
} else if (
!siblingFiles.includes('project.json') &&
siblingFiles.includes('package.json')
) {
const path = joinPathFragments(projectRoot, 'package.json');
const isPackageJsonProject = minimatch(
path,
packageManagerWorkspacesGlob
); );
} finally {
if (!isPackageJsonProject) { writeTargetsToCache(cachePath, targetsCache);
return {};
}
} }
const jestConfigContent = readFileSync(
resolve(context.workspaceRoot, configFilePath),
'utf-8'
);
if (jestConfigContent.includes('getJestProjectsAsync()')) {
// The `getJestProjectsAsync` function uses the project graph, which leads to a
// circular dependency. We can skip this since it's no intended to be used for
// an Nx project.
return {};
}
options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context);
targetsCache[hash] ??= await buildJestTargets(
configFilePath,
projectRoot,
options,
context
);
const { targets, metadata } = targetsCache[hash];
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
}, },
]; ];
/**
* @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead.
* This function will change to the v2 function in Nx 20.
*/
export const createNodes: CreateNodes<JestPluginOptions> = [
jestConfigGlob,
(...args) => {
logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
);
return createNodesInternal(...args, {});
},
];
async function createNodesInternal(
configFilePath,
options,
context,
targetsCache: Record<string, JestTargets>
) {
const projectRoot = dirname(configFilePath);
const packageManagerWorkspacesGlob = combineGlobPatterns(
getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot)
);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
} else if (
!siblingFiles.includes('project.json') &&
siblingFiles.includes('package.json')
) {
const path = joinPathFragments(projectRoot, 'package.json');
const isPackageJsonProject = minimatch(path, packageManagerWorkspacesGlob);
if (!isPackageJsonProject) {
return {};
}
}
const jestConfigContent = readFileSync(
resolve(context.workspaceRoot, configFilePath),
'utf-8'
);
if (jestConfigContent.includes('getJestProjectsAsync()')) {
// The `getJestProjectsAsync` function uses the project graph, which leads to a
// circular dependency. We can skip this since it's no intended to be used for
// an Nx project.
return {};
}
options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context);
targetsCache[hash] ??= await buildJestTargets(
configFilePath,
projectRoot,
options,
context
);
const { targets, metadata } = targetsCache[hash];
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
}
async function buildJestTargets( async function buildJestTargets(
configFilePath: string, configFilePath: string,
projectRoot: string, projectRoot: string,

View File

@ -58,7 +58,7 @@ export type AsyncFn<T extends Function> = T extends (
: never; : never;
export async function createNodesFromFiles<T = unknown>( export async function createNodesFromFiles<T = unknown>(
createNodes: CreateNodesFunction, createNodes: CreateNodesFunction<T>,
configFiles: readonly string[], configFiles: readonly string[],
options: T, options: T,
context: CreateNodesContextV2 context: CreateNodesContextV2