@callstack/react-native-brownfield 5.1.0 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +22 -4
  3. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +3 -1
  4. package/gradle-plugin/brownfield/build.gradle.kts +7 -0
  5. package/gradle-plugin/brownfield/gradle/libs.versions.toml +2 -0
  6. package/gradle-plugin/brownfield/gradle.properties +1 -1
  7. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscoverer.kt +96 -0
  8. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelper.kt +10 -214
  9. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/BrownfieldPublishingInfo.kt +7 -0
  10. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +92 -1
  11. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt +12 -1
  12. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils/BrownfieldPrimitives.kt → shared/DependencyInfo.kt} +3 -8
  13. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/DependencyPublishability.kt +22 -0
  14. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleDependencyCollector.kt +44 -0
  15. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +181 -0
  16. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFiltering.kt +17 -0
  17. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils → shared}/VersionMediatingDependencySet.kt +1 -1
  18. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +11 -0
  19. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscovererTest.kt +171 -0
  20. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelperGradleFallbackTest.kt +47 -0
  21. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +110 -0
  22. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/DependencyPublishabilityTest.kt +67 -0
  23. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +222 -0
  24. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFilteringTest.kt +37 -0
  25. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/VersionMediatingDependencySetTest.kt +42 -0
  26. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +11 -0
  27. package/ios/BrownfieldSurfaceRegistry.swift +93 -0
  28. package/ios/Views/ReactNativeView.swift +34 -5
  29. package/ios/Views/ReactNativeViewController.swift +120 -1
  30. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  31. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  32. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  33. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  34. package/package.json +2 -2
  35. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
@@ -7,6 +7,25 @@ internal import EXUpdates
7
7
  @objc public class ReactNativeViewController: UIViewController {
8
8
  private var moduleName: String
9
9
  private var initialProperties: [String: Any]?
10
+ /// Identifies this surface in `BrownfieldSurfaceRegistry`, which decides
11
+ /// which single surface may act on `popToNative()`.
12
+ private var surfaceID: BrownfieldSurfaceID?
13
+
14
+ /// Set by `ReactNativeView`. SwiftUI owns its navigation state, so an
15
+ /// imperative `popViewController` is reverted; the SwiftUI host dismisses
16
+ /// instead and this controller stands down.
17
+ internal var isHostedBySwiftUI = false
18
+
19
+ /// True from the start of a disappearance transition until the surface next
20
+ /// appears. `view.window` stays non-nil for the whole animation, so this is
21
+ /// what distinguishes "the host already dismissed us" from "nothing happened".
22
+ private var isDisappearing = false
23
+
24
+ /// Whether this surface is the one allowed to act on `popToNative()`.
25
+ internal var isPopTarget: Bool {
26
+ guard let surfaceID else { return false }
27
+ return BrownfieldSurfaceRegistry.shared.isPopTarget(surfaceID)
28
+ }
10
29
 
11
30
  #if canImport(EXUpdates)
12
31
  private let expoUpdatesDelegate = ReactNativeExpoUpdatesDelegate()
@@ -39,6 +58,12 @@ internal import EXUpdates
39
58
  #if !canImport(EXUpdates)
40
59
  renderReactNativeView()
41
60
  #endif
61
+
62
+ // Visibility is resolved live: SwiftUI recreates hosted controllers, so a
63
+ // cached lifecycle flag is stale when the notification arrives.
64
+ surfaceID = BrownfieldSurfaceRegistry.shared.register { [weak self] in
65
+ self?.viewIfLoaded?.window != nil
66
+ }
42
67
 
43
68
  NotificationCenter.default.addObserver(
44
69
  self,
@@ -56,8 +81,31 @@ internal import EXUpdates
56
81
  }
57
82
  }
58
83
 
84
+ public override func viewWillAppear(_ animated: Bool) {
85
+ super.viewWillAppear(animated)
86
+
87
+ isDisappearing = false
88
+
89
+ // Ordered here rather than in `viewDidAppear`: during a modal presentation
90
+ // the covered surface is still in a window, so waiting for `viewDidAppear`
91
+ // would let it out-rank the surface being presented over it.
92
+ if let surfaceID {
93
+ BrownfieldSurfaceRegistry.shared.markShown(surfaceID)
94
+ }
95
+ }
96
+
97
+ public override func viewWillDisappear(_ animated: Bool) {
98
+ super.viewWillDisappear(animated)
99
+
100
+ isDisappearing = true
101
+ }
102
+
59
103
  deinit {
60
104
  NotificationCenter.default.removeObserver(self)
105
+
106
+ if let surfaceID {
107
+ BrownfieldSurfaceRegistry.shared.unregister(surfaceID)
108
+ }
61
109
  }
62
110
 
63
111
  @objc private func togglePopGestureRecognizer(_ notification: Notification) {
@@ -73,11 +121,82 @@ internal import EXUpdates
73
121
  guard let userInfo = notification.userInfo,
74
122
  let animated = userInfo["animated"] as? Bool else { return }
75
123
 
124
+ // `.popToNative` is public API, so a host may post it from any thread.
125
+ // Everything below reads UIKit state — including the registry's visibility
126
+ // check — so hop to main first.
76
127
  DispatchQueue.main.async { [weak self] in
77
- self?.navigationController?.popViewController(animated: animated)
128
+ guard let self else { return }
129
+
130
+ // The notification is app-wide, so every live surface receives it. Only
131
+ // the topmost visible one may act, otherwise surfaces buried under other
132
+ // screens tear down the whole navigation stack.
133
+ // https://github.com/callstack/react-native-brownfield/issues/354
134
+ let target = BrownfieldSurfaceRegistry.shared.popTarget()
135
+
136
+ guard let surfaceID = self.surfaceID, target == surfaceID else {
137
+ if target == nil {
138
+ NSLog("[ReactBrownfield] popToNative(): no React Native surface is on screen; ignoring.")
139
+ }
140
+ return
141
+ }
142
+
143
+ guard self.isHostedBySwiftUI else {
144
+ self.popFromNativeNavigationStack(animated: animated)
145
+ return
146
+ }
147
+
148
+ // SwiftUI owns its navigation state and reverts an imperative pop, so the
149
+ // SwiftUI host dismisses instead. It cannot always succeed though — a
150
+ // `ReactNativeView` inside a `UIHostingController` pushed onto a UIKit
151
+ // navigation stack has nothing for `dismiss()` to act on. Give the host a
152
+ // moment, then fall back, so `popToNative()` is never silently inert.
153
+ //
154
+ // `isDisappearing` — not the delay — is what makes this safe: a dismiss
155
+ // that is merely slow has already begun its transition by the time this
156
+ // runs, so a late timer stands down instead of popping a second entry.
157
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
158
+ guard let self,
159
+ !self.isDisappearing,
160
+ self.viewIfLoaded?.window != nil,
161
+ // Re-established: the entitlement was resolved before the wait.
162
+ self.isPopTarget else { return }
163
+
164
+ self.popFromNativeNavigationStack(animated: animated)
165
+ }
78
166
  }
79
167
  }
80
168
 
169
+ /// Pops this surface off the host's `UINavigationController`.
170
+ ///
171
+ /// Deliberately does not fall back to `dismiss()` when there is no navigation
172
+ /// stack: `presentingViewController` is inherited, so an inline surface hosted
173
+ /// inside a presented native screen would tear down that whole screen — the
174
+ /// same over-dismissal this fix exists to prevent.
175
+ private func popFromNativeNavigationStack(animated: Bool) {
176
+ guard let navigationController else { return }
177
+
178
+ // This controller is usually a child of a host controller (SwiftUI wraps it
179
+ // in one), so find the stack entry containing it rather than assuming it is
180
+ // itself on the stack — and pop to the entry below that, so a screen pushed
181
+ // while we were waiting is not the one that gets popped.
182
+ let stack = navigationController.viewControllers
183
+ guard let index = stack.firstIndex(where: { $0 === self || self.isDescendant(of: $0) }),
184
+ index > 0 else { return }
185
+
186
+ navigationController.popToViewController(stack[index - 1], animated: animated)
187
+ }
188
+
189
+ private func isDescendant(of controller: UIViewController) -> Bool {
190
+ var ancestor = parent
191
+
192
+ while let current = ancestor {
193
+ if current === controller { return true }
194
+ ancestor = current.parent
195
+ }
196
+
197
+ return false
198
+ }
199
+
81
200
  private func renderReactNativeView() {
82
201
  guard !moduleName.isEmpty else { return }
83
202
 
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,"__esModule",{value:true});exports.brownfieldGradlePluginDependency=exports.BROWNFIELD_PLUGIN_VERSION=void 0;var BROWNFIELD_PLUGIN_VERSION=exports.BROWNFIELD_PLUGIN_VERSION='2.0.0-alpha08';var brownfieldGradlePluginDependency=exports.brownfieldGradlePluginDependency=`classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;
1
+ Object.defineProperty(exports,"__esModule",{value:true});exports.brownfieldGradlePluginDependency=exports.BROWNFIELD_PLUGIN_VERSION=void 0;var BROWNFIELD_PLUGIN_VERSION=exports.BROWNFIELD_PLUGIN_VERSION='2.0.0-alpha09';var brownfieldGradlePluginDependency=exports.brownfieldGradlePluginDependency=`classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;
2
2
  //# sourceMappingURL=constants.js.map
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,"__esModule",{value:true});exports.brownfieldGradlePluginDependency=exports.BROWNFIELD_PLUGIN_VERSION=void 0;var BROWNFIELD_PLUGIN_VERSION=exports.BROWNFIELD_PLUGIN_VERSION='2.0.0-alpha08';var brownfieldGradlePluginDependency=exports.brownfieldGradlePluginDependency=`classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;
1
+ Object.defineProperty(exports,"__esModule",{value:true});exports.brownfieldGradlePluginDependency=exports.BROWNFIELD_PLUGIN_VERSION=void 0;var BROWNFIELD_PLUGIN_VERSION=exports.BROWNFIELD_PLUGIN_VERSION='2.0.0-alpha09';var brownfieldGradlePluginDependency=exports.brownfieldGradlePluginDependency=`classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;
2
2
  //# sourceMappingURL=constants.js.map
@@ -1,3 +1,3 @@
1
- export declare const BROWNFIELD_PLUGIN_VERSION = "2.0.0-alpha08";
2
- export declare const brownfieldGradlePluginDependency = "classpath(\"com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha08\")";
1
+ export declare const BROWNFIELD_PLUGIN_VERSION = "2.0.0-alpha09";
2
+ export declare const brownfieldGradlePluginDependency = "classpath(\"com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha09\")";
3
3
  //# sourceMappingURL=constants.d.ts.map
@@ -1,3 +1,3 @@
1
- export declare const BROWNFIELD_PLUGIN_VERSION = "2.0.0-alpha08";
2
- export declare const brownfieldGradlePluginDependency = "classpath(\"com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha08\")";
1
+ export declare const BROWNFIELD_PLUGIN_VERSION = "2.0.0-alpha09";
2
+ export declare const brownfieldGradlePluginDependency = "classpath(\"com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha09\")";
3
3
  //# sourceMappingURL=constants.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/react-native-brownfield",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "license": "MIT",
5
5
  "author": "Michal Chudziak <mike.chudziak@callstack.com>",
6
6
  "bin": {
@@ -89,7 +89,7 @@
89
89
  "@expo/config-plugins": ">=54 <100"
90
90
  },
91
91
  "dependencies": {
92
- "@callstack/brownfield-cli": "^5.1.0"
92
+ "@callstack/brownfield-cli": "^5.1.1"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@babel/core": "^7.29.6",
@@ -1,2 +1,2 @@
1
- export const BROWNFIELD_PLUGIN_VERSION = '2.0.0-alpha08';
1
+ export const BROWNFIELD_PLUGIN_VERSION = '2.0.0-alpha09';
2
2
  export const brownfieldGradlePluginDependency = `classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;