From a4417a1a934eca320e23a66fe15618399eb40814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwa=C5=9Bniewski?= Date: Wed, 20 Dec 2023 10:24:01 +0100 Subject: [PATCH] feat: add platform-cli-apple with reusable utilities for OOT platforms (#2208) * feat: refactor run-ios to separate files, export more utilities * [wip] feat: use builder pattern to easily reuse commands for OOT platforms * feat: add package * docs: document cli-platform-apple * fix: move generated files * fix: indentation * fix: building packages * fix: tests * fix: account for macOS in simulatorDest * fix: recheck pods for build command * feat: add getProjectConfig for OOT platforms * feat: fallback to first available device * refactor: use platformInfo utility * fix: bring back podspecs * fix: apply reviewers comments --- CODEOWNERS | 3 +- CONTRIBUTING.md | 2 +- packages/cli-doctor/package.json | 1 + packages/cli-doctor/src/commands/info.ts | 2 +- .../src/tools/healthchecks/xcodeEnv.ts | 2 +- packages/cli-doctor/tsconfig.json | 1 + packages/cli-platform-apple/README.md | 42 ++ packages/cli-platform-apple/package.json | 35 + .../src/__tests__/pods.test.ts | 11 +- .../commands/buildCommand}/buildOptions.ts | 0 .../commands/buildCommand}/buildProject.ts | 19 +- .../src/commands/buildCommand/createBuild.ts | 64 ++ .../buildCommand}/getConfiguration.ts | 0 .../buildCommand}/getXcodeProjectAndDir.ts | 0 .../buildCommand/simulatorDestinationMap.ts | 6 + .../src/commands/logCommand/createLog.ts | 98 +++ .../src/commands/logCommand/logOptions.ts | 7 + .../src/commands/runCommand/createRun.ts | 320 +++++++++ .../src/commands/runCommand/getBuildPath.ts | 104 +++ .../runCommand/getFallbackSimulator.ts | 32 + .../commands/runCommand/getPlatformInfo.ts | 39 ++ .../src/commands/runCommand/matchingDevice.ts | 48 ++ .../src/commands/runCommand/runOnDevice.ts | 108 +++ .../src/commands/runCommand/runOnSimulator.ts | 108 +++ .../src/commands/runCommand/runOptions.ts | 46 ++ .../config/__fixtures__/files/project.pbxproj | 0 .../src/config/__fixtures__/projects.ts | 0 .../findPodfilePath.test.ts.snap | 0 .../config/__tests__/findPodfilePath.test.ts | 0 .../src/config/__tests__/findPodspec.test.ts | 0 .../config/__tests__/findXcodeProject.test.ts | 0 .../config/__tests__/getProjectConfig.test.ts | 4 +- .../src/config/findAllPodfilePaths.ts | 0 .../src/config/findPodfilePath.ts | 18 +- .../src/config/findPodspec.ts | 0 .../src/config/findXcodeProject.ts | 0 .../cli-platform-apple/src/config/index.ts | 96 +++ packages/cli-platform-apple/src/index.ts | 12 + .../checkIfConfigurationExists.test.ts | 0 .../__tests__/findMatchingSimulator.test.ts | 0 .../src/tools/__tests__/listDevices.test.ts} | 73 +- .../src/tools/checkIfConfigurationExists.ts | 0 .../src/tools/findMatchingSimulator.ts | 0 .../src/tools/getArchitecture.ts | 0 .../getBuildConfigurationFromXcScheme.ts | 0 .../src/tools/getDestinationSimulator.ts | 0 .../src/tools/getInfo.ts | 0 .../src/tools/getSimulators.ts | 0 .../src/tools/installPods.ts | 0 .../src/tools/listDevices.ts} | 19 +- .../src/tools/pods.ts | 34 +- .../src/tools/prompts.ts | 0 .../src/tools/runBundleInstall.ts | 0 .../src/tools/selectFromInteractiveMode.ts | 0 .../src/types.ts | 9 + packages/cli-platform-apple/tsconfig.json | 8 + packages/cli-platform-ios/README.md | 6 +- packages/cli-platform-ios/package.json | 15 +- .../src/commands/buildIOS/index.ts | 34 +- .../src/commands/logIOS/index.ts | 95 +-- .../src/commands/runIOS/index.ts | 659 +----------------- packages/cli-platform-ios/src/config/index.ts | 98 +-- packages/cli-platform-ios/src/index.ts | 11 +- packages/cli-platform-ios/tsconfig.json | 3 +- packages/cli/src/commands/init/init.ts | 2 +- 65 files changed, 1358 insertions(+), 936 deletions(-) create mode 100644 packages/cli-platform-apple/README.md create mode 100644 packages/cli-platform-apple/package.json rename packages/{cli-platform-ios => cli-platform-apple}/src/__tests__/pods.test.ts (91%) rename packages/{cli-platform-ios/src/commands/buildIOS => cli-platform-apple/src/commands/buildCommand}/buildOptions.ts (100%) rename packages/{cli-platform-ios/src/commands/buildIOS => cli-platform-apple/src/commands/buildCommand}/buildProject.ts (89%) create mode 100644 packages/cli-platform-apple/src/commands/buildCommand/createBuild.ts rename packages/{cli-platform-ios/src/commands/buildIOS => cli-platform-apple/src/commands/buildCommand}/getConfiguration.ts (100%) rename packages/{cli-platform-ios/src/commands/buildIOS => cli-platform-apple/src/commands/buildCommand}/getXcodeProjectAndDir.ts (100%) create mode 100644 packages/cli-platform-apple/src/commands/buildCommand/simulatorDestinationMap.ts create mode 100644 packages/cli-platform-apple/src/commands/logCommand/createLog.ts create mode 100644 packages/cli-platform-apple/src/commands/logCommand/logOptions.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/createRun.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/getFallbackSimulator.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/getPlatformInfo.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/matchingDevice.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts create mode 100644 packages/cli-platform-apple/src/commands/runCommand/runOptions.ts rename packages/{cli-platform-ios => cli-platform-apple}/src/config/__fixtures__/files/project.pbxproj (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/__fixtures__/projects.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/__tests__/__snapshots__/findPodfilePath.test.ts.snap (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/__tests__/findPodfilePath.test.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/__tests__/findPodspec.test.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/__tests__/findXcodeProject.test.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/__tests__/getProjectConfig.test.ts (92%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/findAllPodfilePaths.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/findPodfilePath.ts (79%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/findPodspec.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/config/findXcodeProject.ts (100%) create mode 100644 packages/cli-platform-apple/src/config/index.ts create mode 100644 packages/cli-platform-apple/src/index.ts rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/__tests__/checkIfConfigurationExists.test.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/__tests__/findMatchingSimulator.test.ts (100%) rename packages/{cli-platform-ios/src/tools/__tests__/listIOSDevices.test.ts => cli-platform-apple/src/tools/__tests__/listDevices.test.ts} (74%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/checkIfConfigurationExists.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/findMatchingSimulator.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/getArchitecture.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/getBuildConfigurationFromXcScheme.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/getDestinationSimulator.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/getInfo.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/getSimulators.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/installPods.ts (100%) rename packages/{cli-platform-ios/src/tools/listIOSDevices.ts => cli-platform-apple/src/tools/listDevices.ts} (73%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/pods.ts (80%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/prompts.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/runBundleInstall.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/tools/selectFromInteractiveMode.ts (100%) rename packages/{cli-platform-ios => cli-platform-apple}/src/types.ts (69%) create mode 100644 packages/cli-platform-apple/tsconfig.json diff --git a/CODEOWNERS b/CODEOWNERS index 274a7d2aa..1c90db386 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,4 +6,5 @@ packages/cli-doctor/src/tools/healthchecks/android* @cipolleschi # iOS packages/cli-platform-ios/ @cipolleschi -packages/cli-doctor/src/tools/healthchecks/ios* @cipolleschi +packages/cli-platform-apple/ @cipolleschi +packages/cli-doctor/src/tools/healthchecks/ios* @cipolleschi \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c990c4bb1..ab2fafea0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ And then: ```sh cd /my/new/react-native/project/ -yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-clean" "@react-native-community/cli-doctor" "@react-native-community/cli-config" +yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-clean" "@react-native-community/cli-doctor" "@react-native-community/cli-config" "@react-native-community/cli-platform-apple" ``` Once you're done with testing and you'd like to get back to regular setup, run `yarn unlink` instead of `yarn link` from above command. Then `yarn install --force`. diff --git a/packages/cli-doctor/package.json b/packages/cli-doctor/package.json index 173549169..4ac24c3d2 100644 --- a/packages/cli-doctor/package.json +++ b/packages/cli-doctor/package.json @@ -11,6 +11,7 @@ "@react-native-community/cli-config": "13.1.0", "@react-native-community/cli-platform-android": "13.1.0", "@react-native-community/cli-platform-ios": "13.1.0", + "@react-native-community/cli-platform-apple": "13.1.0", "@react-native-community/cli-tools": "13.1.0", "chalk": "^4.1.2", "command-exists": "^1.2.8", diff --git a/packages/cli-doctor/src/commands/info.ts b/packages/cli-doctor/src/commands/info.ts index 3424855f8..4947ca813 100644 --- a/packages/cli-doctor/src/commands/info.ts +++ b/packages/cli-doctor/src/commands/info.ts @@ -8,7 +8,7 @@ import getEnvironmentInfo from '../tools/envinfo'; import {logger, version} from '@react-native-community/cli-tools'; import {Config} from '@react-native-community/cli-types'; -import {getArchitecture} from '@react-native-community/cli-platform-ios'; +import {getArchitecture} from '@react-native-community/cli-platform-apple'; import {readFile} from 'fs-extra'; import path from 'path'; import {stringify} from 'yaml'; diff --git a/packages/cli-doctor/src/tools/healthchecks/xcodeEnv.ts b/packages/cli-doctor/src/tools/healthchecks/xcodeEnv.ts index d1755c171..2ac85fbca 100644 --- a/packages/cli-doctor/src/tools/healthchecks/xcodeEnv.ts +++ b/packages/cli-doctor/src/tools/healthchecks/xcodeEnv.ts @@ -1,4 +1,4 @@ -import {findPodfilePaths} from '@react-native-community/cli-platform-ios'; +import {findPodfilePaths} from '@react-native-community/cli-platform-apple'; import { findProjectRoot, resolveNodeModuleDir, diff --git a/packages/cli-doctor/tsconfig.json b/packages/cli-doctor/tsconfig.json index 89c5c48c2..673c1849f 100644 --- a/packages/cli-doctor/tsconfig.json +++ b/packages/cli-doctor/tsconfig.json @@ -10,5 +10,6 @@ {"path": "../cli-config"}, {"path": "../cli-platform-android"}, {"path": "../cli-platform-ios"}, + {"path": "../cli-platform-apple"}, ] } diff --git a/packages/cli-platform-apple/README.md b/packages/cli-platform-apple/README.md new file mode 100644 index 000000000..f5d7678e2 --- /dev/null +++ b/packages/cli-platform-apple/README.md @@ -0,0 +1,42 @@ +# @react-native-community/cli-platform-apple + +This package is part of the [React Native CLI](../../README.md). It contains utilities for building reusable commands targetting Apple platforms. + +## Installation + +```sh +yarn add @react-native-community/cli-platform-apple +``` + +## Usage + +This package is intended to be used internally in [React Native CLI](../../README.md) and by out of tree platforms. + +It exports builder commands that can be used to create custom `run-`, `log-` and `build-` commands for example: `yarn run-`. + +Inside of `/packages/react-native/react-native.config.js`: + +```js +const { + buildOptions, + createBuild, +} = require('@react-native-community/cli-platform-apple'); + +const buildVisionOS = { + name: 'build-visionos', + description: 'builds your app for visionOS platform', + func: createBuild({platformName: 'visionos'}), + examples: [ + { + desc: 'Build the app for visionOS in Release mode', + cmd: 'npx react-native build-visionos --mode "Release"', + }, + ], + options: buildOptions, +}; + +module.exports = { + commands: [buildVisionOS], // <- Add command here + //.. +}; +``` diff --git a/packages/cli-platform-apple/package.json b/packages/cli-platform-apple/package.json new file mode 100644 index 000000000..725b9845e --- /dev/null +++ b/packages/cli-platform-apple/package.json @@ -0,0 +1,35 @@ +{ + "name": "@react-native-community/cli-platform-apple", + "version": "13.1.0", + "license": "MIT", + "main": "build/index.js", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@react-native-community/cli-tools": "13.1.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-xml-parser": "^4.0.12", + "glob": "^7.1.3", + "ora": "^5.4.1" + }, + "devDependencies": { + "@react-native-community/cli-types": "13.1.0", + "@types/glob": "^7.1.1", + "@types/lodash": "^4.14.149", + "hasbin": "^1.2.3" + }, + "files": [ + "build", + "!*.d.ts", + "!*.map" + ], + "homepage": "https://github.com/react-native-community/cli/tree/main/packages/cli-platform-apple", + "repository": { + "type": "git", + "url": "https://github.com/react-native-community/cli.git", + "directory": "packages/cli-platform-apple" + } +} + \ No newline at end of file diff --git a/packages/cli-platform-ios/src/__tests__/pods.test.ts b/packages/cli-platform-apple/src/__tests__/pods.test.ts similarity index 91% rename from packages/cli-platform-ios/src/__tests__/pods.test.ts rename to packages/cli-platform-apple/src/__tests__/pods.test.ts index 880fdbcbe..215db7f66 100644 --- a/packages/cli-platform-ios/src/__tests__/pods.test.ts +++ b/packages/cli-platform-apple/src/__tests__/pods.test.ts @@ -1,6 +1,9 @@ import {writeFiles, getTempDirectory, cleanup} from '../../../../jest/helpers'; import installPods from '../tools/installPods'; -import resolvePods, {compareMd5Hashes, getIosDependencies} from '../tools/pods'; +import resolvePods, { + compareMd5Hashes, + getPlatformDependencies, +} from '../tools/pods'; const mockGet = jest.fn(); const mockSet = jest.fn(); @@ -71,9 +74,9 @@ describe('compareMd5Hashes', () => { }); }); -describe('getIosDependencies', () => { +describe('getPlatformDependencies', () => { it('should return only dependencies with native code', () => { - const result = getIosDependencies(dependenciesConfig); + const result = getPlatformDependencies(dependenciesConfig); expect(result).toEqual(['dep1@1.0.0', 'dep2@1.0.0']); }); }); @@ -90,7 +93,7 @@ describe('resolvePods', () => { it('should install pods when force option is set to true', async () => { createTempFiles(); - await resolvePods(DIR, {}, {forceInstall: true}); + await resolvePods(DIR, {}, 'ios', {forceInstall: true}); expect(installPods).toHaveBeenCalled(); }); diff --git a/packages/cli-platform-ios/src/commands/buildIOS/buildOptions.ts b/packages/cli-platform-apple/src/commands/buildCommand/buildOptions.ts similarity index 100% rename from packages/cli-platform-ios/src/commands/buildIOS/buildOptions.ts rename to packages/cli-platform-apple/src/commands/buildCommand/buildOptions.ts diff --git a/packages/cli-platform-ios/src/commands/buildIOS/buildProject.ts b/packages/cli-platform-apple/src/commands/buildCommand/buildProject.ts similarity index 89% rename from packages/cli-platform-ios/src/commands/buildIOS/buildProject.ts rename to packages/cli-platform-apple/src/commands/buildCommand/buildProject.ts index 4917cf5ac..8b0d442f2 100644 --- a/packages/cli-platform-ios/src/commands/buildIOS/buildProject.ts +++ b/packages/cli-platform-apple/src/commands/buildCommand/buildProject.ts @@ -11,15 +11,28 @@ import { getLoader, } from '@react-native-community/cli-tools'; import type {BuildFlags} from './buildOptions'; +import {simulatorDestinationMap} from './simulatorDestinationMap'; export function buildProject( xcodeProject: IOSProjectInfo, + platform: string, udid: string | undefined, mode: string, scheme: string, args: BuildFlags, ): Promise { return new Promise((resolve, reject) => { + const simulatorDest = simulatorDestinationMap?.[platform]; + + if (!simulatorDest) { + reject( + new CLIError( + `Unknown platform: ${platform}. Please, use one of: ios, macos, visionos, tvos.`, + ), + ); + return; + } + const xcodebuildArgs = [ xcodeProject.isWorkspace ? '-workspace' : '-project', xcodeProject.name, @@ -33,8 +46,8 @@ export function buildProject( (udid ? `id=${udid}` : mode === 'Debug' - ? 'generic/platform=iOS Simulator' - : 'generic/platform=iOS') + + ? `generic/platform=${simulatorDest}` + : `generic/platform=${platform}`) + (args.destination ? ',' + args.destination : ''), ]; @@ -98,7 +111,7 @@ export function buildProject( reject( new CLIError( ` - Failed to build iOS project. + Failed to build ${platform} project. "xcodebuild" exited with error code '${code}'. To debug build logs further, consider building your app with Xcode.app, by opening diff --git a/packages/cli-platform-apple/src/commands/buildCommand/createBuild.ts b/packages/cli-platform-apple/src/commands/buildCommand/createBuild.ts new file mode 100644 index 000000000..9ba115c6f --- /dev/null +++ b/packages/cli-platform-apple/src/commands/buildCommand/createBuild.ts @@ -0,0 +1,64 @@ +import fs from 'fs'; +import {CLIError} from '@react-native-community/cli-tools'; +import {Config, IOSProjectConfig} from '@react-native-community/cli-types'; +import getArchitecture from '../../tools/getArchitecture'; +import resolvePods from '../../tools/pods'; +import {BuildFlags} from './buildOptions'; +import {buildProject} from './buildProject'; +import {getConfiguration} from './getConfiguration'; +import {getXcodeProjectAndDir} from './getXcodeProjectAndDir'; +import {BuilderCommand} from '../../types'; +import findXcodeProject from '../../config/findXcodeProject'; + +const createBuild = + ({platformName}: BuilderCommand) => + async (_: Array, ctx: Config, args: BuildFlags) => { + const platform = ctx.project[platformName] as IOSProjectConfig; + if (platform === undefined) { + throw new CLIError(`Unable to find ${platform} platform config`); + } + + let {xcodeProject, sourceDir} = getXcodeProjectAndDir(platform); + + let installedPods = false; + if (platform?.automaticPodsInstallation || args.forcePods) { + const isAppRunningNewArchitecture = platform?.sourceDir + ? await getArchitecture(platform?.sourceDir) + : undefined; + + await resolvePods(ctx.root, ctx.dependencies, platformName, { + forceInstall: args.forcePods, + newArchEnabled: isAppRunningNewArchitecture, + }); + + installedPods = true; + } + + // if project is freshly created, revisit Xcode project to verify Pods are installed correctly. + // This is needed because ctx project is created before Pods are installed, so it might have outdated information. + if (installedPods) { + const recheckXcodeProject = findXcodeProject(fs.readdirSync(sourceDir)); + if (recheckXcodeProject) { + xcodeProject = recheckXcodeProject; + } + } + + process.chdir(sourceDir); + + const {scheme, mode} = await getConfiguration( + xcodeProject, + sourceDir, + args, + ); + + return buildProject( + xcodeProject, + platformName, + undefined, + mode, + scheme, + args, + ); + }; + +export default createBuild; diff --git a/packages/cli-platform-ios/src/commands/buildIOS/getConfiguration.ts b/packages/cli-platform-apple/src/commands/buildCommand/getConfiguration.ts similarity index 100% rename from packages/cli-platform-ios/src/commands/buildIOS/getConfiguration.ts rename to packages/cli-platform-apple/src/commands/buildCommand/getConfiguration.ts diff --git a/packages/cli-platform-ios/src/commands/buildIOS/getXcodeProjectAndDir.ts b/packages/cli-platform-apple/src/commands/buildCommand/getXcodeProjectAndDir.ts similarity index 100% rename from packages/cli-platform-ios/src/commands/buildIOS/getXcodeProjectAndDir.ts rename to packages/cli-platform-apple/src/commands/buildCommand/getXcodeProjectAndDir.ts diff --git a/packages/cli-platform-apple/src/commands/buildCommand/simulatorDestinationMap.ts b/packages/cli-platform-apple/src/commands/buildCommand/simulatorDestinationMap.ts new file mode 100644 index 000000000..d2af69246 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/buildCommand/simulatorDestinationMap.ts @@ -0,0 +1,6 @@ +export const simulatorDestinationMap: Record = { + ios: 'iOS Simulator', + macos: 'macOS', + visionos: 'visionOS Simulator', + tvos: 'tvOS Simulator', +}; diff --git a/packages/cli-platform-apple/src/commands/logCommand/createLog.ts b/packages/cli-platform-apple/src/commands/logCommand/createLog.ts new file mode 100644 index 000000000..09f75aede --- /dev/null +++ b/packages/cli-platform-apple/src/commands/logCommand/createLog.ts @@ -0,0 +1,98 @@ +import {CLIError, logger, prompt} from '@react-native-community/cli-tools'; +import {Config, IOSProjectConfig} from '@react-native-community/cli-types'; +import {spawnSync} from 'child_process'; +import os from 'os'; +import path from 'path'; +import getSimulators from '../../tools/getSimulators'; +import listDevices from '../../tools/listDevices'; +import {getPlatformInfo} from '../runCommand/getPlatformInfo'; +import {BuilderCommand} from '../../types'; + +/** + * Starts Apple device syslog tail + */ + +type Args = { + interactive: boolean; +}; + +const createLog = + ({platformName}: BuilderCommand) => + async (_: Array, ctx: Config, args: Args) => { + const platform = ctx.project[platformName] as IOSProjectConfig; + const {readableName: platformReadableName} = getPlatformInfo(platformName); + + if (platform === undefined) { + throw new CLIError(`Unable to find ${platform} platform config`); + } + + // Here we're using two command because first command `xcrun simctl list --json devices` outputs `state` but doesn't return `available`. But second command `xcrun xcdevice list` outputs `available` but doesn't output `state`. So we need to connect outputs of both commands. + const simulators = getSimulators(); + const bootedSimulators = Object.keys(simulators.devices) + .map((key) => simulators.devices[key]) + .reduce((acc, val) => acc.concat(val), []) + .filter(({state}) => state === 'Booted'); + + const {sdkNames} = getPlatformInfo(platformName); + const devices = await listDevices(sdkNames); + + const availableSimulators = devices.filter( + ({type, isAvailable}) => type === 'simulator' && isAvailable, + ); + + if (availableSimulators.length === 0) { + logger.error('No simulators detected. Install simulators via Xcode.'); + return; + } + + const bootedAndAvailableSimulators = bootedSimulators.map((booted) => { + const available = availableSimulators.find( + ({udid}) => udid === booted.udid, + ); + return {...available, ...booted}; + }); + + if (bootedAndAvailableSimulators.length === 0) { + logger.error( + `No booted and available ${platformReadableName} simulators found.`, + ); + return; + } + + if (args.interactive && bootedAndAvailableSimulators.length > 1) { + const {udid} = await prompt({ + type: 'select', + name: 'udid', + message: `Select ${platformReadableName} simulators to tail logs from`, + choices: bootedAndAvailableSimulators.map((simulator) => ({ + title: simulator.name, + value: simulator.udid, + })), + }); + + tailDeviceLogs(udid); + } else { + tailDeviceLogs(bootedAndAvailableSimulators[0].udid); + } + }; + +function tailDeviceLogs(udid: string) { + const logDir = path.join( + os.homedir(), + 'Library', + 'Logs', + 'CoreSimulator', + udid, + 'asl', + ); + + const log = spawnSync('syslog', ['-w', '-F', 'std', '-d', logDir], { + stdio: 'inherit', + }); + + if (log.error !== null) { + throw log.error; + } +} + +export default createLog; diff --git a/packages/cli-platform-apple/src/commands/logCommand/logOptions.ts b/packages/cli-platform-apple/src/commands/logCommand/logOptions.ts new file mode 100644 index 000000000..9522a48b2 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/logCommand/logOptions.ts @@ -0,0 +1,7 @@ +export const logOptions = [ + { + name: '--interactive', + description: + 'Explicitly select simulator to tail logs from. By default it will tail logs from the first booted and available simulator.', + }, +]; diff --git a/packages/cli-platform-apple/src/commands/runCommand/createRun.ts b/packages/cli-platform-apple/src/commands/runCommand/createRun.ts new file mode 100644 index 000000000..4693cd5b7 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/createRun.ts @@ -0,0 +1,320 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import path from 'path'; +import fs from 'fs'; +import chalk from 'chalk'; +import {Config, IOSProjectConfig} from '@react-native-community/cli-types'; +import { + logger, + CLIError, + link, + startServerInNewWindow, + findDevServerPort, + cacheManager, +} from '@react-native-community/cli-tools'; +import findXcodeProject from '../../config/findXcodeProject'; +import getArchitecture from '../../tools/getArchitecture'; +import getSimulators from '../../tools/getSimulators'; +import listDevices from '../../tools/listDevices'; +import resolvePods, {getPackageJson} from '../../tools/pods'; +import {promptForDeviceSelection} from '../../tools/prompts'; +import {BuildFlags} from '../buildCommand/buildOptions'; +import {buildProject} from '../buildCommand/buildProject'; +import {getConfiguration} from '../buildCommand/getConfiguration'; +import {getXcodeProjectAndDir} from '../buildCommand/getXcodeProjectAndDir'; +import {getFallbackSimulator} from './getFallbackSimulator'; +import {getPlatformInfo} from './getPlatformInfo'; +import {printFoundDevices, matchingDevice} from './matchingDevice'; +import {runOnDevice} from './runOnDevice'; +import {runOnSimulator} from './runOnSimulator'; +import {BuilderCommand} from '../../types'; + +export interface FlagsT extends BuildFlags { + simulator?: string; + device?: string | true; + udid?: string; + binaryPath?: string; + listDevices?: boolean; + packager?: boolean; + port: number; + terminal?: string; +} + +const createRun = + ({platformName}: BuilderCommand) => + async (_: Array, ctx: Config, args: FlagsT) => { + // React Native docs assume platform is always ios/android + link.setPlatform('ios'); + const platform = ctx.project[platformName] as IOSProjectConfig; + const {sdkNames, readableName: platformReadableName} = + getPlatformInfo(platformName); + + if (platform === undefined) { + throw new CLIError( + `Unable to find ${platformReadableName} platform config`, + ); + } + + let {packager, port} = args; + let installedPods = false; + // check if pods need to be installed + if (platform?.automaticPodsInstallation || args.forcePods) { + const isAppRunningNewArchitecture = platform?.sourceDir + ? await getArchitecture(platform?.sourceDir) + : undefined; + + await resolvePods(ctx.root, ctx.dependencies, platformName, { + forceInstall: args.forcePods, + newArchEnabled: isAppRunningNewArchitecture, + }); + + installedPods = true; + } + + if (packager) { + const {port: newPort, startPackager} = await findDevServerPort( + port, + ctx.root, + ); + + if (startPackager) { + await startServerInNewWindow( + newPort, + ctx.root, + ctx.reactNativePath, + args.terminal, + ); + } + } + + if (ctx.reactNativeVersion !== 'unknown') { + link.setVersion(ctx.reactNativeVersion); + } + + let {xcodeProject, sourceDir} = getXcodeProjectAndDir(platform); + + // if project is freshly created, revisit Xcode project to verify Pods are installed correctly. + // This is needed because ctx project is created before Pods are installed, so it might have outdated information. + if (installedPods) { + const recheckXcodeProject = findXcodeProject(fs.readdirSync(sourceDir)); + if (recheckXcodeProject) { + xcodeProject = recheckXcodeProject; + } + } + + process.chdir(sourceDir); + + if (args.binaryPath) { + args.binaryPath = path.isAbsolute(args.binaryPath) + ? args.binaryPath + : path.join(ctx.root, args.binaryPath); + + if (!fs.existsSync(args.binaryPath)) { + throw new CLIError( + 'binary-path was specified, but the file was not found.', + ); + } + } + + const {mode, scheme} = await getConfiguration( + xcodeProject, + sourceDir, + args, + ); + + if (platformName === 'macos') { + buildProject(xcodeProject, platformName, undefined, mode, scheme, args); + return; + } + + const devices = await listDevices(sdkNames); + + const availableDevices = devices.filter( + ({isAvailable}) => isAvailable === true, + ); + + if (availableDevices.length === 0) { + return logger.error( + `${platformReadableName} devices or simulators not detected. Install simulators via Xcode or connect a physical ${platformReadableName} device`, + ); + } + + const fallbackSimulator = + platformName === 'ios' ? getFallbackSimulator(args) : availableDevices[0]; + + if (args.listDevices || args.interactive) { + if (args.device || args.udid) { + logger.warn( + `Both ${ + args.device ? 'device' : 'udid' + } and "list-devices" parameters were passed to "run" command. We will list available devices and let you choose from one.`, + ); + } + + const packageJson = getPackageJson(ctx.root); + const preferredDevice = cacheManager.get( + packageJson.name, + 'lastUsedIOSDeviceId', + ); + + const selectedDevice = await promptForDeviceSelection( + availableDevices, + preferredDevice, + ); + + if (!selectedDevice) { + throw new CLIError( + `Failed to select device, please try to run app without ${ + args.listDevices ? 'list-devices' : 'interactive' + } command.`, + ); + } else { + if (selectedDevice.udid !== preferredDevice) { + cacheManager.set( + packageJson.name, + 'lastUsedIOSDeviceId', + selectedDevice.udid, + ); + } + } + + if (selectedDevice.type === 'simulator') { + return runOnSimulator( + xcodeProject, + platformName, + mode, + scheme, + args, + selectedDevice, + ); + } else { + return runOnDevice( + selectedDevice, + platformName, + mode, + scheme, + xcodeProject, + args, + ); + } + } + + if (!args.device && !args.udid && !args.simulator) { + const bootedDevices = availableDevices.filter( + ({type}) => type === 'device', + ); + + const simulators = getSimulators(); + const bootedSimulators = Object.keys(simulators.devices) + .map((key) => simulators.devices[key]) + .reduce((acc, val) => acc.concat(val), []) + .filter(({state}) => state === 'Booted'); + + const booted = [...bootedDevices, ...bootedSimulators]; + if (booted.length === 0) { + logger.info( + 'No booted devices or simulators found. Launching first available simulator...', + ); + return runOnSimulator( + xcodeProject, + platformName, + mode, + scheme, + args, + fallbackSimulator, + ); + } + + logger.info(`Found booted ${booted.map(({name}) => name).join(', ')}`); + + for (const device of devices) { + await runOnDevice( + device, + platformName, + mode, + scheme, + xcodeProject, + args, + ); + } + + for (const simulator of bootedSimulators) { + await runOnSimulator( + xcodeProject, + platformName, + mode, + scheme, + args, + simulator || fallbackSimulator, + ); + } + } + + if (args.device && args.udid) { + return logger.error( + 'The `device` and `udid` options are mutually exclusive.', + ); + } + + if (args.udid) { + const device = availableDevices.find((d) => d.udid === args.udid); + if (!device) { + return logger.error( + `Could not find a device with udid: "${chalk.bold( + args.udid, + )}". ${printFoundDevices(availableDevices)}`, + ); + } + if (device.type === 'simulator') { + return runOnSimulator( + xcodeProject, + platformName, + mode, + scheme, + args, + fallbackSimulator, + ); + } else { + return runOnDevice( + device, + platformName, + mode, + scheme, + xcodeProject, + args, + ); + } + } else if (args.device) { + const physicalDevices = availableDevices.filter( + ({type}) => type !== 'simulator', + ); + const device = matchingDevice(physicalDevices, args.device); + if (device) { + return runOnDevice( + device, + platformName, + mode, + scheme, + xcodeProject, + args, + ); + } + } else { + runOnSimulator( + xcodeProject, + platformName, + mode, + scheme, + args, + fallbackSimulator, + ); + } + }; + +export default createRun; diff --git a/packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts b/packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts new file mode 100644 index 000000000..5a20c5e47 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts @@ -0,0 +1,104 @@ +import child_process from 'child_process'; +import {IOSProjectInfo} from '@react-native-community/cli-types'; +import {CLIError, logger} from '@react-native-community/cli-tools'; +import chalk from 'chalk'; + +export async function getBuildPath( + xcodeProject: IOSProjectInfo, + mode: string, + buildOutput: string, + scheme: string, + target: string | undefined, + isCatalyst: boolean = false, +) { + const buildSettings = child_process.execFileSync( + 'xcodebuild', + [ + xcodeProject.isWorkspace ? '-workspace' : '-project', + xcodeProject.name, + '-scheme', + scheme, + '-sdk', + getPlatformName(buildOutput), + '-configuration', + mode, + '-showBuildSettings', + '-json', + ], + {encoding: 'utf8'}, + ); + + const {targetBuildDir, executableFolderPath} = await getTargetPaths( + buildSettings, + scheme, + target, + ); + + if (!targetBuildDir) { + throw new CLIError('Failed to get the target build directory.'); + } + + if (!executableFolderPath) { + throw new CLIError('Failed to get the app name.'); + } + + return `${targetBuildDir}${ + isCatalyst ? '-maccatalyst' : '' + }/${executableFolderPath}`; +} + +async function getTargetPaths( + buildSettings: string, + scheme: string, + target: string | undefined, +) { + const settings = JSON.parse(buildSettings); + + const targets = settings.map( + ({target: settingsTarget}: any) => settingsTarget, + ); + + let selectedTarget = targets[0]; + + if (target) { + if (!targets.includes(target)) { + logger.info( + `Target ${chalk.bold(target)} not found for scheme ${chalk.bold( + scheme, + )}, automatically selected target ${chalk.bold(selectedTarget)}`, + ); + } else { + selectedTarget = target; + } + } + + // Find app in all building settings - look for WRAPPER_EXTENSION: 'app', + + const targetIndex = targets.indexOf(selectedTarget); + + const wrapperExtension = + settings[targetIndex].buildSettings.WRAPPER_EXTENSION; + + if (wrapperExtension === 'app') { + return { + targetBuildDir: settings[targetIndex].buildSettings.TARGET_BUILD_DIR, + executableFolderPath: + settings[targetIndex].buildSettings.EXECUTABLE_FOLDER_PATH, + }; + } + + return {}; +} + +function getPlatformName(buildOutput: string) { + // Xcode can sometimes escape `=` with a backslash or put the value in quotes + const platformNameMatch = /export PLATFORM_NAME\\?="?(\w+)"?$/m.exec( + buildOutput, + ); + if (!platformNameMatch) { + throw new CLIError( + 'Couldn\'t find "PLATFORM_NAME" variable in xcodebuild output. Please report this issue and run your project with Xcode instead.', + ); + } + return platformNameMatch[1]; +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/getFallbackSimulator.ts b/packages/cli-platform-apple/src/commands/runCommand/getFallbackSimulator.ts new file mode 100644 index 000000000..bc959a3d1 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/getFallbackSimulator.ts @@ -0,0 +1,32 @@ +import {CLIError} from '@react-native-community/cli-tools'; +import {getDestinationSimulator} from '../../tools/getDestinationSimulator'; +import {Device} from '../../types'; +import {FlagsT} from './createRun'; + +export function getFallbackSimulator(args: FlagsT): Device { + /** + * If provided simulator does not exist, try simulators in following order + * - iPhone 14 + * - iPhone 13 + * - iPhone 12 + * - iPhone 11 + */ + + const fallbackSimulators = [ + 'iPhone 14', + 'iPhone 13', + 'iPhone 12', + 'iPhone 11', + ]; + const selectedSimulator = getDestinationSimulator(args, fallbackSimulators); + + if (!selectedSimulator) { + throw new CLIError( + `No simulator available with ${ + args.simulator ? `name "${args.simulator}"` : `udid "${args.udid}"` + }`, + ); + } + + return selectedSimulator; +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/getPlatformInfo.ts b/packages/cli-platform-apple/src/commands/runCommand/getPlatformInfo.ts new file mode 100644 index 000000000..e40962f96 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/getPlatformInfo.ts @@ -0,0 +1,39 @@ +interface PlatformInfo { + readableName: string; + sdkNames: string[]; +} + +/** + * Returns platform readable name and list of SDKs for given platform. + * We can get list of SDKs from `xcodebuild -showsdks` command. + * + * Falls back to iOS if platform is not supported. + */ +export function getPlatformInfo(platform: string): PlatformInfo { + const iosPlatformInfo: PlatformInfo = { + readableName: 'iOS', + sdkNames: ['iphonesimulator', 'iphoneos'], + }; + + switch (platform) { + case 'ios': + return iosPlatformInfo; + case 'tvos': + return { + readableName: 'tvOS', + sdkNames: ['appletvsimulator', 'appletvos'], + }; + case 'visionos': + return { + readableName: 'visionOS', + sdkNames: ['xrsimulator', 'xros'], + }; + case 'macos': + return { + readableName: 'macOS', + sdkNames: ['macosx'], + }; + default: + return iosPlatformInfo; + } +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/matchingDevice.ts b/packages/cli-platform-apple/src/commands/runCommand/matchingDevice.ts new file mode 100644 index 000000000..3224709f5 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/matchingDevice.ts @@ -0,0 +1,48 @@ +import {logger} from '@react-native-community/cli-tools'; +import chalk from 'chalk'; +import {Device} from '../../types'; + +export function matchingDevice( + devices: Array, + deviceName: string | true | undefined, +) { + if (deviceName === true) { + const firstIOSDevice = devices.find((d) => d.type === 'device')!; + if (firstIOSDevice) { + logger.info( + `Using first available device named "${chalk.bold( + firstIOSDevice.name, + )}" due to lack of name supplied.`, + ); + return firstIOSDevice; + } else { + logger.error('No iOS devices connected.'); + return undefined; + } + } + const deviceByName = devices.find( + (device) => + device.name === deviceName || formattedDeviceName(device) === deviceName, + ); + if (!deviceByName) { + logger.error( + `Could not find a device named: "${chalk.bold( + String(deviceName), + )}". ${printFoundDevices(devices)}`, + ); + } + return deviceByName; +} + +export function formattedDeviceName(simulator: Device) { + return simulator.version + ? `${simulator.name} (${simulator.version})` + : simulator.name; +} + +export function printFoundDevices(devices: Array) { + return [ + 'Available devices:', + ...devices.map((device) => ` - ${device.name} (${device.udid})`), + ].join('\n'); +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts b/packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts new file mode 100644 index 000000000..9c0f13c80 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts @@ -0,0 +1,108 @@ +import child_process from 'child_process'; +import {Device} from '../../types'; +import {IOSProjectInfo} from '@react-native-community/cli-types'; +import {CLIError, logger} from '@react-native-community/cli-tools'; +import chalk from 'chalk'; +import {buildProject} from '../buildCommand/buildProject'; +import {getBuildPath} from './getBuildPath'; +import {FlagsT} from './createRun'; + +export async function runOnDevice( + selectedDevice: Device, + platform: string, + mode: string, + scheme: string, + xcodeProject: IOSProjectInfo, + args: FlagsT, +) { + if (args.binaryPath && selectedDevice.type === 'catalyst') { + throw new CLIError( + 'binary-path was specified for catalyst device, which is not supported.', + ); + } + + const isIOSDeployInstalled = child_process.spawnSync( + 'ios-deploy', + ['--version'], + {encoding: 'utf8'}, + ); + + if (isIOSDeployInstalled.error) { + throw new CLIError( + `Failed to install the app on the device because we couldn't execute the "ios-deploy" command. Please install it by running "${chalk.bold( + 'brew install ios-deploy', + )}" and try again.`, + ); + } + + if (selectedDevice.type === 'catalyst') { + const buildOutput = await buildProject( + xcodeProject, + platform, + selectedDevice.udid, + mode, + scheme, + args, + ); + + const appPath = await getBuildPath( + xcodeProject, + mode, + buildOutput, + scheme, + args.target, + true, + ); + const appProcess = child_process.spawn(`${appPath}/${scheme}`, [], { + detached: true, + stdio: 'ignore', + }); + appProcess.unref(); + } else { + let buildOutput, appPath; + if (!args.binaryPath) { + buildOutput = await buildProject( + xcodeProject, + platform, + selectedDevice.udid, + mode, + scheme, + args, + ); + + appPath = await getBuildPath( + xcodeProject, + mode, + buildOutput, + scheme, + args.target, + ); + } else { + appPath = args.binaryPath; + } + + const iosDeployInstallArgs = [ + '--bundle', + appPath, + '--id', + selectedDevice.udid, + '--justlaunch', + ]; + + logger.info(`Installing and launching your app on ${selectedDevice.name}`); + + const iosDeployOutput = child_process.spawnSync( + 'ios-deploy', + iosDeployInstallArgs, + {encoding: 'utf8'}, + ); + + if (iosDeployOutput.error) { + throw new CLIError( + `Failed to install the app on the device. We've encountered an error in "ios-deploy" command: ${iosDeployOutput.error.message}`, + ); + } + } + + return logger.success('Installed the app on the device.'); +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts b/packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts new file mode 100644 index 000000000..e7db29684 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts @@ -0,0 +1,108 @@ +import child_process from 'child_process'; +import {IOSProjectInfo} from '@react-native-community/cli-types'; +import path from 'path'; +import {logger} from '@react-native-community/cli-tools'; +import chalk from 'chalk'; +import {Device} from '../../types'; +import {buildProject} from '../buildCommand/buildProject'; +import {formattedDeviceName} from './matchingDevice'; +import {getBuildPath} from './getBuildPath'; +import {FlagsT} from './createRun'; + +export async function runOnSimulator( + xcodeProject: IOSProjectInfo, + platform: string, + mode: string, + scheme: string, + args: FlagsT, + simulator: Device, +) { + /** + * Booting simulator through `xcrun simctl boot` will boot it in the `headless` mode + * (running in the background). + * + * In order for user to see the app and the simulator itself, we have to make sure + * that the Simulator.app is running. + * + * We also pass it `-CurrentDeviceUDID` so that when we launch it for the first time, + * it will not boot the "default" device, but the one we set. If the app is already running, + * this flag has no effect. + */ + const activeDeveloperDir = child_process + .execFileSync('xcode-select', ['-p'], {encoding: 'utf8'}) + .trim(); + + child_process.execFileSync('open', [ + `${activeDeveloperDir}/Applications/Simulator.app`, + '--args', + '-CurrentDeviceUDID', + simulator.udid, + ]); + + if (simulator.state !== 'Booted') { + bootSimulator(simulator); + } + + let buildOutput, appPath; + if (!args.binaryPath) { + buildOutput = await buildProject( + xcodeProject, + platform, + simulator.udid, + mode, + scheme, + args, + ); + + appPath = await getBuildPath( + xcodeProject, + mode, + buildOutput, + scheme, + args.target, + ); + } else { + appPath = args.binaryPath; + } + + logger.info(`Installing "${chalk.bold(appPath)} on ${simulator.name}"`); + + child_process.spawnSync( + 'xcrun', + ['simctl', 'install', simulator.udid, appPath], + {stdio: 'inherit'}, + ); + + const bundleID = child_process + .execFileSync( + '/usr/libexec/PlistBuddy', + ['-c', 'Print:CFBundleIdentifier', path.join(appPath, 'Info.plist')], + {encoding: 'utf8'}, + ) + .trim(); + + logger.info(`Launching "${chalk.bold(bundleID)}"`); + + const result = child_process.spawnSync('xcrun', [ + 'simctl', + 'launch', + simulator.udid, + bundleID, + ]); + + if (result.status === 0) { + logger.success('Successfully launched the app on the simulator'); + } else { + logger.error( + 'Failed to launch the app on simulator', + result.stderr.toString(), + ); + } +} + +function bootSimulator(selectedSimulator: Device) { + const simulatorFullName = formattedDeviceName(selectedSimulator); + logger.info(`Launching ${simulatorFullName}`); + + child_process.spawnSync('xcrun', ['simctl', 'boot', selectedSimulator.udid]); +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/runOptions.ts b/packages/cli-platform-apple/src/commands/runCommand/runOptions.ts new file mode 100644 index 000000000..f641aaaf4 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/runOptions.ts @@ -0,0 +1,46 @@ +import {getDefaultUserTerminal} from '@react-native-community/cli-tools'; + +export const runOptions = [ + { + name: '--no-packager', + description: 'Do not launch packager while running the app', + }, + { + name: '--port ', + default: process.env.RCT_METRO_PORT || 8081, + parse: Number, + }, + { + name: '--terminal ', + description: + 'Launches the Metro Bundler in a new window using the specified terminal path.', + default: getDefaultUserTerminal(), + }, + { + name: '--binary-path ', + description: + 'Path relative to project root where pre-built .app binary lives.', + }, + { + name: '--list-devices', + description: + 'List all available iOS devices and simulators and let you choose one to run the app. ', + }, + { + name: '--simulator ', + description: + 'Explicitly set the simulator to use. Optionally set the iOS version ' + + 'between parentheses at the end to match an exact version: ' + + '"iPhone 15 (17.0)"', + }, + { + name: '--device ', + description: + 'Explicitly set the device to use by name. The value is not required ' + + 'if you have a single device connected.', + }, + { + name: '--udid ', + description: 'Explicitly set the device to use by UDID', + }, +]; diff --git a/packages/cli-platform-ios/src/config/__fixtures__/files/project.pbxproj b/packages/cli-platform-apple/src/config/__fixtures__/files/project.pbxproj similarity index 100% rename from packages/cli-platform-ios/src/config/__fixtures__/files/project.pbxproj rename to packages/cli-platform-apple/src/config/__fixtures__/files/project.pbxproj diff --git a/packages/cli-platform-ios/src/config/__fixtures__/projects.ts b/packages/cli-platform-apple/src/config/__fixtures__/projects.ts similarity index 100% rename from packages/cli-platform-ios/src/config/__fixtures__/projects.ts rename to packages/cli-platform-apple/src/config/__fixtures__/projects.ts diff --git a/packages/cli-platform-ios/src/config/__tests__/__snapshots__/findPodfilePath.test.ts.snap b/packages/cli-platform-apple/src/config/__tests__/__snapshots__/findPodfilePath.test.ts.snap similarity index 100% rename from packages/cli-platform-ios/src/config/__tests__/__snapshots__/findPodfilePath.test.ts.snap rename to packages/cli-platform-apple/src/config/__tests__/__snapshots__/findPodfilePath.test.ts.snap diff --git a/packages/cli-platform-ios/src/config/__tests__/findPodfilePath.test.ts b/packages/cli-platform-apple/src/config/__tests__/findPodfilePath.test.ts similarity index 100% rename from packages/cli-platform-ios/src/config/__tests__/findPodfilePath.test.ts rename to packages/cli-platform-apple/src/config/__tests__/findPodfilePath.test.ts diff --git a/packages/cli-platform-ios/src/config/__tests__/findPodspec.test.ts b/packages/cli-platform-apple/src/config/__tests__/findPodspec.test.ts similarity index 100% rename from packages/cli-platform-ios/src/config/__tests__/findPodspec.test.ts rename to packages/cli-platform-apple/src/config/__tests__/findPodspec.test.ts diff --git a/packages/cli-platform-ios/src/config/__tests__/findXcodeProject.test.ts b/packages/cli-platform-apple/src/config/__tests__/findXcodeProject.test.ts similarity index 100% rename from packages/cli-platform-ios/src/config/__tests__/findXcodeProject.test.ts rename to packages/cli-platform-apple/src/config/__tests__/findXcodeProject.test.ts diff --git a/packages/cli-platform-ios/src/config/__tests__/getProjectConfig.test.ts b/packages/cli-platform-apple/src/config/__tests__/getProjectConfig.test.ts similarity index 92% rename from packages/cli-platform-ios/src/config/__tests__/getProjectConfig.test.ts rename to packages/cli-platform-apple/src/config/__tests__/getProjectConfig.test.ts index bf0741aca..735d7daf7 100644 --- a/packages/cli-platform-ios/src/config/__tests__/getProjectConfig.test.ts +++ b/packages/cli-platform-apple/src/config/__tests__/getProjectConfig.test.ts @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. * */ -import {projectConfig} from '../index'; +import {getProjectConfig} from '../index'; + +const projectConfig = getProjectConfig({platformName: 'ios'}); jest.mock('path'); jest.mock('fs'); diff --git a/packages/cli-platform-ios/src/config/findAllPodfilePaths.ts b/packages/cli-platform-apple/src/config/findAllPodfilePaths.ts similarity index 100% rename from packages/cli-platform-ios/src/config/findAllPodfilePaths.ts rename to packages/cli-platform-apple/src/config/findAllPodfilePaths.ts diff --git a/packages/cli-platform-ios/src/config/findPodfilePath.ts b/packages/cli-platform-apple/src/config/findPodfilePath.ts similarity index 79% rename from packages/cli-platform-ios/src/config/findPodfilePath.ts rename to packages/cli-platform-apple/src/config/findPodfilePath.ts index a299f35be..26a8d22f4 100644 --- a/packages/cli-platform-ios/src/config/findPodfilePath.ts +++ b/packages/cli-platform-apple/src/config/findPodfilePath.ts @@ -13,20 +13,20 @@ import findAllPodfilePaths from './findAllPodfilePaths'; // Regexp matching all test projects const TEST_PROJECTS = /test|example|sample/i; -// Base iOS folder -const IOS_BASE = 'ios'; - // Podfile in the bundle package const BUNDLE_VENDORED_PODFILE = 'vendor/bundle/ruby'; -export default function findPodfilePath(cwd: string) { +export default function findPodfilePath( + cwd: string, + platformName: string = 'ios', +) { const podfiles = findAllPodfilePaths(cwd) /** * Then, we will run a simple test to rule out most example projects, - * unless they are located in a `ios` folder + * unless they are located in a `platformName` folder */ .filter((project) => { - if (path.dirname(project) === IOS_BASE) { + if (path.dirname(project) === platformName) { // Pick the Podfile in the default project (in the iOS folder) return true; } @@ -45,16 +45,16 @@ export default function findPodfilePath(cwd: string) { return true; }) /** - * Podfile from `ios` folder will be picked up as a first one. + * Podfile from `platformName` folder will be picked up as a first one. */ - .sort((project) => (path.dirname(project) === IOS_BASE ? -1 : 1)); + .sort((project) => (path.dirname(project) === platformName ? -1 : 1)); if (podfiles.length > 0) { if (podfiles.length > 1) { logger.warn( inlineString(` Multiple Podfiles were found: ${podfiles}. Choosing ${podfiles[0]} automatically. - If you would like to select a different one, you can configure it via "project.ios.sourceDir". + If you would like to select a different one, you can configure it via "project.${platformName}.sourceDir". You can learn more about it here: https://github.com/react-native-community/cli/blob/main/docs/configuration.md `), ); diff --git a/packages/cli-platform-ios/src/config/findPodspec.ts b/packages/cli-platform-apple/src/config/findPodspec.ts similarity index 100% rename from packages/cli-platform-ios/src/config/findPodspec.ts rename to packages/cli-platform-apple/src/config/findPodspec.ts diff --git a/packages/cli-platform-ios/src/config/findXcodeProject.ts b/packages/cli-platform-apple/src/config/findXcodeProject.ts similarity index 100% rename from packages/cli-platform-ios/src/config/findXcodeProject.ts rename to packages/cli-platform-apple/src/config/findXcodeProject.ts diff --git a/packages/cli-platform-apple/src/config/index.ts b/packages/cli-platform-apple/src/config/index.ts new file mode 100644 index 000000000..e8f546bcf --- /dev/null +++ b/packages/cli-platform-apple/src/config/index.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import chalk from 'chalk'; +import path from 'path'; +import fs from 'fs'; +import findPodfilePath from './findPodfilePath'; +import findXcodeProject from './findXcodeProject'; +import findPodspec from './findPodspec'; +import findAllPodfilePaths from './findAllPodfilePaths'; +import { + IOSProjectParams, + IOSDependencyParams, + IOSProjectConfig, + IOSDependencyConfig, +} from '@react-native-community/cli-types'; +import {CLIError} from '@react-native-community/cli-tools'; +import {BuilderCommand} from '../types'; + +/** + * Returns project config by analyzing given folder and applying some user defaults + * when constructing final object + */ +export const getProjectConfig = + ({platformName}: BuilderCommand) => + (folder: string, userConfig: IOSProjectParams): IOSProjectConfig | null => { + if (!userConfig) { + return null; + } + + const src = path.join(folder, userConfig.sourceDir ?? ''); + const podfile = findPodfilePath(src, platformName); + + /** + * In certain repos, the Xcode project can be generated by a tool. + * The only file that we can assume to exist on disk is `Podfile`. + */ + if (!podfile) { + return null; + } + + const sourceDir = path.dirname(podfile); + + const xcodeProject = findXcodeProject(fs.readdirSync(sourceDir)); + + return { + sourceDir, + watchModeCommandParams: userConfig.watchModeCommandParams, + xcodeProject, + automaticPodsInstallation: userConfig.automaticPodsInstallation, + }; + }; + +export function dependencyConfig( + folder: string, + userConfig: IOSDependencyParams | null = {}, +): IOSDependencyConfig | null { + if (userConfig === null) { + return null; + } + + const podspecPath = findPodspec(folder); + + if (!podspecPath) { + return null; + } + + let version = 'unresolved'; + + try { + const packageJson = require(path.join(folder, 'package.json')); + + if (packageJson.version) { + version = packageJson.version; + } + } catch { + throw new CLIError( + `Failed to locate package.json file from ${chalk.underline( + folder, + )}. This is most likely issue with your node_modules folder being corrupted. Please force install dependencies and try again`, + ); + } + + return { + podspecPath, + version, + configurations: userConfig.configurations || [], + scriptPhases: userConfig.scriptPhases || [], + }; +} + +export const findPodfilePaths = findAllPodfilePaths; diff --git a/packages/cli-platform-apple/src/index.ts b/packages/cli-platform-apple/src/index.ts new file mode 100644 index 000000000..a096a0f92 --- /dev/null +++ b/packages/cli-platform-apple/src/index.ts @@ -0,0 +1,12 @@ +export {dependencyConfig, getProjectConfig, findPodfilePaths} from './config'; + +export {buildOptions} from './commands/buildCommand/buildOptions'; +export {logOptions} from './commands/logCommand/logOptions'; +export {runOptions} from './commands/runCommand/runOptions'; + +export {default as createBuild} from './commands/buildCommand/createBuild'; +export {default as createLog} from './commands/logCommand/createLog'; +export {default as createRun} from './commands/runCommand/createRun'; + +export {default as getArchitecture} from './tools/getArchitecture'; +export {default as installPods} from './tools/installPods'; diff --git a/packages/cli-platform-ios/src/tools/__tests__/checkIfConfigurationExists.test.ts b/packages/cli-platform-apple/src/tools/__tests__/checkIfConfigurationExists.test.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/__tests__/checkIfConfigurationExists.test.ts rename to packages/cli-platform-apple/src/tools/__tests__/checkIfConfigurationExists.test.ts diff --git a/packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts b/packages/cli-platform-apple/src/tools/__tests__/findMatchingSimulator.test.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts rename to packages/cli-platform-apple/src/tools/__tests__/findMatchingSimulator.test.ts diff --git a/packages/cli-platform-ios/src/tools/__tests__/listIOSDevices.test.ts b/packages/cli-platform-apple/src/tools/__tests__/listDevices.test.ts similarity index 74% rename from packages/cli-platform-ios/src/tools/__tests__/listIOSDevices.test.ts rename to packages/cli-platform-apple/src/tools/__tests__/listDevices.test.ts index a592d4763..5a7cce091 100644 --- a/packages/cli-platform-ios/src/tools/__tests__/listIOSDevices.test.ts +++ b/packages/cli-platform-apple/src/tools/__tests__/listDevices.test.ts @@ -7,7 +7,7 @@ */ import execa from 'execa'; -import listIOSDevices from '../listIOSDevices'; +import listDevices from '../listDevices'; jest.mock('execa', () => { return {sync: jest.fn()}; @@ -161,16 +161,17 @@ const xcrunOut = ` ] `; -describe('listIOSDevices', () => { - it('parses output from xcdevice list', async () => { +describe('listDevices', () => { + it('parses output from xcdevice list for iOS', async () => { (execa.sync as jest.Mock).mockReturnValueOnce({stdout: xcrunOut}); - const devices = await listIOSDevices(); + const devices = await listDevices(['iphoneos', 'iphonesimulator']); // Find all available simulators expect(devices).toContainEqual({ - name: 'iPhone 14 Plus', isAvailable: true, + name: 'iPhone 14 Plus', udid: 'A88CFA2A-05C8-44EE-9B67-7AEFE1624E2F', + sdk: 'com.apple.platform.iphonesimulator', version: '16.0 (20A360)', availabilityError: undefined, type: 'simulator', @@ -179,6 +180,7 @@ describe('listIOSDevices', () => { name: 'iPhone SE (3rd generation)', isAvailable: true, udid: '1202C373-7381-433C-84FA-EF6741078CC1', + sdk: 'com.apple.platform.iphonesimulator', version: '16.0 (20A360)', availabilityError: undefined, type: 'simulator', @@ -190,6 +192,7 @@ describe('listIOSDevices', () => { isAvailable: false, udid: '1234567890-0987654321', version: '16.2 (20C65)', + sdk: 'com.apple.platform.iphoneos', availabilityError: 'To use Adam’s iPhone for development, enable Developer Mode in Settings → Privacy & Security.', type: 'device', @@ -200,6 +203,7 @@ describe('listIOSDevices', () => { name: 'Living Room', udid: '7656fbf922891c8a2c7682c9d845eaa6954c24d8', version: '16.1 (20K71)', + sdk: 'com.apple.platform.appletvos', availabilityError: 'Living Room is not connected', type: 'device', }); @@ -207,6 +211,7 @@ describe('listIOSDevices', () => { isAvailable: true, name: 'Apple TV 4K (2nd generation)', udid: 'F022AD06-DFD3-4B9F-B4DD-3C30E17E7CE6', + sdk: 'com.apple.platform.appletvsimulator', version: '16.0 (20J373)', availabilityError: undefined, type: 'simulator', @@ -221,4 +226,62 @@ describe('listIOSDevices', () => { type: 'device', }); }); + + it('parses output from xcdevice list for tvOS', async () => { + (execa.sync as jest.Mock).mockReturnValueOnce({stdout: xcrunOut}); + const devices = await listDevices(['appletvos', 'appletvsimulator']); + + // Filter out all available simulators + expect(devices).not.toContainEqual({ + isAvailable: true, + name: 'iPhone 14 Plus', + udid: 'A88CFA2A-05C8-44EE-9B67-7AEFE1624E2F', + sdk: 'com.apple.platform.iphonesimulator', + version: '16.0 (20A360)', + availabilityError: undefined, + type: 'simulator', + }); + + // Filter out all available iPhone's event when not available + expect(devices).not.toContainEqual({ + name: 'Adam’s iPhone', + isAvailable: false, + udid: '1234567890-0987654321', + version: '16.2 (20C65)', + sdk: 'com.apple.platform.iphoneos', + availabilityError: + 'To use Adam’s iPhone for development, enable Developer Mode in Settings → Privacy & Security.', + type: 'device', + }); + + // Find AppleTV + expect(devices).toContainEqual({ + isAvailable: false, + name: 'Living Room', + udid: '7656fbf922891c8a2c7682c9d845eaa6954c24d8', + sdk: 'com.apple.platform.appletvos', + version: '16.1 (20K71)', + availabilityError: 'Living Room is not connected', + type: 'device', + }); + expect(devices).toContainEqual({ + isAvailable: true, + name: 'Apple TV 4K (2nd generation)', + udid: 'F022AD06-DFD3-4B9F-B4DD-3C30E17E7CE6', + sdk: 'com.apple.platform.appletvsimulator', + version: '16.0 (20J373)', + availabilityError: undefined, + type: 'simulator', + }); + + // Filter out macOS + expect(devices).not.toContainEqual({ + isAvailable: true, + name: 'My Mac', + udid: '11111111-131230917230918374', + version: '13.0.1 (22A400)', + availabilityError: undefined, + type: 'device', + }); + }); }); diff --git a/packages/cli-platform-ios/src/tools/checkIfConfigurationExists.ts b/packages/cli-platform-apple/src/tools/checkIfConfigurationExists.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/checkIfConfigurationExists.ts rename to packages/cli-platform-apple/src/tools/checkIfConfigurationExists.ts diff --git a/packages/cli-platform-ios/src/tools/findMatchingSimulator.ts b/packages/cli-platform-apple/src/tools/findMatchingSimulator.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/findMatchingSimulator.ts rename to packages/cli-platform-apple/src/tools/findMatchingSimulator.ts diff --git a/packages/cli-platform-ios/src/tools/getArchitecture.ts b/packages/cli-platform-apple/src/tools/getArchitecture.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/getArchitecture.ts rename to packages/cli-platform-apple/src/tools/getArchitecture.ts diff --git a/packages/cli-platform-ios/src/tools/getBuildConfigurationFromXcScheme.ts b/packages/cli-platform-apple/src/tools/getBuildConfigurationFromXcScheme.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/getBuildConfigurationFromXcScheme.ts rename to packages/cli-platform-apple/src/tools/getBuildConfigurationFromXcScheme.ts diff --git a/packages/cli-platform-ios/src/tools/getDestinationSimulator.ts b/packages/cli-platform-apple/src/tools/getDestinationSimulator.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/getDestinationSimulator.ts rename to packages/cli-platform-apple/src/tools/getDestinationSimulator.ts diff --git a/packages/cli-platform-ios/src/tools/getInfo.ts b/packages/cli-platform-apple/src/tools/getInfo.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/getInfo.ts rename to packages/cli-platform-apple/src/tools/getInfo.ts diff --git a/packages/cli-platform-ios/src/tools/getSimulators.ts b/packages/cli-platform-apple/src/tools/getSimulators.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/getSimulators.ts rename to packages/cli-platform-apple/src/tools/getSimulators.ts diff --git a/packages/cli-platform-ios/src/tools/installPods.ts b/packages/cli-platform-apple/src/tools/installPods.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/installPods.ts rename to packages/cli-platform-apple/src/tools/installPods.ts diff --git a/packages/cli-platform-ios/src/tools/listIOSDevices.ts b/packages/cli-platform-apple/src/tools/listDevices.ts similarity index 73% rename from packages/cli-platform-ios/src/tools/listIOSDevices.ts rename to packages/cli-platform-apple/src/tools/listDevices.ts index 47c149fb8..f039aa7a0 100644 --- a/packages/cli-platform-ios/src/tools/listIOSDevices.ts +++ b/packages/cli-platform-apple/src/tools/listDevices.ts @@ -31,20 +31,17 @@ type DeviceOutput = { modelUTI: string; }; -const parseXcdeviceList = (text: string): Device[] => { +const parseXcdeviceList = (text: string, sdkNames: string[] = []): Device[] => { const rawOutput = JSON.parse(text) as DeviceOutput[]; const devices: Device[] = rawOutput - .filter( - (device) => - !device.platform.includes('appletv') && - !device.platform.includes('macos'), - ) + .filter((device) => sdkNames.includes(stripPlatform(device?.platform))) .sort((device) => (device.simulator ? 1 : -1)) .map((device) => ({ isAvailable: device.available, name: device.name, udid: device.identifier, + sdk: device.platform, version: device.operatingSystemVersion, availabilityError: device.error?.description, type: device.simulator ? 'simulator' : 'device', @@ -52,9 +49,13 @@ const parseXcdeviceList = (text: string): Device[] => { return devices; }; -async function listIOSDevices(): Promise { +async function listDevices(sdkNames: string[]): Promise { const out = execa.sync('xcrun', ['xcdevice', 'list']).stdout; - return parseXcdeviceList(out); + return parseXcdeviceList(out, sdkNames); } -export default listIOSDevices; +function stripPlatform(platform: string): string { + return platform.replace('com.apple.platform.', ''); +} + +export default listDevices; diff --git a/packages/cli-platform-ios/src/tools/pods.ts b/packages/cli-platform-apple/src/tools/pods.ts similarity index 80% rename from packages/cli-platform-ios/src/tools/pods.ts rename to packages/cli-platform-apple/src/tools/pods.ts index df38114c9..8791c1fcb 100644 --- a/packages/cli-platform-ios/src/tools/pods.ts +++ b/packages/cli-platform-apple/src/tools/pods.ts @@ -33,14 +33,20 @@ export function getPackageJson(root: string) { } } -export function getIosDependencies(dependencies: NativeDependencies) { +export function getPlatformDependencies( + dependencies: NativeDependencies, + platformName: string = 'ios', +) { return Object.keys(dependencies) - .filter((dependency) => dependencies[dependency].platforms.ios) + .filter((dependency) => dependencies[dependency].platforms?.[platformName]) .map( (dependency) => `${dependency}@${ - (dependencies[dependency].platforms.ios as IOSDependencyConfig) - .version + ( + dependencies[dependency].platforms?.[ + platformName + ] as IOSDependencyConfig + ).version }`, ) .sort(); @@ -85,17 +91,21 @@ async function install( export default async function resolvePods( root: string, nativeDependencies: NativeDependencies, + platformName: string = 'ios', options?: ResolvePodsOptions, ) { const packageJson = getPackageJson(root); - const podfilePath = findPodfilePath(root); - const iosFolderPath = podfilePath + const podfilePath = findPodfilePath(root, platformName); + const platformFolderPath = podfilePath ? podfilePath.slice(0, podfilePath.lastIndexOf('/')) - : path.join(root, 'ios'); - const podsPath = path.join(iosFolderPath, 'Pods'); + : path.join(root, platformName); + const podsPath = path.join(platformFolderPath, 'Pods'); const arePodsInstalled = fs.existsSync(podsPath); - const iosDependencies = getIosDependencies(nativeDependencies); - const dependenciesString = dependenciesToString(iosDependencies); + const platformDependencies = getPlatformDependencies( + nativeDependencies, + platformName, + ); + const dependenciesString = dependenciesToString(platformDependencies); const currentDependenciesHash = generateMd5Hash(dependenciesString); const cachedDependenciesHash = cacheManager.get( packageJson.name, @@ -107,7 +117,7 @@ export default async function resolvePods( packageJson, cachedDependenciesHash, currentDependenciesHash, - iosFolderPath, + platformFolderPath, ); } else if (arePodsInstalled && cachedDependenciesHash === undefined) { cacheManager.set(packageJson.name, 'dependencies', currentDependenciesHash); @@ -121,7 +131,7 @@ export default async function resolvePods( await installPods(loader, { skipBundleInstall: !!cachedDependenciesHash, newArchEnabled: options?.newArchEnabled, - iosFolderPath, + iosFolderPath: platformFolderPath, }); cacheManager.set( packageJson.name, diff --git a/packages/cli-platform-ios/src/tools/prompts.ts b/packages/cli-platform-apple/src/tools/prompts.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/prompts.ts rename to packages/cli-platform-apple/src/tools/prompts.ts diff --git a/packages/cli-platform-ios/src/tools/runBundleInstall.ts b/packages/cli-platform-apple/src/tools/runBundleInstall.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/runBundleInstall.ts rename to packages/cli-platform-apple/src/tools/runBundleInstall.ts diff --git a/packages/cli-platform-ios/src/tools/selectFromInteractiveMode.ts b/packages/cli-platform-apple/src/tools/selectFromInteractiveMode.ts similarity index 100% rename from packages/cli-platform-ios/src/tools/selectFromInteractiveMode.ts rename to packages/cli-platform-apple/src/tools/selectFromInteractiveMode.ts diff --git a/packages/cli-platform-ios/src/types.ts b/packages/cli-platform-apple/src/types.ts similarity index 69% rename from packages/cli-platform-ios/src/types.ts rename to packages/cli-platform-apple/src/types.ts index ce2d52793..22ed7e303 100644 --- a/packages/cli-platform-ios/src/types.ts +++ b/packages/cli-platform-apple/src/types.ts @@ -5,6 +5,7 @@ export interface Device { availability?: string; isAvailable?: boolean; version?: string; + sdk?: string; availabilityError?: string; type?: 'simulator' | 'device' | 'catalyst'; lastBootedAt?: string; @@ -16,3 +17,11 @@ export interface IosInfo { configurations?: string[]; targets?: string[]; } + +export interface BuilderCommand { + /** + * Lowercase name of the platform. + * Example: 'ios', 'visionos' + */ + platformName: string; +} diff --git a/packages/cli-platform-apple/tsconfig.json b/packages/cli-platform-apple/tsconfig.json new file mode 100644 index 000000000..802f5b8a8 --- /dev/null +++ b/packages/cli-platform-apple/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + }, + "references": [{"path": "../cli-tools"}, {"path": "../cli-types"}] +} diff --git a/packages/cli-platform-ios/README.md b/packages/cli-platform-ios/README.md index 20210cd1a..6d8dae878 100644 --- a/packages/cli-platform-ios/README.md +++ b/packages/cli-platform-ios/README.md @@ -113,6 +113,7 @@ Installs passed binary instead of building a fresh one. > default: false List all available iOS devices and simulators and let you choose one to run the app. + #### `--force-pods`, Force running `pod install` before running an app @@ -171,6 +172,7 @@ npx react-native build-ios --extra-params "-jobs 4" #### `--force-pods`, Force running `pod install` before building an app + ### `log-ios` Usage: @@ -186,7 +188,3 @@ Starts iOS device syslog tail. #### `--interactive` Explicitly select simulator to tail logs from. By default it will tail logs from the first booted and available simulator. - -## License - -Everything inside this repository is [MIT licensed](./LICENSE). diff --git a/packages/cli-platform-ios/package.json b/packages/cli-platform-ios/package.json index 21c86bd1d..b12fa5afc 100644 --- a/packages/cli-platform-ios/package.json +++ b/packages/cli-platform-ios/package.json @@ -7,18 +7,7 @@ "access": "public" }, "dependencies": { - "@react-native-community/cli-tools": "13.1.0", - "chalk": "^4.1.2", - "execa": "^5.0.0", - "fast-xml-parser": "^4.0.12", - "glob": "^7.1.3", - "ora": "^5.4.1" - }, - "devDependencies": { - "@react-native-community/cli-types": "13.1.0", - "@types/glob": "^7.1.1", - "@types/lodash": "^4.14.149", - "hasbin": "^1.2.3" + "@react-native-community/cli-platform-apple": "13.1.0" }, "files": [ "build", @@ -30,6 +19,6 @@ "repository": { "type": "git", "url": "https://github.com/react-native-community/cli.git", - "directory": "packages/platform-ios" + "directory": "packages/cli-platform-ios" } } diff --git a/packages/cli-platform-ios/src/commands/buildIOS/index.ts b/packages/cli-platform-ios/src/commands/buildIOS/index.ts index 7fa28aaf8..2763792f5 100644 --- a/packages/cli-platform-ios/src/commands/buildIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/buildIOS/index.ts @@ -6,39 +6,15 @@ * */ -import {Config} from '@react-native-community/cli-types'; -import {buildProject} from './buildProject'; -import {BuildFlags, buildOptions} from './buildOptions'; -import {getConfiguration} from './getConfiguration'; -import {getXcodeProjectAndDir} from './getXcodeProjectAndDir'; -import resolvePods from '../../tools/pods'; -import getArchitecture from '../../tools/getArchitecture'; - -async function buildIOS(_: Array, ctx: Config, args: BuildFlags) { - const {xcodeProject, sourceDir} = getXcodeProjectAndDir(ctx.project.ios); - - if (ctx.project.ios?.automaticPodsInstallation || args.forcePods) { - const isAppRunningNewArchitecture = ctx.project.ios?.sourceDir - ? await getArchitecture(ctx.project.ios?.sourceDir) - : undefined; - - await resolvePods(ctx.root, ctx.dependencies, { - forceInstall: args.forcePods, - newArchEnabled: isAppRunningNewArchitecture, - }); - } - - process.chdir(sourceDir); - - const {scheme, mode} = await getConfiguration(xcodeProject, sourceDir, args); - - return buildProject(xcodeProject, undefined, mode, scheme, args); -} +import { + buildOptions, + createBuild, +} from '@react-native-community/cli-platform-apple'; export default { name: 'build-ios', description: 'builds your app for iOS platform', - func: buildIOS, + func: createBuild({platformName: 'ios'}), examples: [ { desc: 'Build the app for all iOS devices in Release mode', diff --git a/packages/cli-platform-ios/src/commands/logIOS/index.ts b/packages/cli-platform-ios/src/commands/logIOS/index.ts index d35da0b60..be600c3fd 100644 --- a/packages/cli-platform-ios/src/commands/logIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/logIOS/index.ts @@ -6,97 +6,14 @@ * */ -import {spawnSync} from 'child_process'; -import os from 'os'; -import path from 'path'; -import {logger, prompt} from '@react-native-community/cli-tools'; -import listIOSDevices from '../../tools/listIOSDevices'; -import getSimulators from '../../tools/getSimulators'; -import {Config} from '@react-native-community/cli-types'; - -/** - * Starts iOS device syslog tail - */ - -type Args = { - interactive: boolean; -}; - -async function logIOS(_argv: Array, _ctx: Config, args: Args) { - // Here we're using two command because first command `xcrun simctl list --json devices` outputs `state` but doesn't return `available`. But second command `xcrun xcdevice list` outputs `available` but doesn't output `state`. So we need to connect outputs of both commands. - const simulators = getSimulators(); - const bootedSimulators = Object.keys(simulators.devices) - .map((key) => simulators.devices[key]) - .reduce((acc, val) => acc.concat(val), []) - .filter(({state}) => state === 'Booted'); - - const devices = await listIOSDevices(); - const availableSimulators = devices.filter( - ({type, isAvailable}) => type === 'simulator' && isAvailable, - ); - - if (availableSimulators.length === 0) { - logger.error('No simulators detected. Install simulators via Xcode.'); - return; - } - - const bootedAndAvailableSimulators = bootedSimulators.map((booted) => { - const available = availableSimulators.find( - ({udid}) => udid === booted.udid, - ); - return {...available, ...booted}; - }); - - if (bootedAndAvailableSimulators.length === 0) { - logger.error('No booted and available iOS simulators found.'); - return; - } - - if (args.interactive && bootedAndAvailableSimulators.length > 1) { - const {udid} = await prompt({ - type: 'select', - name: 'udid', - message: 'Select iOS simulators to tail logs from', - choices: bootedAndAvailableSimulators.map((simulator) => ({ - title: simulator.name, - value: simulator.udid, - })), - }); - - tailDeviceLogs(udid); - } else { - tailDeviceLogs(bootedAndAvailableSimulators[0].udid); - } -} - -function tailDeviceLogs(udid: string) { - const logDir = path.join( - os.homedir(), - 'Library', - 'Logs', - 'CoreSimulator', - udid, - 'asl', - ); - - const log = spawnSync('syslog', ['-w', '-F', 'std', '-d', logDir], { - stdio: 'inherit', - }); - - if (log.error !== null) { - throw log.error; - } -} +import { + createLog, + logOptions, +} from '@react-native-community/cli-platform-apple'; export default { name: 'log-ios', description: 'starts iOS device syslog tail', - func: logIOS, - options: [ - { - name: '--interactive', - description: - 'Explicitly select simulator to tail logs from. By default it will tail logs from the first booted and available simulator.', - }, - ], + func: createLog({platformName: 'ios'}), + options: logOptions, }; diff --git a/packages/cli-platform-ios/src/commands/runIOS/index.ts b/packages/cli-platform-ios/src/commands/runIOS/index.ts index bd31639b9..7b399e247 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/runIOS/index.ts @@ -6,619 +6,16 @@ * */ -import child_process from 'child_process'; -import path from 'path'; -import fs from 'fs'; -import chalk from 'chalk'; -import {Config, IOSProjectInfo} from '@react-native-community/cli-types'; -import {getDestinationSimulator} from '../../tools/getDestinationSimulator'; import { - logger, - CLIError, - link, - getDefaultUserTerminal, - startServerInNewWindow, - findDevServerPort, - cacheManager, -} from '@react-native-community/cli-tools'; -import {buildProject} from '../buildIOS/buildProject'; -import {BuildFlags, buildOptions} from '../buildIOS/buildOptions'; -import {getConfiguration} from '../buildIOS/getConfiguration'; -import {Device} from '../../types'; -import listIOSDevices from '../../tools/listIOSDevices'; -import {promptForDeviceSelection} from '../../tools/prompts'; -import getSimulators from '../../tools/getSimulators'; -import {getXcodeProjectAndDir} from '../buildIOS/getXcodeProjectAndDir'; -import resolvePods, {getPackageJson} from '../../tools/pods'; -import getArchitecture from '../../tools/getArchitecture'; -import findXcodeProject from '../../config/findXcodeProject'; - -export interface FlagsT extends BuildFlags { - simulator?: string; - device?: string | true; - udid?: string; - binaryPath?: string; - listDevices?: boolean; - packager?: boolean; - port: number; - terminal?: string; -} - -async function runIOS(_: Array, ctx: Config, args: FlagsT) { - link.setPlatform('ios'); - - let {packager, port} = args; - let installedPods = false; - // check if pods need to be installed - if (ctx.project.ios?.automaticPodsInstallation || args.forcePods) { - const isAppRunningNewArchitecture = ctx.project.ios?.sourceDir - ? await getArchitecture(ctx.project.ios?.sourceDir) - : undefined; - - await resolvePods(ctx.root, ctx.dependencies, { - forceInstall: args.forcePods, - newArchEnabled: isAppRunningNewArchitecture, - }); - - installedPods = true; - } - - if (packager) { - const {port: newPort, startPackager} = await findDevServerPort( - port, - ctx.root, - ); - - if (startPackager) { - await startServerInNewWindow( - newPort, - ctx.root, - ctx.reactNativePath, - args.terminal, - ); - } - } - - if (ctx.reactNativeVersion !== 'unknown') { - link.setVersion(ctx.reactNativeVersion); - } - - let {xcodeProject, sourceDir} = getXcodeProjectAndDir(ctx.project.ios); - - // if project is freshly created, revisit Xcode project to verify Pods are installed correctly. - // This is needed because ctx project is created before Pods are installed, so it might have outdated information. - if (installedPods) { - const recheckXcodeProject = findXcodeProject(fs.readdirSync(sourceDir)); - if (recheckXcodeProject) { - xcodeProject = recheckXcodeProject; - } - } - - process.chdir(sourceDir); - - if (args.binaryPath) { - args.binaryPath = path.isAbsolute(args.binaryPath) - ? args.binaryPath - : path.join(ctx.root, args.binaryPath); - - if (!fs.existsSync(args.binaryPath)) { - throw new CLIError( - 'binary-path was specified, but the file was not found.', - ); - } - } - - const {mode, scheme} = await getConfiguration(xcodeProject, sourceDir, args); - - const devices = await listIOSDevices(); - - const availableDevices = devices.filter( - ({isAvailable}) => isAvailable === true, - ); - - if (availableDevices.length === 0) { - return logger.error( - 'iOS devices or simulators not detected. Install simulators via Xcode or connect a physical iOS device', - ); - } - - if (args.listDevices || args.interactive) { - if (args.device || args.udid) { - logger.warn( - `Both ${ - args.device ? 'device' : 'udid' - } and "list-devices" parameters were passed to "run" command. We will list available devices and let you choose from one.`, - ); - } - - const packageJson = getPackageJson(ctx.root); - const preferredDevice = cacheManager.get( - packageJson.name, - 'lastUsedIOSDeviceId', - ); - - const selectedDevice = await promptForDeviceSelection( - availableDevices, - preferredDevice, - ); - - if (!selectedDevice) { - throw new CLIError( - `Failed to select device, please try to run app without ${ - args.listDevices ? 'list-devices' : 'interactive' - } command.`, - ); - } else { - if (selectedDevice.udid !== preferredDevice) { - cacheManager.set( - packageJson.name, - 'lastUsedIOSDeviceId', - selectedDevice.udid, - ); - } - } - - if (selectedDevice.type === 'simulator') { - return runOnSimulator(xcodeProject, mode, scheme, args, selectedDevice); - } else { - return runOnDevice(selectedDevice, mode, scheme, xcodeProject, args); - } - } - - if (!args.device && !args.udid && !args.simulator) { - const bootedDevices = availableDevices.filter( - ({type}) => type === 'device', - ); - - const simulators = getSimulators(); - const bootedSimulators = Object.keys(simulators.devices) - .map((key) => simulators.devices[key]) - .reduce((acc, val) => acc.concat(val), []) - .filter(({state}) => state === 'Booted'); - - const booted = [...bootedDevices, ...bootedSimulators]; - if (booted.length === 0) { - logger.info( - 'No booted devices or simulators found. Launching first available simulator...', - ); - return runOnSimulator(xcodeProject, mode, scheme, args); - } - - logger.info(`Found booted ${booted.map(({name}) => name).join(', ')}`); - - return runOnBootedDevicesSimulators( - mode, - scheme, - xcodeProject, - args, - bootedDevices, - bootedSimulators, - ); - } - - if (args.device && args.udid) { - return logger.error( - 'The `device` and `udid` options are mutually exclusive.', - ); - } - - if (args.udid) { - const device = availableDevices.find((d) => d.udid === args.udid); - if (!device) { - return logger.error( - `Could not find a device with udid: "${chalk.bold( - args.udid, - )}". ${printFoundDevices(availableDevices)}`, - ); - } - if (device.type === 'simulator') { - return runOnSimulator(xcodeProject, mode, scheme, args); - } else { - return runOnDevice(device, mode, scheme, xcodeProject, args); - } - } else if (args.device) { - const physicalDevices = availableDevices.filter( - ({type}) => type !== 'simulator', - ); - const device = matchingDevice(physicalDevices, args.device); - if (device) { - return runOnDevice(device, mode, scheme, xcodeProject, args); - } - } else { - runOnSimulator(xcodeProject, mode, scheme, args); - } -} - -async function runOnBootedDevicesSimulators( - mode: string, - scheme: string, - xcodeProject: IOSProjectInfo, - args: FlagsT, - devices: Device[], - simulators: Device[], -) { - for (const device of devices) { - await runOnDevice(device, mode, scheme, xcodeProject, args); - } - - for (const simulator of simulators) { - await runOnSimulator(xcodeProject, mode, scheme, args, simulator); - } -} - -async function runOnSimulator( - xcodeProject: IOSProjectInfo, - mode: string, - scheme: string, - args: FlagsT, - simulator?: Device, -) { - /** - * If provided simulator does not exist, try simulators in following order - * - iPhone 14 - * - iPhone 13 - * - iPhone 12 - * - iPhone 11 - */ - - let selectedSimulator; - if (simulator) { - selectedSimulator = simulator; - } else { - const fallbackSimulators = [ - 'iPhone 14', - 'iPhone 13', - 'iPhone 12', - 'iPhone 11', - ]; - selectedSimulator = getDestinationSimulator(args, fallbackSimulators); - } - - if (!selectedSimulator) { - throw new CLIError( - `No simulator available with ${ - args.simulator ? `name "${args.simulator}"` : `udid "${args.udid}"` - }`, - ); - } - - /** - * Booting simulator through `xcrun simctl boot` will boot it in the `headless` mode - * (running in the background). - * - * In order for user to see the app and the simulator itself, we have to make sure - * that the Simulator.app is running. - * - * We also pass it `-CurrentDeviceUDID` so that when we launch it for the first time, - * it will not boot the "default" device, but the one we set. If the app is already running, - * this flag has no effect. - */ - const activeDeveloperDir = child_process - .execFileSync('xcode-select', ['-p'], {encoding: 'utf8'}) - .trim(); - - child_process.execFileSync('open', [ - `${activeDeveloperDir}/Applications/Simulator.app`, - '--args', - '-CurrentDeviceUDID', - selectedSimulator.udid, - ]); - - if (selectedSimulator.state !== 'Booted') { - bootSimulator(selectedSimulator); - } - - let buildOutput, appPath; - if (!args.binaryPath) { - buildOutput = await buildProject( - xcodeProject, - selectedSimulator.udid, - mode, - scheme, - args, - ); - - appPath = await getBuildPath( - xcodeProject, - mode, - buildOutput, - scheme, - args.target, - ); - } else { - appPath = args.binaryPath; - } - - logger.info( - `Installing "${chalk.bold(appPath)} on ${selectedSimulator.name}"`, - ); - - child_process.spawnSync( - 'xcrun', - ['simctl', 'install', selectedSimulator.udid, appPath], - {stdio: 'inherit'}, - ); - - const bundleID = child_process - .execFileSync( - '/usr/libexec/PlistBuddy', - ['-c', 'Print:CFBundleIdentifier', path.join(appPath, 'Info.plist')], - {encoding: 'utf8'}, - ) - .trim(); - - logger.info(`Launching "${chalk.bold(bundleID)}"`); - - const result = child_process.spawnSync('xcrun', [ - 'simctl', - 'launch', - selectedSimulator.udid, - bundleID, - ]); - - if (result.status === 0) { - logger.success('Successfully launched the app on the simulator'); - } else { - logger.error( - 'Failed to launch the app on simulator', - result.stderr.toString(), - ); - } -} - -async function runOnDevice( - selectedDevice: Device, - mode: string, - scheme: string, - xcodeProject: IOSProjectInfo, - args: FlagsT, -) { - if (args.binaryPath && selectedDevice.type === 'catalyst') { - throw new CLIError( - 'binary-path was specified for catalyst device, which is not supported.', - ); - } - - const isIOSDeployInstalled = child_process.spawnSync( - 'ios-deploy', - ['--version'], - {encoding: 'utf8'}, - ); - - if (isIOSDeployInstalled.error) { - throw new CLIError( - `Failed to install the app on the device because we couldn't execute the "ios-deploy" command. Please install it by running "${chalk.bold( - 'brew install ios-deploy', - )}" and try again.`, - ); - } - - if (selectedDevice.type === 'catalyst') { - const buildOutput = await buildProject( - xcodeProject, - selectedDevice.udid, - mode, - scheme, - args, - ); - - const appPath = await getBuildPath( - xcodeProject, - mode, - buildOutput, - scheme, - args.target, - true, - ); - const appProcess = child_process.spawn(`${appPath}/${scheme}`, [], { - detached: true, - stdio: 'ignore', - }); - appProcess.unref(); - } else { - let buildOutput, appPath; - if (!args.binaryPath) { - buildOutput = await buildProject( - xcodeProject, - selectedDevice.udid, - mode, - scheme, - args, - ); - - appPath = await getBuildPath( - xcodeProject, - mode, - buildOutput, - scheme, - args.target, - ); - } else { - appPath = args.binaryPath; - } - - const iosDeployInstallArgs = [ - '--bundle', - appPath, - '--id', - selectedDevice.udid, - '--justlaunch', - ]; - - logger.info(`Installing and launching your app on ${selectedDevice.name}`); - - const iosDeployOutput = child_process.spawnSync( - 'ios-deploy', - iosDeployInstallArgs, - {encoding: 'utf8'}, - ); - - if (iosDeployOutput.error) { - throw new CLIError( - `Failed to install the app on the device. We've encountered an error in "ios-deploy" command: ${iosDeployOutput.error.message}`, - ); - } - } - - return logger.success('Installed the app on the device.'); -} - -function bootSimulator(selectedSimulator: Device) { - const simulatorFullName = formattedDeviceName(selectedSimulator); - logger.info(`Launching ${simulatorFullName}`); - - child_process.spawnSync('xcrun', ['simctl', 'boot', selectedSimulator.udid]); -} - -async function getTargetPaths( - buildSettings: string, - scheme: string, - target: string | undefined, -) { - const settings = JSON.parse(buildSettings); - - const targets = settings.map( - ({target: settingsTarget}: any) => settingsTarget, - ); - - let selectedTarget = targets[0]; - - if (target) { - if (!targets.includes(target)) { - logger.info( - `Target ${chalk.bold(target)} not found for scheme ${chalk.bold( - scheme, - )}, automatically selected target ${chalk.bold(selectedTarget)}`, - ); - } else { - selectedTarget = target; - } - } - - // Find app in all building settings - look for WRAPPER_EXTENSION: 'app', - - const targetIndex = targets.indexOf(selectedTarget); - - const wrapperExtension = - settings[targetIndex].buildSettings.WRAPPER_EXTENSION; - - if (wrapperExtension === 'app') { - return { - targetBuildDir: settings[targetIndex].buildSettings.TARGET_BUILD_DIR, - executableFolderPath: - settings[targetIndex].buildSettings.EXECUTABLE_FOLDER_PATH, - }; - } - - return {}; -} - -async function getBuildPath( - xcodeProject: IOSProjectInfo, - mode: string, - buildOutput: string, - scheme: string, - target: string | undefined, - isCatalyst: boolean = false, -) { - const buildSettings = child_process.execFileSync( - 'xcodebuild', - [ - xcodeProject.isWorkspace ? '-workspace' : '-project', - xcodeProject.name, - '-scheme', - scheme, - '-sdk', - getPlatformName(buildOutput), - '-configuration', - mode, - '-showBuildSettings', - '-json', - ], - {encoding: 'utf8'}, - ); - - const {targetBuildDir, executableFolderPath} = await getTargetPaths( - buildSettings, - scheme, - target, - ); - - if (!targetBuildDir) { - throw new CLIError('Failed to get the target build directory.'); - } - - if (!executableFolderPath) { - throw new CLIError('Failed to get the app name.'); - } - - return `${targetBuildDir}${ - isCatalyst ? '-maccatalyst' : '' - }/${executableFolderPath}`; -} - -function getPlatformName(buildOutput: string) { - // Xcode can sometimes escape `=` with a backslash or put the value in quotes - const platformNameMatch = /export PLATFORM_NAME\\?="?(\w+)"?$/m.exec( - buildOutput, - ); - if (!platformNameMatch) { - throw new CLIError( - 'Couldn\'t find "PLATFORM_NAME" variable in xcodebuild output. Please report this issue and run your project with Xcode instead.', - ); - } - return platformNameMatch[1]; -} - -function matchingDevice( - devices: Array, - deviceName: string | true | undefined, -) { - if (deviceName === true) { - const firstIOSDevice = devices.find((d) => d.type === 'device')!; - if (firstIOSDevice) { - logger.info( - `Using first available device named "${chalk.bold( - firstIOSDevice.name, - )}" due to lack of name supplied.`, - ); - return firstIOSDevice; - } else { - logger.error('No iOS devices connected.'); - return undefined; - } - } - const deviceByName = devices.find( - (device) => - device.name === deviceName || formattedDeviceName(device) === deviceName, - ); - if (!deviceByName) { - logger.error( - `Could not find a device named: "${chalk.bold( - String(deviceName), - )}". ${printFoundDevices(devices)}`, - ); - } - return deviceByName; -} - -function formattedDeviceName(simulator: Device) { - return simulator.version - ? `${simulator.name} (${simulator.version})` - : simulator.name; -} - -function printFoundDevices(devices: Array) { - return [ - 'Available devices:', - ...devices.map((device) => ` - ${device.name} (${device.udid})`), - ].join('\n'); -} + buildOptions, + createRun, + runOptions, +} from '@react-native-community/cli-platform-apple'; export default { name: 'run-ios', description: 'builds your app and starts it on iOS simulator', - func: runIOS, + func: createRun({platformName: 'ios'}), examples: [ { desc: 'Run on a different simulator, e.g. iPhone SE (2nd generation)', @@ -633,49 +30,5 @@ export default { cmd: 'npx react-native run-ios --simulator "Apple TV" --scheme "helloworld-tvOS"', }, ], - options: [ - ...buildOptions, - { - name: '--no-packager', - description: 'Do not launch packager while running the app', - }, - { - name: '--port ', - default: process.env.RCT_METRO_PORT || 8081, - parse: Number, - }, - { - name: '--terminal ', - description: - 'Launches the Metro Bundler in a new window using the specified terminal path.', - default: getDefaultUserTerminal(), - }, - { - name: '--binary-path ', - description: - 'Path relative to project root where pre-built .app binary lives.', - }, - { - name: '--list-devices', - description: - 'List all available iOS devices and simulators and let you choose one to run the app. ', - }, - { - name: '--simulator ', - description: - 'Explicitly set the simulator to use. Optionally set the iOS version ' + - 'between parentheses at the end to match an exact version: ' + - '"iPhone 15 (17.0)"', - }, - { - name: '--device ', - description: - 'Explicitly set the device to use by name. The value is not required ' + - 'if you have a single device connected.', - }, - { - name: '--udid ', - description: 'Explicitly set the device to use by UDID', - }, - ], + options: [...buildOptions, ...runOptions], }; diff --git a/packages/cli-platform-ios/src/config/index.ts b/packages/cli-platform-ios/src/config/index.ts index 38f2367d7..528097355 100644 --- a/packages/cli-platform-ios/src/config/index.ts +++ b/packages/cli-platform-ios/src/config/index.ts @@ -1,96 +1,4 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ -import chalk from 'chalk'; -import path from 'path'; -import fs from 'fs'; -import findPodfilePath from './findPodfilePath'; -import findXcodeProject from './findXcodeProject'; -import findPodspec from './findPodspec'; -import findAllPodfilePaths from './findAllPodfilePaths'; -import { - IOSProjectParams, - IOSDependencyParams, - IOSProjectConfig, - IOSDependencyConfig, -} from '@react-native-community/cli-types'; -import {CLIError} from '@react-native-community/cli-tools'; +import {getProjectConfig} from '@react-native-community/cli-platform-apple'; -/** - * Returns project config by analyzing given folder and applying some user defaults - * when constructing final object - */ -export function projectConfig( - folder: string, - userConfig: IOSProjectParams, -): IOSProjectConfig | null { - if (!userConfig) { - return null; - } - - const src = path.join(folder, userConfig.sourceDir ?? ''); - const podfile = findPodfilePath(src); - - /** - * In certain repos, the Xcode project can be generated by a tool. - * The only file that we can assume to exist on disk is `Podfile`. - */ - if (!podfile) { - return null; - } - - const sourceDir = path.dirname(podfile); - - const xcodeProject = findXcodeProject(fs.readdirSync(sourceDir)); - - return { - sourceDir, - watchModeCommandParams: userConfig.watchModeCommandParams, - xcodeProject, - automaticPodsInstallation: userConfig.automaticPodsInstallation, - }; -} - -export function dependencyConfig( - folder: string, - userConfig: IOSDependencyParams | null = {}, -): IOSDependencyConfig | null { - if (userConfig === null) { - return null; - } - - const podspecPath = findPodspec(folder); - - if (!podspecPath) { - return null; - } - - let version = 'unresolved'; - - try { - const packageJson = require(path.join(folder, 'package.json')); - - if (packageJson.version) { - version = packageJson.version; - } - } catch { - throw new CLIError( - `Failed to locate package.json file from ${chalk.underline( - folder, - )}. This is most likely issue with your node_modules folder being corrupted. Please force install dependencies and try again`, - ); - } - - return { - podspecPath, - version, - configurations: userConfig.configurations || [], - scriptPhases: userConfig.scriptPhases || [], - }; -} - -export const findPodfilePaths = findAllPodfilePaths; +export {dependencyConfig} from '@react-native-community/cli-platform-apple'; +export const projectConfig = getProjectConfig({platformName: 'ios'}); diff --git a/packages/cli-platform-ios/src/index.ts b/packages/cli-platform-ios/src/index.ts index 67fccf425..5b950c4c6 100644 --- a/packages/cli-platform-ios/src/index.ts +++ b/packages/cli-platform-ios/src/index.ts @@ -3,6 +3,11 @@ */ export {default as commands} from './commands'; -export {projectConfig, dependencyConfig, findPodfilePaths} from './config'; -export {default as getArchitecture} from './tools/getArchitecture'; -export {default as installPods} from './tools/installPods'; + +export { + findPodfilePaths, + getArchitecture, + installPods, +} from '@react-native-community/cli-platform-apple'; + +export {dependencyConfig, projectConfig} from './config'; diff --git a/packages/cli-platform-ios/tsconfig.json b/packages/cli-platform-ios/tsconfig.json index 604a72878..fc9e31eff 100644 --- a/packages/cli-platform-ios/tsconfig.json +++ b/packages/cli-platform-ios/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "rootDir": "src", "outDir": "build", - "typeRoots": ["definitions"] }, - "references": [{"path": "../cli-tools"}, {"path": "../cli-types"}] + "references": [{"path": "../cli-platform-apple"}] } diff --git a/packages/cli/src/commands/init/init.ts b/packages/cli/src/commands/init/init.ts index ee15b6987..7296ab8d5 100644 --- a/packages/cli/src/commands/init/init.ts +++ b/packages/cli/src/commands/init/init.ts @@ -13,7 +13,7 @@ import { cacheManager, prompt, } from '@react-native-community/cli-tools'; -import {installPods} from '@react-native-community/cli-platform-ios'; +import {installPods} from '@react-native-community/cli-platform-apple'; import { installTemplatePackage, getTemplateConfig,