From dc624c288e56a1e9ff0b56faf45dfccc18f354c7 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Mon, 23 Feb 2026 10:57:14 +0500 Subject: [PATCH 1/7] fix(ios): resolve app target name for Expo pre-55 patch --- .../ios/withBrownfieldIos.ts | 1 + .../expo-config-plugin/ios/xcodeHelpers.ts | 34 +++++++++++++++++++ .../template/ios/patchExpoPre55.sh | 2 +- .../types/ios/BrownfieldPluginIosConfig.ts | 7 ++++ .../src/expo-config-plugin/withBrownfield.ts | 1 + 5 files changed, 44 insertions(+), 1 deletion(-) 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..6612d608 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 @@ -63,6 +63,7 @@ export const withBrownfieldIos: ConfigPlugin< addExpoPre55ShellPatchScriptPhase(project, { frameworkName: props.ios.frameworkName, frameworkTargetUUID: frameworkTargetUUID, + appTargetName: props.ios.appTargetName, }); } else { Logger.logDebug( 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..474a5d20 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 @@ -291,11 +291,22 @@ export function addExpoPre55ShellPatchScriptPhase( { frameworkName, frameworkTargetUUID, + appTargetName, }: { frameworkName: string; frameworkTargetUUID: string; + appTargetName: string; } ) { + const resolvedAppTargetName = + appTargetName || getApplicationTargetName(project); + + if (!resolvedAppTargetName) { + throw new SourceModificationError( + 'Could not determine the iOS app target name from the Xcode project. Please provide the app target name in plugin options.' + ); + } + project.addBuildPhase( [ // no associated files @@ -306,12 +317,35 @@ export function addExpoPre55ShellPatchScriptPhase( { shellPath: '/bin/sh', shellScript: renderTemplate('ios', 'patchExpoPre55.sh', { + '{{APP_TARGET_NAME}}': resolvedAppTargetName, '{{FRAMEWORK_NAME}}': frameworkName, }), } ); } +/** + * Returns the iOS application target name from PBXNativeTarget section. + */ +function getApplicationTargetName(project: XcodeProject): string | null { + const nativeTargets = project.pbxNativeTargetSection(); + + for (const [key, value] of Object.entries(nativeTargets)) { + if (key.endsWith('_comment')) continue; + + const target = value as any; + const productType = String(target?.productType ?? '').replace(/"/g, ''); + if (productType !== 'com.apple.product-type.application') continue; + + const targetName = String(target?.name ?? '') + .replace(/"/g, '') + .trim(); + if (targetName) return targetName; + } + + return null; +} + /** * Makes sure the patch expo modules provider phase is after the expo configure phase, * otherwise the patched file would be overwritten by the expo configure phase 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/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts b/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts index f2f1187a..63ba130e 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts @@ -2,6 +2,13 @@ * iOS-specific configuration for Brownfield config plugin */ export interface BrownfieldPluginIosConfig { + /** + * The name of the iOS app target + * If not provided, the plugin will try to determine the application target name from the Xcode project + * @default - "" + */ + appTargetName?: string; + /** * The name of the framework to create * This will be used as the XCFramework name diff --git a/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts b/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts index 9b9079be..8f38a56d 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts @@ -33,6 +33,7 @@ function resolveConfig( return { ios: expoConfig.ios ? { + appTargetName: config.ios?.appTargetName ?? '', frameworkName: config.ios?.frameworkName ?? 'BrownfieldLib', bundleIdentifier: config.ios?.bundleIdentifier ?? From 9513da202352ed091f84fa4dc80a40d147ed9096 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Mon, 23 Feb 2026 11:44:45 +0500 Subject: [PATCH 2/7] fix: add guard for multiple application targets --- .../expo-config-plugin/ios/xcodeHelpers.ts | 32 ++++++++++++++----- .../types/ios/BrownfieldPluginIosConfig.ts | 5 +-- 2 files changed, 27 insertions(+), 10 deletions(-) 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 474a5d20..7e9bf3ba 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 @@ -298,13 +298,16 @@ export function addExpoPre55ShellPatchScriptPhase( appTargetName: string; } ) { + const applicationTargets = getApplicationTargetNames(project); const resolvedAppTargetName = - appTargetName || getApplicationTargetName(project); + appTargetName || getApplicationTargetName(applicationTargets); if (!resolvedAppTargetName) { - throw new SourceModificationError( - 'Could not determine the iOS app target name from the Xcode project. Please provide the app target name in plugin options.' - ); + const errorMessage = + applicationTargets.length > 1 + ? `Multiple iOS application targets found in the Xcode project (${applicationTargets.join(', ')}). Please set ios.appTargetName in plugin options.` + : 'Could not determine the iOS app target name from the Xcode project. Please provide the app target name in plugin options.'; + throw new SourceModificationError(errorMessage); } project.addBuildPhase( @@ -325,10 +328,21 @@ export function addExpoPre55ShellPatchScriptPhase( } /** - * Returns the iOS application target name from PBXNativeTarget section. + * + * @param applicationTargets iOS application target names + * @returns First iOS application target name if there is exactly one, otherwise null */ -function getApplicationTargetName(project: XcodeProject): string | null { +function getApplicationTargetName(applicationTargets: string[]): string | null { + if (applicationTargets.length !== 1) return null; + return applicationTargets[0]; +} + +/** + * Returns iOS application target names from PBXNativeTarget section. + */ +function getApplicationTargetNames(project: XcodeProject): string[] { const nativeTargets = project.pbxNativeTargetSection(); + const applicationTargets = new Set(); for (const [key, value] of Object.entries(nativeTargets)) { if (key.endsWith('_comment')) continue; @@ -340,10 +354,12 @@ function getApplicationTargetName(project: XcodeProject): string | null { const targetName = String(target?.name ?? '') .replace(/"/g, '') .trim(); - if (targetName) return targetName; + if (targetName) { + applicationTargets.add(targetName); + } } - return null; + return [...applicationTargets]; } /** diff --git a/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts b/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts index 63ba130e..3e51f1ca 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts @@ -4,8 +4,9 @@ export interface BrownfieldPluginIosConfig { /** * The name of the iOS app target - * If not provided, the plugin will try to determine the application target name from the Xcode project - * @default - "" + * If not provided, the plugin will try to determine the application target name from the Xcode project. + * Auto-detection works only when there is exactly one iOS application target. + * @default "" */ appTargetName?: string; From b589fb29f015b6785c091802106c81da6b3b50f6 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Mon, 23 Feb 2026 11:45:56 +0500 Subject: [PATCH 3/7] docs: add appTargetName --- docs/docs/docs/getting-started/expo.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/docs/docs/getting-started/expo.mdx b/docs/docs/docs/getting-started/expo.mdx index 8666e27a..e19bdb88 100644 --- a/docs/docs/docs/getting-started/expo.mdx +++ b/docs/docs/docs/getting-started/expo.mdx @@ -173,6 +173,7 @@ You can pass plugin options through the second item in the `plugins` tuple in `a "@callstack/react-native-brownfield", { "ios": { + "appTargetName": "MyApp", "frameworkName": "MyBrownfieldLib", "bundleIdentifier": "com.example.app.brownfield", ... @@ -190,6 +191,13 @@ You can pass plugin options through the second item in the `plugins` tuple in `a ### iOS +- `appTargetName` (`string`, optional) + - Name of the iOS Application Target. + +> If not provided, the plugin will try to determine the application target name from the Xcode project. +> +> Auto-detection works only when there is exactly one iOS application target. + - `frameworkName` (`string`, default: `"BrownfieldLib"`) - Name of the generated framework. This is also used as the XCFramework name. - `bundleIdentifier` (`string`, default: app bundle identifier + `.brownfield`) From 0d119fe36fc27136b3540347cb397270feb3e166 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Mon, 23 Feb 2026 11:48:24 +0500 Subject: [PATCH 4/7] refactor: move error messages to separate function --- .../src/expo-config-plugin/ios/xcodeHelpers.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 7e9bf3ba..ced18fde 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 @@ -286,6 +286,14 @@ export function copyBundleReactNativePhase( } } +function resolveErrorMessageForAppTargetName( + applicationTargets: string[] +): string { + return applicationTargets.length > 1 + ? `Multiple iOS application targets found in the Xcode project (${applicationTargets.join(', ')}). Please set ios.appTargetName in plugin options.` + : 'Could not determine the iOS app target name from the Xcode project. Please set `ios.appTargetName` in the `react-native-brownfield` plugin configuration in your Expo config (for example, app.json).'; +} + export function addExpoPre55ShellPatchScriptPhase( project: XcodeProject, { @@ -304,9 +312,8 @@ export function addExpoPre55ShellPatchScriptPhase( if (!resolvedAppTargetName) { const errorMessage = - applicationTargets.length > 1 - ? `Multiple iOS application targets found in the Xcode project (${applicationTargets.join(', ')}). Please set ios.appTargetName in plugin options.` - : 'Could not determine the iOS app target name from the Xcode project. Please provide the app target name in plugin options.'; + resolveErrorMessageForAppTargetName(applicationTargets); + throw new SourceModificationError(errorMessage); } From 9ecebfed3c90c68ef2d60038b42b6b00b364ce3a Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 23 Feb 2026 12:41:34 +0100 Subject: [PATCH 5/7] feat: decouple config field for iOS target name in favor of automatic detection --- docs/docs/docs/getting-started/expo.mdx | 36 +++--- packages/react-native-brownfield/package.json | 4 +- .../ios/withBrownfieldIos.ts | 8 +- .../expo-config-plugin/ios/xcodeHelpers.ts | 119 ++++++++++-------- .../types/ios/BrownfieldPluginIosConfig.ts | 8 -- .../src/expo-config-plugin/withBrownfield.ts | 1 - yarn.lock | 4 +- 7 files changed, 95 insertions(+), 85 deletions(-) diff --git a/docs/docs/docs/getting-started/expo.mdx b/docs/docs/docs/getting-started/expo.mdx index e19bdb88..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 @@ -173,7 +173,6 @@ You can pass plugin options through the second item in the `plugins` tuple in `a "@callstack/react-native-brownfield", { "ios": { - "appTargetName": "MyApp", "frameworkName": "MyBrownfieldLib", "bundleIdentifier": "com.example.app.brownfield", ... @@ -191,13 +190,6 @@ You can pass plugin options through the second item in the `plugins` tuple in `a ### iOS -- `appTargetName` (`string`, optional) - - Name of the iOS Application Target. - -> If not provided, the plugin will try to determine the application target name from the Xcode project. -> -> Auto-detection works only when there is exactly one iOS application target. - - `frameworkName` (`string`, default: `"BrownfieldLib"`) - Name of the generated framework. This is also used as the XCFramework name. - `bundleIdentifier` (`string`, default: app bundle identifier + `.brownfield`) @@ -209,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 6612d608..cb5c2ad5 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; @@ -42,6 +41,10 @@ export const withBrownfieldIos: ConfigPlugin< modRequest, props.ios ); + addExpoPre55ShellPatchScriptPhase(modRequest, project, { + frameworkName: props.ios.frameworkName, + frameworkTargetUUID: frameworkTargetUUID, + }); if (targetAlreadyExists) { Logger.logDebug( @@ -60,10 +63,9 @@ export const withBrownfieldIos: ConfigPlugin< `Adding ExpoModulesProvider patch phase for Expo SDK ${config.sdkVersion}` ); - addExpoPre55ShellPatchScriptPhase(project, { + addExpoPre55ShellPatchScriptPhase(modRequest, project, { frameworkName: props.ios.frameworkName, frameworkTargetUUID: frameworkTargetUUID, - appTargetName: props.ios.appTargetName, }); } else { Logger.logDebug( 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 ced18fde..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,35 +290,83 @@ export function copyBundleReactNativePhase( } } -function resolveErrorMessageForAppTargetName( - applicationTargets: string[] -): string { - return applicationTargets.length > 1 - ? `Multiple iOS application targets found in the Xcode project (${applicationTargets.join(', ')}). Please set ios.appTargetName in plugin options.` - : 'Could not determine the iOS app target name from the Xcode project. Please set `ios.appTargetName` in the `react-native-brownfield` plugin configuration in your Expo config (for example, app.json).'; +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, frameworkTargetUUID, - appTargetName, }: { frameworkName: string; frameworkTargetUUID: string; - appTargetName: string; } ) { - const applicationTargets = getApplicationTargetNames(project); - const resolvedAppTargetName = - appTargetName || getApplicationTargetName(applicationTargets); + const resolvedAppTargetName = resolveAppTargetName(project, modRequest); - if (!resolvedAppTargetName) { - const errorMessage = - resolveErrorMessageForAppTargetName(applicationTargets); + Logger.logInfo(`Resolved iOS app target name: ${resolvedAppTargetName}`); - throw new SourceModificationError(errorMessage); + if (!resolvedAppTargetName) { + throw new SourceModificationError( + `Could not determine the iOS app target name from the Xcode project.` + ); } project.addBuildPhase( @@ -334,41 +386,6 @@ export function addExpoPre55ShellPatchScriptPhase( ); } -/** - * - * @param applicationTargets iOS application target names - * @returns First iOS application target name if there is exactly one, otherwise null - */ -function getApplicationTargetName(applicationTargets: string[]): string | null { - if (applicationTargets.length !== 1) return null; - return applicationTargets[0]; -} - -/** - * Returns iOS application target names from PBXNativeTarget section. - */ -function getApplicationTargetNames(project: XcodeProject): string[] { - const nativeTargets = project.pbxNativeTargetSection(); - const applicationTargets = new Set(); - - for (const [key, value] of Object.entries(nativeTargets)) { - if (key.endsWith('_comment')) continue; - - const target = value as any; - const productType = String(target?.productType ?? '').replace(/"/g, ''); - if (productType !== 'com.apple.product-type.application') continue; - - const targetName = String(target?.name ?? '') - .replace(/"/g, '') - .trim(); - if (targetName) { - applicationTargets.add(targetName); - } - } - - return [...applicationTargets]; -} - /** * Makes sure the patch expo modules provider phase is after the expo configure phase, * otherwise the patched file would be overwritten by the expo configure phase diff --git a/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts b/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts index 3e51f1ca..f2f1187a 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts @@ -2,14 +2,6 @@ * iOS-specific configuration for Brownfield config plugin */ export interface BrownfieldPluginIosConfig { - /** - * The name of the iOS app target - * If not provided, the plugin will try to determine the application target name from the Xcode project. - * Auto-detection works only when there is exactly one iOS application target. - * @default "" - */ - appTargetName?: string; - /** * The name of the framework to create * This will be used as the XCFramework name diff --git a/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts b/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts index 8f38a56d..9b9079be 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts @@ -33,7 +33,6 @@ function resolveConfig( return { ios: expoConfig.ios ? { - appTargetName: config.ios?.appTargetName ?? '', frameworkName: config.ios?.frameworkName ?? 'BrownfieldLib', bundleIdentifier: config.ios?.bundleIdentifier ?? 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 From 8e09e598cbffbf245acca877747bfaf3f855c63e Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 23 Feb 2026 12:55:55 +0100 Subject: [PATCH 6/7] chore: remove leftover from testing --- .../src/expo-config-plugin/ios/withBrownfieldIos.ts | 4 ---- 1 file changed, 4 deletions(-) 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 cb5c2ad5..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 @@ -41,10 +41,6 @@ export const withBrownfieldIos: ConfigPlugin< modRequest, props.ios ); - addExpoPre55ShellPatchScriptPhase(modRequest, project, { - frameworkName: props.ios.frameworkName, - frameworkTargetUUID: frameworkTargetUUID, - }); if (targetAlreadyExists) { Logger.logDebug( From 6a5932ec248f908cc0af9e301570b432a044c200 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 23 Feb 2026 13:22:27 +0100 Subject: [PATCH 7/7] chore: add changeset --- .changeset/wicked-clocks-visit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wicked-clocks-visit.md 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