@granite-js/micro-frontend 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GraniteMicroFrontendRuntime.podspec +40 -0
- package/LICENSE +202 -0
- package/LICENSE.react-native-teleport +21 -0
- package/NOTICE +17 -0
- package/README.md +508 -0
- package/android/CMakeLists.txt +34 -0
- package/android/build.gradle +74 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/BundleEvaluator.cpp +64 -0
- package/android/src/main/cpp/BundleEvaluator.h +20 -0
- package/android/src/main/cpp/FileReader.cpp +92 -0
- package/android/src/main/cpp/FileReader.h +27 -0
- package/android/src/main/cpp/onLoad.cpp +9 -0
- package/android/src/main/java/com/teleport/extensions/Float.kt +12 -0
- package/android/src/main/java/com/teleport/extensions/View.kt +10 -0
- package/android/src/main/java/com/teleport/global/PortalRegistry.kt +117 -0
- package/android/src/main/java/com/teleport/host/PortalHostView.kt +97 -0
- package/android/src/main/java/com/teleport/host/PortalHostViewManager.kt +41 -0
- package/android/src/main/java/com/teleport/host/PortalReactRootView.kt +116 -0
- package/android/src/main/java/com/teleport/managers/TeleportViewManager.kt +27 -0
- package/android/src/main/java/com/teleport/portal/PortalLayoutStateController.kt +107 -0
- package/android/src/main/java/com/teleport/portal/PortalView.kt +309 -0
- package/android/src/main/java/com/teleport/portal/PortalViewManager.kt +51 -0
- package/android/src/main/java/run/granite/microfrontend/ActivitySessionBinding.kt +87 -0
- package/android/src/main/java/run/granite/microfrontend/BundleEvaluator.kt +46 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendEvent.kt +62 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouter.kt +186 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeHost.kt +42 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeModule.kt +56 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimePackage.kt +35 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendSessionStore.kt +116 -0
- package/android/src/main/jni/CMakeLists.txt +64 -0
- package/android/src/test/cpp/CMakeLists.txt +35 -0
- package/android/src/test/cpp/FileReaderTest.cpp +44 -0
- package/android/src/test/java/com/teleport/host/PortalReactRootViewContractTest.kt +76 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouterTest.kt +216 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendSessionStoreTest.kt +190 -0
- package/android/test-cpp.sh +12 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/ComponentDescriptors.h +24 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.cpp +31 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.h +29 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewComponentDescriptor.h +22 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.h +43 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewState.h +21 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewComponentDescriptor.h +54 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.h +51 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewState.h +43 -0
- package/dist/module/createMicroFrontendRuntime.js +18 -0
- package/dist/module/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/createRoute.js +43 -0
- package/dist/module/createRoute.js.map +1 -0
- package/dist/module/host/PendingHostComponent.js +70 -0
- package/dist/module/host/PendingHostComponent.js.map +1 -0
- package/dist/module/host/index.js +6 -0
- package/dist/module/host/index.js.map +1 -0
- package/dist/module/host/pendingHostComponentStore.js +164 -0
- package/dist/module/host/pendingHostComponentStore.js.map +1 -0
- package/dist/module/host/resolveParams.js +60 -0
- package/dist/module/host/resolveParams.js.map +1 -0
- package/dist/module/host/routeMatcher.js +92 -0
- package/dist/module/host/routeMatcher.js.map +1 -0
- package/dist/module/host/types.js +4 -0
- package/dist/module/host/types.js.map +1 -0
- package/dist/module/index.js +12 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/package.json +1 -0
- package/dist/module/plugin/index.js +4 -0
- package/dist/module/plugin/index.js.map +1 -0
- package/dist/module/plugin/intoShared.js +32 -0
- package/dist/module/plugin/intoShared.js.map +1 -0
- package/dist/module/plugin/microFrontendPlugin.js +58 -0
- package/dist/module/plugin/microFrontendPlugin.js.map +1 -0
- package/dist/module/plugin/prelude.js +23 -0
- package/dist/module/plugin/prelude.js.map +1 -0
- package/dist/module/plugin/resolver.js +25 -0
- package/dist/module/plugin/resolver.js.map +1 -0
- package/dist/module/plugin/types.js +4 -0
- package/dist/module/plugin/types.js.map +1 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js +79 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/runtime/errors.js +63 -0
- package/dist/module/runtime/errors.js.map +1 -0
- package/dist/module/runtime/parseAppRequest.js +16 -0
- package/dist/module/runtime/parseAppRequest.js.map +1 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js +54 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js.map +1 -0
- package/dist/module/runtime/registry.js +87 -0
- package/dist/module/runtime/registry.js.map +1 -0
- package/dist/module/session/MicroFrontendSessionContext.js +32 -0
- package/dist/module/session/MicroFrontendSessionContext.js.map +1 -0
- package/dist/module/session/useMicroFrontendSessions.js +47 -0
- package/dist/module/session/useMicroFrontendSessions.js.map +1 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js +21 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js.map +1 -0
- package/dist/module/specs/PortalHostViewNativeComponent.ts +9 -0
- package/dist/module/specs/PortalViewNativeComponent.ts +9 -0
- package/dist/module/types.js +2 -0
- package/dist/module/types.js.map +1 -0
- package/dist/typescript/createMicroFrontendRuntime.d.ts +6 -0
- package/dist/typescript/createRoute.d.ts +23 -0
- package/dist/typescript/host/PendingHostComponent.d.ts +14 -0
- package/dist/typescript/host/index.d.ts +5 -0
- package/dist/typescript/host/pendingHostComponentStore.d.ts +11 -0
- package/dist/typescript/host/resolveParams.d.ts +10 -0
- package/dist/typescript/host/routeMatcher.d.ts +7 -0
- package/dist/typescript/host/types.d.ts +32 -0
- package/dist/typescript/index.d.ts +16 -0
- package/dist/typescript/plugin/index.d.ts +2 -0
- package/dist/typescript/plugin/intoShared.d.ts +3 -0
- package/dist/typescript/plugin/microFrontendPlugin.d.ts +3 -0
- package/dist/typescript/plugin/prelude.d.ts +6 -0
- package/dist/typescript/plugin/resolver.d.ts +3 -0
- package/dist/typescript/plugin/types.d.ts +5 -0
- package/dist/typescript/runtime/createMicroFrontendRuntime.d.ts +31 -0
- package/dist/typescript/runtime/errors.d.ts +35 -0
- package/dist/typescript/runtime/parseAppRequest.d.ts +6 -0
- package/dist/typescript/runtime/parseNativeRuntimeEvent.d.ts +3 -0
- package/dist/typescript/runtime/registry.d.ts +35 -0
- package/dist/typescript/session/MicroFrontendSessionContext.d.ts +13 -0
- package/dist/typescript/session/useMicroFrontendSessions.d.ts +8 -0
- package/dist/typescript/specs/NativeGraniteMicroFrontendRuntime.d.ts +21 -0
- package/dist/typescript/specs/PortalHostViewNativeComponent.d.ts +6 -0
- package/dist/typescript/specs/PortalViewNativeComponent.d.ts +6 -0
- package/dist/typescript/types.d.ts +49 -0
- package/ios/GraniteMicroFrontendRuntimeHost+Internal.h +18 -0
- package/ios/GraniteMicroFrontendRuntimeHost.h +34 -0
- package/ios/GraniteMicroFrontendRuntimeHost.mm +485 -0
- package/ios/GraniteMicroFrontendRuntimeModule.h +6 -0
- package/ios/GraniteMicroFrontendRuntimeModule.mm +138 -0
- package/ios/PortalHostContainerView.h +56 -0
- package/ios/PortalHostContainerView.mm +133 -0
- package/ios/PortalHostView.h +39 -0
- package/ios/PortalHostView.mm +153 -0
- package/ios/PortalRegistry.h +38 -0
- package/ios/PortalRegistry.mm +228 -0
- package/ios/PortalView.h +32 -0
- package/ios/PortalView.mm +426 -0
- package/package.json +113 -0
- package/react-native.config.js +13 -0
- package/src/createMicroFrontendRuntime.ts +22 -0
- package/src/createRoute.ts +84 -0
- package/src/host/PendingHostComponent.tsx +92 -0
- package/src/host/index.ts +26 -0
- package/src/host/pendingHostComponentStore.ts +241 -0
- package/src/host/resolveParams.ts +70 -0
- package/src/host/routeMatcher.ts +117 -0
- package/src/host/types.ts +45 -0
- package/src/index.ts +57 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/intoShared.ts +50 -0
- package/src/plugin/microFrontendPlugin.ts +57 -0
- package/src/plugin/prelude.ts +73 -0
- package/src/plugin/resolver.ts +73 -0
- package/src/plugin/types.ts +6 -0
- package/src/runtime/createMicroFrontendRuntime.ts +115 -0
- package/src/runtime/errors.ts +86 -0
- package/src/runtime/parseAppRequest.ts +22 -0
- package/src/runtime/parseNativeRuntimeEvent.ts +54 -0
- package/src/runtime/registry.ts +131 -0
- package/src/session/MicroFrontendSessionContext.tsx +45 -0
- package/src/session/useMicroFrontendSessions.ts +73 -0
- package/src/specs/NativeGraniteMicroFrontendRuntime.ts +44 -0
- package/src/specs/PortalHostViewNativeComponent.ts +9 -0
- package/src/specs/PortalViewNativeComponent.ts +9 -0
- package/src/types.ts +55 -0
|
@@ -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,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
|
+
}
|