Leosvel Pérez Espinosa c936f864b8
fix(misc): register plugins correctly in migration generators (#26670)
<!-- 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 NXP-816 -->

Fixes #
2024-06-25 08:33:24 -04:00

371 lines
10 KiB
TypeScript

import type { Config } from '@jest/types';
import {
createProjectGraphAsync,
formatFiles,
type TargetConfiguration,
type Tree,
} from '@nx/devkit';
import { migrateProjectExecutorsToPlugin } 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 migratedProjects =
await migrateProjectExecutorsToPlugin<JestPluginOptions>(
tree,
projectGraph,
'@nx/jest/plugin',
createNodesV2,
{ targetName: 'test' },
[
{
executors: ['@nx/jest:jest', '@nrwl/jest:jest'],
postTargetTransformer,
targetPluginOptionMapper: (targetName) => ({ targetName }),
},
],
options.project
);
if (migratedProjects.size === 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 updateOptionsObject(
target.options,
projectDetails.root,
tree.root,
jestConfigPath
);
}
if (target.configurations) {
for (const [configName, config] of Object.entries(target.configurations)) {
await updateConfigurationObject(
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 updateOptionsObject(
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;
await updateOptions(
targetOptions,
projectRoot,
workspaceRoot,
jestConfigPath
);
}
async function updateConfigurationObject(
targetOptions: any,
projectRoot: string,
workspaceRoot: string,
defaultJestConfigPath: string | undefined
) {
const jestConfigPath = targetOptions.jestConfig ?? defaultJestConfigPath;
if (targetOptions.jestConfig) {
targetOptions.config = toProjectRelativePath(
targetOptions.jestConfig,
projectRoot
);
delete targetOptions.jestConfig;
} else if (targetOptions.config) {
targetOptions.config = toProjectRelativePath(
targetOptions.config,
projectRoot
);
}
await updateOptions(
targetOptions,
projectRoot,
workspaceRoot,
jestConfigPath
);
}
async function updateOptions(
targetOptions: any,
projectRoot: string,
workspaceRoot: string,
jestConfigPath: string | undefined
) {
// 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, '**');
}