@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,20 @@
1
+ #pragma once
2
+
3
+ #include <fbjni/fbjni.h>
4
+
5
+ namespace granite::microfrontend {
6
+
7
+ struct BundleEvaluator : facebook::jni::HybridClass<BundleEvaluator> {
8
+ static constexpr auto kJavaDescriptor =
9
+ "Lrun/granite/microfrontend/BundleEvaluator;";
10
+
11
+ static void registerNatives();
12
+
13
+ static void evaluateFileSync(
14
+ facebook::jni::alias_ref<jhybridobject>,
15
+ jlong runtimePointer,
16
+ facebook::jni::alias_ref<facebook::jni::JString> filePath,
17
+ facebook::jni::alias_ref<facebook::jni::JString> sourceUrl);
18
+ };
19
+
20
+ } // namespace granite::microfrontend
@@ -0,0 +1,92 @@
1
+ #include "FileReader.h"
2
+
3
+ #include <cerrno>
4
+ #include <cstring>
5
+ #include <fcntl.h>
6
+ #include <new>
7
+ #include <sys/stat.h>
8
+ #include <unistd.h>
9
+
10
+ namespace granite::microfrontend::io {
11
+ namespace {
12
+
13
+ class FileDescriptor final {
14
+ public:
15
+ explicit FileDescriptor(int value) : value_(value) {}
16
+ ~FileDescriptor() { close(value_); }
17
+
18
+ FileDescriptor(const FileDescriptor &) = delete;
19
+ FileDescriptor &operator=(const FileDescriptor &) = delete;
20
+
21
+ int get() const { return value_; }
22
+
23
+ private:
24
+ int value_;
25
+ };
26
+
27
+ std::string errorMessage(const std::string &message, const std::string &path,
28
+ int errorNumber) {
29
+ return message + ": " + path + " (" + std::strerror(errorNumber) + ")";
30
+ }
31
+
32
+ }
33
+
34
+ FileReaderError::FileReaderError(FileReaderErrorKind kind,
35
+ const std::string &message)
36
+ : std::runtime_error(message), kind_(kind) {}
37
+
38
+ FileReaderErrorKind FileReaderError::kind() const { return kind_; }
39
+
40
+ std::string readFileToMemory(const std::string &path) {
41
+ std::string source;
42
+
43
+ {
44
+ const int fileDescriptor = open(path.c_str(), O_RDONLY);
45
+ if (fileDescriptor < 0) {
46
+ const int errorNumber = errno;
47
+ throw FileReaderError(
48
+ FileReaderErrorKind::NotFound,
49
+ errorMessage("Bundle file cannot be opened", path, errorNumber));
50
+ }
51
+ FileDescriptor file(fileDescriptor);
52
+
53
+ struct stat fileStat {};
54
+ if (fstat(file.get(), &fileStat) < 0) {
55
+ const int errorNumber = errno;
56
+ throw FileReaderError(
57
+ FileReaderErrorKind::StatFailed,
58
+ errorMessage("Bundle file cannot be inspected", path, errorNumber));
59
+ }
60
+
61
+ try {
62
+ source.resize(static_cast<size_t>(fileStat.st_size));
63
+ } catch (const std::bad_alloc &) {
64
+ throw FileReaderError(FileReaderErrorKind::AllocationFailed,
65
+ "Bundle file cannot fit in memory: " + path);
66
+ }
67
+
68
+ size_t offset = 0;
69
+ while (offset < source.size()) {
70
+ const ssize_t bytesRead =
71
+ read(file.get(), source.data() + offset, source.size() - offset);
72
+ if (bytesRead < 0) {
73
+ if (errno == EINTR) {
74
+ continue;
75
+ }
76
+ const int errorNumber = errno;
77
+ throw FileReaderError(
78
+ FileReaderErrorKind::ReadFailed,
79
+ errorMessage("Bundle file cannot be read", path, errorNumber));
80
+ }
81
+ if (bytesRead == 0) {
82
+ source.resize(offset);
83
+ break;
84
+ }
85
+ offset += static_cast<size_t>(bytesRead);
86
+ }
87
+ }
88
+
89
+ return source;
90
+ }
91
+
92
+ }
@@ -0,0 +1,27 @@
1
+ #pragma once
2
+
3
+ #include <stdexcept>
4
+ #include <string>
5
+
6
+ namespace granite::microfrontend::io {
7
+
8
+ enum class FileReaderErrorKind {
9
+ NotFound,
10
+ StatFailed,
11
+ AllocationFailed,
12
+ ReadFailed,
13
+ };
14
+
15
+ class FileReaderError : public std::runtime_error {
16
+ public:
17
+ FileReaderError(FileReaderErrorKind kind, const std::string &message);
18
+
19
+ FileReaderErrorKind kind() const;
20
+
21
+ private:
22
+ FileReaderErrorKind kind_;
23
+ };
24
+
25
+ std::string readFileToMemory(const std::string &path);
26
+
27
+ }
@@ -0,0 +1,9 @@
1
+ #include "BundleEvaluator.h"
2
+
3
+ #include <fbjni/fbjni.h>
4
+
5
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
6
+ return facebook::jni::initialize(vm, [] {
7
+ granite::microfrontend::BundleEvaluator::registerNatives();
8
+ });
9
+ }
@@ -0,0 +1,12 @@
1
+ package com.teleport.extensions
2
+
3
+ import android.content.res.Resources
4
+
5
+ internal val Float.dp: Double
6
+ get() = (this / displayDensity).toDouble()
7
+
8
+ internal val Float.px: Double
9
+ get() = (this * displayDensity).toDouble()
10
+
11
+ private val displayDensity: Float
12
+ get() = Resources.getSystem().displayMetrics.density
@@ -0,0 +1,10 @@
1
+ package com.teleport.extensions
2
+
3
+ import android.view.View
4
+
5
+ internal fun View.screenLocation(): IntArray =
6
+ IntArray(2).also {
7
+ getLocationOnScreen(it)
8
+ }
9
+
10
+ internal fun View.isDetached(): Boolean = !isAttachedToWindow
@@ -0,0 +1,117 @@
1
+ package com.teleport.global
2
+
3
+ import android.view.View
4
+ import com.facebook.react.bridge.UiThreadUtil
5
+ import com.teleport.host.PortalHostView
6
+ import com.teleport.portal.PortalView
7
+ import java.lang.ref.WeakReference
8
+
9
+ /**
10
+ * Matches source [PortalView] instances with destination [PortalHostView] instances by name.
11
+ *
12
+ * The registry is confined to the Android UI thread because it stores and invokes [View]
13
+ * instances. Callers must not access it from background threads.
14
+ */
15
+ object PortalRegistry {
16
+ private val hosts: MutableMap<String, MutableList<WeakReference<PortalHostView>>> = HashMap()
17
+ private val pendingPortals: MutableMap<String, MutableList<WeakReference<PortalView>>> = HashMap()
18
+
19
+ fun registerHost(
20
+ name: String,
21
+ view: PortalHostView,
22
+ ) {
23
+ UiThreadUtil.assertOnUiThread()
24
+ val namedHosts = hosts.getOrPut(name) { mutableListOf() }
25
+ namedHosts.removeAll { it.get() == null || it.get() === view }
26
+ namedHosts.add(WeakReference(view))
27
+ notifySubscribers(name)
28
+ }
29
+
30
+ fun unregisterHost(
31
+ name: String,
32
+ view: PortalHostView,
33
+ ) {
34
+ UiThreadUtil.assertOnUiThread()
35
+ hosts[name]?.let { namedHosts ->
36
+ namedHosts.removeAll { it.get() == null || it.get() === view }
37
+ if (namedHosts.isEmpty()) {
38
+ hosts.remove(name)
39
+ }
40
+ }
41
+ notifySubscribers(name)
42
+ }
43
+
44
+ private fun notifySubscribers(name: String) {
45
+ pendingPortals[name]?.let { portals ->
46
+ val iterator = portals.iterator()
47
+ while (iterator.hasNext()) {
48
+ val portalRef = iterator.next()
49
+ portalRef.get()?.onHostChanged() ?: iterator.remove()
50
+ }
51
+ }
52
+ }
53
+
54
+ fun notifyHostLayoutChanged(name: String) {
55
+ UiThreadUtil.assertOnUiThread()
56
+ pendingPortals[name]?.let { portals ->
57
+ val iterator = portals.iterator()
58
+ while (iterator.hasNext()) {
59
+ val portalRef = iterator.next()
60
+ portalRef.get()?.onHostLayoutChanged() ?: iterator.remove()
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Prefer an attached destination in the source View's Window. If the source
67
+ * belongs to a detached controller surface, or no same-window destination
68
+ * exists, use the most recently registered attached host from another Window.
69
+ */
70
+ fun resolveHost(
71
+ name: String?,
72
+ sourceView: View,
73
+ ): PortalHostView? {
74
+ UiThreadUtil.assertOnUiThread()
75
+ if (name == null) return null
76
+
77
+ val namedHosts = hosts[name] ?: return null
78
+ namedHosts.removeAll { it.get() == null }
79
+ if (namedHosts.isEmpty()) {
80
+ hosts.remove(name)
81
+ return null
82
+ }
83
+
84
+ val liveHosts = namedHosts.mapNotNull { it.get() }
85
+ val sourceWindowToken = sourceView.windowToken
86
+ if (sourceView.isAttachedToWindow && sourceWindowToken != null) {
87
+ liveHosts.lastOrNull {
88
+ it.isAttachedToWindow && it.windowToken == sourceWindowToken
89
+ }?.let { return it }
90
+ }
91
+
92
+ return liveHosts.lastOrNull { it.isAttachedToWindow }
93
+ }
94
+
95
+ fun registerPendingPortal(
96
+ hostName: String,
97
+ portal: PortalView,
98
+ ) {
99
+ UiThreadUtil.assertOnUiThread()
100
+ val portals = pendingPortals.getOrPut(hostName) { mutableListOf() }
101
+ portals.removeAll { it.get() == null || it.get() == portal }
102
+ portals.add(WeakReference(portal))
103
+ }
104
+
105
+ fun unregisterPendingPortal(
106
+ hostName: String,
107
+ portal: PortalView,
108
+ ) {
109
+ UiThreadUtil.assertOnUiThread()
110
+ pendingPortals[hostName]?.let { portals ->
111
+ portals.removeAll { it.get() == null || it.get() == portal }
112
+ if (portals.isEmpty()) {
113
+ pendingPortals.remove(hostName)
114
+ }
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,97 @@
1
+ package com.teleport.host
2
+
3
+ import android.content.Context
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import com.facebook.react.views.view.ReactViewGroup
7
+ import com.teleport.global.PortalRegistry
8
+
9
+ /**
10
+ * Native-owned destination for children moved out of a [com.teleport.portal.PortalView].
11
+ *
12
+ * A named host registers while attached to a Window. The matching source view keeps React/Fabric
13
+ * ownership of its children and only reparents their Android views into this container.
14
+ */
15
+ class PortalHostView(
16
+ context: Context?,
17
+ ) : ReactViewGroup(context) {
18
+ private var name: String? = null
19
+ private var isInBatch = false
20
+ private var batchBaseIndex = 0
21
+ private var hasPendingCleanup = false
22
+
23
+ fun setName(newName: String?) {
24
+ if (name == newName) return
25
+
26
+ hasPendingCleanup = false
27
+ name?.let { PortalRegistry.unregisterHost(it, this) }
28
+ name = newName
29
+ newName?.let { PortalRegistry.registerHost(it, this) }
30
+ }
31
+
32
+ fun cleanup() {
33
+ if (isAttachedToWindow) {
34
+ hasPendingCleanup = true
35
+ return
36
+ }
37
+
38
+ cleanupNow()
39
+ }
40
+
41
+ /**
42
+ * Returns the index at which a portal child should be inserted.
43
+ *
44
+ * Within a single Fabric commit all mutations run synchronously on the main
45
+ * thread. The first call in a commit records the current child count as
46
+ * the "base"; subsequent calls in the same commit reuse that base so that
47
+ * bottom-to-top Fabric ordering is compensated by [addView] at a specific index.
48
+ * A [Handler.post] resets the flag after the commit finishes.
49
+ */
50
+ fun nextInsertionIndexForChildAt(childIndex: Int): Int {
51
+ if (!isInBatch) {
52
+ isInBatch = true
53
+ batchBaseIndex = childCount
54
+ Handler(Looper.getMainLooper()).post { isInBatch = false }
55
+ }
56
+ return minOf(batchBaseIndex + childIndex, childCount)
57
+ }
58
+
59
+ override fun onAttachedToWindow() {
60
+ super.onAttachedToWindow()
61
+ // A host can be named before its Activity installs it into the Window.
62
+ // Register again on attach so detached controller surfaces resolve the
63
+ // currently visible Activity host deterministically.
64
+ name?.let { PortalRegistry.registerHost(it, this) }
65
+ }
66
+
67
+ override fun onLayout(
68
+ changed: Boolean,
69
+ left: Int,
70
+ top: Int,
71
+ right: Int,
72
+ bottom: Int,
73
+ ) {
74
+ super.onLayout(changed, left, top, right, bottom)
75
+ name?.let { PortalRegistry.notifyHostLayoutChanged(it) }
76
+ }
77
+
78
+ override fun onDetachedFromWindow() {
79
+ super.onDetachedFromWindow()
80
+
81
+ if (hasPendingCleanup) {
82
+ cleanupNow()
83
+ } else {
84
+ // Keep the name so a temporary detach can register again on attach, but
85
+ // stop routing children into a host that is no longer visible.
86
+ name?.let { PortalRegistry.unregisterHost(it, this) }
87
+ }
88
+ }
89
+
90
+ private fun cleanupNow() {
91
+ name?.let { PortalRegistry.unregisterHost(it, this) }
92
+ name = null
93
+ isInBatch = false
94
+ batchBaseIndex = 0
95
+ hasPendingCleanup = false
96
+ }
97
+ }
@@ -0,0 +1,41 @@
1
+ package com.teleport.host
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.facebook.react.uimanager.ViewManagerDelegate
6
+ import com.facebook.react.uimanager.annotations.ReactProp
7
+ import com.facebook.react.viewmanagers.PortalHostViewManagerDelegate
8
+ import com.facebook.react.viewmanagers.PortalHostViewManagerInterface
9
+ import com.facebook.react.views.view.ReactViewGroup
10
+ import com.teleport.managers.TeleportViewManager
11
+
12
+ @ReactModule(name = PortalHostViewManager.NAME)
13
+ class PortalHostViewManager :
14
+ TeleportViewManager(),
15
+ PortalHostViewManagerInterface<ReactViewGroup> {
16
+ private val delegate = PortalHostViewManagerDelegate(this)
17
+
18
+ override fun getName(): String = NAME
19
+
20
+ override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = delegate
21
+
22
+ override fun createTeleportView(context: ThemedReactContext): ReactViewGroup = PortalHostView(context)
23
+
24
+ override fun onDropViewInstance(view: ReactViewGroup) {
25
+ super.onDropViewInstance(view)
26
+
27
+ (view as? PortalHostView)?.cleanup()
28
+ }
29
+
30
+ @ReactProp(name = "name")
31
+ override fun setName(
32
+ view: ReactViewGroup?,
33
+ name: String?,
34
+ ) {
35
+ (view as? PortalHostView)?.setName(name)
36
+ }
37
+
38
+ companion object {
39
+ const val NAME = "PortalHostView"
40
+ }
41
+ }
@@ -0,0 +1,116 @@
1
+ package com.teleport.host
2
+
3
+ import android.view.MotionEvent
4
+ import android.view.View
5
+ import com.facebook.react.ReactHost
6
+ import com.facebook.react.ReactRootView
7
+ import com.facebook.react.bridge.ReactContext
8
+ import com.facebook.react.config.ReactFeatureFlags
9
+ import com.facebook.react.uimanager.JSPointerDispatcher
10
+ import com.facebook.react.uimanager.JSTouchDispatcher
11
+ import com.facebook.react.uimanager.ThemedReactContext
12
+ import com.facebook.react.uimanager.UIManagerHelper
13
+ import com.facebook.react.uimanager.common.UIManagerType
14
+ import com.facebook.react.uimanager.events.EventDispatcher
15
+
16
+ class PortalReactRootView(
17
+ context: ThemedReactContext,
18
+ private val reactHost: ReactHost,
19
+ surfaceId: Int,
20
+ private val moduleName: String,
21
+ ) : ReactRootView(context) {
22
+ private val touchDispatcher = JSTouchDispatcher(this)
23
+ private val pointerDispatcher =
24
+ if (ReactFeatureFlags.dispatchPointerEvents) {
25
+ JSPointerDispatcher(this)
26
+ } else {
27
+ null
28
+ }
29
+
30
+ private val eventDispatcher: EventDispatcher?
31
+ get() =
32
+ reactHost.currentReactContext?.let {
33
+ UIManagerHelper.getEventDispatcher(it, UIManagerType.FABRIC)
34
+ }
35
+
36
+ init {
37
+ setIsFabric(true)
38
+ setRootViewTag(surfaceId)
39
+ }
40
+
41
+ override fun onMeasure(
42
+ widthMeasureSpec: Int,
43
+ heightMeasureSpec: Int,
44
+ ) {
45
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
46
+
47
+ val childWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
48
+ val childHeightSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
49
+ for (index in 0 until childCount) {
50
+ getChildAt(index).measure(childWidthSpec, childHeightSpec)
51
+ }
52
+ }
53
+
54
+ override fun onLayout(
55
+ changed: Boolean,
56
+ left: Int,
57
+ top: Int,
58
+ right: Int,
59
+ bottom: Int,
60
+ ) {
61
+ for (index in 0 until childCount) {
62
+ getChildAt(index).layout(0, 0, right - left, bottom - top)
63
+ }
64
+ }
65
+
66
+ override fun onChildStartedNativeGesture(
67
+ childView: View?,
68
+ ev: MotionEvent,
69
+ ) {
70
+ val dispatcher = eventDispatcher ?: return
71
+ touchDispatcher.onChildStartedNativeGesture(ev, dispatcher)
72
+ childView?.let {
73
+ pointerDispatcher?.onChildStartedNativeGesture(it, ev, dispatcher)
74
+ }
75
+ }
76
+
77
+ override fun onChildEndedNativeGesture(
78
+ childView: View,
79
+ ev: MotionEvent,
80
+ ) {
81
+ val dispatcher = eventDispatcher ?: return
82
+ touchDispatcher.onChildEndedNativeGesture(ev, dispatcher)
83
+ pointerDispatcher?.onChildEndedNativeGesture()
84
+ }
85
+
86
+ override fun handleException(t: Throwable) {
87
+ val exception = if (t is Exception) t else RuntimeException(t)
88
+ currentReactContext?.handleException(exception) ?: throw exception
89
+ }
90
+
91
+ override fun getUIManagerType(): Int = UIManagerType.FABRIC
92
+
93
+ override fun getJSModuleName(): String = moduleName
94
+
95
+ override fun dispatchJSTouchEvent(event: MotionEvent) {
96
+ val dispatcher = eventDispatcher ?: return
97
+ touchDispatcher.handleTouchEvent(event, dispatcher, currentReactContext)
98
+ }
99
+
100
+ override fun dispatchJSPointerEvent(
101
+ event: MotionEvent,
102
+ isCapture: Boolean,
103
+ ) {
104
+ val dispatcher = eventDispatcher ?: return
105
+ pointerDispatcher?.handleMotionEvent(event, dispatcher, isCapture)
106
+ }
107
+
108
+ override fun hasActiveReactContext(): Boolean =
109
+ currentReactContext?.hasActiveReactInstance() == true
110
+
111
+ override fun hasActiveReactInstance(): Boolean = hasActiveReactContext()
112
+
113
+ override fun getCurrentReactContext(): ReactContext? = reactHost.currentReactContext
114
+
115
+ override fun isViewAttachedToReactInstance(): Boolean = hasActiveReactContext()
116
+ }
@@ -0,0 +1,27 @@
1
+ package com.teleport.managers
2
+
3
+ import com.facebook.react.uimanager.ThemedReactContext
4
+ import com.facebook.react.views.view.ReactViewGroup
5
+ import com.facebook.react.views.view.ReactViewManager
6
+
7
+ abstract class TeleportViewManager : ReactViewManager() {
8
+ protected abstract fun createTeleportView(context: ThemedReactContext): ReactViewGroup
9
+
10
+ fun forceBoxNone(view: ReactViewGroup) {
11
+ super.setPointerEvents(view, "box-none")
12
+ }
13
+
14
+ override fun createViewInstance(context: ThemedReactContext): ReactViewGroup =
15
+ createTeleportView(context).also {
16
+ forceBoxNone(it)
17
+ }
18
+
19
+ override fun prepareToRecycleView(
20
+ reactContext: ThemedReactContext,
21
+ view: ReactViewGroup,
22
+ ): ReactViewGroup {
23
+ super.prepareToRecycleView(reactContext, view)
24
+ forceBoxNone(view)
25
+ return view
26
+ }
27
+ }
@@ -0,0 +1,107 @@
1
+ package com.teleport.portal
2
+
3
+ import android.view.View
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.uimanager.StateWrapper
6
+ import com.teleport.extensions.dp
7
+ import com.teleport.extensions.isDetached
8
+ import com.teleport.extensions.screenLocation
9
+ import com.teleport.host.PortalHostView
10
+
11
+ internal class PortalLayoutStateController(
12
+ private val sourceView: View,
13
+ ) {
14
+ private var lastLayout = PortalLayoutState.EMPTY
15
+ private var stateWrapper: StateWrapper? = null
16
+
17
+ fun setStateWrapper(wrapper: StateWrapper?) {
18
+ stateWrapper = wrapper
19
+ }
20
+
21
+ fun updateIfNeeded(
22
+ hostName: String?,
23
+ host: PortalHostView?,
24
+ ) {
25
+ val wrapper = stateWrapper
26
+ val activeHost = activeHost(hostName, host)
27
+
28
+ when {
29
+ wrapper == null -> Unit
30
+ activeHost == null -> resetIfNeeded()
31
+ else -> publishIfChanged(wrapper, createLayoutState(activeHost))
32
+ }
33
+ }
34
+
35
+ fun resetIfNeeded() {
36
+ val wrapper = stateWrapper
37
+
38
+ if (wrapper != null) {
39
+ publishIfChanged(wrapper, PortalLayoutState.EMPTY)
40
+ }
41
+ }
42
+
43
+ private fun activeHost(
44
+ hostName: String?,
45
+ host: PortalHostView?,
46
+ ): PortalHostView? =
47
+ if (hostName == null || host == null || host.isDetached()) {
48
+ null
49
+ } else {
50
+ host
51
+ }
52
+
53
+ private fun publishIfChanged(
54
+ wrapper: StateWrapper,
55
+ nextLayout: PortalLayoutState,
56
+ ) {
57
+ if (lastLayout != nextLayout) {
58
+ lastLayout = nextLayout
59
+ wrapper.updateState(nextLayout.toMap())
60
+ }
61
+ }
62
+
63
+ private fun createLayoutState(host: PortalHostView): PortalLayoutState {
64
+ val hostLocation = host.screenLocation()
65
+ // A detached controller surface has no Window coordinate space. Its children
66
+ // are already physically parented by the destination host, so use the host
67
+ // origin as the logical source origin and avoid applying a screen inset twice.
68
+ val sourceLocation =
69
+ if (sourceView.isDetached()) {
70
+ hostLocation
71
+ } else {
72
+ sourceView.screenLocation()
73
+ }
74
+
75
+ return PortalLayoutState(
76
+ hostWidth = host.width.toFloat().dp,
77
+ hostHeight = host.height.toFloat().dp,
78
+ offsetX = (hostLocation[0] - sourceLocation[0]).toFloat().dp,
79
+ offsetY = (hostLocation[1] - sourceLocation[1]).toFloat().dp,
80
+ )
81
+ }
82
+ }
83
+
84
+ private data class PortalLayoutState(
85
+ val hostWidth: Double,
86
+ val hostHeight: Double,
87
+ val offsetX: Double,
88
+ val offsetY: Double,
89
+ ) {
90
+ fun toMap() =
91
+ Arguments.createMap().apply {
92
+ putDouble("hostWidth", hostWidth)
93
+ putDouble("hostHeight", hostHeight)
94
+ putDouble("offsetX", offsetX)
95
+ putDouble("offsetY", offsetY)
96
+ }
97
+
98
+ companion object {
99
+ val EMPTY =
100
+ PortalLayoutState(
101
+ hostWidth = 0.0,
102
+ hostHeight = 0.0,
103
+ offsetX = 0.0,
104
+ offsetY = 0.0,
105
+ )
106
+ }
107
+ }