Skip to content

Commit 828bda3

Browse files
hurali97artus9033
andauthored
fix(ios): resolve app target name for Expo pre-55 patch (#239)
* fix(ios): resolve app target name for Expo pre-55 patch * fix: add guard for multiple application targets * docs: add appTargetName * refactor: move error messages to separate function * feat: decouple config field for iOS target name in favor of automatic detection * chore: remove leftover from testing * chore: add changeset --------- Co-authored-by: artus9033 <artus9033@gmail.com>
1 parent 891c995 commit 828bda3

File tree

7 files changed

+104
-18
lines changed

7 files changed

+104
-18
lines changed

.changeset/wicked-clocks-visit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@callstack/react-native-brownfield': patch
3+
---
4+
5+
fix: autodetect iOS app target name for Expo pre-55 patch script phase

docs/docs/docs/getting-started/expo.mdx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { PackageManagerTabs } from '@theme';
55
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.
66

77
## Prerequisites
8+
89
- Install the `@callstack/react-native-brownfield` package from the quick start [section](/docs/getting-started/quick-start#installation)
910

1011
## Configuration
@@ -13,9 +14,7 @@ This guide walks you through packaging your Expo React Native app as an **AAR**
1314

1415
```json
1516
{
16-
"plugins": [
17-
"@callstack/react-native-brownfield",
18-
]
17+
"plugins": ["@callstack/react-native-brownfield"]
1918
}
2019
```
2120

@@ -55,7 +54,7 @@ This should only take a few minutes.
5554

5655
> That is all from the AAR steps. We can now consume the AAR inside a native Android App.
5756
58-
### AAR: Present RN UI
57+
### AAR: Present RN UI
5958

6059
1. Call the `ReactNativeHostManager.initialize` in your Activity or Application:
6160

@@ -80,6 +79,7 @@ override fun onConfigurationChanged(newConfig: Configuration) {
8079
```
8180

8281
3. Use either of the following APIs to present the UI:
82+
8383
- ReactNativeFragment
8484
- 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)
8585
- ReactNativeFragment.createReactNativeFragment
@@ -90,7 +90,7 @@ override fun onConfigurationChanged(newConfig: Configuration) {
9090
9191
4. Build and install the android application 🚀
9292

93-
<hr/>
93+
<hr />
9494

9595
## iOS Integration
9696

@@ -109,23 +109,23 @@ This should only take a few minutes.
109109
##### Pre-Requisites
110110

111111
- Follow the step for adding the frameworks to your iOS App - [here](/docs/getting-started/ios#6-add-the-framework-to-your-ios-app)
112-
<hr/>
112+
<hr />
113113

114114
1. Call the following functions from your Application Entry point:
115115

116116
```swift
117117
@main
118118
struct IosApp: App {
119119
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
120-
120+
121121
init() {
122122
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
123123
ReactNativeBrownfield.shared.startReactNative {
124124
print("React Native has been loaded")
125125
}
126126
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
127127
}
128-
128+
129129
var body: some Scene {
130130
WindowGroup {
131131
ContentView()
@@ -139,7 +139,7 @@ struct IosApp: App {
139139
```swift
140140
class AppDelegate: NSObject, UIApplicationDelegate {
141141
var window: UIWindow?
142-
142+
143143
func application(
144144
_ application: UIApplication,
145145
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
@@ -160,7 +160,7 @@ ReactNativeView(moduleName: "ExpoRNApp")
160160
161161
4. Build and install the iOS application 🚀
162162

163-
<hr/>
163+
<hr />
164164

165165
## Plugin Options
166166

@@ -201,6 +201,14 @@ You can pass plugin options through the second item in the `plugins` tuple in `a
201201
- `frameworkVersion` (`string`, default: `"1"`)
202202
- Framework version used for Apple build settings (must be an integer or floating point value, for example `"1"` or `"2.1"`).
203203

204+
> The plugin will determine the application target automatically. The auto-detection path works as follows:
205+
>
206+
> 1. _Common for all cases_: scan the iOS targets for ones of type `com.apple.product-type.application`
207+
> 2. Use the first matching strategy:
208+
> - CNG-derived name from mod compiler (`modRequest.projectName`) - only if it exists in the filtered application-type list of Xcode project targets
209+
> - Unambiguous first application-type target - if there is exactly one
210+
> - PBX "first native target" fallback - last resort fallback that selects the first native target of any type
211+
204212
### Android
205213

206214
- `moduleName` (`string`, default: `"brownfieldlib"`)

packages/react-native-brownfield/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@
8282
"access": "public"
8383
},
8484
"peerDependencies": {
85-
"react": "*",
86-
"react-native": "*"
85+
"@expo/config-plugins": "^54.0.4"
8786
},
8887
"dependencies": {
8988
"@callstack/brownfield-cli": "workspace:^"
@@ -92,6 +91,7 @@
9291
"@babel/core": "^7.25.2",
9392
"@babel/preset-env": "^7.25.3",
9493
"@babel/runtime": "^7.25.0",
94+
"@expo/config-plugins": "^54.0.4",
9595
"@react-native/babel-preset": "0.82.1",
9696
"@types/jest": "^30.0.0",
9797
"@types/react": "^19.1.1",

packages/react-native-brownfield/src/expo-config-plugin/ios/withBrownfieldIos.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export const withBrownfieldIos: ConfigPlugin<
3232
? parseInt(config.sdkVersion.split('.')[0], 10)
3333
: -1;
3434
const isExpoPre55 = expoMajor < 55;
35-
3635
// Step 1: modify the Xcode project to add framework target &
3736
config = withXcodeProject(config, (xcodeConfig) => {
3837
const { modResults: project, modRequest } = xcodeConfig;
@@ -60,7 +59,7 @@ export const withBrownfieldIos: ConfigPlugin<
6059
`Adding ExpoModulesProvider patch phase for Expo SDK ${config.sdkVersion}`
6160
);
6261

63-
addExpoPre55ShellPatchScriptPhase(project, {
62+
addExpoPre55ShellPatchScriptPhase(modRequest, project, {
6463
frameworkName: props.ios.frameworkName,
6564
frameworkTargetUUID: frameworkTargetUUID,
6665
});

packages/react-native-brownfield/src/expo-config-plugin/ios/xcodeHelpers.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import path from 'node:path';
22

3-
import type { ModProps, XcodeProject } from '@expo/config-plugins';
3+
import {
4+
type ModProps,
5+
type XcodeProject,
6+
IOSConfig,
7+
} from '@expo/config-plugins';
48

59
import { Logger } from '../logging';
610
import type { ResolvedBrownfieldPluginIosConfig } from '../types';
@@ -286,7 +290,66 @@ export function copyBundleReactNativePhase(
286290
}
287291
}
288292

293+
function resolveAppTargetName(
294+
project: XcodeProject,
295+
modRequest: ModProps<XcodeProject>
296+
): string | null {
297+
const appTargets = IOSConfig.Target.getNativeTargets(project)
298+
.map(([, target]) => {
299+
if (
300+
!IOSConfig.Target.isTargetOfType(
301+
target,
302+
IOSConfig.Target.TargetType.APPLICATION
303+
)
304+
) {
305+
return null;
306+
}
307+
308+
const name = IOSConfig.XcodeUtils.unquote(target.name ?? '').trim();
309+
310+
return name ?? null;
311+
})
312+
.filter((name): name is string => !!name);
313+
314+
// 1) Unambiguous first application-type target
315+
if (appTargets.length === 1) {
316+
return appTargets[0];
317+
} else {
318+
Logger.logWarning(
319+
'Multiple application targets found in the Xcode project. Falling back to the CNG-derived name from mod compiler.'
320+
);
321+
}
322+
323+
// 2) CNG-derived name from mod compiler (`modRequest.projectName`) - only if it exists in the filtered application-type list of Xcode project targets
324+
const cngDerivedProjectName = modRequest.projectName;
325+
if (cngDerivedProjectName && appTargets.includes(cngDerivedProjectName)) {
326+
return cngDerivedProjectName;
327+
} else {
328+
Logger.logWarning(
329+
'CNG-derived name from mod compiler is not set or is not an application target. Falling back to the unfiltered-type target name.'
330+
);
331+
}
332+
333+
// 3) PBX "first native target" fallback
334+
try {
335+
const [, firstAppTarget] = IOSConfig.Target.findFirstNativeTarget(project);
336+
const name = IOSConfig.XcodeUtils.unquote(firstAppTarget.name ?? '').trim();
337+
return name || null;
338+
} catch {
339+
Logger.logWarning(
340+
'No first native target of any type found in the Xcode project. This was the last resort fallback.'
341+
);
342+
}
343+
344+
Logger.logError(
345+
`Could not determine the iOS app target name from the Xcode project. Please adjust your Xcode project to have exactly one application target.`
346+
);
347+
348+
return null;
349+
}
350+
289351
export function addExpoPre55ShellPatchScriptPhase(
352+
modRequest: ModProps<XcodeProject>,
290353
project: XcodeProject,
291354
{
292355
frameworkName,
@@ -296,6 +359,16 @@ export function addExpoPre55ShellPatchScriptPhase(
296359
frameworkTargetUUID: string;
297360
}
298361
) {
362+
const resolvedAppTargetName = resolveAppTargetName(project, modRequest);
363+
364+
Logger.logInfo(`Resolved iOS app target name: ${resolvedAppTargetName}`);
365+
366+
if (!resolvedAppTargetName) {
367+
throw new SourceModificationError(
368+
`Could not determine the iOS app target name from the Xcode project.`
369+
);
370+
}
371+
299372
project.addBuildPhase(
300373
[
301374
// no associated files
@@ -306,6 +379,7 @@ export function addExpoPre55ShellPatchScriptPhase(
306379
{
307380
shellPath: '/bin/sh',
308381
shellScript: renderTemplate('ios', 'patchExpoPre55.sh', {
382+
'{{APP_TARGET_NAME}}': resolvedAppTargetName,
309383
'{{FRAMEWORK_NAME}}': frameworkName,
310384
}),
311385
}

packages/react-native-brownfield/src/expo-config-plugin/template/ios/patchExpoPre55.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Patch by @hurali97, source: https://github.com/callstackincubator/rock/issues/492#issuecomment-3225109837
22
# Applicable only to Expo SDK versions prior to 55, which made ExpoModulesProvider internal by default: https://github.com/expo/expo/pull/42317
33
# Path to ExpoModulesProvider.swift
4-
FILE="${SRCROOT}/Pods/Target Support Files/Pods-ExpoApp-{{FRAMEWORK_NAME}}/ExpoModulesProvider.swift"
4+
FILE="${SRCROOT}/Pods/Target Support Files/Pods-{{APP_TARGET_NAME}}-{{FRAMEWORK_NAME}}/ExpoModulesProvider.swift"
55

66
if [ -f "$FILE" ]; then
77
echo "Patching $FILE to hide Expo from public interface"

yarn.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2420,6 +2420,7 @@ __metadata:
24202420
"@babel/preset-env": "npm:^7.25.3"
24212421
"@babel/runtime": "npm:^7.25.0"
24222422
"@callstack/brownfield-cli": "workspace:^"
2423+
"@expo/config-plugins": "npm:^54.0.4"
24232424
"@react-native/babel-preset": "npm:0.82.1"
24242425
"@types/jest": "npm:^30.0.0"
24252426
"@types/react": "npm:^19.1.1"
@@ -2432,8 +2433,7 @@ __metadata:
24322433
react-native-builder-bob: "npm:^0.40.17"
24332434
typescript: "npm:5.9.3"
24342435
peerDependencies:
2435-
react: "*"
2436-
react-native: "*"
2436+
"@expo/config-plugins": ^54.0.4
24372437
bin:
24382438
brownfield: lib/commonjs/scripts/brownfield.js
24392439
languageName: unknown

0 commit comments

Comments
 (0)