@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,426 @@
1
+ //
2
+ // PortalView.mm
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 02/09/2025.
6
+ //
7
+ // allow: SIZE_OK - Fabric portal mounting, layout, and touch handling share one component lifecycle.
8
+
9
+ #import "PortalView.h"
10
+ #import "PortalHostView.h"
11
+ #import "PortalRegistry.h"
12
+
13
+ #import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/EventEmitters.h>
14
+ #import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/Props.h>
15
+ #import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/RCTComponentViewHelpers.h>
16
+ #import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewComponentDescriptor.h>
17
+ #import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.h>
18
+
19
+ #import "RCTFabricComponentsPlugins.h"
20
+
21
+ #import <React/RCTSurfaceTouchHandler.h>
22
+
23
+ using namespace facebook::react;
24
+
25
+ @interface PortalView () <RCTPortalViewViewProtocol>
26
+
27
+ @property (nonatomic, strong) NSString *hostName;
28
+ // Weak so a detached/dead host cannot keep the destination tree alive through
29
+ // the portal after unregister. Falls back to contentView when nil.
30
+ @property (nonatomic, weak) UIView *targetView;
31
+
32
+ @end
33
+
34
+ @implementation PortalView {
35
+ NSMutableArray<UIView *> *_ownChildren;
36
+ PortalViewShadowNode::ConcreteState::Shared _state;
37
+ }
38
+
39
+ static BOOL GranitePortalHostNamesEqual(NSString * _Nullable left, NSString * _Nullable right)
40
+ {
41
+ if (left == right) {
42
+ return YES;
43
+ }
44
+ if (left == nil || right == nil) {
45
+ return NO;
46
+ }
47
+ return [left isEqualToString:right];
48
+ }
49
+
50
+ + (ComponentDescriptorProvider)componentDescriptorProvider
51
+ {
52
+ return concreteComponentDescriptorProvider<PortalViewComponentDescriptor>();
53
+ }
54
+
55
+ - (instancetype)initWithFrame:(CGRect)frame
56
+ {
57
+ if (self = [super initWithFrame:frame]) {
58
+ static const auto defaultProps = std::make_shared<const PortalViewProps>();
59
+ _props = defaultProps;
60
+
61
+ UIView *content = [[UIView alloc] init];
62
+ self.contentView = content;
63
+ self.targetView = content;
64
+ _ownChildren = [NSMutableArray array];
65
+ }
66
+
67
+ return self;
68
+ }
69
+
70
+ - (CGRect)screenRectForView:(UIView *)view
71
+ {
72
+ if (!view.window) {
73
+ return CGRectNull;
74
+ }
75
+
76
+ return [view convertRect:view.bounds toCoordinateSpace:view.window.screen.coordinateSpace];
77
+ }
78
+
79
+ - (void)resetPortalLayoutStateIfNeeded
80
+ {
81
+ if (!_state) {
82
+ return;
83
+ }
84
+
85
+ PortalViewState newData = {};
86
+ _state->updateState(
87
+ [=](const PortalViewShadowNode::ConcreteState::Data &oldData)
88
+ -> PortalViewShadowNode::ConcreteState::SharedData {
89
+ if (oldData.hostWidth == newData.hostWidth && oldData.hostHeight == newData.hostHeight &&
90
+ oldData.offsetX == newData.offsetX && oldData.offsetY == newData.offsetY) {
91
+ return nullptr;
92
+ }
93
+
94
+ return std::make_shared<const PortalViewShadowNode::ConcreteState::Data>(newData);
95
+ });
96
+ }
97
+
98
+ - (UIView *)resolvedTargetView
99
+ {
100
+ return self.targetView ?: self.contentView;
101
+ }
102
+
103
+ - (nullable UIViewController *)nearestViewControllerForView:(UIView *)view
104
+ {
105
+ for (UIResponder *responder = view; responder != nil; responder = responder.nextResponder) {
106
+ if ([responder isKindOfClass:[UIViewController class]]) {
107
+ return (UIViewController *)responder;
108
+ }
109
+ }
110
+ return nil;
111
+ }
112
+
113
+ - (void)updatePortalLayoutStateIfNeeded
114
+ {
115
+ if (!_state) {
116
+ return;
117
+ }
118
+
119
+ UIView *targetView = [self resolvedTargetView];
120
+ if (!self.hostName || ![targetView isKindOfClass:[PortalHostView class]]) {
121
+ [self resetPortalLayoutStateIfNeeded];
122
+ return;
123
+ }
124
+
125
+ CGRect sourceRect = [self screenRectForView:self];
126
+ CGRect hostRect = [self screenRectForView:targetView];
127
+
128
+ if (CGRectIsNull(sourceRect) || CGRectIsNull(hostRect)) {
129
+ return;
130
+ }
131
+
132
+ // Children are physically mounted under the host, but measured through this
133
+ // PortalView shadow node. Store the host's native layout so Fabric
134
+ // measurement follows the destination after it re-layouts.
135
+ CGSize hostSize = targetView.bounds.size;
136
+
137
+ PortalViewState newData = {
138
+ static_cast<Float>(hostSize.width),
139
+ static_cast<Float>(hostSize.height),
140
+ static_cast<Float>(hostRect.origin.x - sourceRect.origin.x),
141
+ static_cast<Float>(hostRect.origin.y - sourceRect.origin.y)};
142
+
143
+ _state->updateState(
144
+ [=](const PortalViewShadowNode::ConcreteState::Data &oldData)
145
+ -> PortalViewShadowNode::ConcreteState::SharedData {
146
+ if (oldData.hostWidth == newData.hostWidth && oldData.hostHeight == newData.hostHeight &&
147
+ oldData.offsetX == newData.offsetX && oldData.offsetY == newData.offsetY) {
148
+ return nullptr;
149
+ }
150
+
151
+ return std::make_shared<const PortalViewShadowNode::ConcreteState::Data>(newData);
152
+ });
153
+ }
154
+
155
+ - (void)moveOwnChildrenToTarget:(UIView *)target
156
+ {
157
+ NSArray<UIView *> *children = [_ownChildren copy];
158
+ NSArray<UIViewController *> *detachedViewControllers =
159
+ [self detachViewControllersFromChildren:children];
160
+
161
+ if ([target isKindOfClass:[PortalHostView class]]) {
162
+ PortalHostView *host = (PortalHostView *)target;
163
+ for (NSInteger i = 0; i < (NSInteger)children.count; i++) {
164
+ NSInteger idx = [host nextInsertionIndexForChildAt:i];
165
+ [target insertSubview:children[i] atIndex:idx];
166
+ }
167
+ } else {
168
+ for (UIView *child in children) {
169
+ [target addSubview:child];
170
+ }
171
+ }
172
+
173
+ [self attachViewControllers:detachedViewControllers toView:target];
174
+ }
175
+
176
+ - (void)collectViewControllersInView:(UIView *)view
177
+ result:(NSMutableSet<UIViewController *> *)viewControllers
178
+ {
179
+ UIResponder *nextResponder = view.nextResponder;
180
+ if ([nextResponder isKindOfClass:[UIViewController class]]) {
181
+ [viewControllers addObject:(UIViewController *)nextResponder];
182
+ }
183
+
184
+ for (UIView *subview in view.subviews) {
185
+ [self collectViewControllersInView:subview result:viewControllers];
186
+ }
187
+ }
188
+
189
+ - (NSArray<UIViewController *> *)detachViewControllersFromChildren:(NSArray<UIView *> *)children
190
+ {
191
+ NSMutableSet<UIViewController *> *viewControllers = [NSMutableSet set];
192
+ for (UIView *child in children) {
193
+ [self collectViewControllersInView:child result:viewControllers];
194
+ }
195
+
196
+ NSMutableArray<UIViewController *> *viewControllersToDetach = [NSMutableArray array];
197
+ for (UIViewController *viewController in viewControllers) {
198
+ UIViewController *parent = viewController.parentViewController;
199
+ if (parent && ![viewControllers containsObject:parent]) {
200
+ [viewController willMoveToParentViewController:nil];
201
+ [viewControllersToDetach addObject:viewController];
202
+ }
203
+ }
204
+
205
+ for (UIView *child in children) {
206
+ [child removeFromSuperview];
207
+ }
208
+
209
+ for (UIViewController *viewController in viewControllersToDetach) {
210
+ [viewController removeFromParentViewController];
211
+ }
212
+
213
+ return viewControllersToDetach;
214
+ }
215
+
216
+ - (void)attachViewControllers:(NSArray<UIViewController *> *)viewControllers
217
+ toView:(UIView *)target
218
+ {
219
+ if (viewControllers.count == 0) {
220
+ return;
221
+ }
222
+
223
+ UIViewController *parentViewController = [self nearestViewControllerForView:target];
224
+ if (parentViewController == nil) {
225
+ return;
226
+ }
227
+
228
+ for (UIViewController *viewController in viewControllers) {
229
+ if (viewController.parentViewController == parentViewController) {
230
+ continue;
231
+ }
232
+ if (viewController.parentViewController != nil) {
233
+ [viewController willMoveToParentViewController:nil];
234
+ [viewController removeFromParentViewController];
235
+ }
236
+ [parentViewController addChildViewController:viewController];
237
+ [viewController didMoveToParentViewController:parentViewController];
238
+ }
239
+ }
240
+
241
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
242
+ {
243
+ const auto &newViewProps = *std::static_pointer_cast<PortalViewProps const>(props);
244
+
245
+ std::string newHostStr = newViewProps.hostName;
246
+ NSString *newHostName =
247
+ newHostStr.empty() ? nil : [NSString stringWithUTF8String:newHostStr.c_str()];
248
+
249
+ if (!GranitePortalHostNamesEqual(self.hostName, newHostName)) {
250
+ if (self.hostName) {
251
+ [[PortalRegistry sharedInstance] unregisterPendingPortal:self withHostName:self.hostName];
252
+ }
253
+
254
+ self.hostName = newHostName;
255
+
256
+ PortalHostView *hostView = nil;
257
+ if (self.hostName) {
258
+ hostView = [[PortalRegistry sharedInstance] resolveHostWithName:self.hostName
259
+ sourceView:self];
260
+ }
261
+
262
+ UIView *newTarget = hostView ? (UIView *)hostView : self.contentView;
263
+
264
+ if (newTarget != [self resolvedTargetView]) {
265
+ self.targetView = newTarget;
266
+ [self moveOwnChildrenToTarget:newTarget];
267
+ }
268
+
269
+ if (self.hostName) {
270
+ [[PortalRegistry sharedInstance] registerPendingPortal:self withHostName:self.hostName];
271
+ }
272
+ }
273
+
274
+ [super updateProps:props oldProps:oldProps];
275
+ [self updatePortalLayoutStateIfNeeded];
276
+ }
277
+
278
+ /// Finds the host index of the first next sibling (in _ownChildren) that is
279
+ /// already present in the host. Returns -1 when none is found (caller should append).
280
+ - (NSInteger)findNextSiblingHostIndex:(NSInteger)ownIndex
281
+ {
282
+ UIView *targetView = [self resolvedTargetView];
283
+ for (NSInteger i = ownIndex + 1; i < (NSInteger)_ownChildren.count; i++) {
284
+ UIView *sibling = _ownChildren[i];
285
+ NSInteger siblingIdx = [targetView.subviews indexOfObject:sibling];
286
+ if (siblingIdx != NSNotFound) {
287
+ return (NSInteger)siblingIdx;
288
+ }
289
+ }
290
+ return -1;
291
+ }
292
+
293
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
294
+ index:(NSInteger)index
295
+ {
296
+ NSInteger clampedIndex = MIN(MAX(index, 0), (NSInteger)_ownChildren.count);
297
+ [_ownChildren insertObject:childComponentView atIndex:clampedIndex];
298
+
299
+ UIView *targetView = [self resolvedTargetView];
300
+ if (targetView == self.contentView) {
301
+ // when adding to self, preserve the React tree order with the provided index
302
+ NSInteger subviewIndex = MIN(clampedIndex, (NSInteger)targetView.subviews.count);
303
+ [targetView insertSubview:childComponentView atIndex:subviewIndex];
304
+ } else {
305
+ NSInteger ownIndex = [_ownChildren indexOfObject:childComponentView];
306
+ NSInteger hostIndex = [self findNextSiblingHostIndex:ownIndex];
307
+ if (hostIndex >= 0) {
308
+ [targetView insertSubview:childComponentView atIndex:hostIndex];
309
+ } else {
310
+ [targetView addSubview:childComponentView];
311
+ }
312
+ }
313
+ }
314
+
315
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
316
+ index:(NSInteger)index
317
+ {
318
+ [_ownChildren removeObject:childComponentView];
319
+ [childComponentView removeFromSuperview];
320
+ }
321
+
322
+ - (void)onHostChanged
323
+ {
324
+ PortalHostView *hostView = [[PortalRegistry sharedInstance] resolveHostWithName:self.hostName
325
+ sourceView:self];
326
+
327
+ if (!hostView) {
328
+ // Match updateProps: when the destination disappears, reclaim children under
329
+ // the local contentView so they are not stuck under a dead host.
330
+ UIView *fallbackTarget = self.contentView;
331
+ if ([self resolvedTargetView] != fallbackTarget) {
332
+ self.targetView = fallbackTarget;
333
+ [self moveOwnChildrenToTarget:fallbackTarget];
334
+ }
335
+ [self resetPortalLayoutStateIfNeeded];
336
+ return;
337
+ }
338
+
339
+ UIView *newTarget = hostView;
340
+ if (newTarget != [self resolvedTargetView]) {
341
+ self.targetView = newTarget;
342
+ [self moveOwnChildrenToTarget:newTarget];
343
+ }
344
+
345
+ [self updatePortalLayoutStateIfNeeded];
346
+ }
347
+
348
+ - (void)onHostLayoutChanged
349
+ {
350
+ [self updatePortalLayoutStateIfNeeded];
351
+ }
352
+
353
+ - (void)layoutSubviews
354
+ {
355
+ [super layoutSubviews];
356
+ [self updatePortalLayoutStateIfNeeded];
357
+ }
358
+
359
+ - (void)updateState:(const facebook::react::State::Shared &)state
360
+ oldState:(const facebook::react::State::Shared &)oldState
361
+ {
362
+ _state = std::static_pointer_cast<const PortalViewShadowNode::ConcreteState>(state);
363
+ [self updatePortalLayoutStateIfNeeded];
364
+ }
365
+
366
+ - (void)prepareForRecycle
367
+ {
368
+ [super prepareForRecycle];
369
+
370
+ if (self.hostName) {
371
+ [[PortalRegistry sharedInstance] unregisterPendingPortal:self withHostName:self.hostName];
372
+ }
373
+
374
+ // Reset all portal state so recycled views don't retain stale host references.
375
+ // Without this, a recycled PortalView's targetView still points to the old host,
376
+ // causing mountChildComponentView to add children to the wrong host and
377
+ // moveOwnChildrenToTarget to operate on a stale source.
378
+ self.hostName = nil;
379
+ self.targetView = self.contentView;
380
+ [_ownChildren removeAllObjects];
381
+ _state.reset();
382
+ }
383
+
384
+ // MARK: touch handling
385
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
386
+ {
387
+ BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
388
+ if (!canReceiveTouchEvents) {
389
+ return nil;
390
+ }
391
+
392
+ // `hitSubview` is the topmost subview which was hit. The hit point can
393
+ // be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
394
+ UIView *hitSubview = nil;
395
+ BOOL isPointInside = [self pointInside:point withEvent:event];
396
+ if (![self clipsToBounds] || isPointInside) {
397
+ // The default behaviour of UIKit is that if a view does not contain a point,
398
+ // then no subviews will be returned from hit testing, even if they contain
399
+ // the hit point. By doing hit testing directly on the subviews, we bypass
400
+ // the strict containment policy (i.e., UIKit guarantees that every ancestor
401
+ // of the hit view will return YES from -pointInside:withEvent:). See:
402
+ // - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
403
+ UIView *targetView = [self resolvedTargetView];
404
+ for (UIView *subview in [targetView.subviews reverseObjectEnumerator]) {
405
+ // Prevent circular hit-testing by checking if we're in the subview's hierarchy
406
+ if ([self isDescendantOfView:subview]) {
407
+ // Skip views that contain us to prevent cycles
408
+ continue;
409
+ }
410
+
411
+ CGPoint convertedPoint = [subview convertPoint:point fromView:self];
412
+ hitSubview = [subview hitTest:convertedPoint withEvent:event];
413
+ if (hitSubview != nil) {
414
+ break;
415
+ }
416
+ }
417
+ }
418
+ return hitSubview;
419
+ }
420
+
421
+ Class<RCTComponentViewProtocol> PortalViewCls(void)
422
+ {
423
+ return PortalView.class;
424
+ }
425
+
426
+ @end
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "@granite-js/micro-frontend",
3
+ "version": "2.0.0",
4
+ "description": "Micro-frontend build, runtime, session, and Portal contracts for Granite brownfield applications",
5
+ "main": "./dist/module/index.js",
6
+ "types": "./dist/typescript/index.d.ts",
7
+ "react-native": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/typescript/index.d.ts",
11
+ "react-native": "./src/index.ts",
12
+ "default": "./dist/module/index.js"
13
+ },
14
+ "./plugin": {
15
+ "types": "./dist/typescript/plugin/index.d.ts",
16
+ "default": "./dist/module/plugin/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "!src/**/*.spec.*",
23
+ "dist",
24
+ "android",
25
+ "ios",
26
+ "common",
27
+ "*.podspec",
28
+ "react-native.config.js",
29
+ "README.md",
30
+ "LICENSE",
31
+ "LICENSE.react-native-teleport",
32
+ "NOTICE",
33
+ "!ios/build",
34
+ "!android/build",
35
+ "!android/gradle",
36
+ "!android/gradlew",
37
+ "!android/gradlew.bat",
38
+ "!android/local.properties",
39
+ "!examples/**",
40
+ "!**/__tests__",
41
+ "!**/__fixtures__",
42
+ "!**/__mocks__",
43
+ "!**/.*"
44
+ ],
45
+ "scripts": {
46
+ "prepack": "yarn build",
47
+ "build": "bob build && tsc --project tsconfig.build.json",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "vitest --run"
50
+ },
51
+ "license": "Apache-2.0",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/toss/granite.git",
55
+ "directory": "packages/micro-frontend"
56
+ },
57
+ "dependencies": {
58
+ "@granite-js/utils": "workspace:*",
59
+ "@standard-schema/spec": "^1.0.0"
60
+ },
61
+ "peerDependencies": {
62
+ "@granite-js/plugin-core": "*",
63
+ "@granite-js/react-native": "*",
64
+ "react": "*",
65
+ "react-native": "*"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "@granite-js/plugin-core": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "@granite-js/plugin-core": "workspace:*",
74
+ "@granite-js/react-native": "workspace:*",
75
+ "@types/node": "catalog:tools",
76
+ "@types/react": "catalog:react-native",
77
+ "@types/react-test-renderer": "^19",
78
+ "react": "catalog:react-native",
79
+ "react-native": "catalog:react-native",
80
+ "react-native-builder-bob": "catalog:tools",
81
+ "react-test-renderer": "19.2.3",
82
+ "typescript": "catalog:tools",
83
+ "vitest": "^4.0.12"
84
+ },
85
+ "react-native-builder-bob": {
86
+ "source": "src",
87
+ "output": "dist",
88
+ "targets": [
89
+ [
90
+ "module",
91
+ {
92
+ "esm": true
93
+ }
94
+ ]
95
+ ],
96
+ "exclude": "{**/{__tests__,__fixtures__,__mocks__}/**,**/*.spec.*,**/*.test.*}"
97
+ },
98
+ "codegenConfig": {
99
+ "name": "GraniteMicroFrontendRuntimeSpec",
100
+ "type": "all",
101
+ "jsSrcsDir": "src/specs",
102
+ "android": {
103
+ "javaPackageName": "run.granite.microfrontend"
104
+ },
105
+ "ios": {
106
+ "componentProvider": {
107
+ "PortalView": "PortalView",
108
+ "PortalHostView": "PortalHostView"
109
+ }
110
+ }
111
+ },
112
+ "sideEffects": false
113
+ }
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ componentDescriptors: [
6
+ 'PortalViewComponentDescriptor',
7
+ 'PortalHostViewComponentDescriptor',
8
+ ],
9
+ cmakeListsPath: '../android/src/main/jni/CMakeLists.txt',
10
+ },
11
+ },
12
+ },
13
+ };
@@ -0,0 +1,22 @@
1
+ import { removePendingHostComponentRoutes } from './host/pendingHostComponentStore';
2
+ import { createMicroFrontendRuntimeWithDependencies } from './runtime/createMicroFrontendRuntime';
3
+ import { parseNativeRuntimeEvent } from './runtime/parseNativeRuntimeEvent';
4
+ import { microFrontendModuleRegistry } from './runtime/registry';
5
+ import NativeGraniteMicroFrontendRuntime from './specs/NativeGraniteMicroFrontendRuntime';
6
+ import type { MicroFrontendAdapter, MicroFrontendRuntimeApi } from './types';
7
+
8
+ export interface CreateMicroFrontendRuntimeOptions {
9
+ readonly adapter: MicroFrontendAdapter;
10
+ readonly onPreloadError?: (error: unknown) => void;
11
+ }
12
+
13
+ export function createMicroFrontendRuntime(options: CreateMicroFrontendRuntimeOptions): MicroFrontendRuntimeApi {
14
+ return createMicroFrontendRuntimeWithDependencies({
15
+ adapter: options.adapter,
16
+ nativeRuntime: NativeGraniteMicroFrontendRuntime,
17
+ onPreloadError: options.onPreloadError ?? (() => undefined),
18
+ parseEvent: parseNativeRuntimeEvent,
19
+ registry: microFrontendModuleRegistry,
20
+ removePendingHostComponentRoutes,
21
+ });
22
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ createRoute as createGraniteRoute,
3
+ getSchemeUri,
4
+ type RegisterScreenInput,
5
+ type RouteOptions,
6
+ useNavigation,
7
+ } from '@granite-js/react-native';
8
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
9
+ import { registerPendingHostComponentRoute } from './host/pendingHostComponentStore';
10
+ import type { PendingHostComponentAppConfig, PendingHostComponentRenderer } from './host/types';
11
+
12
+ export type MicroFrontendRouteOptions<TParams extends Readonly<object> | undefined> = RouteOptions<TParams> & {
13
+ readonly hostPendingComponent?: PendingHostComponentRenderer<TParams>;
14
+ };
15
+
16
+ type SetParamsFunction<TParams> = (params: TParams extends undefined ? undefined : Partial<TParams>) => void;
17
+ type ReplaceParamsFunction<TParams> = (params: TParams extends undefined ? undefined : TParams) => void;
18
+
19
+ type MicroFrontendRouteResult<TInput, TOutput> = {
20
+ readonly _path: keyof RegisterScreenInput;
21
+ readonly useNavigation: typeof useNavigation;
22
+ readonly useParams: () => TOutput;
23
+ readonly useSetParams: () => SetParamsFunction<TInput>;
24
+ readonly useReplaceParams: () => ReplaceParamsFunction<TInput>;
25
+ readonly _inputType: TInput;
26
+ readonly _outputType: TOutput;
27
+ };
28
+
29
+ export function createRoute<TSchema extends StandardSchemaV1<unknown, Readonly<object> | undefined>>(
30
+ path: keyof RegisterScreenInput,
31
+ options: Omit<RouteOptions<StandardSchemaV1.InferOutput<TSchema>>, 'validateParams'> & {
32
+ readonly validateParams: TSchema;
33
+ readonly hostPendingComponent?: PendingHostComponentRenderer<StandardSchemaV1.InferOutput<TSchema>>;
34
+ }
35
+ ): MicroFrontendRouteResult<StandardSchemaV1.InferInput<TSchema>, StandardSchemaV1.InferOutput<TSchema>>;
36
+ export function createRoute<TParams extends Readonly<object> | undefined>(
37
+ path: keyof RegisterScreenInput,
38
+ options: MicroFrontendRouteOptions<TParams>
39
+ ): MicroFrontendRouteResult<TParams, TParams>;
40
+ export function createRoute<TParams extends Readonly<object> | undefined>(
41
+ path: keyof RegisterScreenInput,
42
+ options: MicroFrontendRouteOptions<TParams>
43
+ ) {
44
+ const { hostPendingComponent, ...routeOptions } = options;
45
+
46
+ if (hostPendingComponent != null) {
47
+ const app = getCurrentGraniteApp();
48
+ if (app != null) {
49
+ registerPendingHostComponentRoute(String(path), {
50
+ app,
51
+ component: hostPendingComponent,
52
+ parserParams: routeOptions.parserParams,
53
+ validateParams: routeOptions.validateParams,
54
+ });
55
+ }
56
+ }
57
+
58
+ return createGraniteRoute(path, routeOptions);
59
+ }
60
+
61
+ function getCurrentGraniteApp(): PendingHostComponentAppConfig | null {
62
+ const granite: unknown = Reflect.get(globalThis, '__granite');
63
+ if (!isPropertyMap(granite) || !isPropertyMap(granite['app'])) {
64
+ return null;
65
+ }
66
+
67
+ const host = granite['app']['host'];
68
+ const name = granite['app']['name'];
69
+ if (typeof host !== 'string' || typeof name !== 'string') {
70
+ return null;
71
+ }
72
+
73
+ const scheme = new URL(getSchemeUri()).protocol.replace(/:$/g, '');
74
+
75
+ return {
76
+ name,
77
+ scheme,
78
+ host,
79
+ };
80
+ }
81
+
82
+ function isPropertyMap(value: unknown): value is Readonly<Record<PropertyKey, unknown>> {
83
+ return typeof value === 'object' && value !== null;
84
+ }