@granite-js/micro-frontend 2.0.0

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 (168) hide show
  1. package/GraniteMicroFrontendRuntime.podspec +40 -0
  2. package/LICENSE +202 -0
  3. package/LICENSE.react-native-teleport +21 -0
  4. package/NOTICE +17 -0
  5. package/README.md +508 -0
  6. package/android/CMakeLists.txt +34 -0
  7. package/android/build.gradle +74 -0
  8. package/android/gradle.properties +4 -0
  9. package/android/src/main/AndroidManifest.xml +1 -0
  10. package/android/src/main/cpp/BundleEvaluator.cpp +64 -0
  11. package/android/src/main/cpp/BundleEvaluator.h +20 -0
  12. package/android/src/main/cpp/FileReader.cpp +92 -0
  13. package/android/src/main/cpp/FileReader.h +27 -0
  14. package/android/src/main/cpp/onLoad.cpp +9 -0
  15. package/android/src/main/java/com/teleport/extensions/Float.kt +12 -0
  16. package/android/src/main/java/com/teleport/extensions/View.kt +10 -0
  17. package/android/src/main/java/com/teleport/global/PortalRegistry.kt +117 -0
  18. package/android/src/main/java/com/teleport/host/PortalHostView.kt +97 -0
  19. package/android/src/main/java/com/teleport/host/PortalHostViewManager.kt +41 -0
  20. package/android/src/main/java/com/teleport/host/PortalReactRootView.kt +116 -0
  21. package/android/src/main/java/com/teleport/managers/TeleportViewManager.kt +27 -0
  22. package/android/src/main/java/com/teleport/portal/PortalLayoutStateController.kt +107 -0
  23. package/android/src/main/java/com/teleport/portal/PortalView.kt +309 -0
  24. package/android/src/main/java/com/teleport/portal/PortalViewManager.kt +51 -0
  25. package/android/src/main/java/run/granite/microfrontend/ActivitySessionBinding.kt +87 -0
  26. package/android/src/main/java/run/granite/microfrontend/BundleEvaluator.kt +46 -0
  27. package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendEvent.kt +62 -0
  28. package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouter.kt +186 -0
  29. package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeHost.kt +42 -0
  30. package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeModule.kt +56 -0
  31. package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimePackage.kt +35 -0
  32. package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendSessionStore.kt +116 -0
  33. package/android/src/main/jni/CMakeLists.txt +64 -0
  34. package/android/src/test/cpp/CMakeLists.txt +35 -0
  35. package/android/src/test/cpp/FileReaderTest.cpp +44 -0
  36. package/android/src/test/java/com/teleport/host/PortalReactRootViewContractTest.kt +76 -0
  37. package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouterTest.kt +216 -0
  38. package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendSessionStoreTest.kt +190 -0
  39. package/android/test-cpp.sh +12 -0
  40. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/ComponentDescriptors.h +24 -0
  41. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.cpp +31 -0
  42. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.h +29 -0
  43. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewComponentDescriptor.h +22 -0
  44. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.cpp +7 -0
  45. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.h +43 -0
  46. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewState.h +21 -0
  47. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewComponentDescriptor.h +54 -0
  48. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.cpp +7 -0
  49. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.h +51 -0
  50. package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewState.h +43 -0
  51. package/dist/module/createMicroFrontendRuntime.js +18 -0
  52. package/dist/module/createMicroFrontendRuntime.js.map +1 -0
  53. package/dist/module/createRoute.js +43 -0
  54. package/dist/module/createRoute.js.map +1 -0
  55. package/dist/module/host/PendingHostComponent.js +70 -0
  56. package/dist/module/host/PendingHostComponent.js.map +1 -0
  57. package/dist/module/host/index.js +6 -0
  58. package/dist/module/host/index.js.map +1 -0
  59. package/dist/module/host/pendingHostComponentStore.js +164 -0
  60. package/dist/module/host/pendingHostComponentStore.js.map +1 -0
  61. package/dist/module/host/resolveParams.js +60 -0
  62. package/dist/module/host/resolveParams.js.map +1 -0
  63. package/dist/module/host/routeMatcher.js +92 -0
  64. package/dist/module/host/routeMatcher.js.map +1 -0
  65. package/dist/module/host/types.js +4 -0
  66. package/dist/module/host/types.js.map +1 -0
  67. package/dist/module/index.js +12 -0
  68. package/dist/module/index.js.map +1 -0
  69. package/dist/module/package.json +1 -0
  70. package/dist/module/plugin/index.js +4 -0
  71. package/dist/module/plugin/index.js.map +1 -0
  72. package/dist/module/plugin/intoShared.js +32 -0
  73. package/dist/module/plugin/intoShared.js.map +1 -0
  74. package/dist/module/plugin/microFrontendPlugin.js +58 -0
  75. package/dist/module/plugin/microFrontendPlugin.js.map +1 -0
  76. package/dist/module/plugin/prelude.js +23 -0
  77. package/dist/module/plugin/prelude.js.map +1 -0
  78. package/dist/module/plugin/resolver.js +25 -0
  79. package/dist/module/plugin/resolver.js.map +1 -0
  80. package/dist/module/plugin/types.js +4 -0
  81. package/dist/module/plugin/types.js.map +1 -0
  82. package/dist/module/runtime/createMicroFrontendRuntime.js +79 -0
  83. package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -0
  84. package/dist/module/runtime/errors.js +63 -0
  85. package/dist/module/runtime/errors.js.map +1 -0
  86. package/dist/module/runtime/parseAppRequest.js +16 -0
  87. package/dist/module/runtime/parseAppRequest.js.map +1 -0
  88. package/dist/module/runtime/parseNativeRuntimeEvent.js +54 -0
  89. package/dist/module/runtime/parseNativeRuntimeEvent.js.map +1 -0
  90. package/dist/module/runtime/registry.js +87 -0
  91. package/dist/module/runtime/registry.js.map +1 -0
  92. package/dist/module/session/MicroFrontendSessionContext.js +32 -0
  93. package/dist/module/session/MicroFrontendSessionContext.js.map +1 -0
  94. package/dist/module/session/useMicroFrontendSessions.js +47 -0
  95. package/dist/module/session/useMicroFrontendSessions.js.map +1 -0
  96. package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js +21 -0
  97. package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js.map +1 -0
  98. package/dist/module/specs/PortalHostViewNativeComponent.ts +9 -0
  99. package/dist/module/specs/PortalViewNativeComponent.ts +9 -0
  100. package/dist/module/types.js +2 -0
  101. package/dist/module/types.js.map +1 -0
  102. package/dist/typescript/createMicroFrontendRuntime.d.ts +6 -0
  103. package/dist/typescript/createRoute.d.ts +23 -0
  104. package/dist/typescript/host/PendingHostComponent.d.ts +14 -0
  105. package/dist/typescript/host/index.d.ts +5 -0
  106. package/dist/typescript/host/pendingHostComponentStore.d.ts +11 -0
  107. package/dist/typescript/host/resolveParams.d.ts +10 -0
  108. package/dist/typescript/host/routeMatcher.d.ts +7 -0
  109. package/dist/typescript/host/types.d.ts +32 -0
  110. package/dist/typescript/index.d.ts +16 -0
  111. package/dist/typescript/plugin/index.d.ts +2 -0
  112. package/dist/typescript/plugin/intoShared.d.ts +3 -0
  113. package/dist/typescript/plugin/microFrontendPlugin.d.ts +3 -0
  114. package/dist/typescript/plugin/prelude.d.ts +6 -0
  115. package/dist/typescript/plugin/resolver.d.ts +3 -0
  116. package/dist/typescript/plugin/types.d.ts +5 -0
  117. package/dist/typescript/runtime/createMicroFrontendRuntime.d.ts +31 -0
  118. package/dist/typescript/runtime/errors.d.ts +35 -0
  119. package/dist/typescript/runtime/parseAppRequest.d.ts +6 -0
  120. package/dist/typescript/runtime/parseNativeRuntimeEvent.d.ts +3 -0
  121. package/dist/typescript/runtime/registry.d.ts +35 -0
  122. package/dist/typescript/session/MicroFrontendSessionContext.d.ts +13 -0
  123. package/dist/typescript/session/useMicroFrontendSessions.d.ts +8 -0
  124. package/dist/typescript/specs/NativeGraniteMicroFrontendRuntime.d.ts +21 -0
  125. package/dist/typescript/specs/PortalHostViewNativeComponent.d.ts +6 -0
  126. package/dist/typescript/specs/PortalViewNativeComponent.d.ts +6 -0
  127. package/dist/typescript/types.d.ts +49 -0
  128. package/ios/GraniteMicroFrontendRuntimeHost+Internal.h +18 -0
  129. package/ios/GraniteMicroFrontendRuntimeHost.h +34 -0
  130. package/ios/GraniteMicroFrontendRuntimeHost.mm +485 -0
  131. package/ios/GraniteMicroFrontendRuntimeModule.h +6 -0
  132. package/ios/GraniteMicroFrontendRuntimeModule.mm +138 -0
  133. package/ios/PortalHostContainerView.h +56 -0
  134. package/ios/PortalHostContainerView.mm +133 -0
  135. package/ios/PortalHostView.h +39 -0
  136. package/ios/PortalHostView.mm +153 -0
  137. package/ios/PortalRegistry.h +38 -0
  138. package/ios/PortalRegistry.mm +228 -0
  139. package/ios/PortalView.h +32 -0
  140. package/ios/PortalView.mm +426 -0
  141. package/package.json +113 -0
  142. package/react-native.config.js +13 -0
  143. package/src/createMicroFrontendRuntime.ts +22 -0
  144. package/src/createRoute.ts +84 -0
  145. package/src/host/PendingHostComponent.tsx +92 -0
  146. package/src/host/index.ts +26 -0
  147. package/src/host/pendingHostComponentStore.ts +241 -0
  148. package/src/host/resolveParams.ts +70 -0
  149. package/src/host/routeMatcher.ts +117 -0
  150. package/src/host/types.ts +45 -0
  151. package/src/index.ts +57 -0
  152. package/src/plugin/index.ts +2 -0
  153. package/src/plugin/intoShared.ts +50 -0
  154. package/src/plugin/microFrontendPlugin.ts +57 -0
  155. package/src/plugin/prelude.ts +73 -0
  156. package/src/plugin/resolver.ts +73 -0
  157. package/src/plugin/types.ts +6 -0
  158. package/src/runtime/createMicroFrontendRuntime.ts +115 -0
  159. package/src/runtime/errors.ts +86 -0
  160. package/src/runtime/parseAppRequest.ts +22 -0
  161. package/src/runtime/parseNativeRuntimeEvent.ts +54 -0
  162. package/src/runtime/registry.ts +131 -0
  163. package/src/session/MicroFrontendSessionContext.tsx +45 -0
  164. package/src/session/useMicroFrontendSessions.ts +73 -0
  165. package/src/specs/NativeGraniteMicroFrontendRuntime.ts +44 -0
  166. package/src/specs/PortalHostViewNativeComponent.ts +9 -0
  167. package/src/specs/PortalViewNativeComponent.ts +9 -0
  168. package/src/types.ts +55 -0
@@ -0,0 +1,309 @@
1
+ package com.teleport.portal
2
+
3
+ import android.content.Context
4
+ import android.view.View
5
+ import android.view.ViewGroup
6
+ import com.facebook.common.logging.FLog
7
+ import com.facebook.react.uimanager.StateWrapper
8
+ import com.facebook.react.views.view.ReactViewGroup
9
+ import com.teleport.global.PortalRegistry
10
+ import com.teleport.host.PortalHostView
11
+ import java.util.ArrayList
12
+
13
+ /**
14
+ * React/Fabric-owned source that moves its Android child views into a named [PortalHostView].
15
+ *
16
+ * Children remain logically owned by this view. They stay attached here while no destination is
17
+ * available, move to the matching attached host when it appears, and return here when it detaches.
18
+ */
19
+ class PortalView(
20
+ context: Context,
21
+ ) : ReactViewGroup(context) {
22
+ private var hostName: String? = null
23
+ private val layoutStateController = PortalLayoutStateController(this)
24
+ private val ownChildren: MutableList<View> = ArrayList()
25
+
26
+ private val resolvedHost: PortalHostView?
27
+ get() = PortalRegistry.resolveHost(hostName, this)
28
+
29
+ private val isTeleported: Boolean
30
+ get() = hostName != null && resolvedHost != null
31
+
32
+ // region ViewManager methods
33
+ fun setStateWrapper(wrapper: StateWrapper?) {
34
+ layoutStateController.setStateWrapper(wrapper)
35
+ layoutStateController.updateIfNeeded(hostName, resolvedHost)
36
+ }
37
+
38
+ fun setHostName(name: String?) {
39
+ if (name == hostName) return
40
+
41
+ val children = extractChildren()
42
+
43
+ hostName?.let { PortalRegistry.unregisterPendingPortal(it, this) }
44
+
45
+ hostName = name
46
+
47
+ val resolved = name?.let { PortalRegistry.resolveHost(it, this) }
48
+ if (name != null && resolved == null) {
49
+ // Rendering in place is a legitimate state — the host may simply not have mounted yet — but
50
+ // it looks identical to a typo in hostName, and nothing else in this module says which one
51
+ // happened. Logged here rather than in resolveHost because that runs on every layout pass.
52
+ FLog.w(
53
+ TAG,
54
+ "Portal \"%s\" resolved no attached host; its children stay in the source tree until one registers",
55
+ name,
56
+ )
57
+ }
58
+ val target: ViewGroup = resolved ?: this
59
+
60
+ if (target is PortalHostView) {
61
+ for (i in children.indices) {
62
+ val idx = target.nextInsertionIndexForChildAt(i)
63
+ target.addView(children[i], idx)
64
+ }
65
+ ownChildren.addAll(children)
66
+ } else {
67
+ for (i in children.indices) {
68
+ target.addView(children[i], i)
69
+ }
70
+ }
71
+
72
+ name?.let { PortalRegistry.registerPendingPortal(it, this) }
73
+ layoutStateController.updateIfNeeded(hostName, resolvedHost)
74
+ }
75
+
76
+ fun cleanup() {
77
+ hostName?.let { PortalRegistry.unregisterPendingPortal(it, this) }
78
+ detachOwnChildren()
79
+ hostName = null
80
+ layoutStateController.resetIfNeeded()
81
+ layoutStateController.setStateWrapper(null)
82
+ }
83
+ // endregion
84
+
85
+ // region Host lifecycle callbacks
86
+ internal fun onHostChanged() {
87
+ val host = resolvedHost
88
+ if (host != null) {
89
+ // Host appeared (or was replaced). Move children into it, detaching
90
+ // from their current parent first — that may be `this` (initial mount
91
+ // or after host loss) or a stale, detached host view.
92
+ val children: List<View> =
93
+ if (ownChildren.isEmpty()) extractPhysicalChildren() else detachOwnChildren()
94
+
95
+ for (i in children.indices) {
96
+ val idx = host.nextInsertionIndexForChildAt(i)
97
+ host.addView(children[i], idx)
98
+ }
99
+ ownChildren.addAll(children)
100
+ } else {
101
+ // Host went away. Pull children back to ourselves so they remain
102
+ // attached to a live view tree and React-driven mutations keep working.
103
+ if (ownChildren.isEmpty()) {
104
+ layoutStateController.resetIfNeeded()
105
+ return
106
+ }
107
+ val list = detachOwnChildren()
108
+ FLog.w(
109
+ TAG,
110
+ "Portal \"%s\" lost its host; %d child view(s) returned to the source tree",
111
+ hostName,
112
+ list.size,
113
+ )
114
+ // isTeleported is now false, so super.addView and addView are equivalent;
115
+ // use super to be explicit that this is a physical re-attach.
116
+ for (i in list.indices) {
117
+ super.addView(list[i], i)
118
+ }
119
+ layoutStateController.resetIfNeeded()
120
+ }
121
+ }
122
+
123
+ internal fun onHostLayoutChanged() {
124
+ layoutStateController.updateIfNeeded(hostName, resolvedHost)
125
+ }
126
+ // endregion
127
+
128
+ // region Child relocation helpers
129
+ private fun extractPhysicalChildren(): List<View> {
130
+ // Collect children first, then remove via super.removeView(child).
131
+ // Using super.removeViewAt(i) may call our overridden getChildAt(),
132
+ // which can return null during onHostAvailable (isTeleported flips
133
+ // before ownChildren is populated), leading to an NPE in
134
+ // removeViewInternal. super.removeView(View) avoids this by using
135
+ // indexOfChild without re-fetching the view.
136
+ val count = super.getChildCount()
137
+ val children = ArrayList<View>(count)
138
+ for (i in 0 until count) {
139
+ super.getChildAt(i)?.let { children.add(it) }
140
+ }
141
+ for (child in children) {
142
+ detachFromParent(child)
143
+ }
144
+ return children
145
+ }
146
+
147
+ private fun detachFromParent(child: View) {
148
+ val parent = child.parent as? ViewGroup ?: return
149
+ if (parent === this) {
150
+ super.removeView(child)
151
+ } else {
152
+ parent.removeView(child)
153
+ }
154
+ if (child.parent === parent) {
155
+ parent.endViewTransition(child)
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Detaches every view in [ownChildren] from its current parent (which may be
161
+ * `this`, the active host, or a stale/detached host) and clears the list.
162
+ * Returns the detached views in their original order so the caller can
163
+ * re-attach them somewhere else.
164
+ */
165
+ private fun detachOwnChildren(): List<View> {
166
+ val list = ArrayList<View>(ownChildren.size)
167
+ for (child in ownChildren) {
168
+ if (!list.contains(child)) {
169
+ list.add(child)
170
+ }
171
+ }
172
+ ownChildren.clear()
173
+ for (child in list) {
174
+ detachFromParent(child)
175
+ }
176
+ return list
177
+ }
178
+
179
+ private fun extractChildren(): List<View> {
180
+ // Gather current children (logical if teleported, physical otherwise)
181
+ return if (isTeleported) detachOwnChildren() else extractPhysicalChildren()
182
+ }
183
+
184
+ /**
185
+ * Finds the host index of the first next sibling (in [ownChildren]) that is
186
+ * already present in the host. Returns -1 when none is found (caller should append).
187
+ */
188
+ private fun findNextSiblingHostIndex(
189
+ host: ViewGroup,
190
+ ownIndex: Int,
191
+ ): Int {
192
+ for (i in (ownIndex + 1) until ownChildren.size) {
193
+ val sibling = ownChildren[i]
194
+ val siblingIndex = host.indexOfChild(sibling)
195
+ if (siblingIndex >= 0) return siblingIndex
196
+ }
197
+ return -1
198
+ }
199
+ // endregion
200
+
201
+ // region Children management
202
+ override fun getChildCount(): Int =
203
+ if (isTeleported) {
204
+ ownChildren.size
205
+ } else {
206
+ super.getChildCount()
207
+ }
208
+
209
+ override fun getChildAt(index: Int): View? =
210
+ if (isTeleported) {
211
+ ownChildren.getOrNull(index)
212
+ } else {
213
+ super.getChildAt(index)
214
+ }
215
+
216
+ override fun addView(
217
+ child: View,
218
+ index: Int,
219
+ ) {
220
+ if (isTeleported) {
221
+ val host = resolvedHost
222
+ ownChildren.add(index, child)
223
+ if (host != null) {
224
+ val hostIndex = findNextSiblingHostIndex(host, index)
225
+ if (hostIndex >= 0) {
226
+ host.addView(child, hostIndex)
227
+ } else {
228
+ host.addView(child)
229
+ }
230
+ }
231
+ } else {
232
+ super.addView(child, index)
233
+ }
234
+ }
235
+
236
+ override fun addView(
237
+ child: View,
238
+ index: Int,
239
+ params: LayoutParams,
240
+ ) {
241
+ if (isTeleported) {
242
+ val host = resolvedHost
243
+ ownChildren.add(index, child)
244
+ if (host != null) {
245
+ val hostIndex = findNextSiblingHostIndex(host, index)
246
+ if (hostIndex >= 0) {
247
+ host.addView(child, hostIndex)
248
+ } else {
249
+ host.addView(child, params)
250
+ }
251
+ }
252
+ } else {
253
+ super.addView(child, index, params)
254
+ }
255
+ }
256
+
257
+ override fun removeView(view: View?) {
258
+ if (view == null) return
259
+ if (isTeleported) {
260
+ val host = resolvedHost
261
+ host?.removeView(view)
262
+ ownChildren.remove(view)
263
+ } else {
264
+ super.removeView(view)
265
+ }
266
+ }
267
+
268
+ override fun removeViewAt(index: Int) {
269
+ if (isTeleported) {
270
+ val host = resolvedHost
271
+ val view = ownChildren.getOrNull(index)
272
+ if (view != null) {
273
+ host?.removeView(view)
274
+ ownChildren.removeAt(index)
275
+ }
276
+ } else {
277
+ super.removeViewAt(index)
278
+ }
279
+ }
280
+ // endregion
281
+
282
+ // region Lifecycle
283
+ override fun onLayout(
284
+ changed: Boolean,
285
+ left: Int,
286
+ top: Int,
287
+ right: Int,
288
+ bottom: Int,
289
+ ) {
290
+ super.onLayout(changed, left, top, right, bottom)
291
+ layoutStateController.updateIfNeeded(hostName, resolvedHost)
292
+ }
293
+
294
+ // endregion
295
+
296
+ // region Accessibility
297
+ // Override to prevent accessibility from trying to include non-descendant children
298
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
299
+ if (!isTeleported) {
300
+ super.addChildrenForAccessibility(outChildren)
301
+ }
302
+ // When teleported, do nothing—children are handled by the host's accessibility tree
303
+ }
304
+ // endregion
305
+
306
+ private companion object {
307
+ private const val TAG = "GraniteMicroFrontendPortal"
308
+ }
309
+ }
@@ -0,0 +1,51 @@
1
+ package com.teleport.portal
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.ReactStylesDiffMap
5
+ import com.facebook.react.uimanager.StateWrapper
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.ViewManagerDelegate
8
+ import com.facebook.react.uimanager.annotations.ReactProp
9
+ import com.facebook.react.viewmanagers.PortalViewManagerDelegate
10
+ import com.facebook.react.viewmanagers.PortalViewManagerInterface
11
+ import com.facebook.react.views.view.ReactViewGroup
12
+ import com.teleport.managers.TeleportViewManager
13
+
14
+ @ReactModule(name = PortalViewManager.NAME)
15
+ class PortalViewManager :
16
+ TeleportViewManager(),
17
+ PortalViewManagerInterface<ReactViewGroup> {
18
+ private val delegate = PortalViewManagerDelegate(this)
19
+
20
+ override fun getName(): String = NAME
21
+
22
+ override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = delegate
23
+
24
+ override fun createTeleportView(context: ThemedReactContext): ReactViewGroup = PortalView(context)
25
+
26
+ override fun onDropViewInstance(view: ReactViewGroup) {
27
+ (view as? PortalView)?.cleanup()
28
+ super.onDropViewInstance(view)
29
+ }
30
+
31
+ override fun updateState(
32
+ view: ReactViewGroup,
33
+ props: ReactStylesDiffMap?,
34
+ stateWrapper: StateWrapper?,
35
+ ): Any? {
36
+ (view as? PortalView)?.setStateWrapper(stateWrapper)
37
+ return super.updateState(view, props, stateWrapper)
38
+ }
39
+
40
+ @ReactProp(name = "hostName")
41
+ override fun setHostName(
42
+ view: ReactViewGroup?,
43
+ name: String?,
44
+ ) {
45
+ (view as? PortalView)?.setHostName(name)
46
+ }
47
+
48
+ companion object {
49
+ const val NAME = "PortalView"
50
+ }
51
+ }
@@ -0,0 +1,87 @@
1
+ package run.granite.microfrontend
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.os.Bundle
6
+ import android.os.Looper
7
+
8
+ class ActivitySessionBinding private constructor(
9
+ private val sessionId: String,
10
+ private val application: Application,
11
+ private var activity: Activity?,
12
+ private val registration: GraniteMicroFrontendSessionRegistration,
13
+ ) : AutoCloseable {
14
+ private val callbacks = object : Application.ActivityLifecycleCallbacks {
15
+ override fun onActivityStarted(target: Activity) {
16
+ if (target === activity) {
17
+ registration.setVisible(true)
18
+ }
19
+ }
20
+
21
+ override fun onActivityStopped(target: Activity) {
22
+ if (target === activity) {
23
+ registration.setVisible(false)
24
+ }
25
+ }
26
+
27
+ override fun onActivityDestroyed(target: Activity) {
28
+ if (target !== activity) {
29
+ return
30
+ }
31
+ // A configuration change destroys the Activity instance but not the destination. Keep
32
+ // the session registered and drop the instance so the recreated one can rebind to it.
33
+ if (target.isChangingConfigurations) {
34
+ activity = null
35
+ return
36
+ }
37
+ close()
38
+ }
39
+
40
+ override fun onActivityCreated(target: Activity, savedInstanceState: Bundle?) = Unit
41
+ override fun onActivityResumed(target: Activity) = Unit
42
+ override fun onActivityPaused(target: Activity) = Unit
43
+ override fun onActivitySaveInstanceState(target: Activity, outState: Bundle) = Unit
44
+ }
45
+
46
+ init {
47
+ application.registerActivityLifecycleCallbacks(callbacks)
48
+ }
49
+
50
+ override fun close() {
51
+ synchronized(boundBindings) {
52
+ if (boundBindings[sessionId] !== this) {
53
+ return
54
+ }
55
+ boundBindings.remove(sessionId)
56
+ }
57
+ activity = null
58
+ application.unregisterActivityLifecycleCallbacks(callbacks)
59
+ registration.closeApp()
60
+ registration.close()
61
+ }
62
+
63
+ companion object {
64
+ private val boundBindings = mutableMapOf<String, ActivitySessionBinding>()
65
+
66
+ @JvmStatic
67
+ fun bind(
68
+ activity: Activity,
69
+ sessionId: String,
70
+ appName: String,
71
+ scheme: String,
72
+ ): ActivitySessionBinding = synchronized(boundBindings) {
73
+ check(Looper.myLooper() == Looper.getMainLooper()) {
74
+ "ActivitySessionBinding.bind must be called on the main thread"
75
+ }
76
+ boundBindings[sessionId]?.also { binding ->
77
+ binding.activity = activity
78
+ return@synchronized binding
79
+ }
80
+ val registration = GraniteMicroFrontendRuntimeHost.registerSession(sessionId)
81
+ ActivitySessionBinding(sessionId, activity.application, activity, registration).also { binding ->
82
+ boundBindings[sessionId] = binding
83
+ registration.openApp(appName, scheme)
84
+ }
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,46 @@
1
+ package run.granite.microfrontend
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.common.annotations.FrameworkAPI
5
+ import java.io.File
6
+ import java.io.FileNotFoundException
7
+ import java.io.IOException
8
+
9
+ @OptIn(FrameworkAPI::class)
10
+ internal class BundleEvaluator(
11
+ private val reactContext: ReactApplicationContext,
12
+ ) {
13
+ /**
14
+ * Evaluates a bundle on the JSI runtime reached through `javaScriptContextHolder`. That holder
15
+ * is a framework-internal API, so this path is only verified against the React Native version
16
+ * this package is developed against (`catalog:react-native`, currently 0.84.0).
17
+ * `peerDependencies` declares `*`; a host on another version should re-verify it.
18
+ */
19
+ private external fun evaluateFileSync(
20
+ runtimePointer: Long,
21
+ filePath: String,
22
+ sourceUrl: String,
23
+ )
24
+
25
+ @Throws(FileNotFoundException::class, IOException::class)
26
+ fun evaluateFile(filePath: String) {
27
+ val file = File(filePath)
28
+ require(file.isAbsolute) { "Bundle path must be absolute: $filePath" }
29
+ if (!file.exists()) {
30
+ throw FileNotFoundException("Bundle file does not exist: $filePath")
31
+ }
32
+ if (!file.canRead()) {
33
+ throw IOException("Bundle file is not readable: $filePath")
34
+ }
35
+
36
+ val runtimePointer = reactContext.javaScriptContextHolder?.get()
37
+ ?: throw IllegalStateException("JavaScript runtime is unavailable")
38
+ evaluateFileSync(runtimePointer, filePath, file.toURI().toString())
39
+ }
40
+
41
+ private companion object {
42
+ init {
43
+ System.loadLibrary("granite-micro-frontend")
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,62 @@
1
+ package run.granite.microfrontend
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+
6
+ sealed interface GraniteMicroFrontendEvent {
7
+ fun toWritableMap(): WritableMap
8
+
9
+ data class PreloadApp(
10
+ val appName: String,
11
+ ) : GraniteMicroFrontendEvent {
12
+ override fun toWritableMap(): WritableMap = eventMap(
13
+ name = "preloadApp",
14
+ params = Arguments.createMap().apply {
15
+ putString("appName", appName)
16
+ },
17
+ )
18
+ }
19
+
20
+ data class OpenApp(
21
+ val sessionId: String,
22
+ val appName: String,
23
+ val scheme: String,
24
+ ) : GraniteMicroFrontendEvent {
25
+ override fun toWritableMap(): WritableMap = eventMap(
26
+ name = "openApp",
27
+ params = Arguments.createMap().apply {
28
+ putString("sessionId", sessionId)
29
+ putString("appName", appName)
30
+ putString("scheme", scheme)
31
+ },
32
+ )
33
+ }
34
+
35
+ data class CloseApp(
36
+ val sessionId: String,
37
+ ) : GraniteMicroFrontendEvent {
38
+ override fun toWritableMap(): WritableMap = eventMap(
39
+ name = "closeApp",
40
+ params = Arguments.createMap().apply { putString("sessionId", sessionId) },
41
+ )
42
+ }
43
+
44
+ data class SessionVisibilityChanged(
45
+ val sessionId: String,
46
+ val isVisible: Boolean,
47
+ ) : GraniteMicroFrontendEvent {
48
+ override fun toWritableMap(): WritableMap = eventMap(
49
+ name = "sessionVisibilityChanged",
50
+ params = Arguments.createMap().apply {
51
+ putString("sessionId", sessionId)
52
+ putBoolean("isVisible", isVisible)
53
+ },
54
+ )
55
+ }
56
+ }
57
+
58
+ private fun eventMap(name: String, params: WritableMap): WritableMap =
59
+ Arguments.createMap().apply {
60
+ putString("name", name)
61
+ putMap("params", params)
62
+ }