@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,216 @@
1
+ package run.granite.microfrontend
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertThrows
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+ import java.util.Collections
9
+ import java.util.concurrent.CountDownLatch
10
+ import java.util.concurrent.TimeUnit
11
+ import kotlin.concurrent.thread
12
+
13
+ class GraniteMicroFrontendRuntimeEventRouterTest {
14
+ @Test
15
+ fun `another runtime starting event delivery does not replace the active runtime`() {
16
+ val router = GraniteMicroFrontendRuntimeEventRouter()
17
+ val activeRuntime = RecordingEventTarget()
18
+ val unrelatedRuntime = RecordingEventTarget()
19
+ val event = GraniteMicroFrontendEvent.PreloadApp("shopping")
20
+ router.attach(activeRuntime)
21
+ router.startEventDelivery(activeRuntime)
22
+
23
+ router.attach(unrelatedRuntime)
24
+ router.startEventDelivery(unrelatedRuntime)
25
+ router.emit(event)
26
+
27
+ assertEquals(listOf(event), activeRuntime.events)
28
+ assertEquals(emptyList<GraniteMicroFrontendEvent>(), unrelatedRuntime.events)
29
+ }
30
+
31
+ @Test
32
+ fun `events queued before delivery starts are sent to the runtime that starts delivery`() {
33
+ val router = GraniteMicroFrontendRuntimeEventRouter()
34
+ val runtime = RecordingEventTarget()
35
+ val event = GraniteMicroFrontendEvent.PreloadApp("shopping")
36
+ router.attach(runtime)
37
+ router.emit(event)
38
+
39
+ router.startEventDelivery(runtime)
40
+
41
+ assertEquals(listOf(event), runtime.events)
42
+ }
43
+
44
+ @Test
45
+ fun `new events cannot overtake pending events while delivery starts`() {
46
+ val events = Collections.synchronizedList(mutableListOf<GraniteMicroFrontendEvent>())
47
+ val pendingDeliveryStarted = CountDownLatch(1)
48
+ val continuePendingDelivery = CountDownLatch(1)
49
+ val router = GraniteMicroFrontendRuntimeEventRouter()
50
+ val target = object : GraniteMicroFrontendRuntimeEventTarget {
51
+ override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean {
52
+ if (event is GraniteMicroFrontendEvent.OpenApp) {
53
+ pendingDeliveryStarted.countDown()
54
+ assertTrue(continuePendingDelivery.await(5, TimeUnit.SECONDS))
55
+ }
56
+ events.add(event)
57
+ onDelivered()
58
+ return true
59
+ }
60
+ }
61
+ val open = GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping")
62
+ val close = GraniteMicroFrontendEvent.CloseApp("session-1")
63
+
64
+ router.emit(open)
65
+ router.attach(target)
66
+ val startThread = thread { router.startEventDelivery(target) }
67
+ assertTrue(pendingDeliveryStarted.await(5, TimeUnit.SECONDS))
68
+ val emitThread = thread { router.emit(close) }
69
+ emitThread.join(5_000)
70
+
71
+ assertFalse(emitThread.isAlive)
72
+ assertTrue(events.isEmpty())
73
+ continuePendingDelivery.countDown()
74
+ startThread.join(5_000)
75
+
76
+ assertFalse(startThread.isAlive)
77
+ assertEquals(listOf(open, close), events)
78
+ }
79
+
80
+ @Test
81
+ fun `failed event delivery is retried before newer events`() {
82
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
83
+ val router = GraniteMicroFrontendRuntimeEventRouter()
84
+ val open = GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping")
85
+ val close = GraniteMicroFrontendEvent.CloseApp("session-1")
86
+ var shouldFail = true
87
+ val target = object : GraniteMicroFrontendRuntimeEventTarget {
88
+ override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean {
89
+ if (shouldFail) {
90
+ shouldFail = false
91
+ throw IllegalStateException("delivery failed")
92
+ }
93
+ events.add(event)
94
+ onDelivered()
95
+ return true
96
+ }
97
+ }
98
+ router.attach(target)
99
+ router.startEventDelivery(target)
100
+
101
+ assertThrows(IllegalStateException::class.java) { router.emit(open) }
102
+ router.emit(close)
103
+
104
+ assertEquals(listOf(open, close), events)
105
+ }
106
+
107
+ @Test
108
+ fun `replacement runtime resumes a drain owned by a detached runtime`() {
109
+ val firstEvents = mutableListOf<GraniteMicroFrontendEvent>()
110
+ val secondEvents = mutableListOf<GraniteMicroFrontendEvent>()
111
+ val firstDeliveryStarted = CountDownLatch(1)
112
+ val continueFirstDelivery = CountDownLatch(1)
113
+ val router = GraniteMicroFrontendRuntimeEventRouter()
114
+ val firstTarget = object : GraniteMicroFrontendRuntimeEventTarget {
115
+ override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean {
116
+ firstDeliveryStarted.countDown()
117
+ assertTrue(continueFirstDelivery.await(5, TimeUnit.SECONDS))
118
+ firstEvents.add(event)
119
+ onDelivered()
120
+ return true
121
+ }
122
+ }
123
+ val secondTarget = object : GraniteMicroFrontendRuntimeEventTarget {
124
+ override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean {
125
+ secondEvents.add(event)
126
+ onDelivered()
127
+ return true
128
+ }
129
+ }
130
+ val open = GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping")
131
+ val close = GraniteMicroFrontendEvent.CloseApp("session-1")
132
+ router.attach(firstTarget)
133
+ router.startEventDelivery(firstTarget)
134
+
135
+ val firstEmitThread = thread { router.emit(open) }
136
+ assertTrue(firstDeliveryStarted.await(5, TimeUnit.SECONDS))
137
+ router.detach(firstTarget)
138
+ router.attach(secondTarget)
139
+ router.startEventDelivery(secondTarget)
140
+ router.emit(close)
141
+ continueFirstDelivery.countDown()
142
+ firstEmitThread.join(5_000)
143
+
144
+ assertFalse(firstEmitThread.isAlive)
145
+ assertEquals(listOf(open), firstEvents)
146
+ assertEquals(listOf(close), secondEvents)
147
+ }
148
+
149
+ @Test
150
+ fun `event rejected by an unavailable runtime is delivered to its replacement`() {
151
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
152
+ val router = GraniteMicroFrontendRuntimeEventRouter()
153
+ val unavailableRuntime = object : GraniteMicroFrontendRuntimeEventTarget {
154
+ override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean = false
155
+ }
156
+ val replacementRuntime = RecordingEventTarget()
157
+ val open = GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping")
158
+ router.attach(unavailableRuntime)
159
+ router.startEventDelivery(unavailableRuntime)
160
+
161
+ router.emit(open)
162
+ router.detach(unavailableRuntime)
163
+ router.attach(replacementRuntime)
164
+ router.startEventDelivery(replacementRuntime)
165
+
166
+ assertEquals(listOf(open), replacementRuntime.events)
167
+ }
168
+
169
+ @Test
170
+ fun `event accepted but never acknowledged is redelivered to the replacement runtime`() {
171
+ val acceptedEvents = mutableListOf<GraniteMicroFrontendEvent>()
172
+ val router = GraniteMicroFrontendRuntimeEventRouter()
173
+ val destroyedRuntime = object : GraniteMicroFrontendRuntimeEventTarget {
174
+ override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean {
175
+ acceptedEvents.add(event)
176
+ return true
177
+ }
178
+ }
179
+ val replacementRuntime = RecordingEventTarget()
180
+ val open = GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping")
181
+ router.attach(destroyedRuntime)
182
+ router.startEventDelivery(destroyedRuntime)
183
+
184
+ router.emit(open)
185
+ router.detach(destroyedRuntime)
186
+ router.attach(replacementRuntime)
187
+ router.startEventDelivery(replacementRuntime)
188
+
189
+ assertEquals(listOf(open), acceptedEvents)
190
+ assertEquals(listOf(open), replacementRuntime.events)
191
+ }
192
+
193
+ @Test
194
+ fun `events beyond the pending limit are dropped while no runtime consumes them`() {
195
+ val router = GraniteMicroFrontendRuntimeEventRouter()
196
+ val runtime = RecordingEventTarget()
197
+ val limit = GraniteMicroFrontendRuntimeEventRouter.MAX_PENDING_EVENTS
198
+ val events = (1..limit + 8).map { GraniteMicroFrontendEvent.PreloadApp("app-$it") }
199
+
200
+ events.forEach(router::emit)
201
+ router.attach(runtime)
202
+ router.startEventDelivery(runtime)
203
+
204
+ assertEquals(events.take(limit), runtime.events)
205
+ }
206
+ }
207
+
208
+ private class RecordingEventTarget : GraniteMicroFrontendRuntimeEventTarget {
209
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
210
+
211
+ override fun emit(event: GraniteMicroFrontendEvent, onDelivered: () -> Unit): Boolean {
212
+ events.add(event)
213
+ onDelivered()
214
+ return true
215
+ }
216
+ }
@@ -0,0 +1,190 @@
1
+ package run.granite.microfrontend
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertTrue
6
+ import org.junit.Test
7
+ import java.util.Collections
8
+ import java.util.concurrent.CountDownLatch
9
+ import java.util.concurrent.TimeUnit
10
+ import java.util.concurrent.atomic.AtomicBoolean
11
+ import kotlin.concurrent.thread
12
+
13
+ class GraniteMicroFrontendSessionStoreTest {
14
+ @Test
15
+ fun `session emits open visible close in native lifecycle order`() {
16
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
17
+ val store = GraniteMicroFrontendSessionStore(events::add)
18
+ val registration = store.registerSession("session-1")
19
+
20
+ registration.openApp("shopping", "granite://shopping")
21
+ assertTrue(registration.setVisible(true))
22
+ assertTrue(registration.closeApp())
23
+ registration.close()
24
+
25
+ assertEquals(
26
+ listOf(
27
+ GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping"),
28
+ GraniteMicroFrontendEvent.SessionVisibilityChanged("session-1", true),
29
+ GraniteMicroFrontendEvent.CloseApp("session-1"),
30
+ ),
31
+ events,
32
+ )
33
+ }
34
+
35
+ @Test
36
+ fun `open app is emitted only once per session`() {
37
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
38
+ val store = GraniteMicroFrontendSessionStore(events::add)
39
+ val registration = store.registerSession("session-1")
40
+
41
+ assertTrue(registration.openApp("shopping", "granite://shopping"))
42
+ assertFalse(registration.openApp("shopping", "granite://shopping"))
43
+
44
+ assertEquals(
45
+ listOf(GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping")),
46
+ events,
47
+ )
48
+ }
49
+
50
+ @Test
51
+ fun `session visibility updates are idempotent per session`() {
52
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
53
+ val store = GraniteMicroFrontendSessionStore(events::add)
54
+ val registration = store.registerSession("session-1")
55
+
56
+ assertTrue(registration.openApp("shopping", "granite://shopping"))
57
+ assertTrue(registration.setVisible(true))
58
+ assertFalse(registration.setVisible(true))
59
+ assertTrue(registration.setVisible(false))
60
+ assertFalse(registration.setVisible(false))
61
+
62
+ assertEquals(
63
+ listOf(
64
+ GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping"),
65
+ GraniteMicroFrontendEvent.SessionVisibilityChanged("session-1", true),
66
+ GraniteMicroFrontendEvent.SessionVisibilityChanged("session-1", false),
67
+ ),
68
+ events,
69
+ )
70
+ }
71
+
72
+ @Test
73
+ fun `visibility deduplication is isolated per session`() {
74
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
75
+ val store = GraniteMicroFrontendSessionStore(events::add)
76
+ val first = store.registerSession("session-1")
77
+ val second = store.registerSession("session-2")
78
+
79
+ assertTrue(first.openApp("shopping", "granite://shopping"))
80
+ assertTrue(second.openApp("payment", "granite://payment"))
81
+ assertTrue(first.setVisible(true))
82
+ assertTrue(second.setVisible(true))
83
+ assertFalse(first.setVisible(true))
84
+ assertFalse(second.setVisible(true))
85
+
86
+ assertEquals(
87
+ listOf(
88
+ GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping"),
89
+ GraniteMicroFrontendEvent.OpenApp("session-2", "payment", "granite://payment"),
90
+ GraniteMicroFrontendEvent.SessionVisibilityChanged("session-1", true),
91
+ GraniteMicroFrontendEvent.SessionVisibilityChanged("session-2", true),
92
+ ),
93
+ events,
94
+ )
95
+ }
96
+
97
+ @Test
98
+ fun `concurrent close cannot overtake open event delivery`() {
99
+ val events = Collections.synchronizedList(mutableListOf<GraniteMicroFrontendEvent>())
100
+ val openDeliveryStarted = CountDownLatch(1)
101
+ val continueOpenDelivery = CountDownLatch(1)
102
+ val store = GraniteMicroFrontendSessionStore { event ->
103
+ if (event is GraniteMicroFrontendEvent.OpenApp) {
104
+ openDeliveryStarted.countDown()
105
+ assertTrue(continueOpenDelivery.await(5, TimeUnit.SECONDS))
106
+ }
107
+ events.add(event)
108
+ }
109
+ val registration = store.registerSession("session-1")
110
+ val openResult = AtomicBoolean()
111
+ val closeResult = AtomicBoolean()
112
+ val closeStarted = CountDownLatch(1)
113
+ val closeCompleted = CountDownLatch(1)
114
+
115
+ val openThread = thread {
116
+ openResult.set(registration.openApp("shopping", "granite://shopping"))
117
+ }
118
+ assertTrue(openDeliveryStarted.await(5, TimeUnit.SECONDS))
119
+ val closeThread = thread {
120
+ closeStarted.countDown()
121
+ closeResult.set(registration.closeApp())
122
+ closeCompleted.countDown()
123
+ }
124
+ assertTrue(closeStarted.await(5, TimeUnit.SECONDS))
125
+ val blockedDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5)
126
+ while (closeThread.isAlive && closeThread.state != Thread.State.BLOCKED && System.nanoTime() < blockedDeadline) {
127
+ Thread.yield()
128
+ }
129
+ assertEquals(Thread.State.BLOCKED, closeThread.state)
130
+
131
+ continueOpenDelivery.countDown()
132
+ openThread.join(5_000)
133
+ closeThread.join(5_000)
134
+
135
+ assertFalse(openThread.isAlive)
136
+ assertFalse(closeThread.isAlive)
137
+ assertTrue(openResult.get())
138
+ assertTrue(closeResult.get())
139
+ assertEquals(
140
+ listOf(
141
+ GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping"),
142
+ GraniteMicroFrontendEvent.CloseApp("session-1"),
143
+ ),
144
+ events,
145
+ )
146
+ }
147
+
148
+ @Test
149
+ fun `visibility before open and after close is ignored`() {
150
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
151
+ val store = GraniteMicroFrontendSessionStore(events::add)
152
+ val registration = store.registerSession("session-1")
153
+
154
+ assertFalse(registration.setVisible(true))
155
+ assertTrue(registration.openApp("shopping", "granite://shopping"))
156
+ assertTrue(registration.setVisible(true))
157
+ assertTrue(registration.closeApp())
158
+ assertFalse(registration.setVisible(false))
159
+
160
+ assertEquals(
161
+ listOf(
162
+ GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping"),
163
+ GraniteMicroFrontendEvent.SessionVisibilityChanged("session-1", true),
164
+ GraniteMicroFrontendEvent.CloseApp("session-1"),
165
+ ),
166
+ events,
167
+ )
168
+ }
169
+
170
+ @Test
171
+ fun `close app is emitted only once after open for actual teardown`() {
172
+ val events = mutableListOf<GraniteMicroFrontendEvent>()
173
+ val store = GraniteMicroFrontendSessionStore(events::add)
174
+ val registration = store.registerSession("session-1")
175
+
176
+ assertFalse(registration.closeApp())
177
+ assertTrue(registration.openApp("shopping", "granite://shopping"))
178
+ assertTrue(registration.closeApp())
179
+ assertFalse(registration.closeApp())
180
+ registration.close()
181
+
182
+ assertEquals(
183
+ listOf(
184
+ GraniteMicroFrontendEvent.OpenApp("session-1", "shopping", "granite://shopping"),
185
+ GraniteMicroFrontendEvent.CloseApp("session-1"),
186
+ ),
187
+ events,
188
+ )
189
+ }
190
+ }
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ SOURCE_DIR="${SCRIPT_DIR}/src/test/cpp"
7
+ BUILD_DIR="/tmp/granite-micro-frontend-test-build"
8
+ CMAKE_BIN="${CMAKE_BIN:-cmake}"
9
+
10
+ "${CMAKE_BIN}" -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Release
11
+ "${CMAKE_BIN}" --build "${BUILD_DIR}" --parallel 2
12
+ "${CMAKE_BIN}" --build "${BUILD_DIR}" --target test
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ // This header intentionally shares its path with the codegen-generated
4
+ // ComponentDescriptors.h and shadows it: common/cpp comes before the generated jni directory on
5
+ // the include path. React Native's generated autolinking.cpp includes this path and then names
6
+ // the descriptors listed in react-native.config.js, so the two custom descriptors below have to
7
+ // be reachable through it. Renaming or removing this file breaks the consuming app's build with
8
+ // "use of undeclared identifier 'PortalViewComponentDescriptor'".
9
+
10
+ #include <react/renderer/components/GraniteMicroFrontendRuntimeSpec/ShadowNodes.h>
11
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
12
+ #include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
13
+
14
+ #include <react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewComponentDescriptor.h>
15
+ #include <react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewComponentDescriptor.h>
16
+
17
+ #include <memory>
18
+
19
+ namespace facebook::react {
20
+
21
+ void GraniteMicroFrontendRuntimeSpec_registerComponentDescriptorsFromCodegen(
22
+ std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);
23
+
24
+ } // namespace facebook::react
@@ -0,0 +1,31 @@
1
+ #include "PortalShadowRegistry.h"
2
+
3
+ #include <string>
4
+
5
+ namespace facebook::react {
6
+
7
+ PortalShadowRegistry &PortalShadowRegistry::getInstance() {
8
+ static PortalShadowRegistry instance;
9
+ return instance;
10
+ }
11
+
12
+ void PortalShadowRegistry::unregisterHost(const std::string &name) {
13
+ std::lock_guard<std::mutex> lock(mutex_);
14
+ hostSizes_.erase(name);
15
+ }
16
+
17
+ void PortalShadowRegistry::updateHostSize(const std::string &name, HostSize size) {
18
+ std::lock_guard<std::mutex> lock(mutex_);
19
+ hostSizes_[name] = size;
20
+ }
21
+
22
+ HostSize PortalShadowRegistry::getHostSize(const std::string &name) const {
23
+ std::lock_guard<std::mutex> lock(mutex_);
24
+ auto it = hostSizes_.find(name);
25
+ if (it != hostSizes_.end()) {
26
+ return it->second;
27
+ }
28
+ return HostSize{0, 0};
29
+ }
30
+
31
+ } // namespace facebook::react
@@ -0,0 +1,29 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+ #include <unordered_map>
5
+ #include <mutex>
6
+ #include <memory>
7
+
8
+ namespace facebook::react {
9
+
10
+ struct HostSize {
11
+ float width;
12
+ float height;
13
+ };
14
+
15
+ class PortalShadowRegistry {
16
+ public:
17
+ static PortalShadowRegistry &getInstance();
18
+
19
+ void unregisterHost(const std::string &name);
20
+ void updateHostSize(const std::string &name, HostSize size);
21
+ HostSize getHostSize(const std::string &name) const;
22
+
23
+ private:
24
+ PortalShadowRegistry() = default;
25
+ mutable std::mutex mutex_;
26
+ std::unordered_map<std::string, HostSize> hostSizes_;
27
+ };
28
+
29
+ } // namespace facebook::react
@@ -0,0 +1,22 @@
1
+ #pragma once
2
+
3
+ #include "RNTPortalHostViewShadowNode.h"
4
+ #include "PortalShadowRegistry.h"
5
+
6
+ #include <react/debug/react_native_assert.h>
7
+ #include <react/renderer/components/GraniteMicroFrontendRuntimeSpec/Props.h>
8
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
9
+
10
+ namespace facebook::react {
11
+
12
+ class PortalHostViewComponentDescriptor final
13
+ : public ConcreteComponentDescriptor<PortalHostViewShadowNode> {
14
+ public:
15
+ using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
16
+
17
+ void adopt(ShadowNode &shadowNode) const override {
18
+ ConcreteComponentDescriptor::adopt(shadowNode);
19
+ }
20
+ };
21
+
22
+ } // namespace facebook::react
@@ -0,0 +1,7 @@
1
+ #include "RNTPortalHostViewShadowNode.h"
2
+
3
+ namespace facebook::react {
4
+
5
+ extern const char PortalHostViewComponentName[] = "PortalHostView";
6
+
7
+ } // namespace facebook::react
@@ -0,0 +1,43 @@
1
+ #pragma once
2
+
3
+ #include "RNTPortalHostViewState.h"
4
+ #include "PortalShadowRegistry.h"
5
+
6
+ #include <react/renderer/components/GraniteMicroFrontendRuntimeSpec/EventEmitters.h>
7
+ #include <react/renderer/components/GraniteMicroFrontendRuntimeSpec/Props.h>
8
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
9
+ #include <react/renderer/core/LayoutContext.h>
10
+ #include <jsi/jsi.h>
11
+
12
+ namespace facebook::react {
13
+
14
+ JSI_EXPORT extern const char PortalHostViewComponentName[];
15
+
16
+ /*
17
+ * `ShadowNode` for <PortalHostView> component.
18
+ */
19
+ class PortalHostViewShadowNode : public ConcreteViewShadowNode<
20
+ PortalHostViewComponentName,
21
+ PortalHostViewProps,
22
+ PortalHostViewEventEmitter,
23
+ PortalHostViewState> {
24
+ public:
25
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
26
+
27
+ void layout(LayoutContext layoutContext) override {
28
+ ConcreteViewShadowNode::layout(layoutContext);
29
+
30
+ // After layout is complete, update the size in the registry
31
+ auto &props = static_cast<const PortalHostViewProps &>(*getProps());
32
+ if (!props.name.empty()) {
33
+ auto layoutMetrics = getLayoutMetrics();
34
+ PortalShadowRegistry::getInstance().updateHostSize(
35
+ props.name,
36
+ HostSize{
37
+ static_cast<float>(layoutMetrics.frame.size.width),
38
+ static_cast<float>(layoutMetrics.frame.size.height)});
39
+ }
40
+ }
41
+ };
42
+
43
+ } // namespace facebook::react
@@ -0,0 +1,21 @@
1
+ #pragma once
2
+
3
+ #ifdef ANDROID
4
+ #include <folly/dynamic.h>
5
+ #endif
6
+
7
+ namespace facebook::react {
8
+
9
+ class PortalHostViewState {
10
+ public:
11
+ PortalHostViewState() = default;
12
+
13
+ #ifdef ANDROID
14
+ PortalHostViewState(PortalHostViewState const &previousState, folly::dynamic data) {}
15
+ folly::dynamic getDynamic() const {
16
+ return {};
17
+ }
18
+ #endif
19
+ };
20
+
21
+ } // namespace facebook::react
@@ -0,0 +1,54 @@
1
+ #pragma once
2
+
3
+ #include "RNTPortalViewShadowNode.h"
4
+ #include "PortalShadowRegistry.h"
5
+
6
+ #include <react/debug/react_native_assert.h>
7
+ #include <react/renderer/components/GraniteMicroFrontendRuntimeSpec/Props.h>
8
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
9
+ #include <react/renderer/components/view/YogaLayoutableShadowNode.h>
10
+
11
+ namespace facebook::react {
12
+
13
+ class PortalViewComponentDescriptor final
14
+ : public ConcreteComponentDescriptor<PortalViewShadowNode> {
15
+ public:
16
+ using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
17
+
18
+ void adopt(ShadowNode &shadowNode) const override {
19
+ react_native_assert(dynamic_cast<PortalViewShadowNode *>(&shadowNode));
20
+
21
+ auto &portalViewShadowNode = static_cast<PortalViewShadowNode &>(shadowNode);
22
+ auto &props = static_cast<const PortalViewProps &>(*portalViewShadowNode.getProps());
23
+
24
+ // If this portal is teleported to a host, apply the host's layout constraints
25
+ if (!props.hostName.empty()) {
26
+ auto &yogaPortal = static_cast<YogaLayoutableShadowNode &>(portalViewShadowNode);
27
+
28
+ // Set position to absolute because:
29
+ // - when the view is teleported, we need to "free" its original space
30
+ // - we need to stretch the view beyond the parent layout constraints
31
+ yogaPortal.setPositionType(YGPositionTypeAbsolute);
32
+
33
+ const auto &stateData =
34
+ static_cast<const PortalViewShadowNode::ConcreteState &>(
35
+ *portalViewShadowNode.getState())
36
+ .getData();
37
+
38
+ // Native state tracks the destination host after it re-layouts (for
39
+ // example, on rotation). The registry is the initial-layout fallback.
40
+ HostSize hostSize{
41
+ static_cast<float>(stateData.hostWidth),
42
+ static_cast<float>(stateData.hostHeight)};
43
+ if (hostSize.width == 0 || hostSize.height == 0) {
44
+ hostSize = PortalShadowRegistry::getInstance().getHostSize(props.hostName);
45
+ }
46
+
47
+ portalViewShadowNode.setDimensionsFromHost(hostSize);
48
+ }
49
+
50
+ ConcreteComponentDescriptor::adopt(shadowNode);
51
+ }
52
+ };
53
+
54
+ } // namespace facebook::react
@@ -0,0 +1,7 @@
1
+ #include "RNTPortalViewShadowNode.h"
2
+
3
+ namespace facebook::react {
4
+
5
+ extern const char PortalViewComponentName[] = "PortalView";
6
+
7
+ } // namespace facebook::react