From b0d179904d4ee5ccfc65314b136b13fbb4a46ccf Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Thu, 2 Nov 2023 17:22:16 -0400 Subject: [PATCH] feat(testing): add migration for moving test target defaults (#19993) --- packages/jest/migrations.json | 5 + packages/jest/src/generators/init/init.ts | 2 - .../move-options-to-target-defaults.spec.ts | 562 ++++++++++++++++++ .../move-options-to-target-defaults.ts | 185 ++++++ packages/vite/migrations.json | 5 + .../vite/src/generators/init/init.spec.ts | 2 +- packages/vite/src/generators/init/init.ts | 6 +- .../move-target-defaults.spec.ts | 79 +++ .../update-17-1-0/move-target-defaults.ts | 112 ++++ 9 files changed, 952 insertions(+), 6 deletions(-) create mode 100644 packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.spec.ts create mode 100644 packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.ts create mode 100644 packages/vite/src/migrations/update-17-1-0/move-target-defaults.spec.ts create mode 100644 packages/vite/src/migrations/update-17-1-0/move-target-defaults.ts diff --git a/packages/jest/migrations.json b/packages/jest/migrations.json index d3420bb69a..19cbbdaebc 100644 --- a/packages/jest/migrations.json +++ b/packages/jest/migrations.json @@ -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": { diff --git a/packages/jest/src/generators/init/init.ts b/packages/jest/src/generators/init/init.ts index 64316f6e92..1757985cfb 100644 --- a/packages/jest/src/generators/init/init.ts +++ b/packages/jest/src/generators/init/init.ts @@ -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 {} diff --git a/packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.spec.ts b/packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.spec.ts new file mode 100644 index 0000000000..38e8e15c39 --- /dev/null +++ b/packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.spec.ts @@ -0,0 +1,562 @@ +import { createTree } from '@nx/devkit/testing'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@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, + }, + }, + }); + }); +}); diff --git a/packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.ts b/packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.ts new file mode 100644 index 0000000000..407d8f7b8a --- /dev/null +++ b/packages/jest/src/migrations/update-17-1-0/move-options-to-target-defaults.ts @@ -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(); + + 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> = + (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( + 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, + targetDefaults: TargetDefaults, + projects: Record, + projectMap: Map +) { + 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; +} diff --git a/packages/vite/migrations.json b/packages/vite/migrations.json index dcad886b1b..1f3613f09a 100644 --- a/packages/vite/migrations.json +++ b/packages/vite/migrations.json @@ -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": { diff --git a/packages/vite/src/generators/init/init.spec.ts b/packages/vite/src/generators/init/init.spec.ts index 0d9ea3359f..26fdc56821 100644 --- a/packages/vite/src/generators/init/init.spec.ts +++ b/packages/vite/src/generators/init/init.spec.ts @@ -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( diff --git a/packages/vite/src/generators/init/init.ts b/packages/vite/src/generators/init/init.ts index 999ce5bd62..30b26501aa 100644 --- a/packages/vite/src/generators/init/init.ts +++ b/packages/vite/src/generators/init/init.ts @@ -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', ]; diff --git a/packages/vite/src/migrations/update-17-1-0/move-target-defaults.spec.ts b/packages/vite/src/migrations/update-17-1-0/move-target-defaults.spec.ts new file mode 100644 index 0000000000..f481fb5c04 --- /dev/null +++ b/packages/vite/src/migrations/update-17-1-0/move-target-defaults.spec.ts @@ -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('@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'], + }, + }); + }); +}); diff --git a/packages/vite/src/migrations/update-17-1-0/move-target-defaults.ts b/packages/vite/src/migrations/update-17-1-0/move-target-defaults.ts new file mode 100644 index 0000000000..6c750f674e --- /dev/null +++ b/packages/vite/src/migrations/update-17-1-0/move-target-defaults.ts @@ -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(); + 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> = + (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, + targetDefaults: TargetDefaults, + projects: Record, + projectMap: Map +) { + 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; +}