Skip to content

Commit cbcc18d

Browse files
hurali97artus9033thymikee
authored
feat: unify expo and bare RN in ReactNativeBrownfield (#234)
* chore: bootstrapped demo Expo app * chore: use expo config plugin in expo demo app * chore: reset Expo project * chore: use workspace specifier for monorepo deps * fix: do not toss up source sets in Gradle plugin * feat: base implementation for brownfield Expo config plugin * chore: update lock files * chore: update app.json in ExpoApp * chore: enable debug logging in app.json in ExpoApp * feat: use new logging system in the plugin * feat: templates engine for project modifications * feat: patch for Expo SDK pre v55, fix source file paths * feat: implemented Android brownfield plugin * chore: update ExpoApp scripts * fix: dedent gradle script insertion * fix: depend on Material in AndroidApp; fix typo in gradle scripting comments * fix: package name in ExpoApp * chore: use debug signing config for AndroidApp release variant - for testing purposes * chore: missing indent in gradleHelpers insertion * fix: add missing expo modifications to template build.gradle.kts * chore: use the Expo classes in template ReactNativeHostManager.kt * fix: iOS xcode project processing of target UUID * fix: make Android Expo config explicitly specify versions of RN Android deps to be properly resolved by Gradle when consumed * fix: running package:ios again * chore: udpate rock to 0.12.8 * chore: make nodemon watch template files in dev script for brownfield package * chore: reformat files, extract capitalized string helper, suppress unchecked cast compilation warnings * chore: deintegrate isExpo gradle plugin prop in favor of auto-detection * chore: typo in Android native demo app theme name * feat: config Maven publishing for part of the core expo modules * fix: correct artifact, group and version discovery for manually whitelisted publishable projects * feat: inject transitive dependencies into POM publication files * feat: instead of publishing, inject proper POM and Gradle Module JSON metadata with Expo transitive deps * feat: functional Expo demo in AndroidApp * chore: re-enable signing of gradle plugin * chore: use 0.0.1-snapshot by default in Expo plugin on Android * fix: filter out remaining expo transitive dependencies, add version mediation mechanisms * feat: seamless integration of Expo features into the brownfield CLI * refactor: restructure RNApp sources * feat: reuse RNApp sources in ExpoApp * feat: set up flavors for AndroidApp to consume expo or vanilla artifacts * ci: update CI to account for AndroidApp flavors * chore: remove obsolete comment * fix: monorepo-prone expo detection in CLI * feat: proper code sharing for RN demo apps * fix: make ReactNativeHostManager singatures match across flavors, properly initialize React Native * fix: restore .brownie.ts files in each project for codegen to work properly * feat: added an image view to the demo app * chore: strip obsolete comment * fix: proper module name in ios script * fix: brownie not to crash on Android * fix: handle brownie no stores in project in the CLI * feat: demo apps reorganized, pulled in clean template for expo app * feat: integrate brownie store with Expo demo app * docs: update docs * feat: temporarily deintegrate brownie from expo demo before android is implemented in brownie * chore: rename expo app project * chore: re-enable signing of the gradle plugin * chore: add changesets * refactor: format files * fix(ci): path to RN project in androidapp-road-test * refactor: format files to comply with detekt * chore: revert changes to brownie * fix: paths in packageIos in RN projects * ci: update workflow for Apple apps * feat: demo iOS app supporting interchanging brownfield artifacts * fix: patchExpoPre55.sh * fix: reorder build phases in iOS to first run patching after expo configure * wip: reorder build script phases PoC for Expo config plugin * chore: reorder code properly * chore: use SNAPSHOT for CI and local * chore: add plugin publish and patch scripts for local maven * chore: fix appleapp-road-test ci * chore: use macos runner for appleapp CI * fix(ci): update maven path for expo flavor * fix(ci): expo android * feat: ensure build phase correct order using post_integrate * feat: add ReactNativeHostManager to source files * fix: use the bundleURL in loadView * feat: present Expo RN UI in Apple App * chore: use latest version for brownfield-gradle-plugin * feat: add source files to PBXSourcesBuildPhase * feat: allow entry points other than main * feat: add configuration for vanilla and expo to AppleApp * fix: add info.plist to AppleApp * fix: bundle reference * fix(ci): add guard to only run pods for vanilla * docs: update ExpoApp usages * feat: remove coil dependency from plugin to the lib * refactor: remove TODO comments * feat: guard script reordering against expo version * feat: separate brownie usage by platform * chore: build android app with release variant * docs: add Expo Integration section * chore: bump versions * fix: remove color prop * Update docs/docs/docs/getting-started/expo.mdx Co-authored-by: Michał Pierzchała <thymikee@gmail.com> * Update docs/docs/docs/getting-started/expo.mdx Co-authored-by: Michał Pierzchała <thymikee@gmail.com> * docs: remove images * refactor: fix indentation * feat: unify expo and bare RN in ReactNativeBrownfield * chore: remove changeset bump * refactor: remove unused property and enforce failure * refactor: split classes by concerns * refactor: move duplicate code to a shareable resource * fix: make the class internal * chore: podfile changes * feat: add JSBundle load for Expo and make it reusable * fix: fail early for non-extension bundle path * docs: update --------- Co-authored-by: artus9033 <artus9033@gmail.com> Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
1 parent 5a18a76 commit cbcc18d

File tree

15 files changed

+459
-221
lines changed

15 files changed

+459
-221
lines changed

apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
1010
_ application: UIApplication,
1111
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
1212
) -> Bool {
13-
#if USE_EXPO_HOST
14-
return ReactNativeHostManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
15-
#else
16-
return true
17-
#endif
13+
return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
1814
}
1915
}
2016

@@ -23,14 +19,15 @@ struct BrownfieldAppleApp: App {
2319
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
2420

2521
init() {
26-
#if USE_EXPO_HOST
27-
ReactNativeHostManager.shared.initialize()
28-
#else
2922
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
3023
ReactNativeBrownfield.shared.startReactNative {
3124
print("React Native has been loaded")
3225
}
26+
27+
#if USE_EXPO_HOST
28+
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
3329
#endif
30+
3431
BrownfieldStore.register(initialState)
3532
}
3633

apps/AppleApp/Brownfield Apple App/ContentView.swift

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import ReactBrownfield
2-
import BrownfieldLib
32
import Brownie
43
import SwiftUI
54

@@ -17,33 +16,12 @@ struct ContentView: View {
1716
}
1817
}
1918

20-
#if USE_EXPO_HOST
21-
struct RNViewRepresentable: UIViewRepresentable {
22-
23-
func makeUIView(context: Context) -> UIView {
24-
return ReactNativeHostManager.shared.loadView(moduleName: "RNApp", initialProps: nil, launchOptions: nil)
25-
}
26-
27-
func updateUIView(_ uiView: UIView, context: Context) {
28-
// Update the view when SwiftUI state changes
29-
}
30-
}
31-
#endif
32-
33-
struct MainScreen: View {
34-
var rnView: some View {
35-
#if USE_EXPO_HOST
36-
RNViewRepresentable()
37-
#else
38-
ReactNativeView(moduleName: "RNApp")
39-
#endif
40-
}
41-
19+
struct MainScreen: View {
4220
var body: some View {
4321
VStack(spacing: 16) {
4422
GreetingCard(name: "iOS")
4523

46-
rnView
24+
ReactNativeView(moduleName: "RNApp")
4725
.navigationBarHidden(true)
4826
.clipShape(RoundedRectangle(cornerRadius: 16))
4927
.background(Color(UIColor.systemBackground))

apps/RNApp/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2350,7 +2350,7 @@ PODS:
23502350
- SocketRocket
23512351
- ReactAppDependencyProvider (0.82.1):
23522352
- ReactCodegen
2353-
- ReactBrownfield (2.2.0):
2353+
- ReactBrownfield (3.0.0-rc.1):
23542354
- boost
23552355
- DoubleConversion
23562356
- fast_float
@@ -2843,7 +2843,7 @@ SPEC CHECKSUMS:
28432843
React-utils: f06ff240e06e2bd4b34e48f1b34cac00866e8979
28442844
React-webperformancenativemodule: b3398f8175fa96d992c071b1fa59bd6f9646b840
28452845
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
2846-
ReactBrownfield: 65d6bf15ccb7501d3982ec05f8d37b697e9b3480
2846+
ReactBrownfield: ce231a9060b34e1fe8f91ec8416f21dc6da8b4b5
28472847
ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb
28482848
ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654
28492849
RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,19 @@ This should only take a few minutes.
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)
112112
<hr/>
113113

114-
1. Call the `ReactNativeHostManager.shared.initialize()` from your Application Entry point:
114+
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
120120

121121
init() {
122-
ReactNativeHostManager.shared.initialize()
122+
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
123+
ReactNativeBrownfield.shared.startReactNative {
124+
print("React Native has been loaded")
125+
}
126+
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
123127
}
124128

125129
var body: some Scene {
@@ -140,19 +144,19 @@ class AppDelegate: NSObject, UIApplicationDelegate {
140144
_ application: UIApplication,
141145
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
142146
) -> Bool {
143-
return ReactNativeHostManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
147+
return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
144148
}
145149
}
146150
```
147151

148-
3. Use the `ReactNativeHostManager.loadView` to present the UI
152+
3. Use the `ReactNativeView` or `ReactNativeViewController` to present the UI
149153

150-
```
151-
let RNView = ReactNativeHostManager.shared.loadView(moduleName: "ExpoRNApp", initialProps: nil, launchOptions: nil)
154+
```swift
155+
ReactNativeView(moduleName: "ExpoRNApp")
156+
.background(Color(UIColor.systemBackground))
152157
```
153158

154-
> This returns a UIView, so if you're using SwiftUI, you will have to wrap this in UIViewRrepresentable .
155-
You can see the demo integration in [Apple App](https://github.com/callstack/react-native-brownfield/blob/main/apps/AppleApp/Brownfield%20Apple%20App/BrownfieldAppleApp.swift) as well
159+
> You can also use `ReactNativeBrownfield.shared.view(moduleName, initialProps)` which returns a UIView.
156160
157161
4. Build and install the iOS application 🚀
158162

packages/react-native-brownfield/ReactBrownfield.podspec

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Pod::Spec.new do |spec|
2222

2323
spec.dependency 'ReactAppDependencyProvider'
2424
add_dependency(spec, "React-RCTAppDelegate")
25+
26+
if ENV['REACT_NATIVE_BROWNFIELD_USE_EXPO_HOST'] == '1'
27+
spec.dependency 'Expo'
28+
end
2529

2630
install_modules_dependencies(spec)
2731
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
enum BrownfieldBundlePathResolver {
4+
enum Error: Swift.Error {
5+
case invalidBundlePath(String)
6+
}
7+
8+
static func resourceComponents(from bundlePath: String) throws -> (
9+
resourceName: String,
10+
fileExtension: String
11+
) {
12+
let fileExtension = (bundlePath as NSString).pathExtension
13+
let resourceName = (bundlePath as NSString).deletingPathExtension
14+
15+
guard !fileExtension.isEmpty, !resourceName.isEmpty else {
16+
throw Error.invalidBundlePath(bundlePath)
17+
}
18+
19+
return (resourceName, fileExtension)
20+
}
21+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import UIKit
2+
internal import React
3+
internal import React_RCTAppDelegate
4+
internal import ReactAppDependencyProvider
5+
6+
#if canImport(Expo)
7+
internal import Expo
8+
9+
final class ExpoHostRuntime {
10+
static let shared = ExpoHostRuntime()
11+
12+
private let jsBundleLoadObserver = JSBundleLoadObserver()
13+
private var delegate = ExpoHostRuntimeDelegate()
14+
private var reactNativeFactory: RCTReactNativeFactory?
15+
private var expoDelegate: ExpoAppDelegate?
16+
17+
/**
18+
* Starts React Native with default parameters.
19+
*/
20+
public func startReactNative() {
21+
startReactNative(onBundleLoaded: nil)
22+
}
23+
/**
24+
* Starts React Native with optional callback when bundle is loaded.
25+
*
26+
* @param onBundleLoaded Optional callback invoked after JS bundle is fully loaded.
27+
*/
28+
public func startReactNative(onBundleLoaded: (() -> Void)?) {
29+
guard reactNativeFactory == nil else { return }
30+
31+
let factory = ExpoReactNativeFactory(delegate: delegate)
32+
delegate.dependencyProvider = RCTAppDependencyProvider()
33+
34+
reactNativeFactory = factory
35+
36+
let appDelegate = ExpoAppDelegate()
37+
appDelegate.bindReactNativeFactory(factory)
38+
expoDelegate = appDelegate
39+
40+
if let onBundleLoaded {
41+
jsBundleLoadObserver.observeOnce(onBundleLoaded: onBundleLoaded)
42+
}
43+
}
44+
45+
/**
46+
* Path to JavaScript root.
47+
* Default value: ".expo/.virtual-metro-entry"
48+
*/
49+
public var entryFile: String = ".expo/.virtual-metro-entry" {
50+
didSet {
51+
delegate.entryFile = entryFile
52+
}
53+
}
54+
55+
/**
56+
* Path to JavaScript bundle file.
57+
* Default value: "main.jsbundle"
58+
*/
59+
public var bundlePath: String = "main.jsbundle" {
60+
didSet {
61+
delegate.bundlePath = bundlePath
62+
}
63+
}
64+
/**
65+
* Bundle instance to lookup the JavaScript bundle.
66+
* Default value: Bundle.main
67+
*/
68+
public var bundle: Bundle = Bundle.main {
69+
didSet {
70+
delegate.bundle = bundle
71+
}
72+
}
73+
/**
74+
* Dynamic bundle URL provider called on every bundle load.
75+
* When set, this overrides the default bundleURL() behavior in the delegate.
76+
* Returns a URL to load a custom bundle, or nil to use default behavior.
77+
* Default value: nil
78+
*/
79+
public var bundleURLOverride: (() -> URL?)? = nil {
80+
didSet {
81+
delegate.bundleURLOverride = bundleURLOverride
82+
}
83+
}
84+
85+
func application(
86+
_ application: UIApplication,
87+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
88+
) -> Bool {
89+
return expoDelegate?.application(
90+
application,
91+
didFinishLaunchingWithOptions: launchOptions
92+
) != nil
93+
}
94+
95+
func view(
96+
moduleName: String,
97+
initialProps: [AnyHashable: Any]?,
98+
launchOptions: [AnyHashable: Any]?
99+
) -> UIView? {
100+
let bundleURL = delegate.bundleURL()
101+
102+
return expoDelegate?.recreateRootView(
103+
withBundleURL: bundleURL,
104+
moduleName: moduleName,
105+
initialProps: initialProps,
106+
launchOptions: launchOptions
107+
)
108+
}
109+
}
110+
111+
class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate {
112+
var entryFile = ".expo/.virtual-metro-entry"
113+
var bundlePath = "main.jsbundle"
114+
var bundle = Bundle.main
115+
var bundleURLOverride: (() -> URL?)? = nil
116+
117+
override func sourceURL(for bridge: RCTBridge) -> URL? {
118+
// needed to return the correct URL for expo-dev-client.
119+
bridge.bundleURL ?? bundleURL()
120+
}
121+
122+
override func bundleURL() -> URL? {
123+
if let bundleURLProvider = bundleURLOverride { return bundleURLProvider() }
124+
#if DEBUG
125+
return RCTBundleURLProvider.sharedSettings().jsBundleURL(
126+
forBundleRoot: entryFile)
127+
#else
128+
do {
129+
let (resourceName, fileExtension) = try BrownfieldBundlePathResolver.resourceComponents(
130+
from: bundlePath
131+
)
132+
return bundle.url(forResource: resourceName, withExtension: fileExtension)
133+
} catch {
134+
assertionFailure("Invalid bundlePath '\(bundlePath)': \(error)")
135+
return nil
136+
}
137+
#endif
138+
}
139+
}
140+
#endif
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
internal import React
3+
4+
final class JSBundleLoadObserver {
5+
private var onBundleLoaded: (() -> Void)?
6+
private var observerToken: NSObjectProtocol?
7+
8+
func observeOnce(onBundleLoaded: @escaping () -> Void) {
9+
removeObserverIfNeeded()
10+
self.onBundleLoaded = onBundleLoaded
11+
12+
observerToken = NotificationCenter.default.addObserver(
13+
forName: NSNotification.Name("RCTInstanceDidLoadBundle"),
14+
object: nil,
15+
queue: nil
16+
) { [weak self] _ in
17+
self?.notifyAndClear()
18+
}
19+
}
20+
21+
deinit {
22+
removeObserverIfNeeded()
23+
}
24+
25+
private func notifyAndClear() {
26+
let callback = onBundleLoaded
27+
onBundleLoaded = nil
28+
removeObserverIfNeeded()
29+
callback?()
30+
}
31+
32+
private func removeObserverIfNeeded() {
33+
if let observerToken {
34+
NotificationCenter.default.removeObserver(observerToken)
35+
self.observerToken = nil
36+
}
37+
}
38+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
3+
extension Notification.Name {
4+
/**
5+
* Notification sent when React Native wants to navigate back to native screen.
6+
*/
7+
public static let popToNative = Notification.Name("PopToNativeNotification")
8+
/**
9+
* Notification sent to enable/disable the pop gesture recognizer.
10+
*/
11+
public static let togglePopGestureRecognizer = Notification.Name("TogglePopGestureRecognizerNotification")
12+
}

0 commit comments

Comments
 (0)