diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index e936d428ab4f7..6ab04c4b5c7e1 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -569,7 +569,8 @@ describe('app', () => { it('should add eslint plugin and no lint target to e2e project', async () => { await generateApp(appTree, 'my-app', { linter: Linter.EsLint }); - expect(readNxJson(appTree).plugins).toMatchInlineSnapshot(` + const nxJson = readNxJson(appTree); + expect(nxJson.plugins).toMatchInlineSnapshot(` [ { "options": { @@ -588,6 +589,13 @@ describe('app', () => { }, ] `); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); expect( readProjectConfiguration(appTree, 'my-app-e2e').targets.lint ).toBeUndefined(); diff --git a/packages/angular/src/generators/application/lib/add-e2e.ts b/packages/angular/src/generators/application/lib/add-e2e.ts index 08fa9b2863df0..7c3de1a055e27 100644 --- a/packages/angular/src/generators/application/lib/add-e2e.ts +++ b/packages/angular/src/generators/application/lib/add-e2e.ts @@ -12,6 +12,7 @@ import { import { nxVersion } from '../../../utils/versions'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; import type { NormalizedSchema } from './normalized-schema'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e(tree: Tree, options: NormalizedSchema) { // since e2e are separate projects, default to adding plugins @@ -45,6 +46,14 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) { rootProject: options.rootProject, addPlugin, }); + if (addPlugin) { + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + joinPathFragments(options.e2eProjectRoot, 'cypress.config.ts') + ); + } } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -71,6 +80,14 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) { rootProject: options.rootProject, addPlugin, }); + if (addPlugin) { + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + '^build', + joinPathFragments(options.e2eProjectRoot, 'playwright.config.ts') + ); + } } } diff --git a/packages/angular/src/generators/application/lib/create-project.ts b/packages/angular/src/generators/application/lib/create-project.ts index 5451263d0a9cc..b1597ad2e4831 100644 --- a/packages/angular/src/generators/application/lib/create-project.ts +++ b/packages/angular/src/generators/application/lib/create-project.ts @@ -2,7 +2,7 @@ import { addProjectConfiguration, joinPathFragments, Tree } from '@nx/devkit'; import type { AngularProjectConfiguration } from '../../../utils/types'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; import type { NormalizedSchema } from './normalized-schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function createProject(tree: Tree, options: NormalizedSchema) { const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); diff --git a/packages/angular/src/generators/library/lib/add-project.ts b/packages/angular/src/generators/library/lib/add-project.ts index 9ac3e015a9037..466ed991d8b74 100644 --- a/packages/angular/src/generators/library/lib/add-project.ts +++ b/packages/angular/src/generators/library/lib/add-project.ts @@ -2,7 +2,7 @@ import type { Tree } from '@nx/devkit'; import { addProjectConfiguration, joinPathFragments } from '@nx/devkit'; import type { AngularProjectConfiguration } from '../../../utils/types'; import type { NormalizedSchema } from './normalized-schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function addProject( tree: Tree, diff --git a/packages/cypress/migrations.json b/packages/cypress/migrations.json index b1df577eb07dc..2a7993064badd 100644 --- a/packages/cypress/migrations.json +++ b/packages/cypress/migrations.json @@ -35,6 +35,12 @@ "version": "19.6.0-beta.0", "description": "Update ciWebServerCommand to use previewTargetName if Vite is detected for the application.", "implementation": "./src/migrations/update-19-6-0/update-ci-webserver-for-vite" + }, + "update-19-6-0-add-e2e-ci-target-defaults": { + "cli": "nx", + "version": "19.6.0-beta.0", + "description": "Add inferred ciTargetNames to targetDefaults with dependsOn to ensure dependent application builds are scheduled before atomized tasks.", + "implementation": "./src/migrations/update-19-6-0/add-e2e-ci-target-defaults" } }, "packageJsonUpdates": { diff --git a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts new file mode 100644 index 0000000000000..e82fea4f7de3e --- /dev/null +++ b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts @@ -0,0 +1,366 @@ +import { ProjectGraph, readNxJson, type Tree, updateNxJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('add-e2e-ci-target-defaults', () => { + let tree: Tree; + let tempFs: TempFs; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tempFs = new TempFs('add-e2e-ci'); + tree.root = tempFs.tempDir; + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + tempFs.reset(); + }); + + it('should do nothing when the plugin is not registered', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = []; + updateNxJson(tree, nxJson); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetName and buildTarget when there is one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetNames and buildTargets when there is more than one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['app-e2e/**'], + }, + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + include: ['shop-e2e/**'], + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'build', + ciTargetName: 'cypress:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "cypress:e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should only add the targetDefaults with the correct ciTargetName and buildTargets when there is more than one plugin with only one matching multiple projects', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['cart-e2e/**'], + }, + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'bundle', + ciTargetName: 'cypress:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "cypress:e2e-ci--**/*": { + "dependsOn": [ + "^build", + "^bundle", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should not add the targetDefaults when the ciWebServerCommand is not present', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs, { + appName: 'app', + buildTargetName: 'build', + ciTargetName: 'cypress:e2e-ci', + noCi: true, + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); +}); + +function addProject( + tree: Tree, + tempFs: TempFs, + overrides: { + ciTargetName: string; + buildTargetName: string; + appName: string; + noCi?: boolean; + } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' } +) { + const appProjectConfig = { + name: overrides.appName, + root: overrides.appName, + sourceRoot: `${overrides.appName}/src`, + projectType: 'application', + }; + const viteConfig = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/${overrides.appName}', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/${overrides.appName}', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});`; + + const e2eProjectConfig = { + name: `${overrides.appName}-e2e`, + root: `${overrides.appName}-e2e`, + sourceRoot: `${overrides.appName}-e2e/src`, + projectType: 'application', + }; + + const cypressConfig = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { + cypressDir: 'src', + bundler: 'vite', + webServerCommands: { + default: 'nx run ${overrides.appName}:serve', + production: 'nx run ${overrides.appName}:preview', + }, + ${ + !overrides.noCi + ? `ciWebServerCommand: 'nx run ${overrides.appName}:serve-static',` + : '' + } + }), + baseUrl: 'http://localhost:4200', + }, +}); +`; + + tree.write(`${overrides.appName}/vite.config.ts`, viteConfig); + tree.write( + `${overrides.appName}/project.json`, + JSON.stringify(appProjectConfig) + ); + tree.write(`${overrides.appName}-e2e/cypress.config.ts`, cypressConfig); + tree.write( + `${overrides.appName}-e2e/project.json`, + JSON.stringify(e2eProjectConfig) + ); + tempFs.createFilesSync({ + [`${overrides.appName}/vite.config.ts`]: viteConfig, + [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig), + [`${overrides.appName}-e2e/cypress.config.ts`]: cypressConfig, + [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig), + }); + + projectGraph.nodes[overrides.appName] = { + name: overrides.appName, + type: 'app', + data: { + projectType: 'application', + root: overrides.appName, + targets: { + [overrides.buildTargetName]: {}, + 'serve-static': { + options: { + buildTarget: overrides.buildTargetName, + }, + }, + }, + }, + }; + + projectGraph.nodes[`${overrides.appName}-e2e`] = { + name: `${overrides.appName}-e2e`, + type: 'app', + data: { + projectType: 'application', + root: `${overrides.appName}-e2e`, + targets: { + e2e: {}, + [overrides.ciTargetName]: {}, + }, + }, + }; +} diff --git a/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts new file mode 100644 index 0000000000000..3749d5c43434d --- /dev/null +++ b/packages/cypress/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -0,0 +1,102 @@ +import { + type Tree, + type CreateNodesV2, + formatFiles, + readNxJson, + createProjectGraphAsync, + parseTargetString, +} from '@nx/devkit'; +import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; +import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { + ProjectConfigurationsError, + retrieveProjectConfigurations, +} from 'nx/src/devkit-internals'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { type CypressPluginOptions } from '../../plugins/plugin'; + +export default async function addE2eCiTargetDefaults(tree: Tree) { + const pluginName = '@nx/cypress/plugin'; + const graph = await createProjectGraphAsync(); + const nxJson = readNxJson(tree); + const matchingPluginRegistrations = nxJson.plugins?.filter((p) => + typeof p === 'string' ? p === pluginName : p.plugin === pluginName + ); + + if (!matchingPluginRegistrations) { + return; + } + + const { + createNodesV2, + }: { createNodesV2: CreateNodesV2 } = await import( + pluginName + ); + + for (const plugin of matchingPluginRegistrations) { + let projectConfigs: ConfigurationResult; + try { + const loadedPlugin = new LoadedNxPlugin( + { createNodesV2, name: pluginName }, + plugin + ); + projectConfigs = await retrieveProjectConfigurations( + [loadedPlugin], + tree.root, + nxJson + ); + } catch (e) { + if (e instanceof ProjectConfigurationsError) { + projectConfigs = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } + + for (const configFile of projectConfigs.matchingProjectFiles) { + const configFileContents = tree.read(configFile, 'utf-8'); + if (!configFileContents.includes('ciWebServerCommand')) { + continue; + } + + const ast = tsquery.ast(configFileContents); + const CI_WEBSERVER_COMMAND_SELECTOR = + 'ObjectLiteralExpression PropertyAssignment:has(Identifier[name=ciWebServerCommand]) > StringLiteral'; + const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, { + visitAllChildren: true, + }); + if (!nodes.length) { + continue; + } + const ciWebServerCommand = nodes[0].getText(); + const NX_TARGET_REGEX = "(?<=nx run )[^']+"; + const matches = ciWebServerCommand.match(NX_TARGET_REGEX); + if (!matches) { + continue; + } + const targetString = matches[0]; + const { project, target, configuration } = parseTargetString( + targetString, + graph + ); + + const serveStaticTarget = graph.nodes[project].data.targets[target]; + let resolvedBuildTarget: string; + if (serveStaticTarget.dependsOn) { + resolvedBuildTarget = serveStaticTarget.dependsOn.join(','); + } else { + resolvedBuildTarget = + (configuration + ? serveStaticTarget.configurations[configuration].buildTarget + : serveStaticTarget.options.buildTarget) ?? 'build'; + } + + const buildTarget = `^${resolvedBuildTarget}`; + + await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile); + } + } + + await formatFiles(tree); +} diff --git a/packages/devkit/src/generators/add-build-target-defaults.ts b/packages/devkit/src/generators/add-build-target-defaults.ts deleted file mode 100644 index 6158f6e3b8cd1..0000000000000 --- a/packages/devkit/src/generators/add-build-target-defaults.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { readNxJson, Tree, updateNxJson } from 'nx/src/devkit-exports'; - -export function addBuildTargetDefaults( - tree: Tree, - executorName: string, - buildTargetName = 'build' -): void { - const nxJson = readNxJson(tree); - nxJson.targetDefaults ??= {}; - nxJson.targetDefaults[executorName] ??= { - cache: true, - dependsOn: [`^${buildTargetName}`], - inputs: - nxJson.namedInputs && 'production' in nxJson.namedInputs - ? ['production', '^production'] - : ['default', '^default'], - }; - updateNxJson(tree, nxJson); -} diff --git a/packages/devkit/src/generators/target-defaults-utils.spec.ts b/packages/devkit/src/generators/target-defaults-utils.spec.ts new file mode 100644 index 0000000000000..45769cc4645d7 --- /dev/null +++ b/packages/devkit/src/generators/target-defaults-utils.spec.ts @@ -0,0 +1,377 @@ +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { readNxJson, updateNxJson, type Tree } from 'nx/src/devkit-exports'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { addE2eCiTargetDefaults } from './target-defaults-utils'; +describe('target-defaults-utils', () => { + describe('addE2eCiTargetDefaults', () => { + let tree: Tree; + let tempFs: TempFs; + beforeEach(() => { + tempFs = new TempFs('target-defaults-utils'); + tree = createTreeWithEmptyWorkspace(); + tree.root = tempFs.tempDir; + }); + + afterEach(() => { + tempFs.cleanup(); + jest.resetModules(); + }); + + it('should add e2e-ci--**/* target default for e2e plugin for specified build target when it does not exist', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + + it('should update existing e2e-ci--**/* target default for e2e plugin for specified build target when it does not exist in dependsOn', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['e2e-ci--**/*'] = { + dependsOn: ['^build'], + }; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build-base', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + "^build-base", + ], + } + `); + }); + + it('should read the ciTargetName and add a new entry when it does not exist', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + }); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['e2e-ci--**/*'] = { + dependsOn: ['^build'], + }; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build-base', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build-base", + ], + } + `); + }); + + it('should not add additional e2e-ci--**/* target default for e2e plugin when it already exists with build target', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['e2e-ci--**/*'] = { + dependsOn: ['^build'], + }; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should do nothing when there are no nxJson.plugins does not exist', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = undefined; + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should do nothing when there are nxJson.plugins but e2e plugin is not registered', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should choose the correct plugin when there are includes', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['libs/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + include: ['apps/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + + it('should choose the correct plugin when there are excludes', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + exclude: ['apps/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + exclude: ['libs/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + + it('should use the default name when the plugin registration is a string', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push('@nx/cypress/plugin'); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + '^build', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + const newNxJson = readNxJson(tree); + expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + }); +}); diff --git a/packages/devkit/src/generators/target-defaults-utils.ts b/packages/devkit/src/generators/target-defaults-utils.ts new file mode 100644 index 0000000000000..d2fd347ccafdd --- /dev/null +++ b/packages/devkit/src/generators/target-defaults-utils.ts @@ -0,0 +1,98 @@ +import { + type CreateNodes, + type CreateNodesV2, + type PluginConfiguration, + type Tree, + readNxJson, + updateNxJson, +} from 'nx/src/devkit-exports'; +import { findMatchingConfigFiles } from 'nx/src/devkit-internals'; + +export function addBuildTargetDefaults( + tree: Tree, + executorName: string, + buildTargetName = 'build' +): void { + const nxJson = readNxJson(tree); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults[executorName] ??= { + cache: true, + dependsOn: [`^${buildTargetName}`], + inputs: + nxJson.namedInputs && 'production' in nxJson.namedInputs + ? ['production', '^production'] + : ['default', '^default'], + }; + updateNxJson(tree, nxJson); +} + +export async function addE2eCiTargetDefaults( + tree: Tree, + e2ePlugin: string, + buildTarget: string, + pathToE2EConfigFile: string +): Promise { + const nxJson = readNxJson(tree); + if (!nxJson.plugins) { + return; + } + + const e2ePluginRegistrations = nxJson.plugins.filter((p) => + typeof p === 'string' ? p === e2ePlugin : p.plugin === e2ePlugin + ); + if (!e2ePluginRegistrations.length) { + return; + } + + const resolvedE2ePlugin: { + createNodes?: CreateNodes; + createNodesV2?: CreateNodesV2; + } = await import(e2ePlugin); + const e2ePluginGlob = + resolvedE2ePlugin.createNodesV2?.[0] ?? resolvedE2ePlugin.createNodes?.[0]; + + let foundPluginForApplication: PluginConfiguration; + for (let i = 0; i < e2ePluginRegistrations.length; i++) { + let candidatePluginForApplication = e2ePluginRegistrations[i]; + if (typeof candidatePluginForApplication === 'string') { + foundPluginForApplication = candidatePluginForApplication; + break; + } + + const matchingConfigFiles = findMatchingConfigFiles( + [pathToE2EConfigFile], + e2ePluginGlob, + candidatePluginForApplication.include, + candidatePluginForApplication.exclude + ); + + if (matchingConfigFiles.length) { + foundPluginForApplication = candidatePluginForApplication; + break; + } + } + + if (!foundPluginForApplication) { + return; + } + + const ciTargetName = + typeof foundPluginForApplication === 'string' + ? 'e2e-ci' + : (foundPluginForApplication.options as any)?.ciTargetName ?? 'e2e-ci'; + + const ciTargetNameGlob = `${ciTargetName}--**/*`; + nxJson.targetDefaults ??= {}; + const e2eCiTargetDefaults = nxJson.targetDefaults[ciTargetNameGlob]; + if (!e2eCiTargetDefaults) { + nxJson.targetDefaults[ciTargetNameGlob] = { + dependsOn: [buildTarget], + }; + } else { + e2eCiTargetDefaults.dependsOn ??= []; + if (!e2eCiTargetDefaults.dependsOn.includes(buildTarget)) { + e2eCiTargetDefaults.dependsOn.push(buildTarget); + } + } + updateNxJson(tree, nxJson); +} diff --git a/packages/devkit/src/utils/find-plugin-for-config-file.spec.ts b/packages/devkit/src/utils/find-plugin-for-config-file.spec.ts new file mode 100644 index 0000000000000..941fb356dbcf9 --- /dev/null +++ b/packages/devkit/src/utils/find-plugin-for-config-file.spec.ts @@ -0,0 +1,173 @@ +import { type Tree, readNxJson, updateNxJson } from 'nx/src/devkit-exports'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { findPluginForConfigFile } from './find-plugin-for-config-file'; + +describe('find-plugin-for-config-file', () => { + let tree: Tree; + let tempFs: TempFs; + beforeEach(() => { + tempFs = new TempFs('target-defaults-utils'); + tree = createTreeWithEmptyWorkspace(); + tree.root = tempFs.tempDir; + }); + + afterEach(() => { + tempFs.cleanup(); + jest.resetModules(); + }); + + it('should return the plugin when its registered as just a string', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push('@nx/cypress/plugin'); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toEqual('@nx/cypress/plugin'); + }); + + it('should return the plugin when it does not have an include or exclude', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toMatchInlineSnapshot(` + { + "options": { + "ciTargetName": "e2e-ci", + "targetName": "e2e", + }, + "plugin": "@nx/cypress/plugin", + } + `); + }); + + it('should return the plugin when it the includes finds the config file', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['libs/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + include: ['apps/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toMatchInlineSnapshot(` + { + "include": [ + "apps/**", + ], + "options": { + "ciTargetName": "cypress:e2e-ci", + "targetName": "e2e", + }, + "plugin": "@nx/cypress/plugin", + } + `); + }); + + it('should return a valid plugin when it the excludes does not include the config file', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'cypress:e2e-ci', + }, + exclude: ['apps/**'], + }); + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + exclude: ['libs/**'], + }); + updateNxJson(tree, nxJson); + + tree.write('apps/myapp-e2e/cypress.config.ts', ''); + await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', ''); + + // ACT + const plugin = await findPluginForConfigFile( + tree, + '@nx/cypress/plugin', + 'apps/myapp-e2e/cypress.config.ts' + ); + + // ASSERT + expect(plugin).toBeTruthy(); + expect(plugin).toMatchInlineSnapshot(` + { + "exclude": [ + "libs/**", + ], + "options": { + "ciTargetName": "e2e-ci", + "targetName": "e2e", + }, + "plugin": "@nx/cypress/plugin", + } + `); + }); +}); diff --git a/packages/devkit/src/utils/find-plugin-for-config-file.ts b/packages/devkit/src/utils/find-plugin-for-config-file.ts new file mode 100644 index 0000000000000..7098f07bab8b1 --- /dev/null +++ b/packages/devkit/src/utils/find-plugin-for-config-file.ts @@ -0,0 +1,50 @@ +import { + type Tree, + type PluginConfiguration, + readNxJson, + CreateNodes, + CreateNodesV2, +} from 'nx/src/devkit-exports'; +import { findMatchingConfigFiles } from 'nx/src/devkit-internals'; +export async function findPluginForConfigFile( + tree: Tree, + pluginName: string, + pathToConfigFile: string +): Promise { + const nxJson = readNxJson(tree); + if (!nxJson.plugins) { + return; + } + + const pluginRegistrations: PluginConfiguration[] = nxJson.plugins.filter( + (p) => (typeof p === 'string' ? p === pluginName : p.plugin === pluginName) + ); + + for (const plugin of pluginRegistrations) { + if (typeof plugin === 'string') { + return plugin; + } + + if (!plugin.include && !plugin.exclude) { + return plugin; + } + + if (plugin.include || plugin.exclude) { + const resolvedPlugin: { + createNodes?: CreateNodes; + createNodesV2?: CreateNodesV2; + } = await import(pluginName); + const pluginGlob = + resolvedPlugin.createNodesV2?.[0] ?? resolvedPlugin.createNodes?.[0]; + const matchingConfigFile = findMatchingConfigFiles( + [pathToConfigFile], + pluginGlob, + plugin.include, + plugin.exclude + ); + if (matchingConfigFile.length) { + return plugin; + } + } + } +} diff --git a/packages/esbuild/src/generators/configuration/configuration.ts b/packages/esbuild/src/generators/configuration/configuration.ts index 14d3aa2a5abd3..f838f143ded4a 100644 --- a/packages/esbuild/src/generators/configuration/configuration.ts +++ b/packages/esbuild/src/generators/configuration/configuration.ts @@ -12,7 +12,7 @@ import { getImportPath } from '@nx/js/src/utils/get-import-path'; import { esbuildInitGenerator } from '../init/init'; import { EsBuildExecutorOptions } from '../../executors/esbuild/schema'; import { EsBuildProjectSchema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function configurationGenerator( tree: Tree, diff --git a/packages/expo/src/generators/application/application.spec.ts b/packages/expo/src/generators/application/application.spec.ts index 3b3b574eb10d2..087b6ee6c8cca 100644 --- a/packages/expo/src/generators/application/application.spec.ts +++ b/packages/expo/src/generators/application/application.spec.ts @@ -3,6 +3,7 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; import { getProjects, readJson, + readNxJson, readProjectConfiguration, Tree, } from '@nx/devkit'; @@ -282,4 +283,56 @@ describe('app', () => { }); }); }); + + describe('cypress', () => { + it('should create e2e app with e2e-ci targetDefaults', async () => { + await expoApplicationGenerator(appTree, { + name: 'my-app', + directory: 'my-dir', + linter: Linter.EsLint, + e2eTestRunner: 'cypress', + js: false, + skipFormat: false, + unitTestRunner: 'none', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + + // ASSERT + const nxJson = readNxJson(appTree); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^export", + ], + } + `); + }); + }); + + describe('playwright', () => { + it('should create e2e app with e2e-ci targetDefaults', async () => { + await expoApplicationGenerator(appTree, { + name: 'my-app', + directory: 'my-dir', + linter: Linter.EsLint, + e2eTestRunner: 'playwright', + js: false, + skipFormat: false, + unitTestRunner: 'none', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + + // ASSERT + const nxJson = readNxJson(appTree); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(` + { + "dependsOn": [ + "^export", + ], + } + `); + }); + }); }); diff --git a/packages/expo/src/generators/application/lib/add-e2e.ts b/packages/expo/src/generators/application/lib/add-e2e.ts index 45671e2fb90f3..9b059213f80c2 100644 --- a/packages/expo/src/generators/application/lib/add-e2e.ts +++ b/packages/expo/src/generators/application/lib/add-e2e.ts @@ -4,12 +4,15 @@ import { ensurePackage, getPackageManagerCommand, joinPathFragments, + readNxJson, } from '@nx/devkit'; import { webStaticServeGenerator } from '@nx/web'; import { nxVersion } from '../../../utils/versions'; import { hasExpoPlugin } from '../../../utils/has-expo-plugin'; import { NormalizedSchema } from './normalize-options'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; export async function addE2e( tree: Tree, @@ -18,8 +21,7 @@ export async function addE2e( const hasPlugin = hasExpoPlugin(tree); switch (options.e2eTestRunner) { case 'cypress': { - const hasNxExpoPlugin = hasExpoPlugin(tree); - if (!hasNxExpoPlugin) { + if (!hasPlugin) { await webStaticServeGenerator(tree, { buildTarget: `${options.projectName}:export`, targetName: 'serve-static', @@ -39,7 +41,7 @@ export async function addE2e( tags: [], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, directory: 'src', @@ -49,12 +51,46 @@ export async function addE2e( devServerTarget: `${options.projectName}:${options.e2eWebServerTarget}`, port: options.e2ePort, baseUrl: options.e2eWebServerAddress, - ciWebServerCommand: hasNxExpoPlugin + ciWebServerCommand: hasPlugin ? `nx run ${options.projectName}:serve-static` : undefined, jsx: true, rootProject: options.rootProject, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^export'; + if (hasPlugin) { + const matchingExpoPlugin = await findPluginForConfigFile( + tree, + '@nx/expo/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingExpoPlugin && typeof matchingExpoPlugin !== 'string') { + buildTarget = `^${ + (matchingExpoPlugin.options as any)?.exportTargetName ?? 'export' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } case 'playwright': { const { configurationGenerator } = ensurePackage< @@ -67,7 +103,8 @@ export async function addE2e( targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: options.skipPackageJson, @@ -80,7 +117,39 @@ export async function addE2e( } ${options.name}`, webServerAddress: options.e2eWebServerAddress, rootProject: options.rootProject, + addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^export'; + if (hasPlugin) { + const matchingExpoPlugin = await findPluginForConfigFile( + tree, + '@nx/expo/plugin', + joinPathFragments(options.appProjectRoot, 'app.json') + ); + if (matchingExpoPlugin && typeof matchingExpoPlugin !== 'string') { + buildTarget = `^${ + (matchingExpoPlugin.options as any)?.exportTargetName ?? 'export' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } case 'detox': const { detoxApplicationGenerator } = ensurePackage< diff --git a/packages/expo/src/generators/application/lib/add-project.ts b/packages/expo/src/generators/application/lib/add-project.ts index 1f5e725474464..1f79ea08c95c0 100644 --- a/packages/expo/src/generators/application/lib/add-project.ts +++ b/packages/expo/src/generators/application/lib/add-project.ts @@ -8,7 +8,7 @@ import { import { hasExpoPlugin } from '../../../utils/has-expo-plugin'; import { NormalizedSchema } from './normalize-options'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function addProject(host: Tree, options: NormalizedSchema) { const nxJson = readNxJson(host); diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index 61c3eb58b9696..7751b2e641235 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -32,7 +32,7 @@ import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; import { Schema } from './schema'; import { ensureDependencies } from '../../utils/ensure-dependencies'; import { initRootBabelConfig } from '../../utils/init-root-babel-config'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; export async function expoLibraryGenerator( diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 6ec990008bcfb..4efc7a7e3d9ab 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -25,7 +25,7 @@ import { type ProjectNameAndRootOptions, } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { findMatchingProjects } from 'nx/src/utils/find-matching-projects'; import { type PackageJson } from 'nx/src/utils/package-json'; diff --git a/packages/js/src/generators/setup-build/generator.ts b/packages/js/src/generators/setup-build/generator.ts index b83e251c1b77e..c39416c3e31c0 100644 --- a/packages/js/src/generators/setup-build/generator.ts +++ b/packages/js/src/generators/setup-build/generator.ts @@ -12,7 +12,7 @@ import { addSwcConfig } from '../../utils/swc/add-swc-config'; import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies'; import { nxVersion } from '../../utils/versions'; import { SetupBuildGeneratorSchema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function setupBuildGenerator( tree: Tree, diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 2fb1f976f479f..fa54ed0b95d6d 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -2,6 +2,7 @@ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { getProjects, readJson, + readNxJson, readProjectConfiguration, Tree, } from '@nx/devkit'; @@ -359,28 +360,9 @@ describe('app', () => { const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8'); expect(indexContent).not.toContain(`import styles from './page.module`); expect(indexContent).toContain(`import styled from '@emotion/styled'`); - expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8')) - .toMatchInlineSnapshot(` - "import './global.css'; - - export const metadata = { - title: 'Welcome to ${name}', - description: 'Generated by create-nx-workspace', - }; - - export default function RootLayout({ - children, - }: { - children: React.ReactNode; - }) { - return ( - - {children} - - ); - } - " - `); + expect( + tree.read(`${name}/src/app/layout.tsx`, 'utf-8') + ).toMatchInlineSnapshot(``); }); it('should add jsxImportSource in tsconfig.json', async () => { @@ -559,6 +541,50 @@ describe('app', () => { }); }); + describe('--e2e-test-runner cypress', () => { + it('should generate e2e-ci targetDefaults', async () => { + const name = uniq(); + + await applicationGenerator(tree, { + name, + style: 'css', + e2eTestRunner: 'cypress', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + }); + + describe('--e2e-test-runner playwright', () => { + it('should generate e2e-ci targetDefaults', async () => { + const name = uniq(); + + await applicationGenerator(tree, { + name, + style: 'css', + e2eTestRunner: 'playwright', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + }); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); + }); + }); + it('should generate functional components by default', async () => { const name = uniq(); diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts index 50290052424ca..11cb0eb3f98b6 100644 --- a/packages/next/src/generators/application/lib/add-e2e.ts +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -11,6 +11,8 @@ import { Linter } from '@nx/eslint'; import { nxVersion } from '../../../utils/versions'; import { NormalizedSchema } from './normalize-options'; import { webStaticServeGenerator } from '@nx/web'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e(host: Tree, options: NormalizedSchema) { const nxJson = readNxJson(host); @@ -42,7 +44,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { implicitDependencies: [options.projectName], }); - return configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { ...options, linter: Linter.EsLint, project: options.e2eProjectName, @@ -60,6 +62,40 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { ? `nx run ${options.projectName}:serve-static` : undefined, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/next/plugin', + joinPathFragments(options.appProjectRoot, 'next.config.js') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -71,7 +107,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { tags: [], implicitDependencies: [options.projectName], }); - return configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { rootProject: options.rootProject, project: options.e2eProjectName, skipFormat: true, @@ -86,6 +122,37 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { } ${options.projectName}`, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/next/plugin', + joinPathFragments(options.appProjectRoot, 'next.config.js') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } return () => {}; } diff --git a/packages/next/src/generators/application/lib/add-project.ts b/packages/next/src/generators/application/lib/add-project.ts index 84ad72664cb2f..947d9ce6fbe23 100644 --- a/packages/next/src/generators/application/lib/add-project.ts +++ b/packages/next/src/generators/application/lib/add-project.ts @@ -5,7 +5,7 @@ import { readNxJson, Tree, } from '@nx/devkit'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export function addProject(host: Tree, options: NormalizedSchema) { const targets: Record = {}; diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 9ad70f32eafe0..a6ec2ce256ea3 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -48,7 +48,7 @@ import { initGenerator } from '../init/init'; import { setupDockerGenerator } from '../setup-docker/setup-docker'; import { Schema } from './schema'; import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; export interface NormalizedSchema extends Schema { diff --git a/packages/node/src/generators/library/library.ts b/packages/node/src/generators/library/library.ts index 9499236ced506..2ecec34cf3203 100644 --- a/packages/node/src/generators/library/library.ts +++ b/packages/node/src/generators/library/library.ts @@ -22,7 +22,7 @@ import { join } from 'path'; import { tslibVersion, typesNodeVersion } from '../../utils/versions'; import { initGenerator } from '../init/init'; import { Schema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export interface NormalizedSchema extends Schema { fileName: string; diff --git a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap index d27094dbc898b..2b212d22ab51b 100644 --- a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap @@ -10,6 +10,14 @@ exports[`app generated files content - as-provided - my-app general application .cache" `; +exports[`app generated files content - as-provided - my-app general application should add the nuxt and vitest plugins 1`] = ` +{ + "dependsOn": [ + "^build-static", + ], +} +`; + exports[`app generated files content - as-provided - my-app general application should configure eslint correctly 1`] = ` "{ "extends": ["@nuxt/eslint-config", "../.eslintrc.json"], @@ -341,6 +349,14 @@ exports[`app generated files content - as-provided - myApp general application s .cache" `; +exports[`app generated files content - as-provided - myApp general application should add the nuxt and vitest plugins 1`] = ` +{ + "dependsOn": [ + "^build-static", + ], +} +`; + exports[`app generated files content - as-provided - myApp general application should configure eslint correctly 1`] = ` "{ "extends": ["@nuxt/eslint-config", "../.eslintrc.json"], diff --git a/packages/nuxt/src/generators/application/application.spec.ts b/packages/nuxt/src/generators/application/application.spec.ts index d03bdbe32f7a5..cc7602937ab97 100644 --- a/packages/nuxt/src/generators/application/application.spec.ts +++ b/packages/nuxt/src/generators/application/application.spec.ts @@ -96,6 +96,7 @@ describe('app', () => { nxJson.plugins.find((p) => p.plugin === '@nx/vite/plugin') ) ); + expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchSnapshot(); }); }); diff --git a/packages/nuxt/src/generators/application/lib/add-e2e.ts b/packages/nuxt/src/generators/application/lib/add-e2e.ts index 35fb27fc83484..3c7bc78fe1082 100644 --- a/packages/nuxt/src/generators/application/lib/add-e2e.ts +++ b/packages/nuxt/src/generators/application/lib/add-e2e.ts @@ -7,6 +7,8 @@ import { } from '@nx/devkit'; import { nxVersion } from '../../../utils/versions'; import { NormalizedSchema } from '../schema'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e(host: Tree, options: NormalizedSchema) { if (options.e2eTestRunner === 'cypress') { @@ -21,7 +23,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { tags: [], implicitDependencies: [options.projectName], }); - return await configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { ...options, project: options.e2eProjectName, directory: 'src', @@ -38,6 +40,33 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { jsx: true, addPlugin: true, }); + + let buildTarget = '^build-static'; + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/nuxt/plugin', + joinPathFragments( + options.appProjectRoot, + `nuxt.config.${options.js ? 'js' : 'ts'}` + ) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildStaticTargetName ?? 'build-static' + }`; + } + + await addE2eCiTargetDefaults( + host, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + + return e2eTask; } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -48,7 +77,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(host, { + const e2eTask = await configurationGenerator(host, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: options.skipPackageJson, @@ -62,6 +91,30 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { } ${options.projectName}`, addPlugin: true, }); + + let buildTarget = '^build-static'; + const matchingPlugin = await findPluginForConfigFile( + host, + '@nx/nuxt/plugin', + joinPathFragments( + options.appProjectRoot, + `nuxt.config.${options.js ? 'js' : 'ts'}` + ) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildStaticTargetName ?? 'build-static' + }`; + } + + await addE2eCiTargetDefaults( + host, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + + return e2eTask; } return () => {}; } diff --git a/packages/nx/src/devkit-internals.ts b/packages/nx/src/devkit-internals.ts index 72be433b4c674..d380df1298833 100644 --- a/packages/nx/src/devkit-internals.ts +++ b/packages/nx/src/devkit-internals.ts @@ -8,11 +8,11 @@ export { getExecutorInformation } from './command-line/run/executor-utils'; export { readNxJson as readNxJsonFromDisk } from './config/nx-json'; export { calculateDefaultProjectName } from './config/calculate-default-project-name'; export { retrieveProjectConfigurationsWithAngularProjects } from './project-graph/utils/retrieve-workspace-files'; +export { mergeTargetConfigurations } from './project-graph/utils/project-configuration-utils'; export { - mergeTargetConfigurations, + readProjectConfigurationsFromRootMap, findMatchingConfigFiles, } from './project-graph/utils/project-configuration-utils'; -export { readProjectConfigurationsFromRootMap } from './project-graph/utils/project-configuration-utils'; export { splitTarget } from './utils/split-target'; export { combineOptionsForExecutor } from './utils/params'; export { sortObjectByKeys } from './utils/object-sort'; diff --git a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts new file mode 100644 index 0000000000000..ba65c8f32bf68 --- /dev/null +++ b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.spec.ts @@ -0,0 +1,333 @@ +import { ProjectGraph, readNxJson, type Tree, updateNxJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('add-e2e-ci-target-defaults', () => { + let tree: Tree; + let tempFs: TempFs; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tempFs = new TempFs('add-e2e-ci'); + tree.root = tempFs.tempDir; + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + tempFs.reset(); + }); + + it('should do nothing when the plugin is not registered', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = []; + updateNxJson(tree, nxJson); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetName and buildTarget when there is one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + } + `); + }); + + it('should add the targetDefaults with the correct ciTargetNames and buildTargets when there is more than one plugin', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['app-e2e/**'], + }, + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'playwright:e2e-ci', + }, + include: ['shop-e2e/**'], + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'build', + ciTargetName: 'playwright:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + "lint": { + "cache": true, + }, + "playwright:e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + } + `); + }); + + it('should only add the targetDefaults with the correct ciTargetName and buildTargets when there is more than one plugin with only one matching multiple projects', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.plugins = [ + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + include: ['cart-e2e/**'], + }, + { + plugin: '@nx/playwright/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'playwright:e2e-ci', + }, + }, + ]; + updateNxJson(tree, nxJson); + + addProject(tree, tempFs); + addProject(tree, tempFs, { + buildTargetName: 'bundle', + ciTargetName: 'playwright:e2e-ci', + appName: 'shop', + }); + + // ACT + await addE2eCiTargetDefaults(tree); + + // ASSERT + expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(` + { + "build": { + "cache": true, + }, + "lint": { + "cache": true, + }, + "playwright:e2e-ci--**/*": { + "dependsOn": [ + "^build", + "^bundle", + ], + }, + } + `); + }); +}); + +function addProject( + tree: Tree, + tempFs: TempFs, + overrides: { + ciTargetName: string; + buildTargetName: string; + appName: string; + noCi?: boolean; + } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' } +) { + const appProjectConfig = { + name: overrides.appName, + root: overrides.appName, + sourceRoot: `${overrides.appName}/src`, + projectType: 'application', + }; + const viteConfig = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/${overrides.appName}', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/${overrides.appName}', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +});`; + + const e2eProjectConfig = { + name: `${overrides.appName}-e2e`, + root: `${overrides.appName}-e2e`, + sourceRoot: `${overrides.appName}-e2e/src`, + projectType: 'application', + }; + + const playwrightConfig = `import { defineConfig, devices } from '@playwright/test'; +import { nxE2EPreset } from '@nx/playwright/preset'; + +import { workspaceRoot } from '@nx/devkit'; + +const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; + +export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + use: { + baseURL, + trace: 'on-first-retry', + }, + webServer: { + command: 'npx nx run ${overrides.appName}:serve-static', + url: 'http://localhost:4200', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +});`; + + tree.write(`${overrides.appName}/vite.config.ts`, viteConfig); + tree.write( + `${overrides.appName}/project.json`, + JSON.stringify(appProjectConfig) + ); + tree.write(`${overrides.appName}-e2e/playwright.config.ts`, playwrightConfig); + tree.write( + `${overrides.appName}-e2e/project.json`, + JSON.stringify(e2eProjectConfig) + ); + tempFs.createFilesSync({ + [`${overrides.appName}/vite.config.ts`]: viteConfig, + [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig), + [`${overrides.appName}-e2e/playwright.config.ts`]: playwrightConfig, + [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig), + }); + + projectGraph.nodes[overrides.appName] = { + name: overrides.appName, + type: 'app', + data: { + projectType: 'application', + root: overrides.appName, + targets: { + [overrides.buildTargetName]: {}, + 'serve-static': { + dependsOn: [overrides.buildTargetName], + options: { + buildTarget: overrides.buildTargetName, + }, + }, + }, + }, + }; + + projectGraph.nodes[`${overrides.appName}-e2e`] = { + name: `${overrides.appName}-e2e`, + type: 'app', + data: { + projectType: 'application', + root: `${overrides.appName}-e2e`, + targets: { + e2e: {}, + [overrides.ciTargetName]: {}, + }, + }, + }; +} diff --git a/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts new file mode 100644 index 0000000000000..d108c5a03731e --- /dev/null +++ b/packages/playwright/src/migrations/update-19-6-0/add-e2e-ci-target-defaults.ts @@ -0,0 +1,116 @@ +import { + type Tree, + type CreateNodesV2, + formatFiles, + readNxJson, + createProjectGraphAsync, + parseTargetString, +} from '@nx/devkit'; +import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; +import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; +import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { + ProjectConfigurationsError, + retrieveProjectConfigurations, +} from 'nx/src/devkit-internals'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { type PlaywrightPluginOptions } from '../../plugins/plugin'; + +export default async function addE2eCiTargetDefaults(tree: Tree) { + const pluginName = '@nx/playwright/plugin'; + const graph = await createProjectGraphAsync(); + const nxJson = readNxJson(tree); + const matchingPluginRegistrations = nxJson.plugins?.filter((p) => + typeof p === 'string' ? p === pluginName : p.plugin === pluginName + ); + + if (!matchingPluginRegistrations) { + return; + } + + const { + createNodesV2, + }: { createNodesV2: CreateNodesV2 } = await import( + pluginName + ); + + for (const plugin of matchingPluginRegistrations) { + let projectConfigs: ConfigurationResult; + try { + const loadedPlugin = new LoadedNxPlugin( + { createNodesV2, name: pluginName }, + plugin + ); + projectConfigs = await retrieveProjectConfigurations( + [loadedPlugin], + tree.root, + nxJson + ); + } catch (e) { + if (e instanceof ProjectConfigurationsError) { + projectConfigs = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } + + for (const configFile of projectConfigs.matchingProjectFiles) { + const configFileContents = tree.read(configFile, 'utf-8'); + + const ast = tsquery.ast(configFileContents); + const CI_WEBSERVER_COMMAND_SELECTOR = + 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral'; + const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, { + visitAllChildren: true, + }); + if (!nodes.length) { + continue; + } + const ciWebServerCommand = nodes[0].getText(); + let serveStaticProject: string; + let serveStaticTarget: string; + let serveStaticConfiguration: string; + if (ciWebServerCommand.includes('nx run')) { + const NX_TARGET_REGEX = "(?<=nx run )[^']+"; + const matches = ciWebServerCommand.match(NX_TARGET_REGEX); + if (!matches) { + continue; + } + const targetString = matches[0]; + const { project, target, configuration } = parseTargetString( + targetString, + graph + ); + serveStaticProject = project; + serveStaticTarget = target; + serveStaticConfiguration = configuration; + } else { + const NX_PROJECT_REGEX = 'nx\\s+([^ ]+)\\s+([^ ]+)'; + const matches = ciWebServerCommand.match(NX_PROJECT_REGEX); + if (!matches) { + return; + } + serveStaticTarget = matches[1]; + serveStaticProject = matches[2]; + } + + const resolvedServeStaticTarget = + graph.nodes[serveStaticProject].data.targets[serveStaticTarget]; + + let resolvedBuildTarget: string; + if (resolvedServeStaticTarget.dependsOn) { + resolvedBuildTarget = resolvedServeStaticTarget.dependsOn.join(','); + } else { + resolvedBuildTarget = + (serveStaticConfiguration + ? resolvedServeStaticTarget.configurations[serveStaticConfiguration] + .buildTarget + : resolvedServeStaticTarget.options.buildTarget) ?? 'build'; + } + + const buildTarget = `^${resolvedBuildTarget}`; + + await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile); + } + } +} diff --git a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts index c98bfff41a031..88b58a6e6ddfc 100644 --- a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts +++ b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts @@ -8,6 +8,7 @@ import { visitNotIgnoredFiles, } from '@nx/devkit'; import { tsquery } from '@phenomnomnominal/tsquery'; +import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; export default async function (tree: Tree) { const graph = await createProjectGraphAsync(); @@ -138,5 +139,6 @@ export default async function (tree: Tree) { } }); + await addE2eCiTargetDefaults(tree); await formatFiles(tree); } diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index e223343c079f6..d1d84e6a767bb 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -1086,4 +1086,99 @@ describe('app', () => { } `); }); + + it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with playwright', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + let nxJson = readNxJson(tree); + delete nxJson.targetDefaults; + updateNxJson(tree, nxJson); + + // ACT + await applicationGenerator(tree, { + name: 'myapp', + addPlugin: true, + linter: Linter.None, + style: 'none', + e2eTestRunner: 'playwright', + }); + + // ASSERT + nxJson = readNxJson(tree); + expect(nxJson.targetDefaults).toMatchInlineSnapshot(` + { + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + } + `); + }); + + it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with cypress', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + let nxJson = readNxJson(tree); + delete nxJson.targetDefaults; + updateNxJson(tree, nxJson); + + // ACT + await applicationGenerator(tree, { + name: 'myapp', + addPlugin: true, + linter: Linter.None, + style: 'none', + e2eTestRunner: 'cypress', + }); + + // ASSERT + nxJson = readNxJson(tree); + expect(nxJson.targetDefaults).toMatchInlineSnapshot(` + { + "e2e-ci--**/*": { + "dependsOn": [ + "^build", + ], + }, + } + `); + }); + + it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with cypress and use the defined webpack buildTargetName', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + let nxJson = readNxJson(tree); + delete nxJson.targetDefaults; + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/webpack/plugin', + options: { + buildTargetName: 'build-base', + }, + }); + updateNxJson(tree, nxJson); + + // ACT + await applicationGenerator(tree, { + name: 'myapp', + addPlugin: true, + linter: Linter.None, + style: 'none', + bundler: 'webpack', + e2eTestRunner: 'cypress', + }); + + // ASSERT + nxJson = readNxJson(tree); + expect(nxJson.targetDefaults).toMatchInlineSnapshot(` + { + "e2e-ci--**/*": { + "dependsOn": [ + "^build-base", + ], + }, + } + `); + }); }); diff --git a/packages/react/src/generators/application/lib/add-e2e.ts b/packages/react/src/generators/application/lib/add-e2e.ts index 5da77e12ce49a..2f09da931de21 100644 --- a/packages/react/src/generators/application/lib/add-e2e.ts +++ b/packages/react/src/generators/application/lib/add-e2e.ts @@ -4,6 +4,7 @@ import { ensurePackage, getPackageManagerCommand, joinPathFragments, + readNxJson, } from '@nx/devkit'; import { webStaticServeGenerator } from '@nx/web'; @@ -11,6 +12,8 @@ import { nxVersion } from '../../../utils/versions'; import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; import { hasVitePlugin } from '../../../utils/has-vite-plugin'; import { NormalizedSchema } from '../schema'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e( tree: Tree, @@ -41,7 +44,7 @@ export async function addE2e( tags: [], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, directory: 'src', @@ -64,6 +67,46 @@ export async function addE2e( ciBaseUrl: options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasNxBuildPlugin) { + const configFile = + options.bundler === 'webpack' + ? 'webpack.config.js' + : options.bundler === 'vite' + ? `vite.config.${options.js ? 'js' : 'ts'}` + : 'webpack.config.js'; + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/${options.bundler}/plugin`, + joinPathFragments(options.appProjectRoot, configFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } case 'playwright': { const { configurationGenerator } = ensurePackage< @@ -76,7 +119,7 @@ export async function addE2e( targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: options.skipPackageJson, @@ -91,6 +134,43 @@ export async function addE2e( rootProject: options.rootProject, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasNxBuildPlugin) { + const configFile = + options.bundler === 'webpack' + ? 'webpack.config.js' + : options.bundler === 'vite' + ? `vite.config.${options.js ? 'js' : 'ts'}` + : 'webpack.config.js'; + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/${options.bundler}/plugin`, + joinPathFragments(options.appProjectRoot, configFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } case 'none': default: diff --git a/packages/remix/src/generators/application/application.impl.spec.ts b/packages/remix/src/generators/application/application.impl.spec.ts index 75d8ed5cad92d..f9b0d2576758a 100644 --- a/packages/remix/src/generators/application/application.impl.spec.ts +++ b/packages/remix/src/generators/application/application.impl.spec.ts @@ -1,6 +1,6 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; -import { joinPathFragments, readJson, type Tree } from '@nx/devkit'; +import { joinPathFragments, readJson, readNxJson, type Tree } from '@nx/devkit'; import * as devkit from '@nx/devkit'; import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; @@ -129,6 +129,14 @@ describe('Remix Application', () => { expectTargetsToBeCorrect(tree, '.'); expect(tree.read('e2e/cypress.config.ts', 'utf-8')).toMatchSnapshot(); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }); }); @@ -148,6 +156,14 @@ describe('Remix Application', () => { expectTargetsToBeCorrect(tree, '.'); expect(tree.read('e2e/playwright.config.ts', 'utf-8')).toMatchSnapshot(); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }); }); diff --git a/packages/remix/src/generators/application/application.impl.ts b/packages/remix/src/generators/application/application.impl.ts index fc99f0f8653b8..e86017360f294 100644 --- a/packages/remix/src/generators/application/application.impl.ts +++ b/packages/remix/src/generators/application/application.impl.ts @@ -33,7 +33,7 @@ import { NxRemixGeneratorSchema } from './schema'; import { updateDependencies } from '../utils/update-dependencies'; import initGenerator from '../init/init'; import { initGenerator as jsInitGenerator } from '@nx/js'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { updateJestTestMatch } from '../../utils/testing-config-utils'; diff --git a/packages/remix/src/generators/application/lib/add-e2e.ts b/packages/remix/src/generators/application/lib/add-e2e.ts index 6f5fec99e607c..43283705b7b02 100644 --- a/packages/remix/src/generators/application/lib/add-e2e.ts +++ b/packages/remix/src/generators/application/lib/add-e2e.ts @@ -6,11 +6,19 @@ import { updateProjectConfiguration, ensurePackage, getPackageManagerCommand, + readNxJson, } from '@nx/devkit'; import { type NormalizedSchema } from './normalize-options'; import { getPackageVersion } from '../../../utils/versions'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2E(tree: Tree, options: NormalizedSchema) { + const hasRemixPlugin = readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/remix/plugin' + : p.plugin === '@nx/remix/plugin' + ); if (options.e2eTestRunner === 'cypress') { const { configurationGenerator } = ensurePackage< typeof import('@nx/cypress') @@ -25,7 +33,7 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { implicitDependencies: [options.projectName], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, directory: 'src', skipFormat: true, @@ -33,6 +41,40 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { baseUrl: options.e2eWebServerAddress, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasRemixPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/remix/plugin`, + joinPathFragments(options.projectRoot, 'remix.config.js') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') @@ -47,7 +89,7 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, skipFormat: true, skipPackageJson: false, @@ -62,6 +104,37 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { rootProject: options.rootProject, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasRemixPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/remix/plugin`, + joinPathFragments(options.projectRoot, 'remix.config.js') + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } else { return () => {}; } diff --git a/packages/rollup/src/generators/configuration/configuration.ts b/packages/rollup/src/generators/configuration/configuration.ts index e28a07b3be0e0..f12e0c584b43d 100644 --- a/packages/rollup/src/generators/configuration/configuration.ts +++ b/packages/rollup/src/generators/configuration/configuration.ts @@ -16,7 +16,7 @@ import { getImportPath } from '@nx/js/src/utils/get-import-path'; import { rollupInitGenerator } from '../init/init'; import { RollupExecutorOptions } from '../../executors/rollup/schema'; import { RollupProjectSchema } from './schema'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { ensureDependencies } from '../../utils/ensure-dependencies'; import { hasPlugin } from '../../utils/has-plugin'; import { RollupWithNxPluginOptions } from '../../plugins/with-nx/with-nx-options'; diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts index 0b7c2790f155c..a1a997c02a493 100644 --- a/packages/vite/src/utils/generator-utils.ts +++ b/packages/vite/src/utils/generator-utils.ts @@ -14,7 +14,7 @@ import { VitePreviewServerExecutorOptions } from '../executors/preview-server/sc import { VitestExecutorOptions } from '../executors/test/schema'; import { ViteConfigurationGeneratorSchema } from '../generators/configuration/schema'; import { ensureViteConfigIsCorrect } from './vite-config-edit-utils'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export type Target = 'build' | 'serve' | 'test' | 'preview'; export type TargetFlags = Partial>; diff --git a/packages/vue/src/generators/application/application.spec.ts b/packages/vue/src/generators/application/application.spec.ts index aab1a17a2300c..d52eb53c323ed 100644 --- a/packages/vue/src/generators/application/application.spec.ts +++ b/packages/vue/src/generators/application/application.spec.ts @@ -40,6 +40,7 @@ describe('application generator', () => { ...options, unitTestRunner: 'vitest', e2eTestRunner: 'playwright', + addPlugin: true, }); expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot(); expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot(); @@ -49,6 +50,14 @@ describe('application generator', () => { tree.read('test-e2e/playwright.config.ts', 'utf-8') ).toMatchSnapshot(); expect(listFiles(tree)).toMatchSnapshot(); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }); it('should set up project correctly for cypress', async () => { diff --git a/packages/vue/src/generators/application/lib/add-e2e.ts b/packages/vue/src/generators/application/lib/add-e2e.ts index 9268da0e2bd16..ae2578e334e9c 100644 --- a/packages/vue/src/generators/application/lib/add-e2e.ts +++ b/packages/vue/src/generators/application/lib/add-e2e.ts @@ -10,6 +10,8 @@ import { webStaticServeGenerator } from '@nx/web'; import { nxVersion } from '../../../utils/versions'; import { NormalizedSchema } from '../schema'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; +import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; export async function addE2e( tree: Tree, @@ -52,7 +54,7 @@ export async function addE2e( tags: [], implicitDependencies: [options.projectName], }); - return await configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, directory: 'src', @@ -70,6 +72,43 @@ export async function addE2e( ciWebServerCommand: `nx run ${options.projectName}:${e2eCiWebServerTarget}`, ciBaseUrl: 'http://localhost:4300', }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/vite/plugin`, + joinPathFragments( + options.appProjectRoot, + `vite.config.${options.js ? 'js' : 'ts'}` + ) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments( + options.e2eProjectRoot, + `cypress.config.${options.js ? 'js' : 'ts'}` + ) + ); + } + + return e2eTask; } case 'playwright': { const { configurationGenerator } = ensurePackage< @@ -82,7 +121,7 @@ export async function addE2e( targets: {}, implicitDependencies: [options.projectName], }); - return configurationGenerator(tree, { + const e2eTask = await configurationGenerator(tree, { ...options, project: options.e2eProjectName, skipFormat: true, @@ -96,6 +135,40 @@ export async function addE2e( }:${e2eCiWebServerTarget}`, webServerAddress: 'http://localhost:4300', }); + + if ( + options.addPlugin || + readNxJson(tree).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + tree, + `@nx/vite/plugin`, + joinPathFragments( + options.appProjectRoot, + `vite.config.${options.js ? 'js' : 'ts'}` + ) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + tree, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + + return e2eTask; } case 'none': default: diff --git a/packages/web/src/generators/application/application.spec.ts b/packages/web/src/generators/application/application.spec.ts index 6aec78f324b93..cab0c67e7d4c2 100644 --- a/packages/web/src/generators/application/application.spec.ts +++ b/packages/web/src/generators/application/application.spec.ts @@ -43,6 +43,14 @@ describe('app', () => { expect(readProjectConfiguration(tree, 'my-app-e2e').root).toEqual( 'my-app-e2e' ); + expect(readNxJson(tree).targetDefaults['e2e-ci--**/*']) + .toMatchInlineSnapshot(` + { + "dependsOn": [ + "^build", + ], + } + `); }, 60_000); it('should update tags and implicit dependencies', async () => { diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index 5b0df944b12fc..9dbed0511df91 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -9,6 +9,7 @@ import { joinPathFragments, names, offsetFromRoot, + type PluginConfiguration, readNxJson, readProjectConfiguration, runTasksInSerial, @@ -37,12 +38,15 @@ import { webInitGenerator } from '../init/init'; import { Schema } from './schema'; import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { + addBuildTargetDefaults, + addE2eCiTargetDefaults, +} from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { VitePluginOptions } from '@nx/vite/src/plugins/plugin'; import { WebpackPluginOptions } from '@nx/webpack/src/plugins/plugin'; -import { hasVitePlugin } from '../../utils/has-vite-plugin'; import staticServeConfiguration from '../static-serve/static-serve-configuration'; +import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; interface NormalizedSchema extends Schema { projectName: string; @@ -368,10 +372,20 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { tasks.push(lintTask); } - const hasNxBuildPlugin = - (options.bundler === 'webpack' && hasWebpackPlugin(host)) || - (options.bundler === 'vite' && hasVitePlugin(host)); - if (!hasNxBuildPlugin) { + const nxJson = readNxJson(host); + let hasPlugin: PluginConfiguration | undefined; + let buildPlugin: string; + let buildConfigFile: string; + if (options.bundler === 'webpack' || options.bundler === 'vite') { + buildPlugin = `@nx/${options.bundler}/plugin`; + buildConfigFile = + options.bundler === 'webpack' ? 'webpack.config.js' : `vite.config.ts`; + hasPlugin = nxJson.plugins?.find((p) => + typeof p === 'string' ? p === buildPlugin : p.plugin === buildPlugin + ); + } + + if (!hasPlugin) { await staticServeConfiguration(host, { buildTarget: `${options.projectName}:build`, spa: true, @@ -396,17 +410,47 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { baseUrl: options.e2eWebServerAddress, directory: 'src', skipFormat: true, - webServerCommands: hasNxBuildPlugin + webServerCommands: hasPlugin ? { default: `nx run ${options.projectName}:${options.e2eWebServerTarget}`, production: `nx run ${options.projectName}:preview`, } : undefined, - ciWebServerCommand: hasNxBuildPlugin + ciWebServerCommand: hasPlugin ? `nx run ${options.projectName}:${options.e2eCiWebServerTarget}` : undefined, ciBaseUrl: options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/cypress/plugin' + : p.plugin === '@nx/cypress/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + buildPlugin, + joinPathFragments(options.appProjectRoot, buildConfigFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/cypress/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `cypress.config.ts`) + ); + } + tasks.push(cypressTask); } else if (options.e2eTestRunner === 'playwright') { const { configurationGenerator: playwrightConfigGenerator } = ensurePackage< @@ -434,6 +478,36 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { webServerAddress: options.e2eCiBaseUrl, addPlugin: options.addPlugin, }); + + if ( + options.addPlugin || + readNxJson(host).plugins?.find((p) => + typeof p === 'string' + ? p === '@nx/playwright/plugin' + : p.plugin === '@nx/playwright/plugin' + ) + ) { + let buildTarget = '^build'; + if (hasPlugin) { + const matchingPlugin = await findPluginForConfigFile( + host, + buildPlugin, + joinPathFragments(options.appProjectRoot, buildConfigFile) + ); + if (matchingPlugin && typeof matchingPlugin !== 'string') { + buildTarget = `^${ + (matchingPlugin.options as any)?.buildTargetName ?? 'build' + }`; + } + } + await addE2eCiTargetDefaults( + host, + '@nx/playwright/plugin', + buildTarget, + joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`) + ); + } + tasks.push(playwrightTask); } if (options.unitTestRunner === 'jest') { diff --git a/packages/webpack/src/generators/configuration/configuration.ts b/packages/webpack/src/generators/configuration/configuration.ts index d655004455241..f4cd67321e320 100644 --- a/packages/webpack/src/generators/configuration/configuration.ts +++ b/packages/webpack/src/generators/configuration/configuration.ts @@ -15,7 +15,7 @@ import { webpackInitGenerator } from '../init/init'; import { ConfigurationGeneratorSchema } from './schema'; import { WebpackExecutorOptions } from '../../executors/webpack/schema'; import { hasPlugin } from '../../utils/has-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { ensureDependencies } from '../../utils/ensure-dependencies'; export function configurationGenerator( diff --git a/tsconfig.base.json b/tsconfig.base.json index 4f74457191501..e93b67d7681e8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -115,6 +115,7 @@ "@nx/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"], "@nx/nx-dev/util-ai": ["nx-dev/util-ai/src/index.ts"], "@nx/playwright": ["packages/playwright/index.ts"], + "@nx/playwright/*": ["packages/playwright/*"], "@nx/plugin": ["packages/plugin"], "@nx/plugin/*": ["packages/plugin/*"], "@nx/react": ["packages/react"],