@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,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
@@ -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 */