Skip to content

Commit 9ecebfe

Browse files
committed
feat: decouple config field for iOS target name in favor of automatic detection
1 parent 0d119fe commit 9ecebfe

7 files changed

Lines changed: 95 additions & 85 deletions

File tree

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

Lines changed: 18 additions & 18 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

@@ -173,7 +173,6 @@ You can pass plugin options through the second item in the `plugins` tuple in `a
173173
"@callstack/react-native-brownfield",
174174
{
175175
"ios": {
176-
"appTargetName": "MyApp",
177176
"frameworkName": "MyBrownfieldLib",
178177
"bundleIdentifier": "com.example.app.brownfield",
179178
...
@@ -191,13 +190,6 @@ You can pass plugin options through the second item in the `plugins` tuple in `a
191190

192191
### iOS
193192

194-
- `appTargetName` (`string`, optional)
195-
- Name of the iOS Application Target.
196-
197-
> If not provided, the plugin will try to determine the application target name from the Xcode project.
198-
>
199-
> Auto-detection works only when there is exactly one iOS application target.
200-
201193
- `frameworkName` (`string`, default: `"BrownfieldLib"`)
202194
- Name of the generated framework. This is also used as the XCFramework name.
203195
- `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
209201
- `frameworkVersion` (`string`, default: `"1"`)
210202
- Framework version used for Apple build settings (must be an integer or floating point value, for example `"1"` or `"2.1"`).
211203

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+
212212
### Android
213213

214214
- `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: 5 additions & 3 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;
@@ -42,6 +41,10 @@ export const withBrownfieldIos: ConfigPlugin<
4241
modRequest,
4342
props.ios
4443
);
44+
addExpoPre55ShellPatchScriptPhase(modRequest, project, {
45+
frameworkName: props.ios.frameworkName,
46+
frameworkTargetUUID: frameworkTargetUUID,
47+
});
4548

4649
if (targetAlreadyExists) {
4750
Logger.logDebug(
@@ -60,10 +63,9 @@ export const withBrownfieldIos: ConfigPlugin<
6063
`Adding ExpoModulesProvider patch phase for Expo SDK ${config.sdkVersion}`
6164
);
6265

63-
addExpoPre55ShellPatchScriptPhase(project, {
66+
addExpoPre55ShellPatchScriptPhase(modRequest, project, {
6467
frameworkName: props.ios.frameworkName,
6568
frameworkTargetUUID: frameworkTargetUUID,
66-
appTargetName: props.ios.appTargetName,
6769
});
6870
} else {
6971
Logger.logDebug(

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

Lines changed: 68 additions & 51 deletions
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,35 +290,83 @@ export function copyBundleReactNativePhase(
286290
}
287291
}
288292

289-
function resolveErrorMessageForAppTargetName(
290-
applicationTargets: string[]
291-
): string {
292-
return applicationTargets.length > 1
293-
? `Multiple iOS application targets found in the Xcode project (${applicationTargets.join(', ')}). Please set ios.appTargetName in plugin options.`
294-
: '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).';
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;
295349
}
296350

297351
export function addExpoPre55ShellPatchScriptPhase(
352+
modRequest: ModProps<XcodeProject>,
298353
project: XcodeProject,
299354
{
300355
frameworkName,
301356
frameworkTargetUUID,
302-
appTargetName,
303357
}: {
304358
frameworkName: string;
305359
frameworkTargetUUID: string;
306-
appTargetName: string;
307360
}
308361
) {
309-
const applicationTargets = getApplicationTargetNames(project);
310-
const resolvedAppTargetName =
311-
appTargetName || getApplicationTargetName(applicationTargets);
362+
const resolvedAppTargetName = resolveAppTargetName(project, modRequest);
312363

313-
if (!resolvedAppTargetName) {
314-
const errorMessage =
315-
resolveErrorMessageForAppTargetName(applicationTargets);
364+
Logger.logInfo(`Resolved iOS app target name: ${resolvedAppTargetName}`);
316365

317-
throw new SourceModificationError(errorMessage);
366+
if (!resolvedAppTargetName) {
367+
throw new SourceModificationError(
368+
`Could not determine the iOS app target name from the Xcode project.`
369+
);
318370
}
319371

320372
project.addBuildPhase(
@@ -334,41 +386,6 @@ export function addExpoPre55ShellPatchScriptPhase(
334386
);
335387
}
336388

337-
/**
338-
*
339-
* @param applicationTargets iOS application target names
340-
* @returns First iOS application target name if there is exactly one, otherwise null
341-
*/
342-
function getApplicationTargetName(applicationTargets: string[]): string | null {
343-
if (applicationTargets.length !== 1) return null;
344-
return applicationTargets[0];
345-
}
346-
347-
/**
348-
* Returns iOS application target names from PBXNativeTarget section.
349-
*/
350-
function getApplicationTargetNames(project: XcodeProject): string[] {
351-
const nativeTargets = project.pbxNativeTargetSection();
352-
const applicationTargets = new Set<string>();
353-
354-
for (const [key, value] of Object.entries(nativeTargets)) {
355-
if (key.endsWith('_comment')) continue;
356-
357-
const target = value as any;
358-
const productType = String(target?.productType ?? '').replace(/"/g, '');
359-
if (productType !== 'com.apple.product-type.application') continue;
360-
361-
const targetName = String(target?.name ?? '')
362-
.replace(/"/g, '')
363-
.trim();
364-
if (targetName) {
365-
applicationTargets.add(targetName);
366-
}
367-
}
368-
369-
return [...applicationTargets];
370-
}
371-
372389
/**
373390
* Makes sure the patch expo modules provider phase is after the expo configure phase,
374391
* otherwise the patched file would be overwritten by the expo configure phase

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22
* iOS-specific configuration for Brownfield config plugin
33
*/
44
export interface BrownfieldPluginIosConfig {
5-
/**
6-
* The name of the iOS app target
7-
* If not provided, the plugin will try to determine the application target name from the Xcode project.
8-
* Auto-detection works only when there is exactly one iOS application target.
9-
* @default ""
10-
*/
11-
appTargetName?: string;
12-
135
/**
146
* The name of the framework to create
157
* This will be used as the XCFramework name

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ function resolveConfig(
3333
return {
3434
ios: expoConfig.ios
3535
? {
36-
appTargetName: config.ios?.appTargetName ?? '',
3736
frameworkName: config.ios?.frameworkName ?? 'BrownfieldLib',
3837
bundleIdentifier:
3938
config.ios?.bundleIdentifier ??

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)