diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 9855d857f32d8..06db5726f620e 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -7048,6 +7048,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "convert-to-inferred", + "path": "/nx-api/cypress/generators/convert-to-inferred", + "name": "convert-to-inferred", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 3afc84c02113f..ce13570984cbc 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -545,6 +545,15 @@ "originalFilePath": "/packages/cypress/src/generators/migrate-to-cypress-11/schema.json", "path": "/nx-api/cypress/generators/migrate-to-cypress-11", "type": "generator" + }, + "/nx-api/cypress/generators/convert-to-inferred": { + "description": "Convert existing Cypress project(s) using `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`.", + "file": "generated/packages/cypress/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/cypress/src/generators/convert-to-inferred/schema.json", + "path": "/nx-api/cypress/generators/convert-to-inferred", + "type": "generator" } }, "path": "/nx-api/cypress" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index d632d3256e815..8e5cab6fd0947 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -537,6 +537,15 @@ "originalFilePath": "/packages/cypress/src/generators/migrate-to-cypress-11/schema.json", "path": "cypress/generators/migrate-to-cypress-11", "type": "generator" + }, + { + "description": "Convert existing Cypress project(s) using `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`.", + "file": "generated/packages/cypress/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/cypress/src/generators/convert-to-inferred/schema.json", + "path": "cypress/generators/convert-to-inferred", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/cypress/generators/convert-to-inferred.json b/docs/generated/packages/cypress/generators/convert-to-inferred.json new file mode 100644 index 0000000000000..9bc978de16109 --- /dev/null +++ b/docs/generated/packages/cypress/generators/convert-to-inferred.json @@ -0,0 +1,30 @@ +{ + "name": "convert-to-inferred", + "factory": "./src/generators/convert-to-inferred/convert-to-inferred", + "schema": { + "$schema": "https://json-schema.org/schema", + "$id": "NxCypressConvertToInferred", + "description": "Convert existing Cypress project(s) using `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`.", + "title": "Convert Cypress project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + }, + "presets": [] + }, + "description": "Convert existing Cypress project(s) using `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`.", + "implementation": "/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts", + "aliases": [], + "hidden": false, + "path": "/packages/cypress/src/generators/convert-to-inferred/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index bcae58a5e1a79..301e17e17966a 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -372,6 +372,7 @@ - [configuration](/nx-api/cypress/generators/configuration) - [component-configuration](/nx-api/cypress/generators/component-configuration) - [migrate-to-cypress-11](/nx-api/cypress/generators/migrate-to-cypress-11) + - [convert-to-inferred](/nx-api/cypress/generators/convert-to-inferred) - [detox](/nx-api/detox) - [documents](/nx-api/detox/documents) - [Overview](/nx-api/detox/documents/overview) diff --git a/packages/cypress/generators.json b/packages/cypress/generators.json index 99bf59cfb43fb..113ad18782e21 100644 --- a/packages/cypress/generators.json +++ b/packages/cypress/generators.json @@ -32,6 +32,11 @@ "factory": "./src/generators/migrate-to-cypress-11/migrate-to-cypress-11#migrateCypressProject", "schema": "./src/generators/migrate-to-cypress-11/schema.json", "description": "Migrate existing Cypress e2e projects to Cypress v11" + }, + "convert-to-inferred": { + "factory": "./src/generators/convert-to-inferred/convert-to-inferred", + "schema": "./src/generators/convert-to-inferred/schema.json", + "description": "Convert existing Cypress project(s) using `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`." } } } diff --git a/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.spec.ts b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.spec.ts new file mode 100644 index 0000000000000..78dfad54e21c6 --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.spec.ts @@ -0,0 +1,465 @@ +import { + getRelativeProjectJsonSchemaPath, + updateProjectConfiguration, +} from 'nx/src/generators/utils/project-configuration'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { convertToInferred } from './convert-to-inferred'; +import { + addProjectConfiguration as _addProjectConfiguration, + type ExpandedPluginConfiguration, + joinPathFragments, + type ProjectConfiguration, + type ProjectGraph, + readNxJson, + readProjectConfiguration, + type Tree, + updateNxJson, + writeJson, +} from '@nx/devkit'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { join } from 'node:path'; + +let fs: TempFs; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), + updateProjectConfiguration: jest + .fn() + .mockImplementation((tree, projectName, projectConfiguration) => { + function handleEmptyTargets( + projectName: string, + projectConfiguration: ProjectConfiguration + ): void { + if ( + projectConfiguration.targets && + !Object.keys(projectConfiguration.targets).length + ) { + // Re-order `targets` to appear after the `// target` comment. + delete projectConfiguration.targets; + projectConfiguration[ + '// targets' + ] = `to see all targets run: nx show project ${projectName} --web`; + projectConfiguration.targets = {}; + } else { + delete projectConfiguration['// targets']; + } + } + + const projectConfigFile = joinPathFragments( + projectConfiguration.root, + 'project.json' + ); + + if (!tree.exists(projectConfigFile)) { + throw new Error( + `Cannot update Project ${projectName} at ${projectConfiguration.root}. It either doesn't exist yet, or may not use project.json for configuration. Use \`addProjectConfiguration()\` instead if you want to create a new project.` + ); + } + handleEmptyTargets(projectName, projectConfiguration); + writeJson(tree, projectConfigFile, { + name: projectConfiguration.name ?? projectName, + $schema: getRelativeProjectJsonSchemaPath(tree, projectConfiguration), + ...projectConfiguration, + root: undefined, + }); + projectGraph.nodes[projectName].data = projectConfiguration; + }), +})); + +function addProjectConfiguration( + tree: Tree, + name: string, + project: ProjectConfiguration +) { + _addProjectConfiguration(tree, name, project); + projectGraph.nodes[name] = { + name: name, + type: project.projectType === 'application' ? 'app' : 'lib', + data: { + projectType: project.projectType, + root: project.root, + targets: project.targets, + }, + }; +} + +interface CreateCypressTestProjectOptions { + appName: string; + appRoot: string; + e2eTargetName: string; +} + +const defaultCreateCypressTestProjectOptions: CreateCypressTestProjectOptions = + { + appName: 'myapp-e2e', + appRoot: 'myapp-e2e', + e2eTargetName: 'e2e', + }; + +function createTestProject( + tree: Tree, + opts: Partial = defaultCreateCypressTestProjectOptions +) { + let projectOpts = { ...defaultCreateCypressTestProjectOptions, ...opts }; + const project: ProjectConfiguration = { + name: projectOpts.appName, + root: projectOpts.appRoot, + projectType: 'application', + targets: { + [projectOpts.e2eTargetName]: { + executor: '@nx/cypress:cypress', + options: { + cypressConfig: `${projectOpts.appRoot}/cypress.config.ts`, + testingType: `e2e`, + devServerTarget: 'myapp:serve', + }, + }, + }, + }; + + const cypressConfigContents = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: 'http://localhost:4200', + }, +});`; + + tree.write(`${projectOpts.appRoot}/cypress.config.ts`, cypressConfigContents); + fs.createFileSync( + `${projectOpts.appRoot}/cypress.config.ts`, + cypressConfigContents + ); + jest.doMock( + join(fs.tempDir, `${projectOpts.appRoot}/cypress.config.ts`), + () => ({ + default: { + e2e: { + baseUrl: 'http://localhost:4200', + }, + }, + }), + { + virtual: true, + } + ); + + addProjectConfiguration(tree, project.name, project); + fs.createFileSync( + `${projectOpts.appRoot}/project.json`, + JSON.stringify(project) + ); + return project; +} + +describe('Cypress - Convert Executors To Plugin', () => { + let tree: Tree; + + beforeEach(() => { + fs = new TempFs('cypress'); + tree = createTreeWithEmptyWorkspace(); + tree.root = fs.tempDir; + + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + fs.reset(); + }); + + describe('--project', () => { + it('should setup a new Cypress plugin and only migrate one specific project', async () => { + // ARRANGE + const existingProject = createTestProject(tree, { + appRoot: 'existing', + appName: 'existing', + e2eTargetName: 'e2e', + }); + const project = createTestProject(tree, { + e2eTargetName: 'test', + }); + const secondProject = createTestProject(tree, { + appRoot: 'second', + appName: 'second', + e2eTargetName: 'test', + }); + const thirdProject = createTestProject(tree, { + appRoot: 'third', + appName: 'third', + e2eTargetName: 'integration', + }); + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + // ACT + await convertToInferred(tree, { project: 'myapp-e2e', skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + const targetKeys = Object.keys(updatedProject.targets); + ['test'].forEach((key) => expect(targetKeys).not.toContain(key)); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const addedTestCypressPlugin = nxJsonPlugins.find((plugin) => { + if ( + typeof plugin !== 'string' && + plugin.plugin === '@nx/cypress/plugin' && + plugin.include?.length === 1 + ) { + return true; + } + }); + expect(addedTestCypressPlugin).toBeTruthy(); + expect( + (addedTestCypressPlugin as ExpandedPluginConfiguration).include + ).toEqual(['myapp-e2e/**/*']); + }); + }); + + describe('--all', () => { + it('should successfully migrate a project using Cypress executors to plugin', async () => { + const project = createTestProject(tree); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + const targetKeys = Object.keys(updatedProject.targets); + expect(targetKeys).not.toContain('e2e'); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasCypressPlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/cypress/plugin' + : plugin.plugin === '@nx/cypress/plugin' + ); + expect(hasCypressPlugin).toBeTruthy(); + if (typeof hasCypressPlugin !== 'string') { + [ + ['targetName', 'e2e'], + ['ciTargetName', 'e2e-ci'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasCypressPlugin.options[targetOptionName]).toEqual( + targetName + ); + }); + } + }); + + it('should setup Cypress plugin to match projects', async () => { + // ARRANGE + const project = createTestProject(tree, { + e2eTargetName: 'test', + }); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + const targetKeys = Object.keys(updatedProject.targets); + ['test'].forEach((key) => expect(targetKeys).not.toContain(key)); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasCypressPlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/cypress/plugin' + : plugin.plugin === '@nx/cypress/plugin' + ); + expect(hasCypressPlugin).toBeTruthy(); + if (typeof hasCypressPlugin !== 'string') { + [ + ['targetName', 'test'], + ['ciTargetName', 'e2e-ci'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasCypressPlugin.options[targetOptionName]).toEqual( + targetName + ); + }); + } + }); + + it('should setup a new Cypress plugin to match only projects migrated', async () => { + // ARRANGE + const existingProject = createTestProject(tree, { + appRoot: 'existing', + appName: 'existing', + e2eTargetName: 'e2e', + }); + const project = createTestProject(tree, { + e2eTargetName: 'test', + }); + const secondProject = createTestProject(tree, { + appRoot: 'second', + appName: 'second', + e2eTargetName: 'test', + }); + const thirdProject = createTestProject(tree, { + appRoot: 'third', + appName: 'third', + e2eTargetName: 'integration', + }); + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/cypress/plugin', + options: { + targetName: 'e2e', + ciTargetName: 'e2e-ci', + }, + }); + updateNxJson(tree, nxJson); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + const targetKeys = Object.keys(updatedProject.targets); + ['test'].forEach((key) => expect(targetKeys).not.toContain(key)); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const addedTestCypressPlugin = nxJsonPlugins.find((plugin) => { + if ( + typeof plugin !== 'string' && + plugin.plugin === '@nx/cypress/plugin' && + plugin.include?.length === 2 + ) { + return true; + } + }); + expect(addedTestCypressPlugin).toBeTruthy(); + expect( + (addedTestCypressPlugin as ExpandedPluginConfiguration).include + ).toEqual(['myapp-e2e/**/*', 'second/**/*']); + + const addedIntegrationCypressPlugin = nxJsonPlugins.find((plugin) => { + if ( + typeof plugin !== 'string' && + plugin.plugin === '@nx/cypress/plugin' && + plugin.include?.length === 1 + ) { + return true; + } + }); + expect(addedIntegrationCypressPlugin).toBeTruthy(); + expect( + (addedIntegrationCypressPlugin as ExpandedPluginConfiguration).include + ).toEqual(['third/**/*']); + }); + + it('should keep Cypress options in project.json', async () => { + // ARRANGE + const project = createTestProject(tree); + project.targets.e2e.options.runnerUi = true; + updateProjectConfiguration(tree, project.name, project); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.e2e).toMatchInlineSnapshot(` + { + "options": { + "runner-ui": true, + }, + } + `); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasCypressPlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/cypress/plugin' + : plugin.plugin === '@nx/cypress/plugin' + ); + expect(hasCypressPlugin).toBeTruthy(); + if (typeof hasCypressPlugin !== 'string') { + [ + ['targetName', 'e2e'], + ['ciTargetName', 'e2e-ci'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasCypressPlugin.options[targetOptionName]).toEqual( + targetName + ); + }); + } + }); + + it('should add Cypress options found in targetDefaults for the executor to the project.json', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['@nx/cypress:cypress'] = { + options: { + exit: false, + }, + }; + updateNxJson(tree, nxJson); + const project = createTestProject(tree); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.e2e).toMatchInlineSnapshot(` + { + "options": { + "no-exit": true, + }, + } + `); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasCypressPlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/cypress/plugin' + : plugin.plugin === '@nx/cypress/plugin' + ); + expect(hasCypressPlugin).toBeTruthy(); + if (typeof hasCypressPlugin !== 'string') { + [ + ['targetName', 'e2e'], + ['ciTargetName', 'e2e-ci'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasCypressPlugin.options[targetOptionName]).toEqual( + targetName + ); + }); + } + }); + }); +}); diff --git a/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts new file mode 100644 index 0000000000000..c7948f65a7e7e --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -0,0 +1,136 @@ +import { + CreateNodesContext, + createProjectGraphAsync, + formatFiles, + joinPathFragments, + type TargetConfiguration, + type Tree, +} from '@nx/devkit'; +import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { createNodes } from '../../plugins/plugin'; +import { targetOptionsToCliMap } from './lib/target-options-map'; +import { upsertBaseUrl } from './lib/upsert-baseUrl'; +import { addDevServerTargetToConfig } from './lib/add-dev-server-target-to-config'; +import { addExcludeSpecPattern } from './lib/add-exclude-spec-pattern'; + +interface Schema { + project?: string; + all?: boolean; + skipFormat?: boolean; +} + +export async function convertToInferred(tree: Tree, options: Schema) { + const projectGraph = await createProjectGraphAsync(); + await migrateExecutorToPlugin( + tree, + projectGraph, + '@nx/cypress:cypress', + '@nx/cypress/plugin', + (targetName) => ({ + targetName, + ciTargetName: 'e2e-ci', + }), + postTargetTransformer, + createNodes, + options.project + ); + + if (!options.skipFormat) { + await formatFiles(tree); + } +} + +function postTargetTransformer( + target: TargetConfiguration, + tree: Tree +): TargetConfiguration { + if (target.options) { + const configFilePath = target.options.cypressConfig; + + delete target.options.cypressConfig; + delete target.options.copyFiles; + delete target.options.skipServe; + + for (const key in targetOptionsToCliMap) { + if (target.options[key]) { + target.options[targetOptionsToCliMap[key]] = target.options[key]; + delete target.options[key]; + } + } + + if ('exit' in target.options && !target.options.exit) { + delete target.options.exit; + target.options['no-exit'] = true; + } + + if (target.options.testingType) { + delete target.options.testingType; + } + + if (target.options.watch) { + target.options.headed = true; + target.options['no-exit'] = true; + delete target.options.watch; + } + + if (target.options.baseUrl) { + upsertBaseUrl(tree, configFilePath, target.options.baseUrl); + delete target.options.baseUrl; + } + + if (target.options.devServerTarget) { + const webServerCommands: Record = { + default: `npx nx run ${target.options.devServerTarget}`, + }; + delete target.options.devServerTarget; + + if (target.configurations) { + for (const configuration in target.configurations) { + if (target.configurations[configuration]?.devServerTarget) { + webServerCommands[ + configuration + ] = `npx nx run ${target.configurations[configuration].devServerTarget}`; + delete target.configurations[configuration].devServerTarget; + } + } + } + + addDevServerTargetToConfig( + tree, + configFilePath, + webServerCommands, + target.configurations?.ci?.devServerTarget + ); + } + + if (target.options.ignoreTestFiles) { + addExcludeSpecPattern( + tree, + configFilePath, + target.options.ignoreTestFiles + ); + delete target.options.ignoreTestFiles; + } + + if (Object.keys(target.options).length === 0) { + delete target.options; + } + if ( + target.configurations && + Object.keys(target.configurations).length !== 0 + ) { + for (const configuration in target.configurations) { + if (Object.keys(target.configurations[configuration]).length === 0) { + delete target.configurations[configuration]; + } + } + if (Object.keys(target.configurations).length === 0) { + delete target.configurations; + } + } + } + + return target; +} + +export default convertToInferred; diff --git a/packages/cypress/src/generators/convert-to-inferred/lib/add-dev-server-target-to-config.spec.ts b/packages/cypress/src/generators/convert-to-inferred/lib/add-dev-server-target-to-config.spec.ts new file mode 100644 index 0000000000000..9c4491311ce2d --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/lib/add-dev-server-target-to-config.spec.ts @@ -0,0 +1,211 @@ +import type { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { addDevServerTargetToConfig } from './add-dev-server-target-to-config'; + +describe('addDevServerTargetToConfig', () => { + let tree: Tree; + const configFilePath = 'cypress.config.ts'; + const configFileContents = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, +});`; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write(configFilePath, configFileContents); + }); + + describe('devServerTarget only', () => { + it('should add webServerCommands when it does not exist', () => { + // ACT + addDevServerTargetToConfig(tree, configFilePath, { + default: 'npx nx run myorg:serve', + }); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, {webServerCommands: {"default":"npx nx run myorg:serve"}, cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + + it('should do nothing if the webServerCommands exists and matches the devServerTarget', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {default: "npx nx run myorg:serve"} }), + baseUrl: "http://localhost:4200", + }, +});` + ); + // ACT + addDevServerTargetToConfig(tree, configFilePath, { + default: 'npx nx run myorg:serve', + }); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {"default":"npx nx run myorg:serve"} }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + + it('should update the webServerCommands if it does not match', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {default: "npx nx run test:serve"} }), + baseUrl: "http://localhost:4200", + }, +});` + ); + // ACT + addDevServerTargetToConfig(tree, configFilePath, { + default: 'npx nx run myorg:serve', + }); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {"default":"npx nx run myorg:serve"} }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + }); + + describe('devServerTarget and ci.devServerTarget', () => { + it('should add webServerCommands and ciWebServerCommand when it does not exist', () => { + // ACT + addDevServerTargetToConfig( + tree, + configFilePath, + { default: 'npx nx run myorg:serve' }, + 'myorg:static-serve' + ); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, {ciWebServerCommand: "npx nx run myorg:static-serve",webServerCommands: {"default":"npx nx run myorg:serve"}, cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + + it('should do nothing if the webServerCommands and ciWebServerCommand exists and matches the devServerTarget', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {default: "npx nx run myorg:serve"}, ciWebServerCommand: "npx nx run myorg:static-serve" }), + baseUrl: "http://localhost:4200", + }, +});` + ); + // ACT + addDevServerTargetToConfig( + tree, + configFilePath, + { default: 'npx nx run myorg:serve' }, + 'myorg:static-serve' + ); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {"default":"npx nx run myorg:serve"}, ciWebServerCommand: "npx nx run myorg:static-serve" }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + + it('should update the webServerCommands and ciWebServerCommand if it does not match', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {default: "npx nx run test:serve"}, ciWebServerCommand: "npx nx run test:static-serve" }), + baseUrl: "http://localhost:4200", + }, +});` + ); + // ACT + addDevServerTargetToConfig( + tree, + configFilePath, + { + default: 'npx nx run myorg:serve', + production: 'npx nx run myorg:serve:production', + ci: 'npx nx run myorg-static-serve', + }, + 'myorg:static-serve' + ); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: {"default":"npx nx run myorg:serve","production":"npx nx run myorg:serve:production","ci":"npx nx run myorg-static-serve"}, ciWebServerCommand: "npx nx run myorg:static-serve" }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + }); +}); diff --git a/packages/cypress/src/generators/convert-to-inferred/lib/add-dev-server-target-to-config.ts b/packages/cypress/src/generators/convert-to-inferred/lib/add-dev-server-target-to-config.ts new file mode 100644 index 0000000000000..b069f4df481b9 --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/lib/add-dev-server-target-to-config.ts @@ -0,0 +1,106 @@ +import type { Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +/** + * Add or update the webServerCommands and ciWebServerCommand options in the Cypress Config + * Scenarios Covered: + * 1. Only devServerTarget Exists + * 2. devServerTarget and configuration.ci.devServerTarget Exists + * + * For each, the following scenarios are covered: + * a. The command is not listed in the config, so it is added + * b. Replace the existing webServerCommands with the value passed in + */ +export function addDevServerTargetToConfig( + tree: Tree, + configFilePath: string, + webServerCommands: Record, + ciDevServerTarget?: string +) { + let configFileContents = tree.read(configFilePath, 'utf-8'); + + let ast = tsquery.ast(configFileContents); + + const NX_E2E_PRESET_OPTIONS_SELECTOR = + 'PropertyAssignment:has(Identifier[name=e2e]) CallExpression:has(Identifier[name=nxE2EPreset]) > ObjectLiteralExpression'; + const nxE2ePresetOptionsNodes = tsquery(ast, NX_E2E_PRESET_OPTIONS_SELECTOR, { + visitAllChildren: true, + }); + if (nxE2ePresetOptionsNodes.length !== 0) { + let nxE2ePresetOptionsNode = nxE2ePresetOptionsNodes[0]; + const WEB_SERVER_COMMANDS_SELECTOR = + 'PropertyAssignment:has(Identifier[name=webServerCommands])'; + const webServerCommandsNodes = tsquery( + nxE2ePresetOptionsNode, + WEB_SERVER_COMMANDS_SELECTOR, + { visitAllChildren: true } + ); + if (webServerCommandsNodes.length !== 0) { + // Already exists, replace it + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + webServerCommandsNodes[0].getStart() + )}webServerCommands: ${JSON.stringify( + webServerCommands + )}${configFileContents.slice(webServerCommandsNodes[0].getEnd())}` + ); + } else { + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + nxE2ePresetOptionsNode.getStart() + 1 + )}webServerCommands: ${JSON.stringify( + webServerCommands + )},${configFileContents.slice(nxE2ePresetOptionsNode.getStart() + 1)}` + ); + } + + if (ciDevServerTarget) { + configFileContents = tree.read(configFilePath, 'utf-8'); + ast = tsquery.ast(configFileContents); + nxE2ePresetOptionsNode = tsquery(ast, NX_E2E_PRESET_OPTIONS_SELECTOR, { + visitAllChildren: true, + })[0]; + + const CI_WEB_SERVER_COMMANDS_SELECTOR = + 'PropertyAssignment:has(Identifier[name=ciWebServerCommand])'; + const ciWebServerCommandsNodes = tsquery( + nxE2ePresetOptionsNode, + CI_WEB_SERVER_COMMANDS_SELECTOR, + { visitAllChildren: true } + ); + + if (ciWebServerCommandsNodes.length !== 0) { + const ciWebServerCommandNode = + ciWebServerCommandsNodes[0].getChildAt(2); + const ciWebServerCommand = ciWebServerCommandNode + .getText() + .replace(/["']/g, ''); + if (!ciWebServerCommand.includes(ciDevServerTarget)) { + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + ciWebServerCommandNode.getStart() + )}"npx nx run ${ciDevServerTarget}"${configFileContents.slice( + ciWebServerCommandNode.getEnd() + )}` + ); + } + } else { + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + nxE2ePresetOptionsNode.getStart() + 1 + )}ciWebServerCommand: "npx nx run ${ciDevServerTarget}",${configFileContents.slice( + nxE2ePresetOptionsNode.getStart() + 1 + )}` + ); + } + } + } +} diff --git a/packages/cypress/src/generators/convert-to-inferred/lib/add-exclude-spec-pattern.spec.ts b/packages/cypress/src/generators/convert-to-inferred/lib/add-exclude-spec-pattern.spec.ts new file mode 100644 index 0000000000000..df4897c88290c --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/lib/add-exclude-spec-pattern.spec.ts @@ -0,0 +1,200 @@ +import type { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { addExcludeSpecPattern } from './add-exclude-spec-pattern'; + +describe('addExcludeSpecPattern', () => { + let tree: Tree; + const configFilePath = 'cypress.config.ts'; + const configFileContents = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, +});`; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write(configFilePath, configFileContents); + }); + + it('should add excludeSpecPattern string if it does not exist', () => { + // ACT + addExcludeSpecPattern(tree, configFilePath, 'mytests/**/*.spec.ts'); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: {excludeSpecPattern: "mytests/**/*.spec.ts", + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + + it('should add excludeSpecPattern array if it does not exist', () => { + // ACT + addExcludeSpecPattern(tree, configFilePath, [ + 'mytests/**/*.spec.ts', + 'mysecondtests/**/*.spec.ts', + ]); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: {excludeSpecPattern: ["mytests/**/*.spec.ts","mysecondtests/**/*.spec.ts"], + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); + + it('should update the existing excludeSpecPattern if one exists when using string', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: "somefile.spec.ts" + }, +});` + ); + + // ACT + addExcludeSpecPattern(tree, configFilePath, 'mytests/**/*.spec.ts'); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: ["mytests/**/*.spec.ts"] + }, + });" + `); + }); + + it('should update the existing excludeSpecPattern if one exists when using array', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: ["somefile.spec.ts"] + }, +});` + ); + + // ACT + addExcludeSpecPattern(tree, configFilePath, ['mytests/**/*.spec.ts']); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: ["mytests/**/*.spec.ts"] + }, + });" + `); + }); + + it('should update the existing excludeSpecPattern if one exists when using string with an array of new options', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: "somefile.spec.ts" + }, +});` + ); + + // ACT + addExcludeSpecPattern(tree, configFilePath, [ + 'mytests/**/*.spec.ts', + 'mysecondtests/**/*.spec.ts', + ]); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: ["mytests/**/*.spec.ts","mysecondtests/**/*.spec.ts"] + }, + });" + `); + }); + + it('should update the existing excludeSpecPattern if one exists when using array with a new pattern string', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: ["somefile.spec.ts"] + }, +});` + ); + + // ACT + addExcludeSpecPattern(tree, configFilePath, 'mytests/**/*.spec.ts'); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + excludeSpecPattern: ["mytests/**/*.spec.ts"] + }, + });" + `); + }); +}); diff --git a/packages/cypress/src/generators/convert-to-inferred/lib/add-exclude-spec-pattern.ts b/packages/cypress/src/generators/convert-to-inferred/lib/add-exclude-spec-pattern.ts new file mode 100644 index 0000000000000..7174987eb4f8c --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/lib/add-exclude-spec-pattern.ts @@ -0,0 +1,55 @@ +import type { Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +export function addExcludeSpecPattern( + tree: Tree, + configFilePath: string, + excludeSpecPattern: string | string[] +) { + let configFileContents = tree.read(configFilePath, 'utf-8'); + + let ast = tsquery.ast(configFileContents); + + const E2E_CONFIG_SELECTOR = + 'PropertyAssignment:has(Identifier[name=e2e]) > ObjectLiteralExpression'; + const e2eConfigNodes = tsquery(ast, E2E_CONFIG_SELECTOR, { + visitAllChildren: true, + }); + if (e2eConfigNodes.length !== 0) { + const e2eConfigNode = e2eConfigNodes[0]; + const EXCLUDE_SPEC_PATTERN_SELECTOR = + 'PropertyAssignment:has(Identifier[name="excludeSpecPattern"])'; + const excludeSpecPatternNodes = tsquery( + e2eConfigNode, + EXCLUDE_SPEC_PATTERN_SELECTOR, + { visitAllChildren: true } + ); + + if (excludeSpecPatternNodes.length !== 0) { + const excludeSpecPatternNode = excludeSpecPatternNodes[0]; + + let updatedExcludePattern = Array.isArray(excludeSpecPattern) + ? excludeSpecPattern + : [excludeSpecPattern]; + + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + excludeSpecPatternNode.getStart() + )}excludeSpecPattern: ${JSON.stringify( + updatedExcludePattern + )}${configFileContents.slice(excludeSpecPatternNode.getEnd())}` + ); + } else { + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + e2eConfigNode.getStart() + 1 + )}excludeSpecPattern: ${JSON.stringify( + excludeSpecPattern + )},${configFileContents.slice(e2eConfigNode.getStart() + 1)}` + ); + } + } +} diff --git a/packages/cypress/src/generators/convert-to-inferred/lib/target-options-map.ts b/packages/cypress/src/generators/convert-to-inferred/lib/target-options-map.ts new file mode 100644 index 0000000000000..e0e45e560f938 --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/lib/target-options-map.ts @@ -0,0 +1,18 @@ +export const targetOptionsToCliMap = { + headed: 'headed', + headless: 'headless', + key: 'key', + record: 'record', + parallel: 'parallel', + browser: 'browser', + env: 'env', + spec: 'spec', + ciBuildId: 'ci-build-id', + group: 'group', + reporter: 'reporter', + reporterOptions: 'reporter-options', + tag: 'tag', + port: 'port', + quiet: 'quiet', + runnerUi: 'runner-ui', +}; diff --git a/packages/cypress/src/generators/convert-to-inferred/lib/upsert-baseUrl.spec.ts b/packages/cypress/src/generators/convert-to-inferred/lib/upsert-baseUrl.spec.ts new file mode 100644 index 0000000000000..665b43530b5ee --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/lib/upsert-baseUrl.spec.ts @@ -0,0 +1,78 @@ +import type { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { upsertBaseUrl } from './upsert-baseUrl'; + +describe('upsertBaseUrl', () => { + let tree: Tree; + const configFilePath = 'cypress.config.ts'; + const configFileContents = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, +});`; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write(configFilePath, configFileContents); + }); + + it('should do nothing if the baseUrl value exists and matches', () => { + // ACT + upsertBaseUrl(tree, configFilePath, 'http://localhost:4200'); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toEqual(configFileContents); + }); + + it('should update the config if the baseUrl value exists and does not match', () => { + // ACT + upsertBaseUrl(tree, configFilePath, 'http://localhost:4201'); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4201", + }, + });" + `); + }); + + it('should add the baseUrl property if it does not exist', () => { + // ARRANGE + tree.write( + configFilePath, + `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + }, +});` + ); + // ACT + upsertBaseUrl(tree, configFilePath, 'http://localhost:4200'); + + // ASSERT + expect(tree.read(configFilePath, 'utf-8')).toMatchInlineSnapshot(` + "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + import { defineConfig } from 'cypress'; + + export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { cypressDir: 'src' }), + baseUrl: "http://localhost:4200", + }, + });" + `); + }); +}); diff --git a/packages/cypress/src/generators/convert-to-inferred/lib/upsert-baseUrl.ts b/packages/cypress/src/generators/convert-to-inferred/lib/upsert-baseUrl.ts new file mode 100644 index 0000000000000..f2d443c7caf92 --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/lib/upsert-baseUrl.ts @@ -0,0 +1,56 @@ +import type { Tree } from '@nx/devkit'; + +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function upsertBaseUrl( + tree: Tree, + configFilePath: string, + baseUrlValueInProject: string +) { + const configFileContents = tree.read(configFilePath, 'utf-8'); + + const ast = tsquery.ast(configFileContents); + const BASE_URL_SELECTOR = + 'PropertyAssignment:has(Identifier[name=e2e]) PropertyAssignment:has(Identifier[name="baseUrl"])'; + + const baseUrlNodes = tsquery(ast, BASE_URL_SELECTOR, { + visitAllChildren: true, + }); + if (baseUrlNodes.length !== 0) { + // The property exists in the config + const baseUrlValueNode = baseUrlNodes[0].getChildAt(2); + const baseUrlValue = baseUrlValueNode.getText().replace(/(["'])/, ''); + + if (baseUrlValue === baseUrlValueInProject) { + return; + } + + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + baseUrlValueNode.getStart() + )}"${baseUrlValueInProject}"${configFileContents.slice( + baseUrlValueNode.getEnd() + )}` + ); + } else { + const E2E_OBJECT_SELECTOR = + 'PropertyAssignment:has(Identifier[name=e2e]) ObjectLiteralExpression'; + + const e2eConfigNodes = tsquery(ast, E2E_OBJECT_SELECTOR, { + visitAllChildren: true, + }); + if (e2eConfigNodes.length !== 0) { + const e2eConfigNode = e2eConfigNodes[0]; + tree.write( + configFilePath, + `${configFileContents.slice( + 0, + e2eConfigNode.getEnd() - 1 + )}baseUrl: "${baseUrlValueInProject}", + ${configFileContents.slice(e2eConfigNode.getEnd() - 1)}` + ); + } + } +} diff --git a/packages/cypress/src/generators/convert-to-inferred/schema.json b/packages/cypress/src/generators/convert-to-inferred/schema.json new file mode 100644 index 0000000000000..3b0f5f155b100 --- /dev/null +++ b/packages/cypress/src/generators/convert-to-inferred/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxCypressConvertToInferred", + "description": "Convert existing Cypress project(s) using `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`.", + "title": "Convert Cypress project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/cypress:cypress` executor to use `@nx/cypress/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + } +} diff --git a/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts b/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts index 0f0078d0fe58b..113b49da14aa5 100644 --- a/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts +++ b/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts @@ -32,7 +32,8 @@ const { type PluginOptionsBuilder = (targetName: string) => T; type PostTargetTransformer = ( - targetConfiguration: TargetConfiguration + targetConfiguration: TargetConfiguration, + tree?: Tree ) => TargetConfiguration; type SkipTargetFilter = ( targetConfiguration: TargetConfiguration @@ -129,7 +130,7 @@ class ExecutorToPluginMigrator { delete projectTarget.executor; deleteMatchingProperties(projectTarget, createdTarget); - projectTarget = this.#postTargetTransformer(projectTarget); + projectTarget = this.#postTargetTransformer(projectTarget, this.tree); if ( projectTarget.options &&