@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,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
+ }