@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.
- package/GraniteMicroFrontendRuntime.podspec +40 -0
- package/LICENSE +202 -0
- package/LICENSE.react-native-teleport +21 -0
- package/NOTICE +17 -0
- package/README.md +508 -0
- package/android/CMakeLists.txt +34 -0
- package/android/build.gradle +74 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/BundleEvaluator.cpp +64 -0
- package/android/src/main/cpp/BundleEvaluator.h +20 -0
- package/android/src/main/cpp/FileReader.cpp +92 -0
- package/android/src/main/cpp/FileReader.h +27 -0
- package/android/src/main/cpp/onLoad.cpp +9 -0
- package/android/src/main/java/com/teleport/extensions/Float.kt +12 -0
- package/android/src/main/java/com/teleport/extensions/View.kt +10 -0
- package/android/src/main/java/com/teleport/global/PortalRegistry.kt +117 -0
- package/android/src/main/java/com/teleport/host/PortalHostView.kt +97 -0
- package/android/src/main/java/com/teleport/host/PortalHostViewManager.kt +41 -0
- package/android/src/main/java/com/teleport/host/PortalReactRootView.kt +116 -0
- package/android/src/main/java/com/teleport/managers/TeleportViewManager.kt +27 -0
- package/android/src/main/java/com/teleport/portal/PortalLayoutStateController.kt +107 -0
- package/android/src/main/java/com/teleport/portal/PortalView.kt +309 -0
- package/android/src/main/java/com/teleport/portal/PortalViewManager.kt +51 -0
- package/android/src/main/java/run/granite/microfrontend/ActivitySessionBinding.kt +87 -0
- package/android/src/main/java/run/granite/microfrontend/BundleEvaluator.kt +46 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendEvent.kt +62 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouter.kt +186 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeHost.kt +42 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeModule.kt +56 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimePackage.kt +35 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendSessionStore.kt +116 -0
- package/android/src/main/jni/CMakeLists.txt +64 -0
- package/android/src/test/cpp/CMakeLists.txt +35 -0
- package/android/src/test/cpp/FileReaderTest.cpp +44 -0
- package/android/src/test/java/com/teleport/host/PortalReactRootViewContractTest.kt +76 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouterTest.kt +216 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendSessionStoreTest.kt +190 -0
- package/android/test-cpp.sh +12 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/ComponentDescriptors.h +24 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.cpp +31 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.h +29 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewComponentDescriptor.h +22 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.h +43 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewState.h +21 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewComponentDescriptor.h +54 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.h +51 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewState.h +43 -0
- package/dist/module/createMicroFrontendRuntime.js +18 -0
- package/dist/module/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/createRoute.js +43 -0
- package/dist/module/createRoute.js.map +1 -0
- package/dist/module/host/PendingHostComponent.js +70 -0
- package/dist/module/host/PendingHostComponent.js.map +1 -0
- package/dist/module/host/index.js +6 -0
- package/dist/module/host/index.js.map +1 -0
- package/dist/module/host/pendingHostComponentStore.js +164 -0
- package/dist/module/host/pendingHostComponentStore.js.map +1 -0
- package/dist/module/host/resolveParams.js +60 -0
- package/dist/module/host/resolveParams.js.map +1 -0
- package/dist/module/host/routeMatcher.js +92 -0
- package/dist/module/host/routeMatcher.js.map +1 -0
- package/dist/module/host/types.js +4 -0
- package/dist/module/host/types.js.map +1 -0
- package/dist/module/index.js +12 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/package.json +1 -0
- package/dist/module/plugin/index.js +4 -0
- package/dist/module/plugin/index.js.map +1 -0
- package/dist/module/plugin/intoShared.js +32 -0
- package/dist/module/plugin/intoShared.js.map +1 -0
- package/dist/module/plugin/microFrontendPlugin.js +58 -0
- package/dist/module/plugin/microFrontendPlugin.js.map +1 -0
- package/dist/module/plugin/prelude.js +23 -0
- package/dist/module/plugin/prelude.js.map +1 -0
- package/dist/module/plugin/resolver.js +25 -0
- package/dist/module/plugin/resolver.js.map +1 -0
- package/dist/module/plugin/types.js +4 -0
- package/dist/module/plugin/types.js.map +1 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js +79 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/runtime/errors.js +63 -0
- package/dist/module/runtime/errors.js.map +1 -0
- package/dist/module/runtime/parseAppRequest.js +16 -0
- package/dist/module/runtime/parseAppRequest.js.map +1 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js +54 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js.map +1 -0
- package/dist/module/runtime/registry.js +87 -0
- package/dist/module/runtime/registry.js.map +1 -0
- package/dist/module/session/MicroFrontendSessionContext.js +32 -0
- package/dist/module/session/MicroFrontendSessionContext.js.map +1 -0
- package/dist/module/session/useMicroFrontendSessions.js +47 -0
- package/dist/module/session/useMicroFrontendSessions.js.map +1 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js +21 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js.map +1 -0
- package/dist/module/specs/PortalHostViewNativeComponent.ts +9 -0
- package/dist/module/specs/PortalViewNativeComponent.ts +9 -0
- package/dist/module/types.js +2 -0
- package/dist/module/types.js.map +1 -0
- package/dist/typescript/createMicroFrontendRuntime.d.ts +6 -0
- package/dist/typescript/createRoute.d.ts +23 -0
- package/dist/typescript/host/PendingHostComponent.d.ts +14 -0
- package/dist/typescript/host/index.d.ts +5 -0
- package/dist/typescript/host/pendingHostComponentStore.d.ts +11 -0
- package/dist/typescript/host/resolveParams.d.ts +10 -0
- package/dist/typescript/host/routeMatcher.d.ts +7 -0
- package/dist/typescript/host/types.d.ts +32 -0
- package/dist/typescript/index.d.ts +16 -0
- package/dist/typescript/plugin/index.d.ts +2 -0
- package/dist/typescript/plugin/intoShared.d.ts +3 -0
- package/dist/typescript/plugin/microFrontendPlugin.d.ts +3 -0
- package/dist/typescript/plugin/prelude.d.ts +6 -0
- package/dist/typescript/plugin/resolver.d.ts +3 -0
- package/dist/typescript/plugin/types.d.ts +5 -0
- package/dist/typescript/runtime/createMicroFrontendRuntime.d.ts +31 -0
- package/dist/typescript/runtime/errors.d.ts +35 -0
- package/dist/typescript/runtime/parseAppRequest.d.ts +6 -0
- package/dist/typescript/runtime/parseNativeRuntimeEvent.d.ts +3 -0
- package/dist/typescript/runtime/registry.d.ts +35 -0
- package/dist/typescript/session/MicroFrontendSessionContext.d.ts +13 -0
- package/dist/typescript/session/useMicroFrontendSessions.d.ts +8 -0
- package/dist/typescript/specs/NativeGraniteMicroFrontendRuntime.d.ts +21 -0
- package/dist/typescript/specs/PortalHostViewNativeComponent.d.ts +6 -0
- package/dist/typescript/specs/PortalViewNativeComponent.d.ts +6 -0
- package/dist/typescript/types.d.ts +49 -0
- package/ios/GraniteMicroFrontendRuntimeHost+Internal.h +18 -0
- package/ios/GraniteMicroFrontendRuntimeHost.h +34 -0
- package/ios/GraniteMicroFrontendRuntimeHost.mm +485 -0
- package/ios/GraniteMicroFrontendRuntimeModule.h +6 -0
- package/ios/GraniteMicroFrontendRuntimeModule.mm +138 -0
- package/ios/PortalHostContainerView.h +56 -0
- package/ios/PortalHostContainerView.mm +133 -0
- package/ios/PortalHostView.h +39 -0
- package/ios/PortalHostView.mm +153 -0
- package/ios/PortalRegistry.h +38 -0
- package/ios/PortalRegistry.mm +228 -0
- package/ios/PortalView.h +32 -0
- package/ios/PortalView.mm +426 -0
- package/package.json +113 -0
- package/react-native.config.js +13 -0
- package/src/createMicroFrontendRuntime.ts +22 -0
- package/src/createRoute.ts +84 -0
- package/src/host/PendingHostComponent.tsx +92 -0
- package/src/host/index.ts +26 -0
- package/src/host/pendingHostComponentStore.ts +241 -0
- package/src/host/resolveParams.ts +70 -0
- package/src/host/routeMatcher.ts +117 -0
- package/src/host/types.ts +45 -0
- package/src/index.ts +57 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/intoShared.ts +50 -0
- package/src/plugin/microFrontendPlugin.ts +57 -0
- package/src/plugin/prelude.ts +73 -0
- package/src/plugin/resolver.ts +73 -0
- package/src/plugin/types.ts +6 -0
- package/src/runtime/createMicroFrontendRuntime.ts +115 -0
- package/src/runtime/errors.ts +86 -0
- package/src/runtime/parseAppRequest.ts +22 -0
- package/src/runtime/parseNativeRuntimeEvent.ts +54 -0
- package/src/runtime/registry.ts +131 -0
- package/src/session/MicroFrontendSessionContext.tsx +45 -0
- package/src/session/useMicroFrontendSessions.ts +73 -0
- package/src/specs/NativeGraniteMicroFrontendRuntime.ts +44 -0
- package/src/specs/PortalHostViewNativeComponent.ts +9 -0
- package/src/specs/PortalViewNativeComponent.ts +9 -0
- 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
|
+
}
|