Skip to content

Commit c04301b

Browse files
authored
feat(ios): support bundled JS in debug builds (#317)
* feat(ios): support bundled JS in debug builds * fix(cli): embed debug JS bundle in simulator xcframeworks * feat(ios): enable development loading view configuration in debug mode - Added a method to configure the development loading view based on the debug settings. - Introduced `BrownfieldDevLoadingViewBridge` to manage the loading view state. - Updated `BrownfieldAppleApp` to prefer bundled bundles in debug mode. - Adjusted `ExpoHostRuntime` and `ReactNativeHostRuntime` to call the new configuration method. * fix(cli): resolve packaged framework without scheme * fix(ios): make swift tests self-contained * fix: ftm issue * fix(ios): address pr review follow-ups * fix(expo): sync brownfield framework target updates * refactor(ios): make bundle URL resolver a class * fix: code review * fix(ios): address follow-up review comments * fix: merge conflict
1 parent 7510467 commit c04301b

32 files changed

Lines changed: 1255 additions & 73 deletions

.changeset/tender-poems-rhyme.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': minor
3+
---
4+
5+
Add an opt-in iOS Debug mode for loading the embedded JavaScript bundle with `preferEmbeddedBundleInDebug`, fix `bundleURLOverride` fallback behavior when the override returns `nil`, and add native bundle-resolution tests.

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,25 @@ jobs:
9292
run: |
9393
yarn workspace @callstack/react-native-brownfield brownfield --version
9494
95+
ios-native-tests:
96+
name: iOS native tests
97+
runs-on: macos-26
98+
needs: build-lint
99+
steps:
100+
- name: Checkout
101+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
102+
103+
- name: Setup
104+
uses: ./.github/actions/setup
105+
106+
- name: Run Swift bundle resolver tests
107+
run: |
108+
cd packages/react-native-brownfield/ios/swiftpm
109+
mkdir -p "$RUNNER_TEMP/swift-home" "$RUNNER_TEMP/swift-cache/clang" "$RUNNER_TEMP/swift-cache/swiftpm"
110+
HOME="$RUNNER_TEMP/swift-home" \
111+
CLANG_MODULE_CACHE_PATH="$RUNNER_TEMP/swift-cache/clang" \
112+
swift test --scratch-path "$RUNNER_TEMP/swift-cache/swiftpm"
113+
95114
android-androidapp-expo:
96115
name: Android road test (AndroidApp - Expo ${{ matrix.version }})
97116
runs-on: ubuntu-latest

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@ secring.gpg
8282

8383
# Typescript
8484
**/*.tsbuildinfo
85+
packages/react-native-brownfield/ios/.build/
86+
packages/react-native-brownfield/ios/swiftpm/.build/
8587

8688
# skillgym
8789
.skillgym-results/
8890

89-
.cursor
91+
.cursor

apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ struct BrownfieldAppleApp: App {
9090

9191
init() {
9292
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
93+
ReactNativeBrownfield.shared.preferEmbeddedBundleInDebug = true
9394
ReactNativeBrownfield.shared.startReactNative {
9495
print("React Native has been loaded")
9596
}

apps/AppleApp/Brownfield Apple App/components/ContentView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ struct ContentView: View {
1919
NavigationView {
2020

2121
VStack(spacing: 16) {
22-
GreetingCard(name: "iOS Expo")
22+
GreetingCard(name: "iOS Vanilla")
2323

2424
MessagesView()
2525

2626
ReactNativeView(
27-
moduleName: "main",
27+
moduleName: "RNApp",
2828
initialProperties: [
2929
"nativeOsVersionLabel":
3030
"\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)"

docs/docs/docs/api-reference/react-native-brownfield/objective-c.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ A singleton that keeps an instance of `ReactNativeBrownfield` object.
3333
| `entryFile` | `NSString` | `index` | Path to JavaScript entry file in development. |
3434
| `bundlePath` | `NSString` | `main.jsbundle` | Path to JavaScript bundle file. |
3535
| `bundle` | `NSBundle` | `Bundle.main` | Bundle instance to lookup the JavaScript bundle resource. |
36+
| `preferEmbeddedBundleInDebug` | `BOOL` | `NO` | Prefer the embedded JavaScript bundle instead of Metro when the framework is built in Debug. |
3637
| `bundleURLOverride` | `NSURL *(^)(void)` | `nil` | Dynamic bundle URL provider called on every bundle load. When set, overrides default bundle load behavior. |
3738

3839
---

docs/docs/docs/api-reference/react-native-brownfield/swift.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ ReactNativeBrownfield.shared
3333
| `entryFile` | `String` | `index` | Path to JavaScript entry file in development. |
3434
| `bundlePath` | `String` | `main.jsbundle` | Path to JavaScript bundle file. |
3535
| `bundle` | `Bundle` | `Bundle.main` | Bundle instance to lookup the JavaScript bundle resource. |
36+
| `preferEmbeddedBundleInDebug` | `Bool` | `false` | Prefer the embedded JavaScript bundle instead of Metro when the framework is built in Debug. |
3637
| `bundleURLOverride` | `(() -> URL?)?` | `nil` | Dynamic bundle URL provider called on every bundle load. When set, overrides default behavior. |
3738

3839
---

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@ struct IosApp: App {
138138
}
139139
```
140140

141+
If you package the framework in **Debug** and want to run it without Metro, enable the embedded bundle explicitly before calling `startReactNative`:
142+
143+
```swift
144+
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
145+
ReactNativeBrownfield.shared.preferEmbeddedBundleInDebug = true
146+
ReactNativeBrownfield.shared.startReactNative()
147+
```
148+
141149
2. Propagate the didFinishLaunchingWithOptions
142150

143151
```swift

docs/docs/docs/getting-started/ios.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ When running in **Debug**, React Native Brownfield expects a JS dev server runni
172172
npx react-native start
173173
```
174174

175+
## Embedded bundle in Development
176+
177+
If you want to run a **Debug-built** framework without Metro, enable the embedded bundle explicitly before calling `startReactNative`:
178+
179+
```swift
180+
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
181+
ReactNativeBrownfield.shared.preferEmbeddedBundleInDebug = true
182+
ReactNativeBrownfield.shared.startReactNative()
183+
```
184+
175185
### Release Configuration
176186

177187
In **Release**, the JS bundle is loaded directly from the XCFramework - no dev server needed.

packages/cli/src/brownfield/commands/packageIos.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
} from '../../shared/index.js';
2828
import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
2929
import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
30+
import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
31+
import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
3032
import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
3133

3234
/** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
@@ -163,6 +165,49 @@ export const packageIosCommand = curryOptions(
163165
platformConfig
164166
);
165167

168+
const productsPath = path.join(options.buildFolder, 'Build', 'Products');
169+
const { frameworkName, resolution, candidates } =
170+
resolvePackagedFrameworkName({
171+
explicitScheme: options.scheme,
172+
productsPath,
173+
configuration,
174+
});
175+
176+
if (frameworkName) {
177+
copyDebugBundleToSimulatorSlice({
178+
productsPath,
179+
configuration,
180+
frameworkName,
181+
});
182+
183+
if (configuration.includes('Debug')) {
184+
// Re-merge only Debug frameworks so the simulator slice includes main.jsbundle.
185+
await mergeFrameworks({
186+
sourceDir: userConfig.project.ios.sourceDir,
187+
frameworkPaths: [
188+
path.join(
189+
productsPath,
190+
`${configuration}-iphoneos`,
191+
`${frameworkName}.framework`
192+
),
193+
path.join(
194+
productsPath,
195+
`${configuration}-iphonesimulator`,
196+
`${frameworkName}.framework`
197+
),
198+
],
199+
outputPath: path.join(packageDir, `${frameworkName}.xcframework`),
200+
});
201+
}
202+
} else if (configuration.includes('Debug')) {
203+
const debugResolutionMessage =
204+
resolution === 'ambiguous'
205+
? `Skipping Debug simulator JS bundle copy: found multiple bundled framework candidates (${candidates?.join(', ') ?? 'none'}); pass --scheme explicitly`
206+
: 'Skipping Debug simulator JS bundle copy: could not resolve the packaged framework output automatically; pass --scheme explicitly';
207+
208+
logger.warn(debugResolutionMessage);
209+
}
210+
166211
const reactBrownfieldXcframeworkPath = path.join(
167212
packageDir,
168213
'ReactBrownfield.xcframework'
@@ -175,11 +220,6 @@ export const packageIosCommand = curryOptions(
175220
}
176221

177222
if (hasBrownie) {
178-
const productsPath = path.join(
179-
options.buildFolder,
180-
'Build',
181-
'Products'
182-
);
183223
const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
184224

185225
await mergeFrameworks({
@@ -212,11 +252,6 @@ export const packageIosCommand = curryOptions(
212252
}
213253

214254
if (hasNavigation) {
215-
const productsPath = path.join(
216-
options.buildFolder,
217-
'Build',
218-
'Products'
219-
);
220255
const brownfieldNavigationOutputPath = path.join(
221256
packageDir,
222257
'BrownfieldNavigation.xcframework'

0 commit comments

Comments
 (0)