feat(testing): add convert-to-inferred migration generator for jest (#26259)
<!-- 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 --> ## 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
e5d7805d4b
commit
df3c7522ea
@ -7749,6 +7749,14 @@
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"id": "convert-to-inferred",
|
||||
"path": "/nx-api/jest/generators/convert-to-inferred",
|
||||
"name": "convert-to-inferred",
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
}
|
||||
],
|
||||
"isExternal": false,
|
||||
|
||||
@ -1130,6 +1130,15 @@
|
||||
"originalFilePath": "/packages/jest/src/generators/configuration/schema.json",
|
||||
"path": "/nx-api/jest/generators/configuration",
|
||||
"type": "generator"
|
||||
},
|
||||
"/nx-api/jest/generators/convert-to-inferred": {
|
||||
"description": "Convert existing Jest project(s) using `@nx/jest:jest` executor to use `@nx/jest/plugin`.",
|
||||
"file": "generated/packages/jest/generators/convert-to-inferred.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-inferred",
|
||||
"originalFilePath": "/packages/jest/src/generators/convert-to-inferred/schema.json",
|
||||
"path": "/nx-api/jest/generators/convert-to-inferred",
|
||||
"type": "generator"
|
||||
}
|
||||
},
|
||||
"path": "/nx-api/jest"
|
||||
|
||||
@ -1113,6 +1113,15 @@
|
||||
"originalFilePath": "/packages/jest/src/generators/configuration/schema.json",
|
||||
"path": "jest/generators/configuration",
|
||||
"type": "generator"
|
||||
},
|
||||
{
|
||||
"description": "Convert existing Jest project(s) using `@nx/jest:jest` executor to use `@nx/jest/plugin`.",
|
||||
"file": "generated/packages/jest/generators/convert-to-inferred.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-inferred",
|
||||
"originalFilePath": "/packages/jest/src/generators/convert-to-inferred/schema.json",
|
||||
"path": "jest/generators/convert-to-inferred",
|
||||
"type": "generator"
|
||||
}
|
||||
],
|
||||
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "convert-to-inferred",
|
||||
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxJestConvertToInferred",
|
||||
"description": "Convert existing Jest project(s) using `@nx/jest:jest` executor to use `@nx/jest/plugin`.",
|
||||
"title": "Convert Jest project from executor to plugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/jest:jest` executor to use `@nx/jest/plugin`. If not provided, all projects using the `@nx/jest:jest` executor will be converted.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"presets": []
|
||||
},
|
||||
"description": "Convert existing Jest project(s) using `@nx/jest:jest` executor to use `@nx/jest/plugin`.",
|
||||
"implementation": "/packages/jest/src/generators/convert-to-inferred/convert-to-inferred.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/jest/src/generators/convert-to-inferred/schema.json",
|
||||
"type": "generator"
|
||||
}
|
||||
@ -452,6 +452,7 @@
|
||||
- [generators](/nx-api/jest/generators)
|
||||
- [init](/nx-api/jest/generators/init)
|
||||
- [configuration](/nx-api/jest/generators/configuration)
|
||||
- [convert-to-inferred](/nx-api/jest/generators/convert-to-inferred)
|
||||
- [js](/nx-api/js)
|
||||
- [documents](/nx-api/js/documents)
|
||||
- [Overview](/nx-api/js/documents/overview)
|
||||
|
||||
@ -35,7 +35,7 @@ type PostTargetTransformer = (
|
||||
tree: Tree,
|
||||
projectDetails: { projectName: string; root: string },
|
||||
inferredTargetConfiguration: TargetConfiguration
|
||||
) => TargetConfiguration;
|
||||
) => TargetConfiguration | Promise<TargetConfiguration>;
|
||||
type SkipTargetFilter = (
|
||||
targetConfiguration: TargetConfiguration
|
||||
) => [boolean, string];
|
||||
@ -85,7 +85,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
await this.#init();
|
||||
if (this.#targetAndProjectsToMigrate.size > 0) {
|
||||
for (const targetName of this.#targetAndProjectsToMigrate.keys()) {
|
||||
this.#migrateTarget(targetName);
|
||||
await this.#migrateTarget(targetName);
|
||||
}
|
||||
await this.#addPlugins();
|
||||
}
|
||||
@ -105,12 +105,12 @@ class ExecutorToPluginMigrator<T> {
|
||||
await this.#getCreateNodesResults();
|
||||
}
|
||||
|
||||
#migrateTarget(targetName: string) {
|
||||
async #migrateTarget(targetName: string) {
|
||||
const include: string[] = [];
|
||||
for (const projectName of this.#targetAndProjectsToMigrate.get(
|
||||
targetName
|
||||
)) {
|
||||
include.push(this.#migrateProject(projectName, targetName));
|
||||
include.push(await this.#migrateProject(projectName, targetName));
|
||||
}
|
||||
|
||||
this.#pluginToAddForTarget.set(targetName, {
|
||||
@ -120,7 +120,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
});
|
||||
}
|
||||
|
||||
#migrateProject(projectName: string, targetName: string) {
|
||||
async #migrateProject(projectName: string, targetName: string) {
|
||||
const projectFromGraph = this.#projectGraph.nodes[projectName];
|
||||
const projectConfig = readProjectConfiguration(this.tree, projectName);
|
||||
|
||||
@ -141,7 +141,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
this.#mergeInputs(projectTarget, createdTarget);
|
||||
}
|
||||
|
||||
projectTarget = this.#postTargetTransformer(
|
||||
projectTarget = await this.#postTargetTransformer(
|
||||
projectTarget,
|
||||
this.tree,
|
||||
{ projectName, root: projectFromGraph.data.root },
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { TargetConfiguration } from 'nx/src/devkit-exports';
|
||||
import { relative, resolve } from 'node:path/posix';
|
||||
import { workspaceRoot, type TargetConfiguration } from 'nx/src/devkit-exports';
|
||||
import { interpolate } from 'nx/src/devkit-internals';
|
||||
|
||||
/**
|
||||
@ -128,6 +129,24 @@ export function processTargetOutputs(
|
||||
target.outputs = targetOutputs;
|
||||
}
|
||||
|
||||
export function toProjectRelativePath(
|
||||
path: string,
|
||||
projectRoot: string
|
||||
): string {
|
||||
if (projectRoot === '.') {
|
||||
// workspace and project root are the same, we add a leading './' which is
|
||||
// required by some tools (e.g. Jest)
|
||||
return path.startsWith('.') ? path : `./${path}`;
|
||||
}
|
||||
|
||||
const relativePath = relative(
|
||||
resolve(workspaceRoot, projectRoot),
|
||||
resolve(workspaceRoot, path)
|
||||
);
|
||||
|
||||
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
||||
}
|
||||
|
||||
function updateOutputRenamingOption(
|
||||
output: string,
|
||||
option: string,
|
||||
|
||||
@ -14,6 +14,11 @@
|
||||
"schema": "./src/generators/configuration/schema.json",
|
||||
"description": "Add Jest configuration to a project.",
|
||||
"hidden": true
|
||||
},
|
||||
"convert-to-inferred": {
|
||||
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
|
||||
"schema": "./src/generators/convert-to-inferred/schema.json",
|
||||
"description": "Convert existing Jest project(s) using `@nx/jest:jest` executor to use `@nx/jest/plugin`."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,337 @@
|
||||
import type { Config } from '@jest/types';
|
||||
import {
|
||||
createProjectGraphAsync,
|
||||
formatFiles,
|
||||
type TargetConfiguration,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
|
||||
import {
|
||||
processTargetOutputs,
|
||||
toProjectRelativePath,
|
||||
} from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
|
||||
import { readConfig } from 'jest-config';
|
||||
import { join, normalize, posix } from 'node:path';
|
||||
import { createNodesV2, type JestPluginOptions } from '../../plugins/plugin';
|
||||
import { jestConfigExtensions } from '../../utils/config/config-file';
|
||||
|
||||
interface Schema {
|
||||
project?: string;
|
||||
skipFormat?: boolean;
|
||||
}
|
||||
|
||||
export async function convertToInferred(tree: Tree, options: Schema) {
|
||||
const projectGraph = await createProjectGraphAsync();
|
||||
const migratedProjectsModern =
|
||||
await migrateExecutorToPlugin<JestPluginOptions>(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nx/jest:jest',
|
||||
'@nx/jest/plugin',
|
||||
(targetName) => ({ targetName }),
|
||||
postTargetTransformer,
|
||||
createNodesV2,
|
||||
options.project
|
||||
);
|
||||
|
||||
const migratedProjectsLegacy =
|
||||
await migrateExecutorToPlugin<JestPluginOptions>(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nrwl/jest:jest',
|
||||
'@nx/jest/plugin',
|
||||
(targetName) => ({ targetName }),
|
||||
postTargetTransformer,
|
||||
createNodesV2,
|
||||
options.project
|
||||
);
|
||||
|
||||
const migratedProjects =
|
||||
migratedProjectsModern.size + migratedProjectsLegacy.size;
|
||||
|
||||
if (migratedProjects === 0) {
|
||||
throw new Error('Could not find any targets to migrate.');
|
||||
}
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
}
|
||||
|
||||
async function postTargetTransformer(
|
||||
target: TargetConfiguration,
|
||||
tree: Tree,
|
||||
projectDetails: { projectName: string; root: string },
|
||||
inferredTarget: TargetConfiguration
|
||||
): Promise<TargetConfiguration> {
|
||||
const jestConfigPath = jestConfigExtensions
|
||||
.map((ext) => `jest.config.${ext}`)
|
||||
.find((configFileName) =>
|
||||
tree.exists(posix.join(projectDetails.root, configFileName))
|
||||
);
|
||||
|
||||
if (target.options) {
|
||||
await updateOptions(
|
||||
target.options,
|
||||
projectDetails.root,
|
||||
tree.root,
|
||||
jestConfigPath
|
||||
);
|
||||
}
|
||||
|
||||
if (target.configurations) {
|
||||
for (const [configName, config] of Object.entries(target.configurations)) {
|
||||
await updateOptions(
|
||||
config,
|
||||
projectDetails.root,
|
||||
tree.root,
|
||||
jestConfigPath
|
||||
);
|
||||
|
||||
if (!Object.keys(config).length) {
|
||||
delete target.configurations[configName];
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(target.configurations).length) {
|
||||
delete target.defaultConfiguration;
|
||||
delete target.configurations;
|
||||
}
|
||||
|
||||
if (
|
||||
'defaultConfiguration' in target &&
|
||||
!target.configurations?.[target.defaultConfiguration]
|
||||
) {
|
||||
delete target.defaultConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.outputs) {
|
||||
processTargetOutputs(target, [], inferredTarget, {
|
||||
projectName: projectDetails.projectName,
|
||||
projectRoot: projectDetails.root,
|
||||
});
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
export default convertToInferred;
|
||||
|
||||
async function updateOptions(
|
||||
targetOptions: any,
|
||||
projectRoot: string,
|
||||
workspaceRoot: string,
|
||||
defaultJestConfigPath: string | undefined
|
||||
) {
|
||||
const jestConfigPath = targetOptions.jestConfig ?? defaultJestConfigPath;
|
||||
// inferred targets are only identified after known files that Jest would
|
||||
// pick up, so we can safely remove the config options
|
||||
delete targetOptions.jestConfig;
|
||||
delete targetOptions.config;
|
||||
|
||||
// deprecated and unused
|
||||
delete targetOptions.tsConfig;
|
||||
|
||||
if ('codeCoverage' in targetOptions) {
|
||||
targetOptions.coverage = targetOptions.codeCoverage;
|
||||
delete targetOptions.codeCoverage;
|
||||
}
|
||||
|
||||
const testPathPatterns: string[] = [];
|
||||
if ('testFile' in targetOptions) {
|
||||
testPathPatterns.push(
|
||||
toProjectRelativeRegexPath(targetOptions.testFile, projectRoot)
|
||||
);
|
||||
delete targetOptions.testFile;
|
||||
}
|
||||
|
||||
if ('testPathPattern' in targetOptions) {
|
||||
testPathPatterns.push(
|
||||
...targetOptions.testPathPattern.map((pattern: string) =>
|
||||
toProjectRelativeRegexPath(pattern, projectRoot)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (testPathPatterns.length > 1) {
|
||||
targetOptions.testPathPattern = `\"${testPathPatterns.join('|')}\"`;
|
||||
} else if (testPathPatterns.length === 1) {
|
||||
targetOptions.testPathPattern = testPathPatterns[0];
|
||||
}
|
||||
|
||||
if ('testPathIgnorePatterns' in targetOptions) {
|
||||
if (targetOptions.testPathIgnorePatterns.length > 1) {
|
||||
targetOptions.testPathIgnorePatterns = `\"${targetOptions.testPathIgnorePatterns
|
||||
.map((pattern: string) =>
|
||||
toProjectRelativeRegexPath(pattern, projectRoot)
|
||||
)
|
||||
.join('|')}\"`;
|
||||
} else if (targetOptions.testPathIgnorePatterns.length === 1) {
|
||||
targetOptions.testPathIgnorePatterns = toProjectRelativeRegexPath(
|
||||
targetOptions.testPathIgnorePatterns[0],
|
||||
projectRoot
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ('testMatch' in targetOptions) {
|
||||
targetOptions.testMatch = targetOptions.testMatch
|
||||
.map(
|
||||
(pattern: string) =>
|
||||
`"${toProjectRelativeGlobPath(pattern, projectRoot)}"`
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
if ('findRelatedTests' in targetOptions) {
|
||||
// the executor accepts a comma-separated string, while jest accepts a space-separated string
|
||||
const parsedSourceFiles = targetOptions.findRelatedTests
|
||||
.split(',')
|
||||
.map((s: string) => toProjectRelativePath(s.trim(), projectRoot))
|
||||
.join(' ');
|
||||
targetOptions.args = [`--findRelatedTests ${parsedSourceFiles}`];
|
||||
delete targetOptions.findRelatedTests;
|
||||
}
|
||||
|
||||
if ('setupFile' in targetOptions) {
|
||||
const setupFiles = await processSetupFiles(
|
||||
targetOptions.setupFile,
|
||||
targetOptions.setupFilesAfterEnv,
|
||||
projectRoot,
|
||||
workspaceRoot,
|
||||
jestConfigPath
|
||||
);
|
||||
if (setupFiles.length > 1) {
|
||||
targetOptions.setupFilesAfterEnv = setupFiles
|
||||
.map((sf) => `"${sf}"`)
|
||||
.join(' ');
|
||||
} else if (setupFiles.length === 1) {
|
||||
targetOptions.setupFilesAfterEnv = setupFiles[0];
|
||||
} else {
|
||||
// if there are no setup files, it means they are already defined in the
|
||||
// jest config, so we can remove the option
|
||||
delete targetOptions.setupFilesAfterEnv;
|
||||
}
|
||||
delete targetOptions.setupFile;
|
||||
}
|
||||
|
||||
if ('outputFile' in targetOptions) {
|
||||
// update the output file to be relative to the project root
|
||||
targetOptions.outputFile = toProjectRelativePath(
|
||||
targetOptions.outputFile,
|
||||
projectRoot
|
||||
);
|
||||
}
|
||||
if ('coverageDirectory' in targetOptions) {
|
||||
// update the coverage directory to be relative to the project root
|
||||
targetOptions.coverageDirectory = toProjectRelativePath(
|
||||
targetOptions.coverageDirectory,
|
||||
projectRoot
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function processSetupFiles(
|
||||
setupFile: string,
|
||||
setupFilesAfterEnv: string[] | undefined,
|
||||
projectRoot: string,
|
||||
workspaceRoot: string,
|
||||
jestConfigPath: string | undefined
|
||||
): Promise<string[]> {
|
||||
// the jest executor merges the setupFile with the setupFilesAfterEnv, so
|
||||
// to keep the task working as before we resolve the setupFilesAfterEnv
|
||||
// from the options or the jest config and add the setupFile to it
|
||||
// https://github.com/nrwl/nx/blob/bdd3375256613340899f649eb800d22abcc9f507/packages/jest/src/executors/jest/jest.impl.ts#L107-L113
|
||||
const configSetupFilesAfterEnv: string[] = [];
|
||||
if (jestConfigPath) {
|
||||
const jestConfig = await readConfig(
|
||||
<Config.Argv>{ setupFilesAfterEnv },
|
||||
join(workspaceRoot, jestConfigPath)
|
||||
);
|
||||
if (jestConfig.projectConfig.setupFilesAfterEnv) {
|
||||
configSetupFilesAfterEnv.push(
|
||||
...jestConfig.projectConfig.setupFilesAfterEnv.map((file: string) =>
|
||||
toProjectRelativePath(file, projectRoot)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!configSetupFilesAfterEnv.length) {
|
||||
return [toProjectRelativePath(setupFile, projectRoot)];
|
||||
}
|
||||
|
||||
if (
|
||||
isSetupFileInConfig(
|
||||
configSetupFilesAfterEnv,
|
||||
setupFile,
|
||||
projectRoot,
|
||||
workspaceRoot
|
||||
)
|
||||
) {
|
||||
// the setupFile is already included in the setupFilesAfterEnv
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
...configSetupFilesAfterEnv,
|
||||
toProjectRelativePath(setupFile, projectRoot),
|
||||
];
|
||||
}
|
||||
|
||||
function isSetupFileInConfig(
|
||||
setupFilesAfterEnv: string[],
|
||||
setupFile: string,
|
||||
projectRoot: string,
|
||||
workspaceRoot: string
|
||||
): boolean {
|
||||
const normalizePath = (f: string) =>
|
||||
f.startsWith('<rootDir>')
|
||||
? posix.join(workspaceRoot, projectRoot, f.slice('<rootDir>'.length))
|
||||
: posix.join(workspaceRoot, projectRoot, f);
|
||||
|
||||
const normalizedSetupFiles = new Set(setupFilesAfterEnv.map(normalizePath));
|
||||
|
||||
return normalizedSetupFiles.has(
|
||||
normalizePath(toProjectRelativePath(setupFile, projectRoot))
|
||||
);
|
||||
}
|
||||
|
||||
function toProjectRelativeRegexPath(path: string, projectRoot: string): string {
|
||||
if (projectRoot === '.') {
|
||||
// workspace and project root are the same, keep the path as is
|
||||
return path;
|
||||
}
|
||||
|
||||
const normalizedRoot = normalize(projectRoot);
|
||||
if (
|
||||
new RegExp(`^(?:\\.[\\/\\\\])?${normalizedRoot}(?:[\\/\\\\])?$`).test(path)
|
||||
) {
|
||||
// path includes everything inside project root
|
||||
return '.*';
|
||||
}
|
||||
|
||||
const normalizedPath = normalize(path);
|
||||
const startWithProjectRootRegex = new RegExp(
|
||||
`^(?:\\.[\\/\\\\])?${normalizedRoot}[\\/\\\\]`
|
||||
);
|
||||
|
||||
return startWithProjectRootRegex.test(normalizedPath)
|
||||
? normalizedPath.replace(startWithProjectRootRegex, '')
|
||||
: path;
|
||||
}
|
||||
|
||||
function toProjectRelativeGlobPath(path: string, projectRoot: string): string {
|
||||
if (projectRoot === '.') {
|
||||
// workspace and project root are the same, keep the path as is
|
||||
return path;
|
||||
}
|
||||
|
||||
// globs use forward slashes, so we make sure to normalize the path
|
||||
const normalizedRoot = posix.normalize(projectRoot);
|
||||
|
||||
return path
|
||||
.replace(new RegExp(`\/${normalizedRoot}\/`), '/')
|
||||
.replace(/\*\*\/\*\*/g, '**');
|
||||
}
|
||||
19
packages/jest/src/generators/convert-to-inferred/schema.json
Normal file
19
packages/jest/src/generators/convert-to-inferred/schema.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxJestConvertToInferred",
|
||||
"description": "Convert existing Jest project(s) using `@nx/jest:jest` executor to use `@nx/jest/plugin`.",
|
||||
"title": "Convert Jest project from executor to plugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/jest:jest` executor to use `@nx/jest/plugin`. If not provided, all projects using the `@nx/jest:jest` executor will be converted.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user