@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,485 @@
1
+ #import "GraniteMicroFrontendRuntimeHost+Internal.h"
2
+ #import <objc/runtime.h>
3
+ #import <TargetConditionals.h>
4
+
5
+ #if TARGET_OS_IOS || TARGET_OS_TV
6
+ #import <UIKit/UIKit.h>
7
+ #define GRANITE_MICRO_FRONTEND_HAS_UIKIT 1
8
+ #else
9
+ #define GRANITE_MICRO_FRONTEND_HAS_UIKIT 0
10
+ #endif
11
+
12
+ @class GraniteMicroFrontendSessionEntry;
13
+
14
+ static NSRecursiveLock *runtimeLock;
15
+ static NSMutableDictionary<NSString *, GraniteMicroFrontendSessionEntry *> *sessions;
16
+ static NSMutableArray<NSDictionary *> *pendingEvents;
17
+ static __weak id<GraniteMicroFrontendRuntimeEventSink> eventSink;
18
+ static BOOL isDeliveringEvents;
19
+
20
+ static void GraniteMicroFrontendRequireMainThread(void) {
21
+ if (!NSThread.isMainThread) {
22
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
23
+ reason:@"Session bindings must be used on the main thread"
24
+ userInfo:nil];
25
+ }
26
+ }
27
+
28
+ @interface GraniteMicroFrontendSessionEntry : NSObject
29
+ @property(nonatomic, copy) NSString *token;
30
+ @property(nonatomic, assign) BOOL opened;
31
+ @property(nonatomic, assign) BOOL isVisible;
32
+ @property(nonatomic, assign) BOOL closed;
33
+ @end
34
+
35
+ @implementation GraniteMicroFrontendSessionEntry
36
+ @end
37
+
38
+ @interface GraniteMicroFrontendSessionRegistration ()
39
+ @property(nonatomic, copy) NSString *sessionId;
40
+ @property(nonatomic, copy) NSString *token;
41
+ @property(nonatomic, assign) BOOL invalidated;
42
+ @end
43
+
44
+ @class GraniteMicroFrontendSessionLifecycleObserverViewController;
45
+
46
+ #if GRANITE_MICRO_FRONTEND_HAS_UIKIT
47
+ @interface GraniteMicroFrontendViewControllerSessionBinding ()
48
+ @property(nonatomic, weak) UIViewController *viewController;
49
+ @property(nonatomic, strong) GraniteMicroFrontendSessionRegistration *registration;
50
+ @property(nonatomic, strong) GraniteMicroFrontendSessionLifecycleObserverViewController *observer;
51
+ @property(nonatomic, assign) BOOL viewAppeared;
52
+ @property(nonatomic, assign) BOOL applicationForeground;
53
+ @property(nonatomic, assign) BOOL invalidated;
54
+ - (void)updateVisibility;
55
+ @end
56
+ #endif
57
+
58
+ @interface GraniteMicroFrontendRuntimeHost ()
59
+ + (void)drainEventsToSink:(id<GraniteMicroFrontendRuntimeEventSink>)sink;
60
+ @end
61
+
62
+ @implementation GraniteMicroFrontendSessionRegistration
63
+
64
+ - (void)openAppWithAppName:(NSString *)appName scheme:(NSString *)scheme {
65
+ NSParameterAssert(appName.length > 0);
66
+ NSParameterAssert(scheme.length > 0);
67
+ BOOL shouldEmit = NO;
68
+ [runtimeLock lock];
69
+ @try {
70
+ GraniteMicroFrontendSessionEntry *entry = sessions[_sessionId];
71
+ if ([entry.token isEqualToString:_token] && !entry.opened && !entry.closed) {
72
+ entry.opened = YES;
73
+ shouldEmit = YES;
74
+ }
75
+ } @finally {
76
+ [runtimeLock unlock];
77
+ }
78
+ if (shouldEmit) {
79
+ [GraniteMicroFrontendRuntimeHost emitOpenApp:_sessionId appName:appName scheme:scheme];
80
+ }
81
+ }
82
+
83
+ - (BOOL)setVisible:(BOOL)isVisible {
84
+ BOOL shouldEmit = NO;
85
+ [runtimeLock lock];
86
+ @try {
87
+ GraniteMicroFrontendSessionEntry *entry = sessions[_sessionId];
88
+ if ([entry.token isEqualToString:_token] && entry.opened && !entry.closed &&
89
+ entry.isVisible != isVisible) {
90
+ entry.isVisible = isVisible;
91
+ shouldEmit = YES;
92
+ }
93
+ } @finally {
94
+ [runtimeLock unlock];
95
+ }
96
+ if (shouldEmit) {
97
+ [GraniteMicroFrontendRuntimeHost emitSessionVisibilityChanged:_sessionId
98
+ isVisible:isVisible];
99
+ }
100
+ return shouldEmit;
101
+ }
102
+
103
+ - (BOOL)closeApp {
104
+ BOOL shouldEmit = NO;
105
+ [runtimeLock lock];
106
+ @try {
107
+ GraniteMicroFrontendSessionEntry *entry = sessions[_sessionId];
108
+ if ([entry.token isEqualToString:_token] && entry.opened && !entry.closed) {
109
+ entry.closed = YES;
110
+ shouldEmit = YES;
111
+ }
112
+ } @finally {
113
+ [runtimeLock unlock];
114
+ }
115
+ if (shouldEmit) {
116
+ [GraniteMicroFrontendRuntimeHost emitCloseApp:_sessionId];
117
+ }
118
+ return shouldEmit;
119
+ }
120
+
121
+ - (void)invalidate {
122
+ @synchronized(self) {
123
+ if (_invalidated) {
124
+ return;
125
+ }
126
+ _invalidated = YES;
127
+ }
128
+ [GraniteMicroFrontendRuntimeHost unregisterSession:_sessionId token:_token];
129
+ }
130
+
131
+ - (void)dealloc {
132
+ [self invalidate];
133
+ }
134
+
135
+ @end
136
+
137
+ #if GRANITE_MICRO_FRONTEND_HAS_UIKIT
138
+
139
+ @interface GraniteMicroFrontendSessionLifecycleObserverViewController : UIViewController
140
+ @property(nonatomic, weak) GraniteMicroFrontendViewControllerSessionBinding *binding;
141
+ @end
142
+
143
+ @implementation GraniteMicroFrontendSessionLifecycleObserverViewController
144
+
145
+ - (void)viewWillAppear:(BOOL)animated {
146
+ [super viewWillAppear:animated];
147
+ _binding.viewAppeared = YES;
148
+ }
149
+
150
+ - (void)viewWillDisappear:(BOOL)animated {
151
+ _binding.viewAppeared = NO;
152
+ [super viewWillDisappear:animated];
153
+ }
154
+
155
+ - (void)dealloc {
156
+ [_binding invalidate];
157
+ }
158
+
159
+ @end
160
+
161
+ @implementation GraniteMicroFrontendViewControllerSessionBinding
162
+
163
+ static char GraniteMicroFrontendViewControllerSessionBindingKey;
164
+
165
+ + (nullable GraniteMicroFrontendViewControllerSessionBinding *)bindViewController:(UIViewController *)viewController
166
+ sessionId:(NSString *)sessionId
167
+ appName:(NSString *)appName
168
+ scheme:(NSString *)scheme {
169
+ GraniteMicroFrontendRequireMainThread();
170
+ NSParameterAssert(viewController != nil);
171
+ NSParameterAssert(sessionId.length > 0);
172
+ NSParameterAssert(appName.length > 0);
173
+ NSParameterAssert(scheme.length > 0);
174
+
175
+ GraniteMicroFrontendViewControllerSessionBinding *existingBinding =
176
+ objc_getAssociatedObject(viewController, &GraniteMicroFrontendViewControllerSessionBindingKey);
177
+ if (existingBinding != nil) {
178
+ return existingBinding;
179
+ }
180
+
181
+ GraniteMicroFrontendSessionRegistration *registration =
182
+ [GraniteMicroFrontendRuntimeHost registerSession:sessionId];
183
+ if (registration == nil) {
184
+ return nil;
185
+ }
186
+ GraniteMicroFrontendViewControllerSessionBinding *binding =
187
+ [[GraniteMicroFrontendViewControllerSessionBinding alloc] init];
188
+ GraniteMicroFrontendSessionLifecycleObserverViewController *observer =
189
+ [[GraniteMicroFrontendSessionLifecycleObserverViewController alloc] init];
190
+ observer.binding = binding;
191
+ binding.viewController = viewController;
192
+ binding.registration = registration;
193
+ binding.observer = observer;
194
+ binding.applicationForeground =
195
+ UIApplication.sharedApplication.applicationState != UIApplicationStateBackground;
196
+
197
+ [NSNotificationCenter.defaultCenter addObserver:binding
198
+ selector:@selector(applicationWillEnterForeground)
199
+ name:UIApplicationWillEnterForegroundNotification
200
+ object:nil];
201
+ [NSNotificationCenter.defaultCenter addObserver:binding
202
+ selector:@selector(applicationDidEnterBackground)
203
+ name:UIApplicationDidEnterBackgroundNotification
204
+ object:nil];
205
+
206
+ [viewController addChildViewController:observer];
207
+ observer.view.hidden = YES;
208
+ observer.view.frame = CGRectZero;
209
+ observer.view.userInteractionEnabled = NO;
210
+ [viewController.view addSubview:observer.view];
211
+ [observer didMoveToParentViewController:viewController];
212
+
213
+ objc_setAssociatedObject(
214
+ viewController,
215
+ &GraniteMicroFrontendViewControllerSessionBindingKey,
216
+ binding,
217
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
218
+ [registration openAppWithAppName:appName scheme:scheme];
219
+ [binding updateVisibility];
220
+ return binding;
221
+ }
222
+
223
+ - (void)setViewAppeared:(BOOL)viewAppeared {
224
+ if (_viewAppeared == viewAppeared) {
225
+ return;
226
+ }
227
+ _viewAppeared = viewAppeared;
228
+ [self updateVisibility];
229
+ }
230
+
231
+ - (void)setApplicationForeground:(BOOL)applicationForeground {
232
+ if (_applicationForeground == applicationForeground) {
233
+ return;
234
+ }
235
+ _applicationForeground = applicationForeground;
236
+ [self updateVisibility];
237
+ }
238
+
239
+ - (void)applicationWillEnterForeground {
240
+ self.applicationForeground = YES;
241
+ }
242
+
243
+ - (void)applicationDidEnterBackground {
244
+ self.applicationForeground = NO;
245
+ }
246
+
247
+ - (void)updateVisibility {
248
+ GraniteMicroFrontendRequireMainThread();
249
+ BOOL invalidated = NO;
250
+ @synchronized(self) {
251
+ invalidated = _invalidated;
252
+ }
253
+ if (invalidated) {
254
+ return;
255
+ }
256
+ [_registration setVisible:(_viewAppeared && _applicationForeground)];
257
+ }
258
+
259
+ - (void)invalidate {
260
+ GraniteMicroFrontendRequireMainThread();
261
+ @synchronized(self) {
262
+ if (_invalidated) {
263
+ return;
264
+ }
265
+ _invalidated = YES;
266
+ }
267
+
268
+ [NSNotificationCenter.defaultCenter removeObserver:self
269
+ name:UIApplicationWillEnterForegroundNotification
270
+ object:nil];
271
+ [NSNotificationCenter.defaultCenter removeObserver:self
272
+ name:UIApplicationDidEnterBackgroundNotification
273
+ object:nil];
274
+
275
+ UIViewController *viewController = _viewController;
276
+ _observer.binding = nil;
277
+ if (_observer.parentViewController != nil) {
278
+ [_observer willMoveToParentViewController:nil];
279
+ [_observer.view removeFromSuperview];
280
+ [_observer removeFromParentViewController];
281
+ }
282
+ [_registration closeApp];
283
+ [_registration invalidate];
284
+ if (viewController != nil &&
285
+ objc_getAssociatedObject(viewController, &GraniteMicroFrontendViewControllerSessionBindingKey) == self) {
286
+ objc_setAssociatedObject(
287
+ viewController,
288
+ &GraniteMicroFrontendViewControllerSessionBindingKey,
289
+ nil,
290
+ OBJC_ASSOCIATION_ASSIGN);
291
+ }
292
+ }
293
+
294
+ - (void)dealloc {
295
+ [self invalidate];
296
+ }
297
+
298
+ @end
299
+
300
+ #else
301
+
302
+ @implementation GraniteMicroFrontendViewControllerSessionBinding
303
+
304
+ + (nullable GraniteMicroFrontendViewControllerSessionBinding *)bindViewController:(UIViewController *)viewController
305
+ sessionId:(NSString *)sessionId
306
+ appName:(NSString *)appName
307
+ scheme:(NSString *)scheme {
308
+ NSParameterAssert(NO);
309
+ return nil;
310
+ }
311
+
312
+ - (void)invalidate {
313
+ }
314
+
315
+ @end
316
+
317
+ #endif
318
+
319
+ @implementation GraniteMicroFrontendRuntimeHost
320
+
321
+ + (void)initialize {
322
+ if (self != GraniteMicroFrontendRuntimeHost.class) {
323
+ return;
324
+ }
325
+ runtimeLock = [[NSRecursiveLock alloc] init];
326
+ sessions = [[NSMutableDictionary alloc] init];
327
+ pendingEvents = [[NSMutableArray alloc] init];
328
+ }
329
+
330
+ + (nullable GraniteMicroFrontendSessionRegistration *)registerSession:(NSString *)sessionId {
331
+ NSParameterAssert(sessionId.length > 0);
332
+
333
+ NSString *token = NSUUID.UUID.UUIDString;
334
+ GraniteMicroFrontendSessionEntry *entry = [[GraniteMicroFrontendSessionEntry alloc] init];
335
+ entry.token = token;
336
+
337
+ [runtimeLock lock];
338
+ if (sessions[sessionId] != nil) {
339
+ [runtimeLock unlock];
340
+ // Do not throw: Swift cannot catch NSException and a reuse race during
341
+ // VC teardown would crash the host. Callers must use unique sessionIds and
342
+ // invalidate the previous registration before reusing an id.
343
+ return nil;
344
+ }
345
+ sessions[sessionId] = entry;
346
+ [runtimeLock unlock];
347
+
348
+ GraniteMicroFrontendSessionRegistration *registration =
349
+ [[GraniteMicroFrontendSessionRegistration alloc] init];
350
+ registration.sessionId = sessionId;
351
+ registration.token = token;
352
+ return registration;
353
+ }
354
+
355
+ + (void)emitPreloadApp:(NSString *)appName {
356
+ [self emitEvent:@{ @"name" : @"preloadApp", @"params" : @{ @"appName" : appName } }];
357
+ }
358
+
359
+ + (void)emitOpenApp:(NSString *)sessionId
360
+ appName:(NSString *)appName
361
+ scheme:(NSString *)scheme {
362
+ [self emitEvent:@{
363
+ @"name" : @"openApp",
364
+ @"params" : @{
365
+ @"sessionId" : sessionId,
366
+ @"appName" : appName,
367
+ @"scheme" : scheme,
368
+ },
369
+ }];
370
+ }
371
+
372
+ + (void)emitCloseApp:(NSString *)sessionId {
373
+ [self emitEvent:@{ @"name" : @"closeApp", @"params" : @{ @"sessionId" : sessionId } }];
374
+ }
375
+
376
+ + (void)emitSessionVisibilityChanged:(NSString *)sessionId
377
+ isVisible:(BOOL)isVisible {
378
+ [self emitEvent:@{
379
+ @"name" : @"sessionVisibilityChanged",
380
+ @"params" : @{
381
+ @"sessionId" : sessionId,
382
+ @"isVisible" : @(isVisible),
383
+ },
384
+ }];
385
+ }
386
+
387
+ + (void)emitEvent:(NSDictionary *)event {
388
+ [runtimeLock lock];
389
+ [pendingEvents addObject:event];
390
+ id<GraniteMicroFrontendRuntimeEventSink> sink = eventSink;
391
+ BOOL shouldDrain = sink != nil && !isDeliveringEvents;
392
+ if (shouldDrain) {
393
+ isDeliveringEvents = YES;
394
+ }
395
+ [runtimeLock unlock];
396
+ if (shouldDrain) {
397
+ [self drainEventsToSink:sink];
398
+ }
399
+ }
400
+
401
+ + (void)startEventDeliveryToEventSink:(id<GraniteMicroFrontendRuntimeEventSink>)sink {
402
+ [runtimeLock lock];
403
+ if (eventSink != nil && eventSink != sink) {
404
+ [runtimeLock unlock];
405
+ return;
406
+ }
407
+ eventSink = sink;
408
+ BOOL shouldDrain = !isDeliveringEvents && pendingEvents.count > 0;
409
+ if (shouldDrain) {
410
+ isDeliveringEvents = YES;
411
+ }
412
+ [runtimeLock unlock];
413
+ if (shouldDrain) {
414
+ [self drainEventsToSink:sink];
415
+ }
416
+ }
417
+
418
+ + (void)drainEventsToSink:(id<GraniteMicroFrontendRuntimeEventSink>)sink {
419
+ id<GraniteMicroFrontendRuntimeEventSink> currentSink = sink;
420
+ while (YES) {
421
+ [runtimeLock lock];
422
+ id<GraniteMicroFrontendRuntimeEventSink> activeSink = eventSink;
423
+ if (activeSink == nil || pendingEvents.count == 0) {
424
+ isDeliveringEvents = NO;
425
+ [runtimeLock unlock];
426
+ return;
427
+ }
428
+ currentSink = activeSink;
429
+ NSDictionary *event = pendingEvents.firstObject;
430
+ [runtimeLock unlock];
431
+
432
+ @try {
433
+ if (![currentSink enqueueRuntimeEvent:event]) {
434
+ [runtimeLock lock];
435
+ isDeliveringEvents = NO;
436
+ id<GraniteMicroFrontendRuntimeEventSink> replacementSink = eventSink;
437
+ BOOL shouldResumeWithReplacement =
438
+ replacementSink != nil && replacementSink != currentSink && pendingEvents.count > 0;
439
+ if (shouldResumeWithReplacement) {
440
+ isDeliveringEvents = YES;
441
+ }
442
+ [runtimeLock unlock];
443
+ if (shouldResumeWithReplacement) {
444
+ [self drainEventsToSink:replacementSink];
445
+ }
446
+ return;
447
+ }
448
+ [runtimeLock lock];
449
+ [pendingEvents removeObjectAtIndex:0];
450
+ [runtimeLock unlock];
451
+ } @catch (NSException *exception) {
452
+ [runtimeLock lock];
453
+ isDeliveringEvents = NO;
454
+ id<GraniteMicroFrontendRuntimeEventSink> replacementSink = eventSink;
455
+ BOOL shouldResumeWithReplacement =
456
+ replacementSink != nil && replacementSink != currentSink && pendingEvents.count > 0;
457
+ if (shouldResumeWithReplacement) {
458
+ isDeliveringEvents = YES;
459
+ }
460
+ [runtimeLock unlock];
461
+ if (shouldResumeWithReplacement) {
462
+ [self drainEventsToSink:replacementSink];
463
+ }
464
+ @throw exception;
465
+ }
466
+ }
467
+ }
468
+
469
+ + (void)detachEventSink:(id<GraniteMicroFrontendRuntimeEventSink>)sink {
470
+ [runtimeLock lock];
471
+ if (eventSink == sink) {
472
+ eventSink = nil;
473
+ }
474
+ [runtimeLock unlock];
475
+ }
476
+
477
+ + (void)unregisterSession:(NSString *)sessionId token:(NSString *)token {
478
+ [runtimeLock lock];
479
+ if ([sessions[sessionId].token isEqualToString:token]) {
480
+ [sessions removeObjectForKey:sessionId];
481
+ }
482
+ [runtimeLock unlock];
483
+ }
484
+
485
+ @end
@@ -0,0 +1,6 @@
1
+ #import <GraniteMicroFrontendRuntimeSpec/GraniteMicroFrontendRuntimeSpec.h>
2
+ #import <React/RCTBridgeModule.h>
3
+
4
+ @interface GraniteMicroFrontendRuntimeModule
5
+ : NativeGraniteMicroFrontendRuntimeSpecBase <NativeGraniteMicroFrontendRuntimeSpec>
6
+ @end
@@ -0,0 +1,138 @@
1
+ #import "GraniteMicroFrontendRuntimeModule.h"
2
+
3
+ #import "GraniteMicroFrontendRuntimeHost+Internal.h"
4
+ #import <React/RCTBridge.h>
5
+ #import <React/RCTBridgeProxy+Cxx.h>
6
+ #import <React-callinvoker/ReactCommon/CallInvoker.h>
7
+ #import <jsi/jsi.h>
8
+
9
+ static NSString *const GraniteMicroFrontendRuntimeErrorDomain =
10
+ @"GraniteMicroFrontendRuntime";
11
+
12
+ @interface RCTBridge (GraniteMicroFrontendRuntime)
13
+ - (void *)runtime;
14
+ - (std::shared_ptr<facebook::react::CallInvoker>)jsCallInvoker;
15
+ @end
16
+
17
+ @interface GraniteMicroFrontendRuntimeModule () <GraniteMicroFrontendRuntimeEventSink>
18
+ @property(nonatomic, weak) RCTBridge *bridge;
19
+ - (BOOL)scheduleRuntimeEvent:(NSDictionary *)event;
20
+ @end
21
+
22
+ @implementation GraniteMicroFrontendRuntimeModule
23
+
24
+ RCT_EXPORT_MODULE(GraniteMicroFrontendRuntime)
25
+
26
+ + (BOOL)requiresMainQueueSetup {
27
+ return NO;
28
+ }
29
+
30
+ - (void)dealloc {
31
+ [GraniteMicroFrontendRuntimeHost detachEventSink:self];
32
+ }
33
+
34
+ - (std::shared_ptr<facebook::react::CallInvoker>)jsCallInvokerIfAvailable {
35
+ RCTBridge *bridge = self.bridge;
36
+ if (bridge == nil) {
37
+ return nullptr;
38
+ }
39
+ // Prefer the public CallInvoker on RCTBridge / bridge proxy without casting
40
+ // to a concrete bridge subclass.
41
+ return bridge.jsCallInvoker;
42
+ }
43
+
44
+ - (facebook::jsi::Runtime *)jsRuntimeIfAvailable {
45
+ RCTBridge *bridge = self.bridge;
46
+ if (bridge == nil || ![bridge respondsToSelector:@selector(runtime)]) {
47
+ return nullptr;
48
+ }
49
+ return reinterpret_cast<facebook::jsi::Runtime *>(bridge.runtime);
50
+ }
51
+
52
+ - (void)evaluateScript:(JS::NativeGraniteMicroFrontendRuntime::EvaluateScriptRequest &)request
53
+ resolve:(RCTPromiseResolveBlock)resolve
54
+ reject:(RCTPromiseRejectBlock)reject {
55
+ NSString *filePath = request.filePath();
56
+ if (![filePath isAbsolutePath]) {
57
+ reject(@"INVALID_BUNDLE_PATH", @"Bundle path must be absolute", nil);
58
+ return;
59
+ }
60
+
61
+ __weak GraniteMicroFrontendRuntimeModule *weakSelf = self;
62
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
63
+ NSError *readError = nil;
64
+ NSData *scriptData = [NSData dataWithContentsOfFile:filePath
65
+ options:NSDataReadingMappedIfSafe
66
+ error:&readError];
67
+ if (scriptData == nil) {
68
+ reject(@"BUNDLE_READ_FAILED", readError.localizedDescription, readError);
69
+ return;
70
+ }
71
+
72
+ std::string source{static_cast<const char *>(scriptData.bytes), scriptData.length};
73
+ std::string sourceUrl{[NSURL fileURLWithPath:filePath].absoluteString.UTF8String};
74
+ GraniteMicroFrontendRuntimeModule *strongSelf = weakSelf;
75
+ if (strongSelf == nil) {
76
+ reject(@"RUNTIME_UNAVAILABLE", @"JavaScript runtime is unavailable", nil);
77
+ return;
78
+ }
79
+
80
+ std::shared_ptr<facebook::react::CallInvoker> callInvoker =
81
+ [strongSelf jsCallInvokerIfAvailable];
82
+ if (callInvoker == nullptr) {
83
+ reject(@"RUNTIME_UNAVAILABLE", @"JavaScript runtime is unavailable", nil);
84
+ return;
85
+ }
86
+
87
+ // Read the JSI runtime only on the JS queue owned by callInvoker.
88
+ callInvoker->invokeAsync([strongSelf,
89
+ source = std::move(source),
90
+ sourceUrl = std::move(sourceUrl),
91
+ resolve,
92
+ reject]() mutable {
93
+ facebook::jsi::Runtime *runtime = [strongSelf jsRuntimeIfAvailable];
94
+ if (runtime == nullptr) {
95
+ reject(@"RUNTIME_UNAVAILABLE", @"JavaScript runtime is unavailable", nil);
96
+ return;
97
+ }
98
+ try {
99
+ runtime->evaluateJavaScript(
100
+ std::make_unique<facebook::jsi::StringBuffer>(std::move(source)),
101
+ sourceUrl);
102
+ resolve(nil);
103
+ } catch (const std::exception &exception) {
104
+ NSString *message = [NSString stringWithUTF8String:exception.what()];
105
+ NSError *error = [NSError errorWithDomain:GraniteMicroFrontendRuntimeErrorDomain
106
+ code:500
107
+ userInfo:@{ NSLocalizedDescriptionKey : message }];
108
+ reject(@"EVALUATE_SCRIPT_FAILED", message, error);
109
+ }
110
+ });
111
+ });
112
+ }
113
+
114
+ - (void)startEventDelivery {
115
+ [GraniteMicroFrontendRuntimeHost startEventDeliveryToEventSink:self];
116
+ }
117
+
118
+ - (BOOL)enqueueRuntimeEvent:(NSDictionary *)event {
119
+ return [self scheduleRuntimeEvent:event];
120
+ }
121
+
122
+ - (BOOL)scheduleRuntimeEvent:(NSDictionary *)event {
123
+ std::shared_ptr<facebook::react::CallInvoker> callInvoker = [self jsCallInvokerIfAvailable];
124
+ if (callInvoker == nullptr) {
125
+ return NO;
126
+ }
127
+ callInvoker->invokeAsync([self, event] {
128
+ [self emitOnEvent:event];
129
+ });
130
+ return YES;
131
+ }
132
+
133
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
134
+ (const facebook::react::ObjCTurboModule::InitParams &)params {
135
+ return std::make_shared<facebook::react::NativeGraniteMicroFrontendRuntimeSpecJSI>(params);
136
+ }
137
+
138
+ @end
@@ -0,0 +1,56 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /// UIKit-owned container for hosting teleported portal content outside the
6
+ /// React tree. This header intentionally imports UIKit only, so native
7
+ /// integrations can include it in build configurations where the React
8
+ /// renderer headers are unavailable (e.g. prebuilt React distributions).
9
+ @interface PortalHostContainerView : UIView
10
+
11
+ /// Creates a container that activates immediately (see `-activateIfNeeded`).
12
+ - (instancetype)initWithFrame:(CGRect)frame;
13
+
14
+ /// Creates a container that postpones creating the underlying Fabric host
15
+ /// view until `-activateIfNeeded` is called.
16
+ ///
17
+ /// Deferred activation exists because constructing the host view runs
18
+ /// `RCTViewComponentView` initialization, which reads ReactNativeFeatureFlags.
19
+ /// React only allows overriding those flags before their first read, so a
20
+ /// container created before the app's React factory has booted (a parked
21
+ /// native destination, for example) would otherwise make the boot-time
22
+ /// override throw. Defer activation and call `-activateIfNeeded` once the
23
+ /// runtime is up — typically right before asking React to mount content.
24
+ - (instancetype)initWithFrame:(CGRect)frame deferredActivation:(BOOL)deferredActivation;
25
+
26
+ /// Registers the container under `name` so `<Portal hostName={name}>` content
27
+ /// teleports into it. Safe to call before activation; the name is applied
28
+ /// when the host view is created. Pass nil to unregister.
29
+ - (void)setName:(nullable NSString *)name;
30
+
31
+ /// Creates the underlying Fabric host view, attaches the touch handler, and
32
+ /// applies any pending name. No-op when already activated. Must be called on
33
+ /// the main thread after the React runtime has booted.
34
+ - (void)activateIfNeeded;
35
+
36
+ /// Whether the underlying Fabric host view has been created.
37
+ @property (nonatomic, readonly) BOOL isActivated;
38
+
39
+ /// Whether any teleported content view is currently attached.
40
+ @property (nonatomic, readonly) BOOL hasAttachedContent;
41
+
42
+ /// Called on the main thread when the first content view attaches.
43
+ @property (nonatomic, copy, nullable) void (^onContentDidAttach)(void);
44
+
45
+ /// Called on the main thread when the last content view detaches. Transient
46
+ /// remove-and-reinsert churn within one mount commit is re-checked on the
47
+ /// next run loop before this fires.
48
+ @property (nonatomic, copy, nullable) void (^onContentDidDetach)(void);
49
+
50
+ /// Unregisters the host and clears lifecycle callbacks. Call when the owning
51
+ /// screen is torn down; the container stops receiving teleported content.
52
+ - (void)invalidate;
53
+
54
+ @end
55
+
56
+ NS_ASSUME_NONNULL_END