@callstack/react-native-brownfield 5.0.2 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +47 -4
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +23 -1
- package/gradle-plugin/brownfield/build.gradle.kts +7 -0
- package/gradle-plugin/brownfield/gradle/libs.versions.toml +2 -0
- package/gradle-plugin/brownfield/gradle.properties +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscoverer.kt +96 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelper.kt +10 -214
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/BrownfieldPublishingInfo.kt +7 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +92 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt +12 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils/BrownfieldPrimitives.kt → shared/DependencyInfo.kt} +3 -8
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/DependencyPublishability.kt +22 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleDependencyCollector.kt +44 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +181 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFiltering.kt +17 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils → shared}/VersionMediatingDependencySet.kt +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +11 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscovererTest.kt +171 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelperGradleFallbackTest.kt +47 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +110 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/DependencyPublishabilityTest.kt +67 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +222 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFilteringTest.kt +37 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/VersionMediatingDependencySetTest.kt +42 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +11 -0
- package/ios/BrownfieldReactHostPreloader.h +30 -0
- package/ios/BrownfieldReactHostPreloader.m +113 -0
- package/ios/BrownfieldSurfaceRegistry.swift +93 -0
- package/ios/Expo/ExpoHostRuntime.swift +70 -8
- package/ios/JSBundleLoadObserver.swift +64 -17
- package/ios/ReactHostPreloading.swift +177 -0
- package/ios/ReactNativeBrownfield.swift +36 -1
- package/ios/Vanilla/ReactNativeHostRuntime.swift +28 -6
- package/ios/Views/ReactNativeView.swift +34 -5
- package/ios/Views/ReactNativeViewController.swift +120 -1
- package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +6 -1
- package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +6 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/package.json +2 -2
- package/src/expo-config-plugin/android/utils/constants.ts +1 -1
- package/src/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +6 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#import "BrownfieldReactHostPreloader.h"
|
|
2
|
+
|
|
3
|
+
// This file is plain Objective-C, and not Objective-C++. The umbrella header of
|
|
4
|
+
// React-RCTAppDelegate imports the Hermes and JSI headers if `__cplusplus` is set. This pod does
|
|
5
|
+
// not have the necessary search paths for those headers. Without C++, the compiler ignores those
|
|
6
|
+
// imports. The compiler then builds the module in the same way that Swift imports it.
|
|
7
|
+
//
|
|
8
|
+
// The name of the pod changes if the build makes it a framework. Thus you must try each possible
|
|
9
|
+
// name. Expo uses the same method in `RCTAppDelegateUmbrella.h`.
|
|
10
|
+
#if __has_include(<React_RCTAppDelegate/RCTRootViewFactory.h>)
|
|
11
|
+
#import <React_RCTAppDelegate/RCTReactNativeFactory.h>
|
|
12
|
+
#import <React_RCTAppDelegate/RCTRootViewFactory.h>
|
|
13
|
+
#elif __has_include(<React-RCTAppDelegate/RCTRootViewFactory.h>)
|
|
14
|
+
#import <React-RCTAppDelegate/RCTReactNativeFactory.h>
|
|
15
|
+
#import <React-RCTAppDelegate/RCTRootViewFactory.h>
|
|
16
|
+
#else
|
|
17
|
+
#import <React/RCTReactNativeFactory.h>
|
|
18
|
+
#import <React/RCTRootViewFactory.h>
|
|
19
|
+
#endif
|
|
20
|
+
|
|
21
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
22
|
+
|
|
23
|
+
// `RCTBundleConfiguration` first exists in React Native 0.84. React Native 0.83 does not declare
|
|
24
|
+
// the class. This forward declaration lets the file name the type with both versions.
|
|
25
|
+
@class RCTBundleConfiguration;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The two shapes of `initializeReactHostWithLaunchOptions:...`. React Native 0.83 has the shape
|
|
29
|
+
* without `bundleConfiguration:`. React Native 0.84 added that parameter and removed the earlier
|
|
30
|
+
* shape. No header tells the two versions apart at compile time, thus this file declares both
|
|
31
|
+
* shapes and `respondsToSelector:` selects one at run time. The declarations are equal to the
|
|
32
|
+
* React Native declarations. The compiler then accepts the file with both versions.
|
|
33
|
+
*/
|
|
34
|
+
@protocol BrownfieldReactHostInitializing <NSObject>
|
|
35
|
+
|
|
36
|
+
- (void)initializeReactHostWithLaunchOptions:(NSDictionary *__nullable)launchOptions
|
|
37
|
+
bundleConfiguration:(RCTBundleConfiguration *)bundleConfiguration
|
|
38
|
+
devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration;
|
|
39
|
+
|
|
40
|
+
- (void)initializeReactHostWithLaunchOptions:(NSDictionary *__nullable)launchOptions
|
|
41
|
+
devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration;
|
|
42
|
+
|
|
43
|
+
@end
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `RCTReactNativeFactory.bundleConfiguration` also first exists in React Native 0.84.
|
|
47
|
+
*/
|
|
48
|
+
@protocol BrownfieldBundleConfigurationProviding <NSObject>
|
|
49
|
+
|
|
50
|
+
@property (nonatomic, readonly) RCTBundleConfiguration *bundleConfiguration;
|
|
51
|
+
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
NS_ASSUME_NONNULL_END
|
|
55
|
+
|
|
56
|
+
@implementation BrownfieldReactHostPreloader
|
|
57
|
+
|
|
58
|
+
+ (void)preloadWithReactNativeFactory:(id)reactNativeFactory
|
|
59
|
+
launchOptions:(NSDictionary *_Nullable)launchOptions
|
|
60
|
+
{
|
|
61
|
+
RCTReactNativeFactory *factory = (RCTReactNativeFactory *)reactNativeFactory;
|
|
62
|
+
RCTRootViewFactory *rootViewFactory = factory.rootViewFactory;
|
|
63
|
+
|
|
64
|
+
// `initializeReactHostWithLaunchOptions:...` returns early if a view, or an earlier preload,
|
|
65
|
+
// created a host. Thus this method makes no other check. React Native declares
|
|
66
|
+
// `rootViewFactory` as not nullable, and this guard is only a protection.
|
|
67
|
+
if (rootViewFactory == nil) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
id<BrownfieldReactHostInitializing> hostInitializer = (id)rootViewFactory;
|
|
72
|
+
SEL initializeWithBundleConfiguration =
|
|
73
|
+
@selector(initializeReactHostWithLaunchOptions:bundleConfiguration:devMenuConfiguration:);
|
|
74
|
+
SEL initializeWithDevMenuConfiguration =
|
|
75
|
+
@selector(initializeReactHostWithLaunchOptions:devMenuConfiguration:);
|
|
76
|
+
|
|
77
|
+
// Get the configurations from the factory. Do not make new configurations from the default
|
|
78
|
+
// values. The Expo runtime sends `RCTReactNativeFactory.devMenuConfiguration` from
|
|
79
|
+
// `ExpoReactNativeFactory.recreateRootView:`, thus the factory value keeps the two runtimes
|
|
80
|
+
// equal.
|
|
81
|
+
//
|
|
82
|
+
// The preloaded host is equal to a host that a view creates later, but the two paths send
|
|
83
|
+
// different objects. The bare runtime creates the view with
|
|
84
|
+
// `viewWithModuleName:initialProperties:launchOptions:`, and that method sends
|
|
85
|
+
// `[RCTBundleConfiguration defaultConfiguration]` and `[RCTDevMenuConfiguration
|
|
86
|
+
// defaultConfiguration]`. The values are equal:
|
|
87
|
+
// - `RCTReactNativeFactory.bundleConfiguration` gives `[RCTBundleConfiguration
|
|
88
|
+
// defaultConfiguration]` while nobody sets another value.
|
|
89
|
+
// - `RCTReactNativeFactory.devMenuConfiguration` is nil by default.
|
|
90
|
+
// `RCTDevMenuConfigurationDecorator` does nothing with nil, and `RCTDevMenu` and
|
|
91
|
+
// `RCTDevSettings` keep their own default values. Those values are equal to
|
|
92
|
+
// `[RCTDevMenuConfiguration defaultConfiguration]`, because the two sides read `RCT_DEV_MENU`.
|
|
93
|
+
if ([hostInitializer respondsToSelector:initializeWithBundleConfiguration]) {
|
|
94
|
+
id<BrownfieldBundleConfigurationProviding> configurationProvider = (id)factory;
|
|
95
|
+
|
|
96
|
+
[hostInitializer initializeReactHostWithLaunchOptions:launchOptions
|
|
97
|
+
bundleConfiguration:configurationProvider.bundleConfiguration
|
|
98
|
+
devMenuConfiguration:factory.devMenuConfiguration];
|
|
99
|
+
} else if ([hostInitializer respondsToSelector:initializeWithDevMenuConfiguration]) {
|
|
100
|
+
[hostInitializer initializeReactHostWithLaunchOptions:launchOptions
|
|
101
|
+
devMenuConfiguration:factory.devMenuConfiguration];
|
|
102
|
+
} else {
|
|
103
|
+
// A React Native version with a third shape of the method. Do not crash. The first React
|
|
104
|
+
// Native view creates the host, as without a preload.
|
|
105
|
+
NSLog(
|
|
106
|
+
@"%@",
|
|
107
|
+
@"ReactNativeBrownfield: startReactNative did not preload the JavaScript bundle, because "
|
|
108
|
+
@"this React Native version has no known initializeReactHostWithLaunchOptions: method. The "
|
|
109
|
+
@"first React Native view loads the bundle.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Identifies one live React Native surface. Vended by `BrownfieldSurfaceRegistry`.
|
|
4
|
+
struct BrownfieldSurfaceID: Hashable, CustomStringConvertible {
|
|
5
|
+
fileprivate let rawValue: UInt64
|
|
6
|
+
|
|
7
|
+
var description: String { "surface#\(rawValue)" }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/// Decides which React Native surface may act on a `popToNative()` request.
|
|
11
|
+
///
|
|
12
|
+
/// `popToNative()` is delivered as an app-wide notification, so every live React
|
|
13
|
+
/// Native surface receives it — including ones buried under other screens. Acting
|
|
14
|
+
/// on all of them tears down the whole navigation stack
|
|
15
|
+
/// (https://github.com/callstack/react-native-brownfield/issues/354).
|
|
16
|
+
///
|
|
17
|
+
/// The registry resolves that to a single surface: whichever on-screen surface
|
|
18
|
+
/// was shown most recently.
|
|
19
|
+
///
|
|
20
|
+
/// Visibility is asked for at query time instead of being cached from lifecycle
|
|
21
|
+
/// callbacks. Hosts recreate view controllers — SwiftUI does it freely — and a
|
|
22
|
+
/// cached flag is stale exactly when the notification arrives.
|
|
23
|
+
///
|
|
24
|
+
/// Deliberately free of UIKit so it can be unit tested on macOS via `swift test`.
|
|
25
|
+
/// Internal, like the sibling `BrownfieldBundleSupport` target: it is an
|
|
26
|
+
/// implementation detail of the pod, not API for host apps.
|
|
27
|
+
final class BrownfieldSurfaceRegistry {
|
|
28
|
+
static let shared = BrownfieldSurfaceRegistry()
|
|
29
|
+
|
|
30
|
+
private struct Surface {
|
|
31
|
+
let isOnScreen: () -> Bool
|
|
32
|
+
var shownAt: UInt64
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private let lock = NSLock()
|
|
36
|
+
private var nextSurfaceValue: UInt64 = 0
|
|
37
|
+
private var nextShownValue: UInt64 = 0
|
|
38
|
+
private var surfaces: [BrownfieldSurfaceID: Surface] = [:]
|
|
39
|
+
|
|
40
|
+
init() {}
|
|
41
|
+
|
|
42
|
+
/// - Parameter isOnScreen: queried on every `popTarget()`. Capture the host
|
|
43
|
+
/// weakly so a released surface stops winning the election.
|
|
44
|
+
func register(isOnScreen: @escaping () -> Bool) -> BrownfieldSurfaceID {
|
|
45
|
+
lock.lock()
|
|
46
|
+
defer { lock.unlock() }
|
|
47
|
+
|
|
48
|
+
nextSurfaceValue += 1
|
|
49
|
+
let id = BrownfieldSurfaceID(rawValue: nextSurfaceValue)
|
|
50
|
+
surfaces[id] = Surface(isOnScreen: isOnScreen, shownAt: 0)
|
|
51
|
+
return id
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func unregister(_ surface: BrownfieldSurfaceID) {
|
|
55
|
+
lock.lock()
|
|
56
|
+
defer { lock.unlock() }
|
|
57
|
+
|
|
58
|
+
surfaces.removeValue(forKey: surface)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Records that a surface was just shown, ordering it ahead of other on-screen
|
|
62
|
+
/// surfaces. Only a tiebreaker — a surface never marked shown can still win
|
|
63
|
+
/// when it is the one on screen.
|
|
64
|
+
func markShown(_ surface: BrownfieldSurfaceID) {
|
|
65
|
+
lock.lock()
|
|
66
|
+
defer { lock.unlock() }
|
|
67
|
+
|
|
68
|
+
guard var entry = surfaces[surface] else { return }
|
|
69
|
+
|
|
70
|
+
nextShownValue += 1
|
|
71
|
+
entry.shownAt = nextShownValue
|
|
72
|
+
surfaces[surface] = entry
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// The single surface allowed to handle `popToNative()`, or `nil` when no
|
|
76
|
+
/// React Native surface is on screen.
|
|
77
|
+
func popTarget() -> BrownfieldSurfaceID? {
|
|
78
|
+
lock.lock()
|
|
79
|
+
let candidates = surfaces
|
|
80
|
+
lock.unlock()
|
|
81
|
+
|
|
82
|
+
return candidates
|
|
83
|
+
.filter { $0.value.isOnScreen() }
|
|
84
|
+
.max { lhs, rhs in
|
|
85
|
+
(lhs.value.shownAt, lhs.key.rawValue) < (rhs.value.shownAt, rhs.key.rawValue)
|
|
86
|
+
}?
|
|
87
|
+
.key
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func isPopTarget(_ surface: BrownfieldSurfaceID) -> Bool {
|
|
91
|
+
popTarget() == surface
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -11,7 +11,7 @@ internal import EXUpdates
|
|
|
11
11
|
final class ExpoHostRuntime {
|
|
12
12
|
static let shared = ExpoHostRuntime()
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
let preloadState = ReactHostPreloadState()
|
|
15
15
|
private var delegate = ExpoHostRuntimeDelegate()
|
|
16
16
|
private var reactNativeFactory: RCTReactNativeFactory?
|
|
17
17
|
private var expoDelegate: ExpoAppDelegate?
|
|
@@ -35,9 +35,15 @@ final class ExpoHostRuntime {
|
|
|
35
35
|
/**
|
|
36
36
|
* Starts React Native with optional callback when bundle is loaded.
|
|
37
37
|
*
|
|
38
|
-
* @param onBundleLoaded Optional callback invoked after JS bundle is fully loaded.
|
|
38
|
+
* @param onBundleLoaded Optional callback invoked on the main thread after JS bundle is fully loaded.
|
|
39
39
|
*/
|
|
40
40
|
public func startReactNative(onBundleLoaded: (() -> Void)?) {
|
|
41
|
+
// The callback registration is outside of the guard below. An earlier `startReactNative` call
|
|
42
|
+
// can already have made the factory, and the callback must still run.
|
|
43
|
+
if let onBundleLoaded {
|
|
44
|
+
preloadState.jsBundleLoadObserver.observe(onBundleLoaded: onBundleLoaded)
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
guard reactNativeFactory == nil else { return }
|
|
42
48
|
|
|
43
49
|
let appDelegate = ExpoAppDelegate()
|
|
@@ -50,10 +56,6 @@ final class ExpoHostRuntime {
|
|
|
50
56
|
appDelegate.bindReactNativeFactory(reactNativeFactory)
|
|
51
57
|
#endif
|
|
52
58
|
expoDelegate = appDelegate
|
|
53
|
-
|
|
54
|
-
if let onBundleLoaded {
|
|
55
|
-
jsBundleLoadObserver.observeOnce(onBundleLoaded: onBundleLoaded)
|
|
56
|
-
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -70,6 +72,7 @@ final class ExpoHostRuntime {
|
|
|
70
72
|
}
|
|
71
73
|
reactNativeFactory = nil
|
|
72
74
|
expoDelegate = nil
|
|
75
|
+
preloadState.reset()
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
/**
|
|
@@ -169,6 +172,8 @@ final class ExpoHostRuntime {
|
|
|
169
172
|
let bundleURL = delegate.bundleURL()
|
|
170
173
|
configureDevLoadingView(with: bundleURL)
|
|
171
174
|
|
|
175
|
+
let resolvedLaunchOptions = preloadState.launchOptions(overriddenBy: launchOptions)
|
|
176
|
+
|
|
172
177
|
// below: https://github.com/expo/expo/commit/2013760c46cde1404872d181a691da72fbf207a4
|
|
173
178
|
// has moved the recreateRootView method to ExpoReactNativeFactory
|
|
174
179
|
#if EXPO_SDK_GTE_55 // this define comes from the Brownfield Expo config plugin
|
|
@@ -176,19 +181,51 @@ final class ExpoHostRuntime {
|
|
|
176
181
|
withBundleURL: bundleURL,
|
|
177
182
|
moduleName: moduleName,
|
|
178
183
|
initialProps: initialProps,
|
|
179
|
-
launchOptions:
|
|
184
|
+
launchOptions: resolvedLaunchOptions
|
|
180
185
|
)
|
|
181
186
|
#else
|
|
182
187
|
return expoDelegate?.recreateRootView(
|
|
183
188
|
withBundleURL: bundleURL,
|
|
184
189
|
moduleName: moduleName,
|
|
185
190
|
initialProps: initialProps,
|
|
186
|
-
launchOptions:
|
|
191
|
+
launchOptions: resolvedLaunchOptions
|
|
187
192
|
)
|
|
188
193
|
#endif
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
extension ExpoHostRuntime: ReactHostPreloading {
|
|
198
|
+
var reactNativeFactoryForPreload: AnyObject? {
|
|
199
|
+
return reactNativeFactory
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* expo-dev-launcher finds the Metro URL only after the user selects an app in the launcher. This
|
|
204
|
+
* is a Debug behavior. In Release the class is in the binary, but it does nothing. Thus this
|
|
205
|
+
* check is also only in Debug.
|
|
206
|
+
*
|
|
207
|
+
* The check reads the class name, because this pod has no dependency on expo-dev-launcher.
|
|
208
|
+
* `EXDevLauncherController` is the name in expo-dev-launcher 56 and 57. If a later version
|
|
209
|
+
* changes the name, this method gives `true`, and a preload can keep a bundle of the launcher
|
|
210
|
+
* screen. Examine the name again when you add a new Expo SDK.
|
|
211
|
+
*
|
|
212
|
+
* `ExpoHostRuntimeDelegate.isBundleURLStable` covers the other conditions.
|
|
213
|
+
*/
|
|
214
|
+
func canPreloadReactNative() -> Bool {
|
|
215
|
+
#if DEBUG
|
|
216
|
+
if NSClassFromString("EXDevLauncherController") != nil {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
#endif
|
|
220
|
+
|
|
221
|
+
return delegate.isBundleURLStable
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func prepareDevLoadingView() {
|
|
225
|
+
configureDevLoadingView()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
192
229
|
class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate {
|
|
193
230
|
var entryFile = ".expo/.virtual-metro-entry"
|
|
194
231
|
var bundlePath = "main.jsbundle"
|
|
@@ -242,5 +279,30 @@ class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate {
|
|
|
242
279
|
return nil
|
|
243
280
|
}
|
|
244
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* `true` if `bundleURL()` gives now the same result as a later resolution. This property follows
|
|
285
|
+
* the steps of `bundleURL()`, and it must change together with them.
|
|
286
|
+
*
|
|
287
|
+
* The override wins over every other step, thus an override makes the URL stable.
|
|
288
|
+
*
|
|
289
|
+
* expo-updates selects the launch asset while `AppController` starts. `ReactNativeViewController`
|
|
290
|
+
* starts `AppController`. Before this operation is complete, `launchAssetUrl()` is nil, and
|
|
291
|
+
* `bundleURL()` gives the embedded bundle. A host from that time keeps the embedded bundle, and
|
|
292
|
+
* Expo never applies the update. Thus the URL is stable only after `launchAssetUrl()` has a
|
|
293
|
+
* value. The class can be in the binary while the app does not use it, thus this check reads the
|
|
294
|
+
* state and not the class.
|
|
295
|
+
*/
|
|
296
|
+
var isBundleURLStable: Bool {
|
|
297
|
+
if bundleURLOverride?() != nil {
|
|
298
|
+
return true
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#if canImport(EXUpdates) && !DEBUG
|
|
302
|
+
return AppController.isInitialized() && AppController.sharedInstance.launchAssetUrl() != nil
|
|
303
|
+
#else
|
|
304
|
+
return true
|
|
305
|
+
#endif
|
|
306
|
+
}
|
|
245
307
|
}
|
|
246
308
|
#endif
|
|
@@ -1,38 +1,85 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
internal import React
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Watches the `RCTInstanceDidLoadBundle` notification, and calls the callback that waits for it.
|
|
5
|
+
*
|
|
6
|
+
* React Native sends the notification from the JavaScript thread. This class always calls the
|
|
7
|
+
* callback on the main thread, because the callback changes the user interface.
|
|
8
|
+
*
|
|
9
|
+
* The class starts to watch at the initialization, and not at the first callback. The class then
|
|
10
|
+
* knows that React Native loaded the bundle, also if nobody waited for the notification.
|
|
11
|
+
*/
|
|
4
12
|
final class JSBundleLoadObserver {
|
|
5
|
-
private var
|
|
13
|
+
private var pendingCallback: (() -> Void)?
|
|
14
|
+
private var didLoadBundle = false
|
|
6
15
|
private var observerToken: NSObjectProtocol?
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
removeObserverIfNeeded()
|
|
10
|
-
self.onBundleLoaded = onBundleLoaded
|
|
11
|
-
|
|
17
|
+
init() {
|
|
12
18
|
observerToken = NotificationCenter.default.addObserver(
|
|
13
19
|
forName: NSNotification.Name("RCTInstanceDidLoadBundle"),
|
|
14
20
|
object: nil,
|
|
15
|
-
queue:
|
|
21
|
+
queue: .main
|
|
16
22
|
) { [weak self] _ in
|
|
17
|
-
self?.
|
|
23
|
+
self?.bundleDidLoad()
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
deinit {
|
|
22
|
-
|
|
28
|
+
if let observerToken {
|
|
29
|
+
NotificationCenter.default.removeObserver(observerToken)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Keeps one callback, and calls it one time. A new callback replaces the callback that waits. If
|
|
35
|
+
* React Native already loaded the bundle, the class calls the callback in the next turn of the
|
|
36
|
+
* main run loop.
|
|
37
|
+
*
|
|
38
|
+
* @param onBundleLoaded The class always calls this callback on the main thread.
|
|
39
|
+
*/
|
|
40
|
+
func observe(onBundleLoaded: @escaping () -> Void) {
|
|
41
|
+
onMainThread { [weak self] in
|
|
42
|
+
self?.register(onBundleLoaded)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Removes the callback that waits, and forgets the bundle of the earlier session. Call this
|
|
48
|
+
* method when you stop React Native. A callback of the earlier session must not run in the next
|
|
49
|
+
* session.
|
|
50
|
+
*/
|
|
51
|
+
func reset() {
|
|
52
|
+
onMainThread { [weak self] in
|
|
53
|
+
self?.pendingCallback = nil
|
|
54
|
+
self?.didLoadBundle = false
|
|
55
|
+
}
|
|
23
56
|
}
|
|
24
57
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
58
|
+
// MARK: - Main thread only
|
|
59
|
+
|
|
60
|
+
private func register(_ onBundleLoaded: @escaping () -> Void) {
|
|
61
|
+
guard !didLoadBundle else {
|
|
62
|
+
DispatchQueue.main.async(execute: onBundleLoaded)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pendingCallback = onBundleLoaded
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private func bundleDidLoad() {
|
|
70
|
+
didLoadBundle = true
|
|
71
|
+
|
|
72
|
+
let callback = pendingCallback
|
|
73
|
+
pendingCallback = nil
|
|
74
|
+
|
|
29
75
|
callback?()
|
|
30
76
|
}
|
|
31
77
|
|
|
32
|
-
private func
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
|
|
78
|
+
private func onMainThread(_ work: @escaping () -> Void) {
|
|
79
|
+
if Thread.isMainThread {
|
|
80
|
+
work()
|
|
81
|
+
} else {
|
|
82
|
+
DispatchQueue.main.async(execute: work)
|
|
36
83
|
}
|
|
37
84
|
}
|
|
38
85
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The preload state that `startReactNative`, `view` and `stopReactNative` share.
|
|
5
|
+
*/
|
|
6
|
+
final class ReactHostPreloadState {
|
|
7
|
+
let jsBundleLoadObserver = JSBundleLoadObserver()
|
|
8
|
+
|
|
9
|
+
private let lock = NSLock()
|
|
10
|
+
private var storedLaunchOptions: [AnyHashable: Any]?
|
|
11
|
+
private var hasPreloadRequest = false
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Keeps the launch options of a `startReactNative` call.
|
|
15
|
+
*
|
|
16
|
+
* `startReactNative` can move its preload work to the main thread. A view on the main thread can
|
|
17
|
+
* create the React Host before that. Thus this method must run at the call, and not after the
|
|
18
|
+
* change of thread. The view then finds the options.
|
|
19
|
+
*/
|
|
20
|
+
func storeLaunchOptions(_ launchOptions: [AnyHashable: Any]?) {
|
|
21
|
+
guard let launchOptions else { return }
|
|
22
|
+
|
|
23
|
+
lock.lock()
|
|
24
|
+
defer { lock.unlock() }
|
|
25
|
+
|
|
26
|
+
storedLaunchOptions = launchOptions
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Records that a `startReactNative` call must preload the bundle. `consumePreloadRequest` reads
|
|
31
|
+
* the request one time, and `reset` cancels it.
|
|
32
|
+
*/
|
|
33
|
+
func requestPreload() {
|
|
34
|
+
lock.lock()
|
|
35
|
+
defer { lock.unlock() }
|
|
36
|
+
|
|
37
|
+
hasPreloadRequest = true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `true` if a preload request waits. The method also removes the request.
|
|
42
|
+
*
|
|
43
|
+
* `startReactNative` can move its preload work to the main thread. `stopReactNative` on the main
|
|
44
|
+
* thread can run before that work. The stop cancels the request, thus this method then gives
|
|
45
|
+
* `false`, and React Native stays stopped.
|
|
46
|
+
*/
|
|
47
|
+
func consumePreloadRequest() -> Bool {
|
|
48
|
+
lock.lock()
|
|
49
|
+
defer { lock.unlock() }
|
|
50
|
+
|
|
51
|
+
let hadRequest = hasPreloadRequest
|
|
52
|
+
hasPreloadRequest = false
|
|
53
|
+
|
|
54
|
+
return hadRequest
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The launch options for a call that can create the React Host. The options of the caller win
|
|
59
|
+
* over the options of an earlier `startReactNative` call.
|
|
60
|
+
*/
|
|
61
|
+
func launchOptions(
|
|
62
|
+
overriddenBy explicitLaunchOptions: [AnyHashable: Any]? = nil
|
|
63
|
+
) -> [AnyHashable: Any]? {
|
|
64
|
+
lock.lock()
|
|
65
|
+
defer { lock.unlock() }
|
|
66
|
+
|
|
67
|
+
return explicitLaunchOptions ?? storedLaunchOptions
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Removes the state of the session. Call this method when you stop React Native.
|
|
72
|
+
*/
|
|
73
|
+
func reset() {
|
|
74
|
+
jsBundleLoadObserver.reset()
|
|
75
|
+
|
|
76
|
+
lock.lock()
|
|
77
|
+
defer { lock.unlock() }
|
|
78
|
+
|
|
79
|
+
storedLaunchOptions = nil
|
|
80
|
+
hasPreloadRequest = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The preload sequence that the two host runtimes share. Only one runtime is in a build. The
|
|
86
|
+
* shared sequence keeps the behavior of the two runtimes equal.
|
|
87
|
+
*/
|
|
88
|
+
protocol ReactHostPreloading: AnyObject {
|
|
89
|
+
var preloadState: ReactHostPreloadState { get }
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The React Native factory, or nil while React Native did not start. The type is `AnyObject`,
|
|
93
|
+
* because `BrownfieldReactHostPreloader` also takes the factory as `id`. This file then needs no
|
|
94
|
+
* React Native import, and it builds with both runtimes.
|
|
95
|
+
*/
|
|
96
|
+
var reactNativeFactoryForPreload: AnyObject? { get }
|
|
97
|
+
|
|
98
|
+
func startReactNative()
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `false` while the bundle URL can still change. The runtime must not create the host in this
|
|
102
|
+
* condition, because the host keeps the first bundle that it evaluates.
|
|
103
|
+
*/
|
|
104
|
+
func canPreloadReactNative() -> Bool
|
|
105
|
+
|
|
106
|
+
func prepareDevLoadingView()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
extension ReactHostPreloading {
|
|
110
|
+
/**
|
|
111
|
+
* The implementation of
|
|
112
|
+
* `ReactNativeBrownfield.startReactNative(launchOptions:preloadBundle:onBundleLoaded:)`, which
|
|
113
|
+
* holds the contract.
|
|
114
|
+
*/
|
|
115
|
+
func startReactNative(
|
|
116
|
+
launchOptions: [AnyHashable: Any]?,
|
|
117
|
+
preloadBundle: Bool,
|
|
118
|
+
onBundleLoaded: (() -> Void)?
|
|
119
|
+
) {
|
|
120
|
+
preloadState.storeLaunchOptions(launchOptions)
|
|
121
|
+
|
|
122
|
+
if let onBundleLoaded {
|
|
123
|
+
preloadState.jsBundleLoadObserver.observe(onBundleLoaded: onBundleLoaded)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
guard preloadBundle else {
|
|
127
|
+
startReactNative()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
preloadState.requestPreload()
|
|
132
|
+
|
|
133
|
+
if Thread.isMainThread {
|
|
134
|
+
createReactHost()
|
|
135
|
+
} else {
|
|
136
|
+
DispatchQueue.main.async { [weak self] in
|
|
137
|
+
self?.createReactHost()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func createReactHost() {
|
|
143
|
+
// `stopReactNative` can run between the request and this method, because a caller that is not
|
|
144
|
+
// on the main thread moves the work to the main thread. The stop cancels the request, and
|
|
145
|
+
// React Native must then stay stopped.
|
|
146
|
+
guard preloadState.consumePreloadRequest() else { return }
|
|
147
|
+
|
|
148
|
+
startReactNative()
|
|
149
|
+
|
|
150
|
+
guard let factory = reactNativeFactoryForPreload else {
|
|
151
|
+
logSkippedPreload("React Native did not make the factory")
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
guard canPreloadReactNative() else {
|
|
156
|
+
logSkippedPreload("the bundle URL can still change")
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// You must configure the dev loading view before React Native loads the bundle. Usually the
|
|
161
|
+
// `view` method does this.
|
|
162
|
+
prepareDevLoadingView()
|
|
163
|
+
|
|
164
|
+
BrownfieldReactHostPreloader.preload(
|
|
165
|
+
withReactNativeFactory: factory,
|
|
166
|
+
launchOptions: preloadState.launchOptions()
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func logSkippedPreload(_ reason: String) {
|
|
171
|
+
NSLog(
|
|
172
|
+
"%@",
|
|
173
|
+
"ReactNativeBrownfield: startReactNative did not preload the JavaScript bundle, because "
|
|
174
|
+
+ reason + ". The first React Native view loads the bundle."
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -192,7 +192,9 @@ internal import Expo
|
|
|
192
192
|
/**
|
|
193
193
|
* Starts React Native with optional callback when bundle is loaded.
|
|
194
194
|
*
|
|
195
|
-
* @param onBundleLoaded Optional callback invoked after JS bundle is
|
|
195
|
+
* @param onBundleLoaded Optional callback invoked on the main thread after the JS bundle is
|
|
196
|
+
* fully loaded. It runs in the next turn of the main run loop when the bundle is already
|
|
197
|
+
* loaded. A new callback replaces the callback that waits.
|
|
196
198
|
*/
|
|
197
199
|
@objc public func startReactNative(onBundleLoaded: (() -> Void)?) {
|
|
198
200
|
#if canImport(Expo)
|
|
@@ -202,6 +204,39 @@ internal import Expo
|
|
|
202
204
|
#endif
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Starts React Native, and optionally loads the JavaScript bundle immediately. Without a
|
|
209
|
+
* preload, React Native loads the bundle when it creates the first React Native view. On
|
|
210
|
+
* Android, `ReactNativeBrownfield.initialize` does the same operation.
|
|
211
|
+
*
|
|
212
|
+
* @param launchOptions The launch options for the React Host. Only the call that creates the
|
|
213
|
+
* host reads these options. This method keeps the options. A view that creates the host later
|
|
214
|
+
* uses them, also if this method cannot create the host. Options that you give to `view` win
|
|
215
|
+
* over these options.
|
|
216
|
+
* @param preloadBundle `true` loads and evaluates the JavaScript bundle now. A preload adds
|
|
217
|
+
* work to the section of the caller, but the first React Native screen appears faster.
|
|
218
|
+
* @param onBundleLoaded An optional callback, with the rules of `startReactNative`.
|
|
219
|
+
*/
|
|
220
|
+
@objc public func startReactNative(
|
|
221
|
+
launchOptions: [AnyHashable: Any]?,
|
|
222
|
+
preloadBundle: Bool,
|
|
223
|
+
onBundleLoaded: (() -> Void)?
|
|
224
|
+
) {
|
|
225
|
+
#if canImport(Expo)
|
|
226
|
+
ExpoHostRuntime.shared.startReactNative(
|
|
227
|
+
launchOptions: launchOptions,
|
|
228
|
+
preloadBundle: preloadBundle,
|
|
229
|
+
onBundleLoaded: onBundleLoaded
|
|
230
|
+
)
|
|
231
|
+
#else
|
|
232
|
+
ReactNativeHostRuntime.shared.startReactNative(
|
|
233
|
+
launchOptions: launchOptions,
|
|
234
|
+
preloadBundle: preloadBundle,
|
|
235
|
+
onBundleLoaded: onBundleLoaded
|
|
236
|
+
)
|
|
237
|
+
#endif
|
|
238
|
+
}
|
|
239
|
+
|
|
205
240
|
/**
|
|
206
241
|
* Send a serialized JSON message to the React Native JS application.
|
|
207
242
|
* The message is delivered as a `brownfieldMessage` DeviceEventEmitter event.
|