feat(testing): add migration for moving test target defaults (#19993)

This commit is contained in:
Jason Jean 2023-11-02 17:22:16 -04:00 committed by GitHub
parent 95ae6fd2a3
commit b0d179904d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 952 additions and 6 deletions

View File

@ -29,6 +29,11 @@
"version": "16.5.0-beta.2",
"description": "Add test-setup.ts to ignored files in production input",
"implementation": "./src/migrations/update-16-5-0/add-test-setup-to-inputs-ignore"
},
"move-options-to-target-defaults": {
"version": "17.1.0-beta.2",
"description": "Move jest executor options to nx.json targetDefaults",
"implementation": "./src/migrations/update-17-1-0/move-options-to-target-defaults"
}
},
"packageJsonUpdates": {

View File

@ -2,7 +2,6 @@ import {
addDependenciesToPackageJson,
GeneratorCallback,
getProjects,
joinPathFragments,
readNxJson,
removeDependenciesFromPackageJson,
runTasksInSerial,
@ -27,7 +26,6 @@ import {
typesNodeVersion,
} from '../../utils/versions';
import { JestInitSchema } from './schema';
import { JestExecutorOptions } from '../../executors/jest/schema';
interface NormalizedSchema extends ReturnType<typeof normalizeOptions> {}

View File

@ -0,0 +1,562 @@
import { createTree } from '@nx/devkit/testing';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
import {
addProjectConfiguration as _addProjectConfiguration,
ProjectGraph,
readNxJson,
readProjectConfiguration,
Tree,
updateNxJson,
writeJson,
} from '@nx/devkit';
function addProjectConfiguration(tree, name, project) {
_addProjectConfiguration(tree, name, project);
projectGraph.nodes[name] = {
name: name,
type: 'lib',
data: {
root: project.root,
targets: project.targets,
},
};
}
import update from './move-options-to-target-defaults';
describe('move-options-to-target-defaults migration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTree();
writeJson(tree, 'nx.json', {
namedInputs: {
production: ['default'],
},
targetDefaults: {},
});
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
it('should add config to nx.json and remove it from projects', async () => {
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
passWithNoTests: true,
},
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
},
},
});
addProjectConfiguration(tree, 'proj2', {
root: 'proj2',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
passWithNoTests: true,
},
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
},
},
});
await update(tree);
expect(readProjectConfiguration(tree, 'proj1').targets.test).toEqual({
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
},
});
expect(readProjectConfiguration(tree, 'proj2').targets.test).toEqual({
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
},
});
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/jest:jest': {
cache: true,
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
inputs: ['default', '^production'],
options: {
passWithNoTests: true,
},
},
});
});
it('should use test target defaults if all jest targets are test', async () => {
const nxJson = readNxJson(tree);
nxJson.targetDefaults['test'] = {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: false,
},
};
updateNxJson(tree, nxJson);
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
passWithNoTests: true,
},
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
},
},
});
await update(tree);
expect(readProjectConfiguration(tree, 'proj1').targets.test).toEqual({
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
},
});
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/jest:jest': {
cache: false,
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
passWithNoTests: true,
watch: false,
},
},
});
});
it('should not remove config which does not match', async () => {
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/jest:jest',
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
jestConfig: 'jest.config.js',
passWithNoTests: true,
watch: false,
},
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
},
},
});
await update(tree);
expect(readProjectConfiguration(tree, 'proj1').targets.test).toEqual({
executor: '@nx/jest:jest',
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
jestConfig: 'jest.config.js',
watch: false,
},
});
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/jest:jest': {
cache: true,
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
inputs: ['default', '^production'],
options: {
passWithNoTests: true,
},
},
});
});
it('should not remove defaults if target uses other executors', async () => {
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
passWithNoTests: true,
},
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
},
},
});
addProjectConfiguration(tree, 'proj2', {
root: 'proj2',
targets: {
test: {
executor: '@nx/vite:vitest',
options: {},
},
},
});
await update(tree);
expect(readProjectConfiguration(tree, 'proj1').targets.test).toEqual({
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
},
});
expect(readProjectConfiguration(tree, 'proj2').targets.test).toEqual({
executor: '@nx/vite:vitest',
options: {},
});
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/jest:jest': {
cache: true,
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
inputs: ['default', '^production'],
options: {
passWithNoTests: true,
},
},
});
});
it('should handle when jest and vite are used for test and jest and cypress are used for e2e', async () => {
const nxJson = readNxJson(tree);
nxJson.targetDefaults['test'] = {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: true,
},
};
nxJson.targetDefaults['e2e'] = {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: false,
},
};
updateNxJson(tree, nxJson);
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
passWithNoTests: true,
},
},
e2e: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
passWithNoTests: true,
},
},
},
});
addProjectConfiguration(tree, 'proj2', {
root: 'proj2',
targets: {
test: {
executor: '@nx/vite:vitest',
options: {},
},
e2e: {
executor: '@nx/cypress:cypress',
options: {},
},
},
});
await update(tree);
expect(readProjectConfiguration(tree, 'proj1').targets).toEqual({
e2e: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
});
expect(readProjectConfiguration(tree, 'proj2').targets).toEqual({
e2e: {
executor: '@nx/cypress:cypress',
options: {},
},
test: {
executor: '@nx/vite:vitest',
options: {},
},
});
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/jest:jest': {
cache: true,
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
inputs: ['default', '^production'],
options: {
passWithNoTests: true,
},
},
e2e: {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: false,
},
},
test: {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: true,
},
},
});
});
it('should not assign things that had a default already', async () => {
const nxJson = readNxJson(tree);
nxJson.targetDefaults['test'] = {
cache: true,
inputs: ['default', '^production'],
options: {
passWithNoTests: true,
},
};
updateNxJson(tree, nxJson);
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
},
});
await update(tree);
expect(readProjectConfiguration(tree, 'proj1').targets).toEqual({
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
});
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/jest:jest': {
cache: true,
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
inputs: ['default', '^production'],
options: {
passWithNoTests: true,
},
},
});
});
it('should remove target defaults which are not used anymore', async () => {
const nxJson = readNxJson(tree);
nxJson.targetDefaults['@nx/vite:test'] = {
cache: false,
inputs: ['default', '^production'],
};
nxJson.targetDefaults['test'] = {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: true,
},
};
nxJson.targetDefaults['e2e'] = {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: false,
},
};
updateNxJson(tree, nxJson);
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
passWithNoTests: true,
},
},
e2e: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
passWithNoTests: true,
},
},
},
});
addProjectConfiguration(tree, 'proj2', {
root: 'proj2',
targets: {
test: {
executor: '@nx/vite:test',
options: {},
},
e2e: {
executor: '@nx/cypress:cypress',
options: {},
},
},
});
await update(tree);
expect(readProjectConfiguration(tree, 'proj1').targets).toEqual({
e2e: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
});
expect(readProjectConfiguration(tree, 'proj2').targets).toEqual({
e2e: {
executor: '@nx/cypress:cypress',
options: {},
},
test: {
executor: '@nx/vite:test',
options: {},
},
});
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/jest:jest': {
cache: true,
configurations: {
ci: {
ci: true,
codeCoverage: true,
},
},
inputs: ['default', '^production'],
options: {
passWithNoTests: true,
},
},
'@nx/vite:test': {
cache: false,
inputs: ['default', '^production'],
},
e2e: {
cache: false,
inputs: ['default', '^production', '{workspaceRoot}/other-file.txt'],
options: {
watch: false,
},
},
});
});
});

View File

@ -0,0 +1,185 @@
import {
createProjectGraphAsync,
formatFiles,
getProjects,
ProjectConfiguration,
ProjectGraphProjectNode,
readNxJson,
TargetConfiguration,
TargetDefaults,
Tree,
updateNxJson,
updateProjectConfiguration,
} from '@nx/devkit';
import { JestExecutorOptions } from '../../executors/jest/schema';
import {
forEachExecutorOptions,
forEachExecutorOptionsInGraph,
} from '@nx/devkit/src/generators/executor-options-utils';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
export default async function update(tree: Tree) {
const nxJson = readNxJson(tree);
// Don't override anything if there are already target defaults for jest
if (nxJson.targetDefaults?.['@nx/jest:jest']) {
return;
}
nxJson.targetDefaults ??= {};
/**
* A set of targets which does not use any other executors
*/
const jestTargets = new Set<string>();
const graph = await createProjectGraphAsync();
forEachExecutorOptionsInGraph(
graph,
'@nx/jest:jest',
(value, proj, targetName) => {
jestTargets.add(targetName);
}
);
// Workspace does not use jest?
if (jestTargets.size === 0) {
return;
}
// Use the project graph so targets which are inferred are considered
const projects = graph.nodes;
const projectMap = getProjects(tree);
const jestDefaults: TargetConfiguration<Partial<JestExecutorOptions>> =
(nxJson.targetDefaults['@nx/jest:jest'] = {});
// All jest targets have the same name
if (jestTargets.size === 1) {
const targetName = Array.from(jestTargets)[0];
if (nxJson.targetDefaults[targetName]) {
Object.assign(jestDefaults, nxJson.targetDefaults[targetName]);
}
}
jestDefaults.cache ??= true;
const inputs = ['default'];
inputs.push(nxJson.namedInputs?.production ? '^production' : '^default');
if (tree.exists('jest.preset.js')) {
inputs.push('{workspaceRoot}/jest.preset.js');
}
jestDefaults.inputs ??= inputs;
// Remember if there were already defaults so we don't assume the executor default
const passWithNoTestsPreviouslyInDefaults =
jestDefaults.options?.passWithNoTests !== undefined;
const ciCiPreviouslyInDefaults =
jestDefaults.configurations?.ci?.ci !== undefined;
const ciCodeCoveragePreviouslyInDefaults =
jestDefaults.configurations?.ci?.codeCoverage !== undefined;
jestDefaults.options ??= {};
jestDefaults.options.passWithNoTests ??= true;
jestDefaults.configurations ??= {};
jestDefaults.configurations.ci ??= {};
jestDefaults.configurations.ci.ci ??= true;
jestDefaults.configurations.ci.codeCoverage ??= true;
// Cleanup old target defaults
for (const [targetDefaultKey, targetDefault] of Object.entries(
nxJson.targetDefaults
)) {
if (
!isTargetDefaultUsed(
targetDefault,
nxJson.targetDefaults,
projects,
projectMap
)
) {
delete nxJson.targetDefaults[targetDefaultKey];
}
}
updateNxJson(tree, nxJson);
forEachExecutorOptions<JestExecutorOptions>(
tree,
'@nx/jest:jest',
(value, proj, targetName, configuration) => {
const projConfig = projectMap.get(proj);
if (!configuration) {
// Options
if (value.passWithNoTests === jestDefaults.options.passWithNoTests) {
delete projConfig.targets[targetName].options.passWithNoTests;
} else if (!passWithNoTestsPreviouslyInDefaults) {
projConfig.targets[targetName].options.passWithNoTests ??= false;
}
if (Object.keys(projConfig.targets[targetName].options).length === 0) {
delete projConfig.targets[targetName].options;
}
} else if (configuration === 'ci') {
// CI Config
if (value.ci === jestDefaults.configurations.ci.ci) {
delete projConfig.targets[targetName].configurations.ci.ci;
} else if (ciCiPreviouslyInDefaults) {
projConfig.targets[targetName].configurations.ci.ci ??= false;
}
if (
value.codeCoverage === jestDefaults.configurations.ci.codeCoverage
) {
delete projConfig.targets[targetName].configurations.ci.codeCoverage;
} else if (ciCodeCoveragePreviouslyInDefaults) {
projConfig.targets[targetName].configurations.ci.codeCoverage ??=
false;
}
if (
Object.keys(projConfig.targets[targetName].configurations.ci)
.length === 0
) {
delete projConfig.targets[targetName].configurations.ci;
}
if (
Object.keys(projConfig.targets[targetName].configurations).length ===
0
) {
delete projConfig.targets[targetName].configurations;
}
}
updateProjectConfiguration(tree, proj, projConfig);
}
);
await formatFiles(tree);
}
/**
* Checks every target on every project to see if one of them uses the target default
*/
function isTargetDefaultUsed(
targetDefault: Partial<TargetConfiguration>,
targetDefaults: TargetDefaults,
projects: Record<string, ProjectGraphProjectNode>,
projectMap: Map<string, ProjectConfiguration>
) {
for (const p of Object.values(projects)) {
for (const targetName in p.data?.targets ?? {}) {
if (
readTargetDefaultsForTarget(
targetName,
targetDefaults,
// It might seem like we should use the graph here too but we don't want to pass an executor which was processed in the graph
projectMap.get(p.name).targets?.[targetName]?.executor
) === targetDefault
) {
return true;
}
}
}
return false;
}

View File

@ -35,6 +35,11 @@
"description": "Change vite-tsconfig-paths plugin for first party nx-vite-tsconfig-paths plugin",
"cli": "nx",
"implementation": "./src/migrations/update-16-6-0-change-ts-paths-plugin/change-ts-paths-plugin"
},
"move-target-defaults": {
"version": "17.1.0-beta.2",
"description": "Move target defaults",
"implementation": "./src/migrations/update-17-1-0/move-target-defaults"
}
},
"packageJsonUpdates": {

View File

@ -81,7 +81,7 @@ describe('@nx/vite:init', () => {
const productionNamedInputs = readJson(tree, 'nx.json').namedInputs
.production;
const vitestDefaults = readJson(tree, 'nx.json').targetDefaults[
'@nx/vite:vitest'
'@nx/vite:test'
];
expect(productionNamedInputs).toContain(

View File

@ -93,9 +93,9 @@ export function createVitestConfig(tree: Tree) {
}
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/vite:vitest'] ??= {};
nxJson.targetDefaults['@nx/vite:vitest'].cache ??= true;
nxJson.targetDefaults['@nx/vite:vitest'].inputs ??= [
nxJson.targetDefaults['@nx/vite:test'] ??= {};
nxJson.targetDefaults['@nx/vite:test'].cache ??= true;
nxJson.targetDefaults['@nx/vite:test'].inputs ??= [
'default',
productionFileSet ? '^production' : '^default',
];

View File

@ -0,0 +1,79 @@
import { createTree } from '@nx/devkit/testing';
import {
addProjectConfiguration as _addProjectConfiguration,
ProjectGraph,
readNxJson,
Tree,
writeJson,
} from '@nx/devkit';
import update from './move-target-defaults';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
function addProjectConfiguration(tree, name, project) {
_addProjectConfiguration(tree, name, project);
projectGraph.nodes[name] = {
name: name,
type: 'lib',
data: {
root: project.root,
targets: project.targets,
},
};
}
describe('move-target-defaults migration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTree();
writeJson(tree, 'nx.json', {
namedInputs: {
production: ['default'],
},
targetDefaults: {
test: {
cache: true,
inputs: ['default', '^production'],
},
},
});
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
it('should add options to nx.json target defaults and remove them from projects', async () => {
addProjectConfiguration(tree, 'proj1', {
root: 'proj1',
targets: {
test: {
executor: '@nx/vite:test',
options: {
passWithNoTests: true,
reportsDirectory: '../../reports',
},
},
},
});
await update(tree);
expect(readNxJson(tree).targetDefaults).toEqual({
'@nx/vite:test': {
cache: true,
inputs: ['default', '^production'],
},
});
});
});

View File

@ -0,0 +1,112 @@
import {
createProjectGraphAsync,
formatFiles,
getProjects,
ProjectConfiguration,
ProjectGraphProjectNode,
readNxJson,
TargetConfiguration,
TargetDefaults,
Tree,
updateNxJson,
} from '@nx/devkit';
import { forEachExecutorOptionsInGraph } from '@nx/devkit/src/generators/executor-options-utils';
import { VitestExecutorOptions } from '../../executors/test/schema';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
export default async function update(tree: Tree) {
const nxJson = readNxJson(tree);
// Don't override anything if there are already target defaults for vitest
if (nxJson.targetDefaults?.['@nx/vite:test']) {
return;
}
nxJson.targetDefaults ??= {};
/**
* A set of targets which does not use any other executors
*/
const vitestTargets = new Set<string>();
const graph = await createProjectGraphAsync();
const projectMap = getProjects(tree);
forEachExecutorOptionsInGraph(
graph,
'@nx/vite:test',
(value, proj, targetName) => {
vitestTargets.add(targetName);
}
);
// Workspace does not use vitest
if (vitestTargets.size === 0) {
return;
}
// Use the project graph nodes so that targets which are inferred are considered
const projects = graph.nodes;
const vitestDefaults: TargetConfiguration<Partial<VitestExecutorOptions>> =
(nxJson.targetDefaults['@nx/vite:test'] = {});
// All vitest targets have the same name
if (vitestTargets.size === 1) {
const targetName = Array.from(vitestTargets)[0];
if (nxJson.targetDefaults[targetName]) {
Object.assign(vitestDefaults, nxJson.targetDefaults[targetName]);
}
}
vitestDefaults.cache ??= true;
const inputs = ['default'];
inputs.push(nxJson.namedInputs?.production ? '^production' : '^default');
vitestDefaults.inputs ??= inputs;
// Cleanup old target defaults
for (const [targetDefaultKey, targetDefault] of Object.entries(
nxJson.targetDefaults
)) {
if (
!isTargetDefaultUsed(
targetDefault,
nxJson.targetDefaults,
projects,
projectMap
)
) {
delete nxJson.targetDefaults[targetDefaultKey];
}
}
updateNxJson(tree, nxJson);
await formatFiles(tree);
}
/**
* Checks every target on every project to see if one of them uses the target default
*/
function isTargetDefaultUsed(
targetDefault: Partial<TargetConfiguration>,
targetDefaults: TargetDefaults,
projects: Record<string, ProjectGraphProjectNode>,
projectMap: Map<string, ProjectConfiguration>
) {
for (const p of Object.values(projects)) {
for (const targetName in p.data?.targets ?? {}) {
if (
readTargetDefaultsForTarget(
targetName,
targetDefaults,
// It might seem like we should use the graph here too but we don't want to pass an executor which was processed in the graph
projectMap.get(p.name).targets?.[targetName]?.executor
) === targetDefault
) {
return true;
}
}
}
return false;
}