@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,133 @@
|
|
|
1
|
+
#import "PortalHostContainerView.h"
|
|
2
|
+
|
|
3
|
+
#import <React/RCTSurfaceTouchHandler.h>
|
|
4
|
+
#import "PortalHostView.h"
|
|
5
|
+
|
|
6
|
+
@implementation PortalHostContainerView {
|
|
7
|
+
PortalHostView *_portalHostView;
|
|
8
|
+
RCTSurfaceTouchHandler *_touchHandler;
|
|
9
|
+
NSString *_pendingName;
|
|
10
|
+
BOOL _hasAttachedContent;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
14
|
+
{
|
|
15
|
+
return [self initWithFrame:frame deferredActivation:NO];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
- (instancetype)initWithFrame:(CGRect)frame deferredActivation:(BOOL)deferredActivation
|
|
19
|
+
{
|
|
20
|
+
if (self = [super initWithFrame:frame]) {
|
|
21
|
+
if (!deferredActivation) {
|
|
22
|
+
[self activateIfNeeded];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return self;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (nullable instancetype)initWithCoder:(NSCoder *)coder
|
|
29
|
+
{
|
|
30
|
+
if (self = [super initWithCoder:coder]) {
|
|
31
|
+
[self activateIfNeeded];
|
|
32
|
+
}
|
|
33
|
+
return self;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (BOOL)isActivated
|
|
37
|
+
{
|
|
38
|
+
return _portalHostView != nil;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (BOOL)hasAttachedContent
|
|
42
|
+
{
|
|
43
|
+
return _hasAttachedContent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
- (void)activateIfNeeded
|
|
47
|
+
{
|
|
48
|
+
if (_portalHostView != nil) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_portalHostView = [[PortalHostView alloc] initWithFrame:self.bounds];
|
|
53
|
+
_portalHostView.autoresizingMask =
|
|
54
|
+
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
55
|
+
__weak PortalHostContainerView *weakSelf = self;
|
|
56
|
+
_portalHostView.onSubviewCountChanged = ^{
|
|
57
|
+
[weakSelf handleSubviewCountChanged];
|
|
58
|
+
};
|
|
59
|
+
[self addSubview:_portalHostView];
|
|
60
|
+
|
|
61
|
+
_touchHandler = [RCTSurfaceTouchHandler new];
|
|
62
|
+
[_touchHandler attachToView:self];
|
|
63
|
+
|
|
64
|
+
if (_pendingName) {
|
|
65
|
+
[_portalHostView setName:_pendingName];
|
|
66
|
+
_pendingName = nil;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
- (void)setName:(nullable NSString *)name
|
|
71
|
+
{
|
|
72
|
+
if (_portalHostView) {
|
|
73
|
+
[_portalHostView setName:name];
|
|
74
|
+
} else {
|
|
75
|
+
_pendingName = [name copy];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (void)handleSubviewCountChanged
|
|
80
|
+
{
|
|
81
|
+
NSUInteger count = _portalHostView.subviews.count;
|
|
82
|
+
if (count > 0) {
|
|
83
|
+
if (!_hasAttachedContent) {
|
|
84
|
+
_hasAttachedContent = YES;
|
|
85
|
+
if (self.onContentDidAttach) {
|
|
86
|
+
self.onContentDidAttach();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!_hasAttachedContent) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Removal and re-insertion can happen within a single mount commit (e.g.
|
|
97
|
+
// the content moves between hosts). Confirm emptiness on the next run loop
|
|
98
|
+
// before reporting a detach.
|
|
99
|
+
__weak PortalHostContainerView *weakSelf = self;
|
|
100
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
101
|
+
PortalHostContainerView *strongSelf = weakSelf;
|
|
102
|
+
if (!strongSelf || !strongSelf->_hasAttachedContent) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (strongSelf->_portalHostView.subviews.count == 0) {
|
|
106
|
+
strongSelf->_hasAttachedContent = NO;
|
|
107
|
+
if (strongSelf.onContentDidDetach) {
|
|
108
|
+
strongSelf.onContentDidDetach();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
- (void)invalidate
|
|
115
|
+
{
|
|
116
|
+
_pendingName = nil;
|
|
117
|
+
_portalHostView.onSubviewCountChanged = nil;
|
|
118
|
+
[_portalHostView setName:nil];
|
|
119
|
+
self.onContentDidAttach = nil;
|
|
120
|
+
self.onContentDidDetach = nil;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
- (void)dealloc
|
|
124
|
+
{
|
|
125
|
+
[_portalHostView setName:nil];
|
|
126
|
+
// Do not call -detachFromView: here. RCTSurfaceTouchHandler asserts that
|
|
127
|
+
// the recognizer is still attached to the given view, and during dealloc
|
|
128
|
+
// UIKit may have released that association already, turning teardown into
|
|
129
|
+
// an abort. UIView teardown removes the recognizer on its own.
|
|
130
|
+
_touchHandler = nil;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PortalHostView.h
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 02/09/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <React/RCTViewComponentView.h>
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
|
|
11
|
+
#ifndef PortalHostViewNativeComponent_h
|
|
12
|
+
#define PortalHostViewNativeComponent_h
|
|
13
|
+
|
|
14
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
15
|
+
|
|
16
|
+
@interface PortalHostView : RCTViewComponentView
|
|
17
|
+
|
|
18
|
+
- (void)setName:(nullable NSString *)name;
|
|
19
|
+
|
|
20
|
+
/// Called on the main thread whenever teleported content subviews are added
|
|
21
|
+
/// or removed. Removal notifies on the next run loop so observers read the
|
|
22
|
+
/// post-removal count. Set by native host owners (PortalHostContainerView);
|
|
23
|
+
/// stays nil for hosts mounted inside the React tree.
|
|
24
|
+
@property (nonatomic, copy, nullable) void (^onSubviewCountChanged)(void);
|
|
25
|
+
|
|
26
|
+
/// Returns the index at which a portal child should be inserted.
|
|
27
|
+
///
|
|
28
|
+
/// Within a single Fabric commit all mutations run synchronously on the main
|
|
29
|
+
/// thread. The first call in a commit records the current subview count as
|
|
30
|
+
/// the "base"; subsequent calls in the same commit reuse that base so that
|
|
31
|
+
/// bottom-to-top Fabric ordering is compensated by `insertSubview:atIndex:`.
|
|
32
|
+
/// A `dispatch_async` resets the flag after the commit finishes.
|
|
33
|
+
- (NSInteger)nextInsertionIndexForChildAt:(NSInteger)childIndex;
|
|
34
|
+
|
|
35
|
+
@end
|
|
36
|
+
|
|
37
|
+
NS_ASSUME_NONNULL_END
|
|
38
|
+
|
|
39
|
+
#endif /* PortalHostViewNativeComponent_h */
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PortalHostView.mm
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 02/09/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import "PortalHostView.h"
|
|
9
|
+
#import "PortalRegistry.h"
|
|
10
|
+
|
|
11
|
+
#import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/EventEmitters.h>
|
|
12
|
+
#import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/Props.h>
|
|
13
|
+
#import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/RCTComponentViewHelpers.h>
|
|
14
|
+
#import <react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewComponentDescriptor.h>
|
|
15
|
+
|
|
16
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
17
|
+
|
|
18
|
+
using namespace facebook::react;
|
|
19
|
+
|
|
20
|
+
@interface PortalHostView () <RCTPortalHostViewViewProtocol>
|
|
21
|
+
|
|
22
|
+
@property (nonatomic, strong) NSString *registeredName;
|
|
23
|
+
@property (nonatomic, assign) BOOL isInBatch;
|
|
24
|
+
@property (nonatomic, assign) NSInteger batchBaseIndex;
|
|
25
|
+
|
|
26
|
+
@end
|
|
27
|
+
|
|
28
|
+
@implementation PortalHostView
|
|
29
|
+
|
|
30
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
31
|
+
{
|
|
32
|
+
return concreteComponentDescriptorProvider<PortalHostViewComponentDescriptor>();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
36
|
+
{
|
|
37
|
+
if (self = [super initWithFrame:frame]) {
|
|
38
|
+
static const auto defaultProps = std::make_shared<const PortalHostViewProps>();
|
|
39
|
+
_props = defaultProps;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return self;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (void)setName:(nullable NSString *)name
|
|
46
|
+
{
|
|
47
|
+
if ([self.registeredName isEqualToString:name]) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (self.registeredName) {
|
|
52
|
+
[[PortalRegistry sharedInstance] unregisterHost:self withName:self.registeredName];
|
|
53
|
+
}
|
|
54
|
+
self.registeredName = name;
|
|
55
|
+
if (name) {
|
|
56
|
+
[[PortalRegistry sharedInstance] registerHost:self withName:name];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
- (void)didMoveToWindow
|
|
61
|
+
{
|
|
62
|
+
[super didMoveToWindow];
|
|
63
|
+
|
|
64
|
+
if (!self.registeredName) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (self.window) {
|
|
68
|
+
[[PortalRegistry sharedInstance] registerHost:self withName:self.registeredName];
|
|
69
|
+
} else {
|
|
70
|
+
[[PortalRegistry sharedInstance] unregisterHost:self withName:self.registeredName];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (void)didAddSubview:(UIView *)subview
|
|
75
|
+
{
|
|
76
|
+
[super didAddSubview:subview];
|
|
77
|
+
if (self.onSubviewCountChanged) {
|
|
78
|
+
self.onSubviewCountChanged();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
- (void)willRemoveSubview:(UIView *)subview
|
|
83
|
+
{
|
|
84
|
+
[super willRemoveSubview:subview];
|
|
85
|
+
if (!self.onSubviewCountChanged) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Notify on the next run loop so observers read the post-removal count.
|
|
89
|
+
__weak PortalHostView *weakSelf = self;
|
|
90
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
91
|
+
PortalHostView *strongSelf = weakSelf;
|
|
92
|
+
if (strongSelf && strongSelf.onSubviewCountChanged) {
|
|
93
|
+
strongSelf.onSubviewCountChanged();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
99
|
+
{
|
|
100
|
+
const auto &newViewProps = *std::static_pointer_cast<PortalHostViewProps const>(props);
|
|
101
|
+
|
|
102
|
+
std::string nameStr = newViewProps.name;
|
|
103
|
+
NSString *newName = nameStr.empty() ? nil : [NSString stringWithUTF8String:nameStr.c_str()];
|
|
104
|
+
[self setName:newName];
|
|
105
|
+
|
|
106
|
+
[super updateProps:props oldProps:oldProps];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
- (void)layoutSubviews
|
|
110
|
+
{
|
|
111
|
+
[super layoutSubviews];
|
|
112
|
+
|
|
113
|
+
if (self.registeredName) {
|
|
114
|
+
[[PortalRegistry sharedInstance] notifyHostLayoutChangedWithName:self.registeredName];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
- (NSInteger)nextInsertionIndexForChildAt:(NSInteger)childIndex
|
|
119
|
+
{
|
|
120
|
+
if (!self.isInBatch) {
|
|
121
|
+
self.isInBatch = YES;
|
|
122
|
+
self.batchBaseIndex = (NSInteger)self.subviews.count;
|
|
123
|
+
__weak PortalHostView *weakSelf = self;
|
|
124
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
125
|
+
weakSelf.isInBatch = NO;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return MIN(self.batchBaseIndex + childIndex, (NSInteger)self.subviews.count);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
- (void)prepareForRecycle
|
|
132
|
+
{
|
|
133
|
+
[super prepareForRecycle];
|
|
134
|
+
|
|
135
|
+
self.isInBatch = NO;
|
|
136
|
+
self.batchBaseIndex = 0;
|
|
137
|
+
|
|
138
|
+
[self setName:nil];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
- (void)dealloc
|
|
142
|
+
{
|
|
143
|
+
if (self.registeredName) {
|
|
144
|
+
[[PortalRegistry sharedInstance] unregisterHost:self withName:self.registeredName];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Class<RCTComponentViewProtocol> PortalHostViewCls(void)
|
|
149
|
+
{
|
|
150
|
+
return PortalHostView.class;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PortalRegistry.h
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 04/09/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <Foundation/Foundation.h>
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
|
|
11
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
12
|
+
|
|
13
|
+
@class PortalHostView;
|
|
14
|
+
@class PortalView;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Main-thread-only registry that joins Portal destinations to Portal views.
|
|
18
|
+
*
|
|
19
|
+
* Multiple hosts may share one name; resolve prefers the last registered host in
|
|
20
|
+
* the same window as the portal source, otherwise the last registered host that
|
|
21
|
+
* still has a window.
|
|
22
|
+
*/
|
|
23
|
+
@interface PortalRegistry : NSObject
|
|
24
|
+
|
|
25
|
+
+ (instancetype)sharedInstance;
|
|
26
|
+
|
|
27
|
+
- (void)registerHost:(PortalHostView *)host withName:(NSString *)name;
|
|
28
|
+
- (void)unregisterHost:(PortalHostView *)host withName:(NSString *)name;
|
|
29
|
+
- (nullable PortalHostView *)resolveHostWithName:(nullable NSString *)name
|
|
30
|
+
sourceView:(UIView *)sourceView;
|
|
31
|
+
- (void)notifyHostLayoutChangedWithName:(NSString *)name;
|
|
32
|
+
|
|
33
|
+
- (void)registerPendingPortal:(PortalView *)portal withHostName:(NSString *)hostName;
|
|
34
|
+
- (void)unregisterPendingPortal:(PortalView *)portal withHostName:(NSString *)hostName;
|
|
35
|
+
|
|
36
|
+
@end
|
|
37
|
+
|
|
38
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PortalRegistry.m
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 04/09/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import "PortalRegistry.h"
|
|
9
|
+
#import "PortalHostView.h"
|
|
10
|
+
#import "PortalView.h"
|
|
11
|
+
|
|
12
|
+
// NSPointerArray.compact is a documented no-op for sparse weak arrays unless a
|
|
13
|
+
// NULL pointer is appended first (long-standing Foundation behavior / rdar).
|
|
14
|
+
static void GranitePortalCompact(NSPointerArray *array)
|
|
15
|
+
{
|
|
16
|
+
if (array == nil) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
[array addPointer:NULL];
|
|
20
|
+
[array compact];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static NSArray *GranitePortalSnapshotNonNullPointers(NSPointerArray *array)
|
|
24
|
+
{
|
|
25
|
+
GranitePortalCompact(array);
|
|
26
|
+
NSMutableArray *snapshot = [NSMutableArray arrayWithCapacity:array.count];
|
|
27
|
+
for (NSUInteger i = 0; i < array.count; i++) {
|
|
28
|
+
void *pointer = [array pointerAtIndex:i];
|
|
29
|
+
if (pointer != NULL) {
|
|
30
|
+
[snapshot addObject:(__bridge id)pointer];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return snapshot;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@interface PortalRegistry ()
|
|
37
|
+
|
|
38
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSPointerArray *> *hosts;
|
|
39
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSPointerArray *> *pendingPortals;
|
|
40
|
+
|
|
41
|
+
@end
|
|
42
|
+
|
|
43
|
+
@implementation PortalRegistry
|
|
44
|
+
|
|
45
|
+
+ (instancetype)sharedInstance
|
|
46
|
+
{
|
|
47
|
+
static PortalRegistry *sharedInstance = nil;
|
|
48
|
+
static dispatch_once_t onceToken;
|
|
49
|
+
dispatch_once(&onceToken, ^{
|
|
50
|
+
sharedInstance = [[self alloc] init];
|
|
51
|
+
});
|
|
52
|
+
return sharedInstance;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
- (instancetype)init
|
|
56
|
+
{
|
|
57
|
+
self = [super init];
|
|
58
|
+
if (self) {
|
|
59
|
+
_hosts = [NSMutableDictionary dictionary];
|
|
60
|
+
_pendingPortals = [NSMutableDictionary dictionary];
|
|
61
|
+
}
|
|
62
|
+
return self;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
- (void)registerHost:(PortalHostView *)host withName:(NSString *)name
|
|
66
|
+
{
|
|
67
|
+
if (!name || !host) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
NSPointerArray *namedHosts = self.hosts[name];
|
|
72
|
+
if (!namedHosts) {
|
|
73
|
+
namedHosts = [NSPointerArray weakObjectsPointerArray];
|
|
74
|
+
self.hosts[name] = namedHosts;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
GranitePortalCompact(namedHosts);
|
|
78
|
+
for (NSInteger index = (NSInteger)namedHosts.count - 1; index >= 0; index--) {
|
|
79
|
+
PortalHostView *existingHost = (__bridge PortalHostView *)[namedHosts pointerAtIndex:index];
|
|
80
|
+
if (existingHost == host) {
|
|
81
|
+
[namedHosts removePointerAtIndex:index];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
[namedHosts addPointer:(__bridge void *)host];
|
|
85
|
+
[self notifySubscribersForName:name];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
- (void)unregisterHost:(PortalHostView *)host withName:(NSString *)name
|
|
89
|
+
{
|
|
90
|
+
if (!name || !host) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
NSPointerArray *namedHosts = self.hosts[name];
|
|
95
|
+
if (namedHosts) {
|
|
96
|
+
for (NSInteger index = (NSInteger)namedHosts.count - 1; index >= 0; index--) {
|
|
97
|
+
PortalHostView *existingHost = (__bridge PortalHostView *)[namedHosts pointerAtIndex:index];
|
|
98
|
+
if (!existingHost || existingHost == host) {
|
|
99
|
+
[namedHosts removePointerAtIndex:index];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
GranitePortalCompact(namedHosts);
|
|
103
|
+
if (namedHosts.count == 0) {
|
|
104
|
+
[self.hosts removeObjectForKey:name];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
[self notifySubscribersForName:name];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
- (void)notifySubscribersForName:(NSString *)name
|
|
111
|
+
{
|
|
112
|
+
NSPointerArray *portals = self.pendingPortals[name];
|
|
113
|
+
if (!portals) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Snapshot before notifying so a re-entrant register/unregister during
|
|
118
|
+
// onHostChanged cannot skip remaining subscribers.
|
|
119
|
+
NSArray *subscribers = GranitePortalSnapshotNonNullPointers(portals);
|
|
120
|
+
for (PortalView *portal in subscribers) {
|
|
121
|
+
[portal onHostChanged];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
- (nullable PortalHostView *)resolveHostWithName:(nullable NSString *)name
|
|
126
|
+
sourceView:(UIView *)sourceView
|
|
127
|
+
{
|
|
128
|
+
if (!name) {
|
|
129
|
+
return nil;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
NSPointerArray *namedHosts = self.hosts[name];
|
|
133
|
+
if (!namedHosts) {
|
|
134
|
+
return nil;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
GranitePortalCompact(namedHosts);
|
|
138
|
+
if (namedHosts.count == 0) {
|
|
139
|
+
[self.hosts removeObjectForKey:name];
|
|
140
|
+
return nil;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Last registered host wins when multiple hosts share a name. Prefer the host
|
|
144
|
+
// in the same window as the portal source when available.
|
|
145
|
+
UIWindow *sourceWindow = sourceView.window;
|
|
146
|
+
if (sourceWindow) {
|
|
147
|
+
for (NSInteger index = (NSInteger)namedHosts.count - 1; index >= 0; index--) {
|
|
148
|
+
PortalHostView *host = (__bridge PortalHostView *)[namedHosts pointerAtIndex:index];
|
|
149
|
+
if (host.window == sourceWindow) {
|
|
150
|
+
return host;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (NSInteger index = (NSInteger)namedHosts.count - 1; index >= 0; index--) {
|
|
156
|
+
PortalHostView *host = (__bridge PortalHostView *)[namedHosts pointerAtIndex:index];
|
|
157
|
+
if (host.window) {
|
|
158
|
+
return host;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return nil;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (void)notifyHostLayoutChangedWithName:(NSString *)name
|
|
165
|
+
{
|
|
166
|
+
if (!name) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
NSPointerArray *portals = self.pendingPortals[name];
|
|
171
|
+
if (!portals) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
NSArray *subscribers = GranitePortalSnapshotNonNullPointers(portals);
|
|
176
|
+
for (PortalView *portal in subscribers) {
|
|
177
|
+
[portal onHostLayoutChanged];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
- (void)registerPendingPortal:(PortalView *)portal withHostName:(NSString *)hostName
|
|
182
|
+
{
|
|
183
|
+
if (!hostName || !portal) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
NSPointerArray *portals = self.pendingPortals[hostName];
|
|
188
|
+
if (!portals) {
|
|
189
|
+
portals = [NSPointerArray weakObjectsPointerArray];
|
|
190
|
+
self.pendingPortals[hostName] = portals;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
GranitePortalCompact(portals);
|
|
194
|
+
for (NSInteger index = (NSInteger)portals.count - 1; index >= 0; index--) {
|
|
195
|
+
PortalView *existingPortal = (__bridge PortalView *)[portals pointerAtIndex:index];
|
|
196
|
+
if (existingPortal == portal) {
|
|
197
|
+
[portals removePointerAtIndex:index];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
[portals addPointer:(__bridge void *)portal];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
- (void)unregisterPendingPortal:(PortalView *)portal withHostName:(NSString *)hostName
|
|
204
|
+
{
|
|
205
|
+
if (!hostName || !portal) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
NSPointerArray *portals = self.pendingPortals[hostName];
|
|
210
|
+
if (!portals) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (NSUInteger i = 0; i < portals.count; i++) {
|
|
215
|
+
PortalView *existingPortal = (__bridge PortalView *)[portals pointerAtIndex:i];
|
|
216
|
+
if (existingPortal == portal) {
|
|
217
|
+
[portals removePointerAtIndex:i];
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
GranitePortalCompact(portals);
|
|
223
|
+
if (portals.count == 0) {
|
|
224
|
+
[self.pendingPortals removeObjectForKey:hostName];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@end
|
package/ios/PortalView.h
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PortalView.h
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 02/09/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <React/RCTViewComponentView.h>
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
|
|
11
|
+
#ifndef PortalViewNativeComponent_h
|
|
12
|
+
#define PortalViewNativeComponent_h
|
|
13
|
+
|
|
14
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fabric portal that reparents its React children under a named host.
|
|
18
|
+
*
|
|
19
|
+
* Child UIViewControllers under teleported content are reattached to the nearest
|
|
20
|
+
* parent of the destination view when content moves. Hosts that do not own a
|
|
21
|
+
* view controller will leave those children without a parent VC.
|
|
22
|
+
*/
|
|
23
|
+
@interface PortalView : RCTViewComponentView
|
|
24
|
+
|
|
25
|
+
- (void)onHostChanged;
|
|
26
|
+
- (void)onHostLayoutChanged;
|
|
27
|
+
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
NS_ASSUME_NONNULL_END
|
|
31
|
+
|
|
32
|
+
#endif /* PortalViewNativeComponent_h */
|