@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
package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouter.kt
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package run.granite.microfrontend
|
|
2
|
+
|
|
3
|
+
import com.facebook.common.logging.FLog
|
|
4
|
+
|
|
5
|
+
internal interface GraniteMicroFrontendRuntimeEventTarget {
|
|
6
|
+
/**
|
|
7
|
+
* Hands [event] to the runtime. Returning `true` means the target accepted the event, not that
|
|
8
|
+
* delivery happened: the target must invoke [onDelivered] once it has handed the event to the
|
|
9
|
+
* runtime's event emitter. An accepted event that is never acknowledged stays queued and is
|
|
10
|
+
* redelivered to the next runtime.
|
|
11
|
+
*
|
|
12
|
+
* [onDelivered] does not prove that JavaScript observed the event. The emitter dispatches to
|
|
13
|
+
* whatever listeners are registered at that moment and schedules each one on a later tick, so
|
|
14
|
+
* an event acknowledged while nothing is subscribed is dropped from the queue without anyone
|
|
15
|
+
* seeing it. The queue covers the gap between runtimes, not the gap between a live runtime and
|
|
16
|
+
* a subscribed listener.
|
|
17
|
+
*/
|
|
18
|
+
fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
internal class GraniteMicroFrontendRuntimeEventRouter {
|
|
22
|
+
private val lock = Any()
|
|
23
|
+
private val pendingEvents = ArrayDeque<GraniteMicroFrontendEvent>()
|
|
24
|
+
private val attachedTargets = mutableSetOf<GraniteMicroFrontendRuntimeEventTarget>()
|
|
25
|
+
private var activeTarget: GraniteMicroFrontendRuntimeEventTarget? = null
|
|
26
|
+
private var isDelivering = false
|
|
27
|
+
private var inFlight: Delivery? = null
|
|
28
|
+
private var didLoseActiveTarget = false
|
|
29
|
+
|
|
30
|
+
fun attach(target: GraniteMicroFrontendRuntimeEventTarget) {
|
|
31
|
+
synchronized(lock) {
|
|
32
|
+
attachedTargets.add(target)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fun startEventDelivery(target: GraniteMicroFrontendRuntimeEventTarget) {
|
|
37
|
+
val shouldDrain = synchronized(lock) {
|
|
38
|
+
check(target in attachedTargets) { "Runtime must be attached before starting event delivery" }
|
|
39
|
+
if (activeTarget != null && activeTarget !== target) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (didLoseActiveTarget) {
|
|
43
|
+
didLoseActiveTarget = false
|
|
44
|
+
FLog.w(
|
|
45
|
+
TAG,
|
|
46
|
+
"Runtime replaced while %d event(s) were pending; session state is redelivered from the queue",
|
|
47
|
+
pendingEvents.size,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
activeTarget = target
|
|
51
|
+
startDrainIfNeeded()
|
|
52
|
+
}
|
|
53
|
+
if (shouldDrain) {
|
|
54
|
+
drain()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun emit(event: GraniteMicroFrontendEvent) {
|
|
59
|
+
val shouldDrain = synchronized(lock) {
|
|
60
|
+
if (pendingEvents.size >= MAX_PENDING_EVENTS) {
|
|
61
|
+
FLog.w(
|
|
62
|
+
TAG,
|
|
63
|
+
"Dropping %s: %d event(s) already pending with no runtime consuming them",
|
|
64
|
+
event::class.java.simpleName,
|
|
65
|
+
pendingEvents.size,
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
pendingEvents.addLast(event)
|
|
70
|
+
activeTarget ?: return
|
|
71
|
+
if (!startDrainIfNeeded()) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
true
|
|
75
|
+
}
|
|
76
|
+
if (shouldDrain) {
|
|
77
|
+
drain()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun detach(target: GraniteMicroFrontendRuntimeEventTarget) {
|
|
82
|
+
synchronized(lock) {
|
|
83
|
+
attachedTargets.remove(target)
|
|
84
|
+
if (activeTarget === target) {
|
|
85
|
+
activeTarget = null
|
|
86
|
+
didLoseActiveTarget = true
|
|
87
|
+
}
|
|
88
|
+
val delivery = inFlight
|
|
89
|
+
if (delivery != null && delivery.target === target && !delivery.isLoopOwned) {
|
|
90
|
+
inFlight = null
|
|
91
|
+
isDelivering = false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun startDrainIfNeeded(): Boolean {
|
|
97
|
+
if (isDelivering || pendingEvents.isEmpty()) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
isDelivering = true
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun drain() {
|
|
105
|
+
while (true) {
|
|
106
|
+
val delivery = synchronized(lock) {
|
|
107
|
+
val target = activeTarget
|
|
108
|
+
if (target == null || pendingEvents.isEmpty()) {
|
|
109
|
+
isDelivering = false
|
|
110
|
+
inFlight = null
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
Delivery(target, pendingEvents.first()).also { inFlight = it }
|
|
114
|
+
}
|
|
115
|
+
val accepted = try {
|
|
116
|
+
delivery.target.emit(delivery.event) { acknowledge(delivery) }
|
|
117
|
+
} catch (error: Throwable) {
|
|
118
|
+
val shouldResumeWithReplacement = synchronized(lock) {
|
|
119
|
+
delivery.isLoopOwned = false
|
|
120
|
+
inFlight = null
|
|
121
|
+
isDelivering = false
|
|
122
|
+
activeTarget != null && activeTarget !== delivery.target && startDrainIfNeeded()
|
|
123
|
+
}
|
|
124
|
+
if (shouldResumeWithReplacement) {
|
|
125
|
+
try {
|
|
126
|
+
drain()
|
|
127
|
+
} catch (replacementError: Throwable) {
|
|
128
|
+
error.addSuppressed(replacementError)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
synchronized(lock) {
|
|
134
|
+
delivery.isLoopOwned = false
|
|
135
|
+
if (!accepted) {
|
|
136
|
+
inFlight = null
|
|
137
|
+
isDelivering = false
|
|
138
|
+
val shouldResumeWithReplacement =
|
|
139
|
+
activeTarget != null && activeTarget !== delivery.target && startDrainIfNeeded()
|
|
140
|
+
if (!shouldResumeWithReplacement) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
} else if (!delivery.isAcknowledged) {
|
|
144
|
+
// Delivery is in flight. acknowledge() resumes the drain once it completes.
|
|
145
|
+
return
|
|
146
|
+
} else {
|
|
147
|
+
inFlight = null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private fun acknowledge(delivery: Delivery) {
|
|
154
|
+
val shouldDrain = synchronized(lock) {
|
|
155
|
+
if (delivery.isAcknowledged) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
delivery.isAcknowledged = true
|
|
159
|
+
if (pendingEvents.firstOrNull() === delivery.event) {
|
|
160
|
+
pendingEvents.removeFirst()
|
|
161
|
+
}
|
|
162
|
+
if (delivery.isLoopOwned) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
inFlight = null
|
|
166
|
+
isDelivering = false
|
|
167
|
+
activeTarget != null && startDrainIfNeeded()
|
|
168
|
+
}
|
|
169
|
+
if (shouldDrain) {
|
|
170
|
+
drain()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private class Delivery(
|
|
175
|
+
val target: GraniteMicroFrontendRuntimeEventTarget,
|
|
176
|
+
val event: GraniteMicroFrontendEvent,
|
|
177
|
+
) {
|
|
178
|
+
var isLoopOwned = true
|
|
179
|
+
var isAcknowledged = false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
internal companion object {
|
|
183
|
+
private const val TAG = "GraniteMicroFrontendRuntime"
|
|
184
|
+
internal const val MAX_PENDING_EVENTS = 64
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package run.granite.microfrontend
|
|
2
|
+
|
|
3
|
+
object GraniteMicroFrontendRuntimeHost {
|
|
4
|
+
private val eventRouter = GraniteMicroFrontendRuntimeEventRouter()
|
|
5
|
+
private val sessionStore = GraniteMicroFrontendSessionStore(eventRouter::emit)
|
|
6
|
+
|
|
7
|
+
@JvmStatic
|
|
8
|
+
fun registerSession(sessionId: String): GraniteMicroFrontendSessionRegistration =
|
|
9
|
+
sessionStore.registerSession(sessionId)
|
|
10
|
+
|
|
11
|
+
@JvmSynthetic
|
|
12
|
+
internal fun emit(event: GraniteMicroFrontendEvent) = eventRouter.emit(event)
|
|
13
|
+
|
|
14
|
+
@JvmStatic
|
|
15
|
+
fun emitPreloadApp(appName: String) = emit(GraniteMicroFrontendEvent.PreloadApp(appName))
|
|
16
|
+
|
|
17
|
+
@JvmSynthetic
|
|
18
|
+
internal fun emitOpenApp(sessionId: String, appName: String, scheme: String) =
|
|
19
|
+
emit(GraniteMicroFrontendEvent.OpenApp(sessionId, appName, scheme))
|
|
20
|
+
|
|
21
|
+
@JvmSynthetic
|
|
22
|
+
internal fun emitCloseApp(sessionId: String) = emit(GraniteMicroFrontendEvent.CloseApp(sessionId))
|
|
23
|
+
|
|
24
|
+
@JvmSynthetic
|
|
25
|
+
internal fun emitSessionVisibilityChanged(sessionId: String, isVisible: Boolean) =
|
|
26
|
+
emit(GraniteMicroFrontendEvent.SessionVisibilityChanged(sessionId, isVisible))
|
|
27
|
+
|
|
28
|
+
@JvmSynthetic
|
|
29
|
+
internal fun attach(module: GraniteMicroFrontendRuntimeModule) = eventRouter.attach(module)
|
|
30
|
+
|
|
31
|
+
@JvmSynthetic
|
|
32
|
+
internal fun startEventDelivery(module: GraniteMicroFrontendRuntimeModule) =
|
|
33
|
+
eventRouter.startEventDelivery(module)
|
|
34
|
+
|
|
35
|
+
@JvmSynthetic
|
|
36
|
+
internal fun detach(module: GraniteMicroFrontendRuntimeModule) = eventRouter.detach(module)
|
|
37
|
+
|
|
38
|
+
@JvmSynthetic
|
|
39
|
+
internal fun unregisterSession(sessionId: String, token: String) {
|
|
40
|
+
sessionStore.unregisterSession(sessionId, token)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package run.granite.microfrontend
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap
|
|
6
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
7
|
+
|
|
8
|
+
@ReactModule(name = NativeGraniteMicroFrontendRuntimeSpec.NAME)
|
|
9
|
+
class GraniteMicroFrontendRuntimeModule(
|
|
10
|
+
reactContext: ReactApplicationContext,
|
|
11
|
+
) : NativeGraniteMicroFrontendRuntimeSpec(reactContext), GraniteMicroFrontendRuntimeEventTarget {
|
|
12
|
+
override fun initialize() {
|
|
13
|
+
super.initialize()
|
|
14
|
+
GraniteMicroFrontendRuntimeHost.attach(this)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override fun invalidate() {
|
|
18
|
+
GraniteMicroFrontendRuntimeHost.detach(this)
|
|
19
|
+
super.invalidate()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override fun evaluateScript(request: ReadableMap, promise: Promise) {
|
|
23
|
+
val filePath = request.getString("filePath")
|
|
24
|
+
if (filePath == null) {
|
|
25
|
+
promise.reject(ERROR_INVALID_REQUEST, "evaluateScript requires request.filePath")
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
val enqueued = reactApplicationContext.runOnJSQueueThread {
|
|
30
|
+
try {
|
|
31
|
+
BundleEvaluator(reactApplicationContext).evaluateFile(filePath)
|
|
32
|
+
promise.resolve(null)
|
|
33
|
+
} catch (error: Exception) {
|
|
34
|
+
promise.reject(ERROR_EVALUATE_SCRIPT, error.message, error)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!enqueued) {
|
|
38
|
+
promise.reject(ERROR_EVALUATE_SCRIPT, "Failed to enqueue script evaluation on the JS queue")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun startEventDelivery() {
|
|
43
|
+
GraniteMicroFrontendRuntimeHost.startEventDelivery(this)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean =
|
|
47
|
+
reactApplicationContext.runOnJSQueueThread {
|
|
48
|
+
emitOnEvent(event.toWritableMap())
|
|
49
|
+
onDelivered()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private companion object {
|
|
53
|
+
const val ERROR_INVALID_REQUEST = "INVALID_REQUEST"
|
|
54
|
+
const val ERROR_EVALUATE_SCRIPT = "EVALUATE_SCRIPT_FAILED"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package run.granite.microfrontend
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager
|
|
9
|
+
import com.teleport.host.PortalHostViewManager
|
|
10
|
+
import com.teleport.portal.PortalViewManager
|
|
11
|
+
|
|
12
|
+
class GraniteMicroFrontendRuntimePackage : BaseReactPackage() {
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
|
14
|
+
listOf(PortalHostViewManager(), PortalViewManager())
|
|
15
|
+
|
|
16
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
17
|
+
if (name == NativeGraniteMicroFrontendRuntimeSpec.NAME) {
|
|
18
|
+
GraniteMicroFrontendRuntimeModule(reactContext)
|
|
19
|
+
} else {
|
|
20
|
+
null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
24
|
+
mapOf(
|
|
25
|
+
NativeGraniteMicroFrontendRuntimeSpec.NAME to ReactModuleInfo(
|
|
26
|
+
name = NativeGraniteMicroFrontendRuntimeSpec.NAME,
|
|
27
|
+
className = GraniteMicroFrontendRuntimeModule::class.java.name,
|
|
28
|
+
canOverrideExistingModule = false,
|
|
29
|
+
needsEagerInit = false,
|
|
30
|
+
isCxxModule = false,
|
|
31
|
+
isTurboModule = true,
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
package run.granite.microfrontend
|
|
2
|
+
|
|
3
|
+
import com.facebook.common.logging.FLog
|
|
4
|
+
|
|
5
|
+
internal class GraniteMicroFrontendSessionStore(
|
|
6
|
+
private val emit: (GraniteMicroFrontendEvent) -> Unit,
|
|
7
|
+
) {
|
|
8
|
+
private val lock = Any()
|
|
9
|
+
private val sessions = mutableMapOf<String, SessionEntry>()
|
|
10
|
+
|
|
11
|
+
fun registerSession(sessionId: String): GraniteMicroFrontendSessionRegistration {
|
|
12
|
+
require(sessionId.isNotBlank()) { "sessionId must not be blank" }
|
|
13
|
+
val token = java.util.UUID.randomUUID().toString()
|
|
14
|
+
val liveSessions = synchronized(lock) {
|
|
15
|
+
check(sessions[sessionId] == null) { "Session '$sessionId' is already registered" }
|
|
16
|
+
sessions[sessionId] = SessionEntry(token)
|
|
17
|
+
sessions.size
|
|
18
|
+
}
|
|
19
|
+
FLog.d(TAG, "Registered session '%s' (%d live)", sessionId, liveSessions)
|
|
20
|
+
return GraniteMicroFrontendSessionRegistration(this, sessionId, token)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun openApp(sessionId: String, token: String, appName: String, scheme: String): Boolean {
|
|
24
|
+
require(appName.isNotBlank()) { "appName must not be blank" }
|
|
25
|
+
require(scheme.isNotBlank()) { "scheme must not be blank" }
|
|
26
|
+
return synchronized(lock) {
|
|
27
|
+
val session = sessions[sessionId] ?: return false
|
|
28
|
+
if (session.token != token || session.opened || session.closed) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
session.opened = true
|
|
32
|
+
emit(GraniteMicroFrontendEvent.OpenApp(sessionId, appName, scheme))
|
|
33
|
+
true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun setVisible(sessionId: String, token: String, isVisible: Boolean): Boolean {
|
|
38
|
+
return synchronized(lock) {
|
|
39
|
+
val session = sessions[sessionId] ?: return false
|
|
40
|
+
if (session.token != token || !session.opened || session.closed || session.isVisible == isVisible) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
session.isVisible = isVisible
|
|
44
|
+
emit(GraniteMicroFrontendEvent.SessionVisibilityChanged(sessionId, isVisible))
|
|
45
|
+
true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun closeApp(sessionId: String, token: String): Boolean {
|
|
50
|
+
return synchronized(lock) {
|
|
51
|
+
val session = sessions[sessionId] ?: return false
|
|
52
|
+
if (session.token != token || !session.opened || session.closed) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
session.closed = true
|
|
56
|
+
emit(GraniteMicroFrontendEvent.CloseApp(sessionId))
|
|
57
|
+
true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fun unregisterSession(sessionId: String, token: String) {
|
|
62
|
+
val removed = synchronized(lock) {
|
|
63
|
+
val entry = sessions[sessionId]
|
|
64
|
+
when {
|
|
65
|
+
entry == null -> Outcome.UNKNOWN
|
|
66
|
+
entry.token != token -> Outcome.STALE_TOKEN
|
|
67
|
+
else -> {
|
|
68
|
+
sessions.remove(sessionId)
|
|
69
|
+
Outcome.REMOVED
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Both misses are silent no-ops today. They are worth a warning because each points at a
|
|
74
|
+
// different lifecycle bug and neither leaves any other trace.
|
|
75
|
+
when (removed) {
|
|
76
|
+
Outcome.REMOVED -> FLog.d(TAG, "Unregistered session '%s'", sessionId)
|
|
77
|
+
// Nothing to remove: this registration was already released, or was never made here.
|
|
78
|
+
Outcome.UNKNOWN ->
|
|
79
|
+
FLog.w(TAG, "Ignoring unregister for unknown session '%s'", sessionId)
|
|
80
|
+
// An entry under this id exists but belongs to a different registration, so whoever
|
|
81
|
+
// owns it never released it. The id stays occupied and registerSession() will keep
|
|
82
|
+
// throwing for it.
|
|
83
|
+
Outcome.STALE_TOKEN ->
|
|
84
|
+
FLog.w(TAG, "Ignoring unregister for session '%s': token belongs to another registration", sessionId)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private enum class Outcome { REMOVED, UNKNOWN, STALE_TOKEN }
|
|
89
|
+
|
|
90
|
+
private data class SessionEntry(
|
|
91
|
+
val token: String,
|
|
92
|
+
var opened: Boolean = false,
|
|
93
|
+
var isVisible: Boolean = false,
|
|
94
|
+
var closed: Boolean = false,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
private companion object {
|
|
98
|
+
private const val TAG = "GraniteMicroFrontendRuntime"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class GraniteMicroFrontendSessionRegistration internal constructor(
|
|
103
|
+
private val store: GraniteMicroFrontendSessionStore,
|
|
104
|
+
private val sessionId: String,
|
|
105
|
+
private val token: String,
|
|
106
|
+
) : AutoCloseable {
|
|
107
|
+
fun openApp(appName: String, scheme: String): Boolean = store.openApp(sessionId, token, appName, scheme)
|
|
108
|
+
|
|
109
|
+
fun setVisible(isVisible: Boolean): Boolean = store.setVisible(sessionId, token, isVisible)
|
|
110
|
+
|
|
111
|
+
fun closeApp(): Boolean = store.closeApp(sessionId, token)
|
|
112
|
+
|
|
113
|
+
override fun close() {
|
|
114
|
+
store.unregisterSession(sessionId, token)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.13)
|
|
2
|
+
set(CMAKE_VERBOSE_MAKEFILE ON)
|
|
3
|
+
|
|
4
|
+
# Autolinking owns this file. React Native's application CMake add_subdirectory()s it, so the
|
|
5
|
+
# consuming app builds the codegen target exactly once. It must stay separate from the module's
|
|
6
|
+
# own externalNativeBuild entry point (../../../CMakeLists.txt): pointing both at the same file
|
|
7
|
+
# builds every target in it twice and lets mergeNativeLibs pick between two copies of each .so.
|
|
8
|
+
#
|
|
9
|
+
# This file is included from an app project, so it must not call project() and does not need
|
|
10
|
+
# find_package(): ReactNative-application.cmake resolves ReactAndroid and fbjni before including
|
|
11
|
+
# the autolinking file.
|
|
12
|
+
|
|
13
|
+
set(CODEGEN_LIBRARY_NAME GraniteMicroFrontendRuntimeSpec)
|
|
14
|
+
set(CODEGEN_TARGET_NAME react_codegen_${CODEGEN_LIBRARY_NAME})
|
|
15
|
+
|
|
16
|
+
set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
|
|
17
|
+
set(COMMON_DIR ${LIB_ANDROID_DIR}/../common/cpp)
|
|
18
|
+
set(COMMON_COMPONENTS_DIR ${COMMON_DIR}/react/renderer/components/${CODEGEN_LIBRARY_NAME})
|
|
19
|
+
set(GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni)
|
|
20
|
+
set(GENERATED_COMPONENTS_DIR ${GENERATED_JNI_DIR}/react/renderer/components/${CODEGEN_LIBRARY_NAME})
|
|
21
|
+
|
|
22
|
+
file(GLOB CODEGEN_CUSTOM_SOURCES CONFIGURE_DEPENDS ${COMMON_COMPONENTS_DIR}/*.cpp)
|
|
23
|
+
file(GLOB CODEGEN_GENERATED_SOURCES CONFIGURE_DEPENDS
|
|
24
|
+
${GENERATED_JNI_DIR}/*.cpp
|
|
25
|
+
${GENERATED_COMPONENTS_DIR}/*.cpp
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
add_library(
|
|
29
|
+
${CODEGEN_TARGET_NAME}
|
|
30
|
+
SHARED
|
|
31
|
+
${CODEGEN_CUSTOM_SOURCES}
|
|
32
|
+
${CODEGEN_GENERATED_SOURCES}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
target_include_directories(
|
|
36
|
+
${CODEGEN_TARGET_NAME}
|
|
37
|
+
PUBLIC
|
|
38
|
+
${COMMON_DIR}
|
|
39
|
+
${GENERATED_JNI_DIR}
|
|
40
|
+
${GENERATED_COMPONENTS_DIR}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
target_link_libraries(
|
|
44
|
+
${CODEGEN_TARGET_NAME}
|
|
45
|
+
ReactAndroid::reactnative
|
|
46
|
+
ReactAndroid::jsi
|
|
47
|
+
fbjni::fbjni
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if(COMMAND target_compile_reactnative_options)
|
|
51
|
+
target_compile_reactnative_options(${CODEGEN_TARGET_NAME} PRIVATE)
|
|
52
|
+
else()
|
|
53
|
+
target_compile_options(
|
|
54
|
+
${CODEGEN_TARGET_NAME}
|
|
55
|
+
PRIVATE
|
|
56
|
+
-DLOG_TAG=\"ReactNative\"
|
|
57
|
+
-DFOLLY_NO_CONFIG=1
|
|
58
|
+
-fexceptions
|
|
59
|
+
-frtti
|
|
60
|
+
-std=c++20
|
|
61
|
+
-Wall
|
|
62
|
+
)
|
|
63
|
+
target_compile_definitions(${CODEGEN_TARGET_NAME} PRIVATE RN_SERIALIZABLE_STATE)
|
|
64
|
+
endif()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.14)
|
|
2
|
+
project(granite-micro-frontend-tests LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
7
|
+
|
|
8
|
+
include(FetchContent)
|
|
9
|
+
FetchContent_Declare(
|
|
10
|
+
googletest
|
|
11
|
+
GIT_REPOSITORY https://github.com/google/googletest.git
|
|
12
|
+
GIT_TAG v1.14.0
|
|
13
|
+
)
|
|
14
|
+
FetchContent_MakeAvailable(googletest)
|
|
15
|
+
|
|
16
|
+
enable_testing()
|
|
17
|
+
|
|
18
|
+
set(MAIN_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../main/cpp")
|
|
19
|
+
|
|
20
|
+
add_executable(
|
|
21
|
+
FileReaderTest
|
|
22
|
+
FileReaderTest.cpp
|
|
23
|
+
${MAIN_CPP_DIR}/FileReader.cpp
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
target_include_directories(
|
|
27
|
+
FileReaderTest
|
|
28
|
+
PRIVATE
|
|
29
|
+
"${MAIN_CPP_DIR}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
target_link_libraries(FileReaderTest GTest::gtest_main)
|
|
33
|
+
|
|
34
|
+
include(GoogleTest)
|
|
35
|
+
gtest_discover_tests(FileReaderTest)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#include "FileReader.h"
|
|
2
|
+
|
|
3
|
+
#include <filesystem>
|
|
4
|
+
#include <fstream>
|
|
5
|
+
|
|
6
|
+
#include <gtest/gtest.h>
|
|
7
|
+
|
|
8
|
+
namespace fs = std::filesystem;
|
|
9
|
+
|
|
10
|
+
namespace granite::microfrontend::io {
|
|
11
|
+
|
|
12
|
+
class FileReaderTest : public ::testing::Test {
|
|
13
|
+
protected:
|
|
14
|
+
void SetUp() override {
|
|
15
|
+
tempDirectory =
|
|
16
|
+
fs::temp_directory_path() / "granite_micro_frontend_file_reader_test";
|
|
17
|
+
fs::create_directories(tempDirectory);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
void TearDown() override { fs::remove_all(tempDirectory); }
|
|
21
|
+
|
|
22
|
+
fs::path createBundle(const std::string &content) const {
|
|
23
|
+
const auto path = tempDirectory / "remote.hbc";
|
|
24
|
+
std::ofstream output(path, std::ios::binary);
|
|
25
|
+
output.write(content.data(), static_cast<std::streamsize>(content.size()));
|
|
26
|
+
output.close();
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fs::path tempDirectory;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
TEST_F(FileReaderTest, KeepsBundleBytesAfterSourceFileIsDeleted) {
|
|
34
|
+
const std::string bytecode{"\xc6\x1f\xbc\x03\x00\x01\x00\x02", 8};
|
|
35
|
+
const auto path = createBundle(bytecode);
|
|
36
|
+
|
|
37
|
+
const std::string loadedBytecode = readFileToMemory(path.string());
|
|
38
|
+
ASSERT_TRUE(fs::remove(path));
|
|
39
|
+
ASSERT_FALSE(fs::exists(path));
|
|
40
|
+
|
|
41
|
+
EXPECT_EQ(loadedBytecode, bytecode);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package com.teleport.host
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactHost
|
|
4
|
+
import com.facebook.react.ReactRootView
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.common.UIManagerType
|
|
7
|
+
import org.junit.Assert.assertEquals
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import org.mockito.Mockito.mock
|
|
10
|
+
import org.mockito.Mockito.verifyNoInteractions
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pins what host apps rely on when they attach their own Fabric root: it renders through Fabric, it
|
|
14
|
+
* carries the surface id the host allocated, and it reports the JS module name the host selected.
|
|
15
|
+
* That it stays a [ReactRootView] needs no assertion of its own — `rootViewTag` is the one property
|
|
16
|
+
* read below that `PortalReactRootView` does not declare, so that read does not compile otherwise.
|
|
17
|
+
*
|
|
18
|
+
* The example app used to assert these by matching substrings in `PortalReactRootView.kt`, which
|
|
19
|
+
* proved nothing about whether the code compiled or behaved. Here the symbols resolve at compile
|
|
20
|
+
* time and the values come off a constructed instance.
|
|
21
|
+
*
|
|
22
|
+
* Three preconditions live outside this file, and all three can break without anyone touching it.
|
|
23
|
+
*
|
|
24
|
+
* 1. `testOptions.unitTests.returnDefaultValues = true` in `android/build.gradle`. The superclass
|
|
25
|
+
* constructor calls `View.MeasureSpec.makeMeasureSpec` and `setClipChildren`; without the flag the
|
|
26
|
+
* first one throws `Method makeMeasureSpec in android.view.View$MeasureSpec not mocked.`
|
|
27
|
+
* `GraniteMicroFrontendRuntimeEventRouterTest` needs the flag too, for `android.util.Log`.
|
|
28
|
+
* 2. The React Native version, which this module does not pin: `build.gradle` declares
|
|
29
|
+
* `com.facebook.react:react-android` without one, so today's 0.81.0 comes from the consuming app
|
|
30
|
+
* (`examples/portal/package.json`). On 0.81 `ReactRootView.init()` is pure JVM; from 0.82 it also
|
|
31
|
+
* reads `enableFontScaleChangesUpdatingLayout`, whose default accessor reaches
|
|
32
|
+
* `ReactNativeFeatureFlagsCxxInterop`, whose `<clinit>` loads `react_featureflagsjni` through
|
|
33
|
+
* SoLoader. Stubbing that accessor and leaving the flag at its default only moves the failure —
|
|
34
|
+
* `init()` then dereferences `getContext()`, which the mockable `android.jar` returns null for.
|
|
35
|
+
* This lands in consumer builds as well: the library is autolinked as a Gradle subproject, so a
|
|
36
|
+
* host app running `./gradlew test` runs these tests against its own React Native.
|
|
37
|
+
* 3. Mockito's inline mock maker, the default in mockito-core 5.x. `ThemedReactContext` is `final`,
|
|
38
|
+
* so the subclass mock maker cannot stand in for it.
|
|
39
|
+
*/
|
|
40
|
+
class PortalReactRootViewContractTest {
|
|
41
|
+
private val context = mock(ThemedReactContext::class.java)
|
|
42
|
+
private val reactHost = mock(ReactHost::class.java)
|
|
43
|
+
|
|
44
|
+
private fun createRootView(moduleName: String) =
|
|
45
|
+
PortalReactRootView(context, reactHost, SURFACE_ID, moduleName)
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun `renders through Fabric`() {
|
|
49
|
+
assertEquals(UIManagerType.FABRIC, createRootView("example-controller").uiManagerType)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun `carries the surface id the host allocated`() {
|
|
54
|
+
assertEquals(SURFACE_ID, createRootView("example-controller").rootViewTag)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun `reports each host's own JS module name rather than one baked-in name`() {
|
|
59
|
+
assertEquals("example-controller", createRootView("example-controller").jsModuleName)
|
|
60
|
+
assertEquals("another-controller", createRootView("another-controller").jsModuleName)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun `builds without calling into the context or the host`() {
|
|
65
|
+
createRootView("example-controller")
|
|
66
|
+
|
|
67
|
+
verifyNoInteractions(context, reactHost)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private companion object {
|
|
71
|
+
// Deliberately not 1, 11, 21, ... — ReactRootViewTagGenerator hands those out from the
|
|
72
|
+
// superclass constructor, so the surface-id assertion would still pass if
|
|
73
|
+
// PortalReactRootView stopped setting the tag itself.
|
|
74
|
+
const val SURFACE_ID = 42
|
|
75
|
+
}
|
|
76
|
+
}
|