diff --git a/.changeset/wicked-clocks-visit.md b/.changeset/wicked-clocks-visit.md new file mode 100644 index 00000000..97902069 --- /dev/null +++ b/.changeset/wicked-clocks-visit.md @@ -0,0 +1,5 @@ +--- +'@callstack/react-native-brownfield': patch +--- + +fix: autodetect iOS app target name for Expo pre-55 patch script phase diff --git a/docs/docs/docs/getting-started/expo.mdx b/docs/docs/docs/getting-started/expo.mdx index 8666e27a..e815e102 100644 --- a/docs/docs/docs/getting-started/expo.mdx +++ b/docs/docs/docs/getting-started/expo.mdx @@ -5,6 +5,7 @@ import { PackageManagerTabs } from '@theme'; This guide walks you through packaging your Expo React Native app as an **AAR** or **XCFramework** and integrating it into your native **Android** or **iOS** application. ## Prerequisites + - Install the `@callstack/react-native-brownfield` package from the quick start [section](/docs/getting-started/quick-start#installation) ## Configuration @@ -13,9 +14,7 @@ This guide walks you through packaging your Expo React Native app as an **AAR** ```json { - "plugins": [ - "@callstack/react-native-brownfield", - ] + "plugins": ["@callstack/react-native-brownfield"] } ``` @@ -55,7 +54,7 @@ This should only take a few minutes. > That is all from the AAR steps. We can now consume the AAR inside a native Android App. -### AAR: Present RN UI +### AAR: Present RN UI 1. Call the `ReactNativeHostManager.initialize` in your Activity or Application: @@ -80,6 +79,7 @@ override fun onConfigurationChanged(newConfig: Configuration) { ``` 3. Use either of the following APIs to present the UI: + - ReactNativeFragment - See the example [here](https://github.com/callstack/react-native-brownfield/blob/41c81059acda8b134b6fea6bbbcf918c20d16552/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt#L133) - ReactNativeFragment.createReactNativeFragment @@ -90,7 +90,7 @@ override fun onConfigurationChanged(newConfig: Configuration) { 4. Build and install the android application 🚀 -
+
## iOS Integration @@ -109,7 +109,7 @@ This should only take a few minutes. ##### Pre-Requisites - Follow the step for adding the frameworks to your iOS App - [here](/docs/getting-started/ios#6-add-the-framework-to-your-ios-app) -
+
1. Call the following functions from your Application Entry point: @@ -117,7 +117,7 @@ This should only take a few minutes. @main struct IosApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - + init() { ReactNativeBrownfield.shared.bundle = ReactNativeBundle ReactNativeBrownfield.shared.startReactNative { @@ -125,7 +125,7 @@ struct IosApp: App { } ReactNativeBrownfield.shared.ensureExpoModulesProvider() } - + var body: some Scene { WindowGroup { ContentView() @@ -139,7 +139,7 @@ struct IosApp: App { ```swift class AppDelegate: NSObject, UIApplicationDelegate { var window: UIWindow? - + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil @@ -160,7 +160,7 @@ ReactNativeView(moduleName: "ExpoRNApp") 4. Build and install the iOS application 🚀 -
+
## Plugin Options @@ -201,6 +201,14 @@ You can pass plugin options through the second item in the `plugins` tuple in `a - `frameworkVersion` (`string`, default: `"1"`) - Framework version used for Apple build settings (must be an integer or floating point value, for example `"1"` or `"2.1"`). +> The plugin will determine the application target automatically. The auto-detection path works as follows: +> +> 1. _Common for all cases_: scan the iOS targets for ones of type `com.apple.product-type.application` +> 2. Use the first matching strategy: +> - CNG-derived name from mod compiler (`modRequest.projectName`) - only if it exists in the filtered application-type list of Xcode project targets +> - Unambiguous first application-type target - if there is exactly one +> - PBX "first native target" fallback - last resort fallback that selects the first native target of any type + ### Android - `moduleName` (`string`, default: `"brownfieldlib"`) diff --git a/packages/react-native-brownfield/package.json b/packages/react-native-brownfield/package.json index 77fe3c08..1e56e4b7 100644 --- a/packages/react-native-brownfield/package.json +++ b/packages/react-native-brownfield/package.json @@ -82,8 +82,7 @@ "access": "public" }, "peerDependencies": { - "react": "*", - "react-native": "*" + "@expo/config-plugins": "^54.0.4" }, "dependencies": { "@callstack/brownfield-cli": "workspace:^" @@ -92,6 +91,7 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", + "@expo/config-plugins": "^54.0.4", "@react-native/babel-preset": "0.82.1", "@types/jest": "^30.0.0", "@types/react": "^19.1.1", diff --git a/packages/react-native-brownfield/src/expo-config-plugin/ios/withBrownfieldIos.ts b/packages/react-native-brownfield/src/expo-config-plugin/ios/withBrownfieldIos.ts index 61653c4c..894af550 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/ios/withBrownfieldIos.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/ios/withBrownfieldIos.ts @@ -32,7 +32,6 @@ export const withBrownfieldIos: ConfigPlugin< ? parseInt(config.sdkVersion.split('.')[0], 10) : -1; const isExpoPre55 = expoMajor < 55; - // Step 1: modify the Xcode project to add framework target & config = withXcodeProject(config, (xcodeConfig) => { const { modResults: project, modRequest } = xcodeConfig; @@ -60,7 +59,7 @@ export const withBrownfieldIos: ConfigPlugin< `Adding ExpoModulesProvider patch phase for Expo SDK ${config.sdkVersion}` ); - addExpoPre55ShellPatchScriptPhase(project, { + addExpoPre55ShellPatchScriptPhase(modRequest, project, { frameworkName: props.ios.frameworkName, frameworkTargetUUID: frameworkTargetUUID, }); diff --git a/packages/react-native-brownfield/src/expo-config-plugin/ios/xcodeHelpers.ts b/packages/react-native-brownfield/src/expo-config-plugin/ios/xcodeHelpers.ts index 58579077..82f577eb 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/ios/xcodeHelpers.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/ios/xcodeHelpers.ts @@ -1,6 +1,10 @@ import path from 'node:path'; -import type { ModProps, XcodeProject } from '@expo/config-plugins'; +import { + type ModProps, + type XcodeProject, + IOSConfig, +} from '@expo/config-plugins'; import { Logger } from '../logging'; import type { ResolvedBrownfieldPluginIosConfig } from '../types'; @@ -286,7 +290,66 @@ export function copyBundleReactNativePhase( } } +function resolveAppTargetName( + project: XcodeProject, + modRequest: ModProps +): string | null { + const appTargets = IOSConfig.Target.getNativeTargets(project) + .map(([, target]) => { + if ( + !IOSConfig.Target.isTargetOfType( + target, + IOSConfig.Target.TargetType.APPLICATION + ) + ) { + return null; + } + + const name = IOSConfig.XcodeUtils.unquote(target.name ?? '').trim(); + + return name ?? null; + }) + .filter((name): name is string => !!name); + + // 1) Unambiguous first application-type target + if (appTargets.length === 1) { + return appTargets[0]; + } else { + Logger.logWarning( + 'Multiple application targets found in the Xcode project. Falling back to the CNG-derived name from mod compiler.' + ); + } + + // 2) CNG-derived name from mod compiler (`modRequest.projectName`) - only if it exists in the filtered application-type list of Xcode project targets + const cngDerivedProjectName = modRequest.projectName; + if (cngDerivedProjectName && appTargets.includes(cngDerivedProjectName)) { + return cngDerivedProjectName; + } else { + Logger.logWarning( + 'CNG-derived name from mod compiler is not set or is not an application target. Falling back to the unfiltered-type target name.' + ); + } + + // 3) PBX "first native target" fallback + try { + const [, firstAppTarget] = IOSConfig.Target.findFirstNativeTarget(project); + const name = IOSConfig.XcodeUtils.unquote(firstAppTarget.name ?? '').trim(); + return name || null; + } catch { + Logger.logWarning( + 'No first native target of any type found in the Xcode project. This was the last resort fallback.' + ); + } + + Logger.logError( + `Could not determine the iOS app target name from the Xcode project. Please adjust your Xcode project to have exactly one application target.` + ); + + return null; +} + export function addExpoPre55ShellPatchScriptPhase( + modRequest: ModProps, project: XcodeProject, { frameworkName, @@ -296,6 +359,16 @@ export function addExpoPre55ShellPatchScriptPhase( frameworkTargetUUID: string; } ) { + const resolvedAppTargetName = resolveAppTargetName(project, modRequest); + + Logger.logInfo(`Resolved iOS app target name: ${resolvedAppTargetName}`); + + if (!resolvedAppTargetName) { + throw new SourceModificationError( + `Could not determine the iOS app target name from the Xcode project.` + ); + } + project.addBuildPhase( [ // no associated files @@ -306,6 +379,7 @@ export function addExpoPre55ShellPatchScriptPhase( { shellPath: '/bin/sh', shellScript: renderTemplate('ios', 'patchExpoPre55.sh', { + '{{APP_TARGET_NAME}}': resolvedAppTargetName, '{{FRAMEWORK_NAME}}': frameworkName, }), } diff --git a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/patchExpoPre55.sh b/packages/react-native-brownfield/src/expo-config-plugin/template/ios/patchExpoPre55.sh index 80d9c30b..11fbd2bf 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/patchExpoPre55.sh +++ b/packages/react-native-brownfield/src/expo-config-plugin/template/ios/patchExpoPre55.sh @@ -1,7 +1,7 @@ # Patch by @hurali97, source: https://github.com/callstackincubator/rock/issues/492#issuecomment-3225109837 # Applicable only to Expo SDK versions prior to 55, which made ExpoModulesProvider internal by default: https://github.com/expo/expo/pull/42317 # Path to ExpoModulesProvider.swift -FILE="${SRCROOT}/Pods/Target Support Files/Pods-ExpoApp-{{FRAMEWORK_NAME}}/ExpoModulesProvider.swift" +FILE="${SRCROOT}/Pods/Target Support Files/Pods-{{APP_TARGET_NAME}}-{{FRAMEWORK_NAME}}/ExpoModulesProvider.swift" if [ -f "$FILE" ]; then echo "Patching $FILE to hide Expo from public interface" diff --git a/yarn.lock b/yarn.lock index 4e0b91b3..5d6a6047 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2420,6 +2420,7 @@ __metadata: "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" "@callstack/brownfield-cli": "workspace:^" + "@expo/config-plugins": "npm:^54.0.4" "@react-native/babel-preset": "npm:0.82.1" "@types/jest": "npm:^30.0.0" "@types/react": "npm:^19.1.1" @@ -2432,8 +2433,7 @@ __metadata: react-native-builder-bob: "npm:^0.40.17" typescript: "npm:5.9.3" peerDependencies: - react: "*" - react-native: "*" + "@expo/config-plugins": ^54.0.4 bin: brownfield: lib/commonjs/scripts/brownfield.js languageName: unknown