From 2b6f5a09f8154ae1cce60de6a7e1578ceb2c94d3 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Wed, 7 Aug 2024 22:25:26 -0400 Subject: [PATCH] feat(detox): add convert-to-inferred generator for Detox --- docs/generated/manifests/menus.json | 8 + docs/generated/manifests/nx-api.json | 9 + docs/generated/packages-metadata.json | 9 + .../detox/generators/convert-to-inferred.json | 30 ++ docs/shared/reference/sitemap.md | 1 + packages/detox/generators.json | 5 + .../application/application.spec.ts | 20 +- .../files/app/.detoxrc.json.template | 6 +- .../convert-to-inferred.spec.ts | 432 ++++++++++++++++++ .../convert-to-inferred.ts | 112 +++++ .../lib/post-target-transformer.ts | 70 +++ .../lib/process-build-options.ts | 35 ++ .../lib/process-test-options.ts | 76 +++ .../convert-to-inferred/schema.json | 19 + .../application/application.spec.ts | 12 +- .../application/application.spec.ts | 8 +- .../rename-upgrade-target-name.ts | 13 +- 17 files changed, 830 insertions(+), 35 deletions(-) create mode 100644 docs/generated/packages/detox/generators/convert-to-inferred.json create mode 100644 packages/detox/src/generators/convert-to-inferred/convert-to-inferred.spec.ts create mode 100644 packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts create mode 100644 packages/detox/src/generators/convert-to-inferred/lib/post-target-transformer.ts create mode 100644 packages/detox/src/generators/convert-to-inferred/lib/process-build-options.ts create mode 100644 packages/detox/src/generators/convert-to-inferred/lib/process-test-options.ts create mode 100644 packages/detox/src/generators/convert-to-inferred/schema.json diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index c13b93383309c..8bcdabd6db8eb 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -7227,6 +7227,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "convert-to-inferred", + "path": "/nx-api/detox/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 a92ccf1cdd902..8a23e50a27bfa 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -607,6 +607,15 @@ "originalFilePath": "/packages/detox/src/generators/application/schema.json", "path": "/nx-api/detox/generators/application", "type": "generator" + }, + "/nx-api/detox/generators/convert-to-inferred": { + "description": "Convert existing Detox project(s) using `@nx/detox:*` executors to use `@nx/detox/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "file": "generated/packages/detox/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/detox/src/generators/convert-to-inferred/schema.json", + "path": "/nx-api/detox/generators/convert-to-inferred", + "type": "generator" } }, "path": "/nx-api/detox" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index fe202d2685c1c..6dde7664767a4 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -598,6 +598,15 @@ "originalFilePath": "/packages/detox/src/generators/application/schema.json", "path": "detox/generators/application", "type": "generator" + }, + { + "description": "Convert existing Detox project(s) using `@nx/detox:*` executors to use `@nx/detox/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "file": "generated/packages/detox/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/detox/src/generators/convert-to-inferred/schema.json", + "path": "detox/generators/convert-to-inferred", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/detox/generators/convert-to-inferred.json b/docs/generated/packages/detox/generators/convert-to-inferred.json new file mode 100644 index 0000000000000..8ba164c53ebba --- /dev/null +++ b/docs/generated/packages/detox/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": "NxDetoxConvertToInferred", + "description": "Convert existing Detox project(s) using `@nx/detox:*` executors to use `@nx/detox/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Detox project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/detox:*` executors to use `@nx/detox/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + }, + "presets": [] + }, + "description": "Convert existing Detox project(s) using `@nx/detox:*` executors to use `@nx/detox/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "implementation": "/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts", + "aliases": [], + "hidden": false, + "path": "/packages/detox/src/generators/convert-to-inferred/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index a6116d3727005..9b22caecfb956 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -387,6 +387,7 @@ - [generators](/nx-api/detox/generators) - [init](/nx-api/detox/generators/init) - [application](/nx-api/detox/generators/application) + - [convert-to-inferred](/nx-api/detox/generators/convert-to-inferred) - [devkit](/nx-api/devkit) - [documents](/nx-api/devkit/documents) - [Overview](/nx-api/devkit/documents/nx_devkit) diff --git a/packages/detox/generators.json b/packages/detox/generators.json index 56e5ba7673cec..ed4d4e9e19b0d 100644 --- a/packages/detox/generators.json +++ b/packages/detox/generators.json @@ -15,6 +15,11 @@ "aliases": ["app"], "x-type": "application", "description": "Create a Detox application." + }, + "convert-to-inferred": { + "factory": "./src/generators/convert-to-inferred/convert-to-inferred", + "schema": "./src/generators/convert-to-inferred/schema.json", + "description": "Convert existing Detox project(s) using `@nx/detox:*` executors to use `@nx/detox/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target." } } } diff --git a/packages/detox/src/generators/application/application.spec.ts b/packages/detox/src/generators/application/application.spec.ts index b0e1ee0e5b9cd..1bca1d9098cb3 100644 --- a/packages/detox/src/generators/application/application.spec.ts +++ b/packages/detox/src/generators/application/application.spec.ts @@ -64,14 +64,14 @@ describe('detox application generator', () => { binaryPath: '../my-app/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.release': { binaryPath: '../my-app/ios/build/Build/Products/Release-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); @@ -136,14 +136,14 @@ describe('detox application generator', () => { binaryPath: '../my-dir/my-app/ios/build/Build/Products/Debug-iphonesimulator/MyDirMyApp.app', build: - "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.release': { binaryPath: '../my-dir/my-app/ios/build/Build/Products/Release-iphonesimulator/MyDirMyApp.app', build: - "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); @@ -208,14 +208,14 @@ describe('detox application generator', () => { binaryPath: '../my-dir/my-app/ios/build/Build/Products/Debug-iphonesimulator/MyDirMyApp.app', build: - "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.release': { binaryPath: '../my-dir/my-app/ios/build/Build/Products/Release-iphonesimulator/MyDirMyApp.app', build: - "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); @@ -279,14 +279,14 @@ describe('detox application generator', () => { binaryPath: '../../my-dir/my-app/ios/build/Build/Products/Debug-iphonesimulator/MyDirMyApp.app', build: - "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.release': { binaryPath: '../../my-dir/my-app/ios/build/Build/Products/Release-iphonesimulator/MyDirMyApp.app', build: - "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); @@ -355,7 +355,7 @@ describe('detox application generator', () => { binaryPath: '../../my-dir/my-app/ios/build/Build/Products/Debug-iphonesimulator/MyDirMyApp.app', build: - "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.local': { @@ -368,7 +368,7 @@ describe('detox application generator', () => { binaryPath: '../../my-dir/my-app/ios/build/Build/Products/Release-iphonesimulator/MyDirMyApp.app', build: - "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../../my-dir/my-app/ios && xcodebuild -workspace MyDirMyApp.xcworkspace -scheme MyDirMyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); diff --git a/packages/detox/src/generators/application/files/app/.detoxrc.json.template b/packages/detox/src/generators/application/files/app/.detoxrc.json.template index 622d7e779da76..01d9f72d300de 100644 --- a/packages/detox/src/generators/application/files/app/.detoxrc.json.template +++ b/packages/detox/src/generators/application/files/app/.detoxrc.json.template @@ -11,12 +11,12 @@ "apps": { "ios.debug": { "type": "ios.app", - "build": "cd <%= offsetFromRoot %><%= appRoot %>/ios && xcodebuild -workspace <%= appClassName %>.xcworkspace -scheme <%= appClassName %> -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "build": "cd <%= offsetFromRoot %><%= appRoot %>/ios && xcodebuild -workspace <%= appClassName %>.xcworkspace -scheme <%= appClassName %> -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", "binaryPath": "<%= offsetFromRoot %><%= appRoot %>/ios/build/Build/Products/Debug-iphonesimulator/<%= appClassName %>.app" }, "ios.release": { "type": "ios.app", - "build": "cd <%= offsetFromRoot %><%= appRoot %>/ios && xcodebuild -workspace <%= appClassName %>.xcworkspace -scheme <%= appClassName %> -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "build": "cd <%= offsetFromRoot %><%= appRoot %>/ios && xcodebuild -workspace <%= appClassName %>.xcworkspace -scheme <%= appClassName %> -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", "binaryPath": "<%= offsetFromRoot %><%= appRoot %>/ios/build/Build/Products/Release-iphonesimulator/<%= appClassName %>.app" }, <% if (framework === 'expo') { %> @@ -48,7 +48,7 @@ "simulator": { "type": "ios.simulator", "device": { - "type": "iPhone 14" + "type": "iPhone 15 Plus" } }, "emulator": { diff --git a/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.spec.ts b/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.spec.ts new file mode 100644 index 0000000000000..4ee0305479ea9 --- /dev/null +++ b/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.spec.ts @@ -0,0 +1,432 @@ +import { + addProjectConfiguration, + joinPathFragments, + type ProjectConfiguration, + type ProjectGraph, + readNxJson, + readProjectConfiguration, + type Tree, + writeJson, +} from '@nx/devkit'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { join } from 'node:path'; +import { getRelativeProjectJsonSchemaPath } from 'nx/src/generators/utils/project-configuration'; +import { convertToInferred } from './convert-to-inferred'; + +let fs: TempFs; +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest + .fn() + .mockImplementation(() => Promise.resolve(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; + }), +})); +jest.mock('nx/src/devkit-internals', () => ({ + ...jest.requireActual('nx/src/devkit-internals'), + getExecutorInformation: jest + .fn() + .mockImplementation((pkg, ...args) => + jest + .requireActual('nx/src/devkit-internals') + .getExecutorInformation('@nx/webpack', ...args) + ), +})); + +function addProject(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 ProjectOptions { + appName: string; + appRoot: string; + buildAndroidTargetName: string; + buildIosTargetName: string; + testAndroidTargetName: string; + testIosTargetName: string; +} + +const defaultProjectOptions: ProjectOptions = { + appName: 'demo-e2e', + appRoot: 'apps/demo-e2e', + buildAndroidTargetName: 'build-android', + buildIosTargetName: 'build-ios', + testAndroidTargetName: 'test-android', + testIosTargetName: 'test-ios', +}; + +const detoxConfig = { + testRunner: { + args: { + $0: 'jest', + config: './jest.config.json', + }, + jest: { + setupTimeout: 120000, + }, + }, + apps: { + 'ios.debug': { + type: 'ios.app', + build: + "cd ../demo/ios && xcodebuild -workspace Demo.xcworkspace -scheme Demo -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", + binaryPath: + '../demo/ios/build/Build/Products/Debug-iphonesimulator/Demo.app', + }, + 'ios.release': { + type: 'ios.app', + build: + "cd ../demo/ios && xcodebuild -workspace Demo.xcworkspace -scheme Demo -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", + binaryPath: + '../demo/ios/build/Build/Products/Release-iphonesimulator/Demo.app', + }, + + 'android.debug': { + type: 'android.apk', + build: + 'cd ../demo/android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', + binaryPath: '../demo/android/app/build/outputs/apk/debug/app-debug.apk', + }, + 'android.release': { + type: 'android.apk', + build: + 'cd ../demo/android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release', + binaryPath: + '../demo/android/app/build/outputs/apk/release/app-release.apk', + }, + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { + type: 'iPhone 15 Plus', + }, + }, + emulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_4a_API_30', + }, + }, + }, + configurations: { + 'ios.sim.release': { + device: 'simulator', + app: 'ios.release', + }, + 'ios.sim.debug': { + device: 'simulator', + app: 'ios.debug', + }, + + 'android.emu.release': { + device: 'emulator', + app: 'android.release', + }, + 'android.emu.debug': { + device: 'emulator', + app: 'android.debug', + }, + }, +}; + +function writeDetoxConfig(tree: Tree, projectRoot: string) { + tree.write(`${projectRoot}/.detoxrc.json`, JSON.stringify(detoxConfig)); + fs.createFileSync( + `${projectRoot}/.detoxrc.json`, + JSON.stringify(detoxConfig) + ); + jest.doMock( + join(fs.tempDir, projectRoot, '.detoxrc.json'), + () => detoxConfig, + { + virtual: true, + } + ); +} + +function createProject( + tree: Tree, + options: Partial = {}, + extraTargetOptions?: Record>, + extraTargetConfigurations?: Record< + string, + Record> + > +) { + let projectOptions = { ...defaultProjectOptions, ...options }; + const project: ProjectConfiguration = { + name: projectOptions.appName, + root: projectOptions.appRoot, + projectType: 'application', + targets: { + [projectOptions.buildAndroidTargetName]: { + executor: '@nx/detox:build', + options: { + detoxConfiguration: 'android.emu.debug', + ...extraTargetOptions?.[projectOptions.buildAndroidTargetName], + }, + configurations: { + production: { + ...extraTargetConfigurations?.[ + projectOptions.buildAndroidTargetName + ].production, + detoxConfiguration: 'android.emu.release', + }, + }, + }, + [projectOptions.buildIosTargetName]: { + executor: '@nx/detox:build', + options: { + detoxConfiguration: 'ios.sim.debug', + ...extraTargetOptions?.[projectOptions.buildIosTargetName], + }, + configurations: { + production: { + ...extraTargetConfigurations?.[projectOptions.buildIosTargetName] + .production, + detoxConfiguration: 'ios.sim.release', + }, + }, + }, + [projectOptions.testAndroidTargetName]: { + executor: '@nx/detox:test', + options: { + detoxConfiguration: 'android.emu.debug', + buildTarget: 'demo-e2e:build-android', + ...extraTargetOptions?.[projectOptions.testAndroidTargetName], + }, + configurations: { + production: { + detoxConfiguration: 'android.emu.release', + buildTarget: 'demo-e2e:build-android:production', + ...extraTargetConfigurations?.[projectOptions.testAndroidTargetName] + .production, + }, + }, + }, + [projectOptions.testIosTargetName]: { + executor: '@nx/detox:test', + options: { + detoxConfiguration: 'ios.sim.debug', + buildTarget: 'demo-e2e:build-ios', + ...extraTargetOptions?.[projectOptions.testIosTargetName], + }, + configurations: { + production: { + detoxConfiguration: 'ios.sim.release', + buildTarget: 'demo-e2e:build-ios:production', + ...extraTargetConfigurations?.[projectOptions.testIosTargetName] + .production, + }, + }, + }, + }, + }; + + addProject(tree, project.name, project); + fs.createFileSync( + `${projectOptions.appRoot}/project.json`, + JSON.stringify(project) + ); + + return project; +} + +describe('convert-to-inferred', () => { + let tree: Tree; + + beforeEach(() => { + fs = new TempFs('detox'); + tree = createTreeWithEmptyWorkspace(); + tree.root = fs.tempDir; + + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + fs.cleanup(); + jest.resetModules(); + }); + + it('should convert project to use inference plugin', async () => { + const project = createProject( + tree, + {}, + { + 'build-android': { + configPath: '.detoxrc.dev.json', + }, + 'build-ios': { + configPath: '.detoxrc.dev.json', + }, + 'test-android': { + configPath: '.detoxrc.dev.json', + }, + 'test-ios': { + configPath: '.detoxrc.dev.json', + }, + }, + { + 'build-android': { + production: { configPath: '.detoxrc.prod.json' }, + }, + 'build-ios': { + production: { configPath: '.detoxrc.prod.json' }, + }, + 'test-android': { + production: { configPath: '.detoxrc.prod.json' }, + }, + 'test-ios': { + production: { configPath: '.detoxrc.prod.json' }, + }, + } + ); + writeDetoxConfig(tree, project.root); + + await convertToInferred(tree, { project: project.name }); + + const projectConfig = readProjectConfiguration(tree, project.name); + const nxJson = readNxJson(tree); + expect(nxJson.plugins).toEqual([ + { + options: { + buildTargetName: 'build', + startTargetName: 'start', + testTargetName: 'test', + }, + plugin: '@nx/detox/plugin', + }, + ]); + expect(projectConfig.targets['build-android']).toEqual({ + command: 'nx run demo-e2e:build', + options: { + args: [ + '--args="-c android.emu.debug"', + '--config-path', + '.detoxrc.dev.json', + ], + }, + configurations: { + production: { + args: [ + '--args="-c android.emu.release"', + '--config-path', + '.detoxrc.prod.json', + ], + }, + }, + }); + expect(projectConfig.targets['build-ios']).toEqual({ + command: 'nx run demo-e2e:build', + options: { + args: [ + '--args="-c ios.sim.debug"', + '--config-path', + '.detoxrc.dev.json', + ], + }, + configurations: { + production: { + args: [ + '--args="-c ios.sim.release"', + '--config-path', + '.detoxrc.prod.json', + ], + }, + }, + }); + expect(projectConfig.targets['test-android']).toEqual({ + command: 'nx run demo-e2e:test', + options: { + args: [ + '--args="-c android.emu.debug"', + '--config-path', + '.detoxrc.dev.json', + ], + }, + configurations: { + production: { + args: [ + '--args="-c android.emu.release"', + '--config-path', + '.detoxrc.prod.json', + ], + }, + }, + }); + expect(projectConfig.targets['test-ios']).toEqual({ + command: 'nx run demo-e2e:test', + options: { + args: [ + '--args="-c ios.sim.debug"', + '--config-path', + '.detoxrc.dev.json', + ], + }, + configurations: { + production: { + args: [ + '--args="-c ios.sim.release"', + '--config-path', + '.detoxrc.prod.json', + ], + }, + }, + }); + }); +}); diff --git a/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts new file mode 100644 index 0000000000000..57bd700154bed --- /dev/null +++ b/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -0,0 +1,112 @@ +import { + createProjectGraphAsync, + formatFiles, + readNxJson, + readProjectConfiguration, + type Tree, + updateNxJson, +} from '@nx/devkit'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; +import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { createNodes } from '../../plugins/plugin'; +import { processBuildOptions } from './lib/process-build-options'; +import { postTargetTransformer } from './lib/post-target-transformer'; +import { processTestOptions } from './lib/process-test-options'; + +interface Schema { + project?: string; + skipFormat?: boolean; +} + +export async function convertToInferred(tree: Tree, options: Schema) { + const projectGraph = await createProjectGraphAsync(); + const migrationLogs = new AggregatedLog(); + const migratedProjects = await migrateProjectExecutorsToPluginV1( + tree, + projectGraph, + '@nx/detox/plugin', + createNodes, + { + buildTargetName: 'build', + startTargetName: 'start', + testTargetName: 'test', + }, + [ + { + executors: ['@nx/detox:build'], + postTargetTransformer: postTargetTransformer( + migrationLogs, + processBuildOptions + ), + targetPluginOptionMapper: (targetName) => ({ + buildTargetName: targetName, // We should use "build" instead of "build-ios" or "build-android". We'll handle this later. + }), + }, + { + executors: ['@nx/detox:test'], + postTargetTransformer: postTargetTransformer( + migrationLogs, + processTestOptions + ), + targetPluginOptionMapper: (targetName) => ({ + testTargetName: targetName, // We should use "test" instead of "test-ios" or "test-android". We'll handle this later. + }), + }, + ], + options.project + ); + + const nxJson = readNxJson(tree); + const detoxPlugins = nxJson.plugins?.filter( + (p) => typeof p !== 'string' && p.plugin === '@nx/detox/plugin' + ); + + // These were either `build-ios`, `test-ios`, etc., and we need to set them back to their generic names. + // The per-project targets will call these with additional `--args` passed to maintain the same + // behavior as previous executor-based targets. + for (const p of detoxPlugins) { + if (typeof p === 'string') continue; + p.options['buildTargetName'] = 'build'; + p.options['testTargetName'] = 'test'; + } + + // Inform the users that the inferred targets are platform-agnostic, and they can remove the old targets if unnecessary. + for (const [project] of migratedProjects) { + migrationLogs.addLog({ + project, + executorName: '@nx/detox:build', + log: `The "build-android" target was migrated to use "nx run ${project}:build", which is platform-agnostic. If you no longer need this target, you can remove it.`, + }); + migrationLogs.addLog({ + project, + executorName: '@nx/detox:test', + log: `The "test-android" target was migrated to use "nx run ${project}:test", which is platform-agnostic. If you no longer need this target, you can remove it.`, + }); + migrationLogs.addLog({ + project, + executorName: '@nx/detox:build', + log: `The "build-ios" target was migrated to use "nx run ${project}:build", which is platform-agnostic. If you no longer need this target, you can remove it.`, + }); + migrationLogs.addLog({ + project, + executorName: '@nx/detox:test', + log: `The "test-ios" target was migrated to use "nx run ${project}:test", which is platform-agnostic. If you no longer need this target, you can remove it.`, + }); + } + + updateNxJson(tree, nxJson); + + if (migratedProjects.size === 0) { + throw new Error('Could not find any targets to migrate.'); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return () => { + migrationLogs.flushLogs(); + }; +} + +export default convertToInferred; diff --git a/packages/detox/src/generators/convert-to-inferred/lib/post-target-transformer.ts b/packages/detox/src/generators/convert-to-inferred/lib/post-target-transformer.ts new file mode 100644 index 0000000000000..84fc057a98e58 --- /dev/null +++ b/packages/detox/src/generators/convert-to-inferred/lib/post-target-transformer.ts @@ -0,0 +1,70 @@ +import type { TargetConfiguration, Tree } from '@nx/devkit'; +import type { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; +import { processTargetOutputs } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils'; + +export function postTargetTransformer( + migrationLogs: AggregatedLog, + processOptions: ( + tree: Tree, + options: any, + projectName: string, + projectRoot: string, + target: TargetConfiguration | undefined, + migrationLogs: AggregatedLog + ) => void +) { + return ( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTargetConfiguration: TargetConfiguration + ) => { + if (target.options) { + processOptions( + tree, + target.options, + projectDetails.projectName, + projectDetails.root, + target, + migrationLogs + ); + } + + if (target.configurations) { + for (const configurationName in target.configurations) { + const configuration = target.configurations[configurationName]; + processOptions( + tree, + configuration, + projectDetails.projectName, + projectDetails.root, + undefined, + migrationLogs + ); + } + + if (Object.keys(target.configurations).length === 0) { + if ('defaultConfiguration' in target) { + delete target.defaultConfiguration; + } + delete target.configurations; + } + + if ( + 'defaultConfiguration' in target && + !target.configurations[target.defaultConfiguration] + ) { + delete target.defaultConfiguration; + } + } + + if (target.outputs) { + processTargetOutputs(target, [], inferredTargetConfiguration, { + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + }); + } + + return target; + }; +} diff --git a/packages/detox/src/generators/convert-to-inferred/lib/process-build-options.ts b/packages/detox/src/generators/convert-to-inferred/lib/process-build-options.ts new file mode 100644 index 0000000000000..5e651457122a8 --- /dev/null +++ b/packages/detox/src/generators/convert-to-inferred/lib/process-build-options.ts @@ -0,0 +1,35 @@ +import { names, type TargetConfiguration, type Tree } from '@nx/devkit'; +import type { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; + +export function processBuildOptions( + _tree: Tree, + options: any, + projectName: string, + _projectRoot: string, + target: TargetConfiguration | undefined, + _migrationLogs: AggregatedLog +): void { + const args: string[] = []; + + if ('detoxConfiguration' in options) { + // Need to wrap in --args since --configuration/-c is swallowed by Nx CLI. + args.push(`--args="-c ${options.detoxConfiguration}"`); + delete options.detoxConfiguration; + } + + for (const key of Object.keys(options)) { + let value = options[key]; + if (typeof value === 'boolean') { + if (value) args.push(`--${names(key).fileName}`); + } else { + args.push(`--${names(key).fileName}`, value); + } + delete options[key]; + } + + if (target) { + target.command = `nx run ${projectName}:build`; + } + + options.args = args; +} diff --git a/packages/detox/src/generators/convert-to-inferred/lib/process-test-options.ts b/packages/detox/src/generators/convert-to-inferred/lib/process-test-options.ts new file mode 100644 index 0000000000000..b779323ce124e --- /dev/null +++ b/packages/detox/src/generators/convert-to-inferred/lib/process-test-options.ts @@ -0,0 +1,76 @@ +import { names, type TargetConfiguration, type Tree } from '@nx/devkit'; +import type { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; + +export function processTestOptions( + _tree: Tree, + options: any, + projectName: string, + _projectRoot: string, + target: TargetConfiguration | undefined, + migrationLogs: AggregatedLog +): void { + const args: string[] = []; + + if ('detoxConfiguration' in options) { + // Need to wrap in --args since --configuration/-c is swallowed by Nx CLI. + args.push(`--args="-c ${options.detoxConfiguration}"`); + delete options.detoxConfiguration; + } + + if ('deviceBootArgs' in options) { + args.push(`--device-boot-args="${options.deviceBootArgs}"`); // the value must be specified after an equal sign (=) and inside quotes: https://wix.github.io/Detox/docs/cli/test + delete options.deviceBootArgs; + } + + if ('appLaunchArgs' in options) { + args.push(`--app-launch-args="${options.appLaunchArgs}"`); // the value must be specified after an equal sign (=) and inside quotes: https://wix.github.io/Detox/docs/cli/test + delete options.appLaunchArgs; + } + + if ('color' in options) { + // detox only accepts --no-color, not --color + if (!options.color) args.push('--no-color'); + delete options.color; + } + + if ('buildTarget' in options) { + migrationLogs.addLog({ + project: projectName, + executorName: '@nx/expo:test', + log: 'Unable to migrate `buildTarget` for Detox test. Use "nx run :run-ios" or "nx run :run-android", and pass "--reuse" option when running tests.', + }); + delete options.buildTarget; + } + + const deprecatedOptions = [ + 'runnerConfig', + 'recordTimeline', + 'workers', + 'deviceLaunchArgs', + ]; + for (const key of deprecatedOptions) { + if (!(key in options)) continue; + migrationLogs.addLog({ + project: projectName, + executorName: '@nx/expo:test', + log: `Option "${key}" is not migrated since it was removed in Detox 20.`, + }); + delete options[key]; + } + + for (const key of Object.keys(options)) { + let value = options[key]; + if (typeof value === 'boolean') { + if (value) args.push(`--${names(key).fileName}`); + } else { + args.push(`--${names(key).fileName}`, value); + } + delete options[key]; + } + + if (target) { + target.command = `nx run ${projectName}:test`; + } + + options.args = args; +} diff --git a/packages/detox/src/generators/convert-to-inferred/schema.json b/packages/detox/src/generators/convert-to-inferred/schema.json new file mode 100644 index 0000000000000..b909649b53720 --- /dev/null +++ b/packages/detox/src/generators/convert-to-inferred/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxDetoxConvertToInferred", + "description": "Convert existing Detox project(s) using `@nx/detox:*` executors to use `@nx/detox/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Detox project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/detox:*` executors to use `@nx/detox/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + } +} diff --git a/packages/expo/src/generators/application/application.spec.ts b/packages/expo/src/generators/application/application.spec.ts index 087b6ee6c8cca..6e1942d03259c 100644 --- a/packages/expo/src/generators/application/application.spec.ts +++ b/packages/expo/src/generators/application/application.spec.ts @@ -139,7 +139,7 @@ describe('app', () => { binaryPath: '../my-dir/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: - "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.local': { @@ -152,7 +152,7 @@ describe('app', () => { binaryPath: '../my-dir/ios/build/Build/Products/Release-iphonesimulator/MyApp.app', build: - "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); @@ -200,7 +200,7 @@ describe('app', () => { binaryPath: '../my-app/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.local': { @@ -213,7 +213,7 @@ describe('app', () => { binaryPath: '../my-app/ios/build/Build/Products/Release-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); @@ -264,7 +264,7 @@ describe('app', () => { binaryPath: '../my-app/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.local': { @@ -277,7 +277,7 @@ describe('app', () => { binaryPath: '../my-app/ios/build/Build/Products/Release-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); diff --git a/packages/react-native/src/generators/application/application.spec.ts b/packages/react-native/src/generators/application/application.spec.ts index 6e68f3dc82678..6b7b5b4be25e8 100644 --- a/packages/react-native/src/generators/application/application.spec.ts +++ b/packages/react-native/src/generators/application/application.spec.ts @@ -173,14 +173,14 @@ describe('app', () => { binaryPath: '../my-dir/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: - "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.release': { binaryPath: '../my-dir/ios/build/Build/Products/Release-iphonesimulator/MyApp.app', build: - "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-dir/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); @@ -224,14 +224,14 @@ describe('app', () => { binaryPath: '../my-app/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, 'ios.release': { binaryPath: '../my-app/ios/build/Build/Products/Release-iphonesimulator/MyApp.app', build: - "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath ./build -quiet", + "cd ../my-app/ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Plus' -derivedDataPath ./build -quiet", type: 'ios.app', }, }); diff --git a/packages/react-native/src/migrations/update-19-6-0/rename-upgrade-target-name.ts b/packages/react-native/src/migrations/update-19-6-0/rename-upgrade-target-name.ts index 24f6ddc852d68..6ea9b9e89b490 100644 --- a/packages/react-native/src/migrations/update-19-6-0/rename-upgrade-target-name.ts +++ b/packages/react-native/src/migrations/update-19-6-0/rename-upgrade-target-name.ts @@ -1,18 +1,7 @@ -import { - Tree, - formatFiles, - getProjects, - readNxJson, - updateNxJson, - updateProjectConfiguration, - workspaceRoot, -} from '@nx/devkit'; - -import { runCliUpgrade } from '../../executors/upgrade/upgrade.impl'; +import { formatFiles, readNxJson, Tree, updateNxJson } from '@nx/devkit'; /** * There was a typo in @nx/react-native/plugin, where "upgradeTargetName" was "upgradeTargetname" - * Remove pod-install from dependsOn for all targets, it does pod-install when creating the app */ export default async function update(tree: Tree) { const nxJson = readNxJson(tree);