@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
package/README.md ADDED
@@ -0,0 +1,508 @@
1
+ # @granite-js/micro-frontend
2
+
3
+ Micro-frontend contracts for Granite React Native brownfield applications.
4
+
5
+ This package is the complete micro-frontend feature boundary. It contains:
6
+
7
+ - the build plugin that produces self-registering remote bundles;
8
+ - the JavaScript runtime that loads, evaluates, and imports remote modules;
9
+ - the Mono-Hermes native session and visibility contract;
10
+ - the host pending component contract; and
11
+ - the Portal primitive that attaches a mounted React subtree to an
12
+ `Activity`- or `UIViewController`-owned destination.
13
+
14
+ `@granite-js/portal` is not a separate dependency. Portal is coupled to the
15
+ shared-runtime session contract and is exported from this package.
16
+
17
+ ## Ownership model
18
+
19
+ ```text
20
+ Granite brownfield host
21
+ ├── native screen lifecycle
22
+ │ ├── session identity
23
+ │ └── presentation visibility
24
+ ├── Brownfield Brick registry
25
+ │ └── close the current native view
26
+ ├── one retained React Native runtime
27
+ │ ├── evaluate remote bundle
28
+ │ ├── keep one React tree per session
29
+ │ └── render each tree through Portal
30
+ └── native Portal destination
31
+ └── attach content by the same sessionId
32
+ ```
33
+
34
+ Native screen lifecycle is the source of truth for visibility. Portal only
35
+ owns where content is attached. A Portal host can remain attached while its
36
+ screen is stopped or covered, so Portal attachment must not be used to infer
37
+ presentation visibility.
38
+
39
+ The session identifier joins the contracts:
40
+
41
+ ```text
42
+ register native session(sessionId)
43
+ → openApp(sessionId, appName, scheme)
44
+ → <MicroFrontendSessionProvider sessionId={sessionId}>
45
+ → <Portal hostName={sessionId}>
46
+ → native Portal host named sessionId
47
+ ```
48
+
49
+ ## Installation
50
+
51
+ ```sh
52
+ yarn add @granite-js/micro-frontend
53
+ ```
54
+
55
+ The package autolinks one Android library and one iOS Pod. Its React Native
56
+ Codegen library, `GraniteMicroFrontendRuntimeSpec`, contains both the
57
+ `GraniteMicroFrontendRuntime` TurboModule and the `PortalView` / `PortalHostView`
58
+ Fabric components.
59
+
60
+ ## Build plugin
61
+
62
+ The app name comes from `granite.config.ts`. Hosts do not maintain a separate
63
+ remote-app list.
64
+
65
+ ```ts
66
+ // Remote app
67
+ import { microFrontend } from '@granite-js/micro-frontend/plugin';
68
+ import { defineConfig } from '@granite-js/react-native/config';
69
+
70
+ export default defineConfig({
71
+ appName: 'cart',
72
+ plugins: [
73
+ microFrontend({
74
+ exposes: {
75
+ './App': './src/_app.tsx',
76
+ },
77
+ shared: ['react', 'react-native'],
78
+ }),
79
+ ],
80
+ });
81
+ ```
82
+
83
+ ```ts
84
+ // Host app
85
+ import { microFrontend } from '@granite-js/micro-frontend/plugin';
86
+ import { defineConfig } from '@granite-js/react-native/config';
87
+
88
+ export default defineConfig({
89
+ appName: 'shared',
90
+ plugins: [
91
+ microFrontend({
92
+ shared: {
93
+ react: { eager: true },
94
+ 'react-native': { eager: true },
95
+ },
96
+ }),
97
+ ],
98
+ });
99
+ ```
100
+
101
+ When evaluated, a remote bundle registers its `appName` container and exposed
102
+ modules in the shared runtime registry.
103
+
104
+ ## JavaScript runtime
105
+
106
+ The adapter owns bundle selection, download, integrity verification, and
107
+ caching. It returns an absolute local bundle path.
108
+
109
+ ```ts
110
+ import { createMicroFrontendRuntime } from '@granite-js/micro-frontend';
111
+ import type { ComponentType } from 'react';
112
+
113
+ const runtime = createMicroFrontendRuntime({
114
+ adapter: {
115
+ async loadBundle({ appName }) {
116
+ const filePath = await bundleStore.loadBundle(appName);
117
+ return { filePath };
118
+ },
119
+ },
120
+ });
121
+
122
+ await runtime.preloadApp('cart');
123
+
124
+ const App = await runtime.importApp<{
125
+ readonly default: ComponentType<{ readonly scheme: string }>;
126
+ }>('cart/App');
127
+ ```
128
+
129
+ `importApp('cart/App')` performs:
130
+
131
+ ```text
132
+ adapter.loadBundle({ appName: 'cart' })
133
+ → GraniteMicroFrontendRuntime.evaluateScript({ filePath })
134
+ → verify that the cart container registered itself
135
+ → return the cart container's ./App module
136
+ ```
137
+
138
+ Concurrent preload/import calls for one app share an evaluation. A successful
139
+ evaluation remains cached until its last native session closes. Failed
140
+ evaluation removes the partial container so a later request can retry.
141
+ Native `preloadApp` events invoke `preloadApp()` inside the runtime and are not
142
+ forwarded to session listeners. `onPreloadError` is optional and lets the host
143
+ route best-effort native preload failures to its observability provider.
144
+
145
+ The public runtime API is:
146
+
147
+ | API | Responsibility |
148
+ | --- | --- |
149
+ | `preloadApp(appName)` | Load and evaluate one app without importing an exposed module. |
150
+ | `importApp(request)` | Ensure the app is evaluated and import `appName/exposedModule`. |
151
+ | `evaluateScript(filePath)` | Evaluate an already-local bundle in the retained runtime. |
152
+ | `onEvent(listener)` | Subscribe to native open, close, and visibility events. |
153
+
154
+ ## Session rendering
155
+
156
+ The brownfield host owns the product-specific session track. Compose the Portal
157
+ destination with `MicroFrontendSessionProvider` so native presentation state
158
+ joins Granite's existing visibility context.
159
+
160
+ ```tsx
161
+ const sessions = useMicroFrontendSessions(runtime);
162
+
163
+ function SessionRoot({ session }: { readonly session: MicroFrontendSessionState }) {
164
+ const App = useMemo(
165
+ () => lazy(() => runtime.importApp(`${session.appName}/App`)),
166
+ [session.appName]
167
+ );
168
+
169
+ return (
170
+ <Portal hostName={session.sessionId}>
171
+ <MicroFrontendSessionProvider
172
+ sessionId={session.sessionId}
173
+ presentationVisibility={session.isVisible}
174
+ >
175
+ <App scheme={session.scheme} />
176
+ </MicroFrontendSessionProvider>
177
+ </Portal>
178
+ );
179
+ }
180
+ ```
181
+
182
+ `useMicroFrontendSessions(runtime)` owns the fixed React subscription and folds
183
+ native open, close, and visibility events into session descriptors. The host
184
+ continues to own module selection and the rendered Portal tree.
185
+
186
+ The provider exposes the native session identity and combines
187
+ `presentationVisibility` with Granite's existing `VisibilityChangedProvider`.
188
+ Remote apps continue to read the final app, navigation, and native-session
189
+ visibility through `useVisibility()`.
190
+
191
+ Remote apps use Granite's `useVisibility()` for visibility and
192
+ `closeView()` to close the current brownfield view. They do not receive
193
+ `sessionId` as an application prop.
194
+
195
+ ## Host pending component
196
+
197
+ Remote apps register a route-level pending component with this package's
198
+ `createRoute` wrapper.
199
+
200
+ ```tsx
201
+ import { createRoute, hidePendingHostComponent } from '@granite-js/micro-frontend';
202
+ import { useEffect } from 'react';
203
+
204
+ export const Route = createRoute('/products/:productId', {
205
+ component: ProductPage,
206
+ validateParams: parseProductParams,
207
+ hostPendingComponent: ({ thumbnailUrl }) => (
208
+ <ProductPendingComponent thumbnailUrl={thumbnailUrl} />
209
+ ),
210
+ });
211
+
212
+ function ProductPage() {
213
+ useEffect(() => {
214
+ hidePendingHostComponent();
215
+ }, []);
216
+
217
+ return <Product />;
218
+ }
219
+ ```
220
+
221
+ The host resolves `PendingHostComponent` from the incoming scheme. The route
222
+ registry and hidden state live on the JavaScript global object, so separately
223
+ bundled host and remote package instances share the same state.
224
+
225
+ ## Native event contract
226
+
227
+ The Codegen TurboModule is named `GraniteMicroFrontendRuntime`.
228
+
229
+ ```ts
230
+ interface Spec extends TurboModule {
231
+ evaluateScript(request: { readonly filePath: string }): Promise<void>;
232
+ startEventDelivery(): void;
233
+ readonly onEvent: CodegenTypes.EventEmitter<NativeMicroFrontendRuntimeEvent>;
234
+ }
235
+ ```
236
+
237
+ These methods are implemented by this package. A brownfield application does
238
+ not implement another TurboModule. Request objects are used so optional fields
239
+ can be added without changing positional arguments. `startEventDelivery()`
240
+ is internal runtime plumbing.
241
+
242
+ Native emits the following events:
243
+
244
+ | Event | Required params | Meaning |
245
+ | --- | --- | --- |
246
+ | `preloadApp` | `appName` | Warm an app. |
247
+ | `openApp` | `sessionId`, `appName`, `scheme` | Create the session's React tree. |
248
+ | `sessionVisibilityChanged` | `sessionId`, `isVisible` | Update presentation visibility from native lifecycle. |
249
+ | `closeApp` | `sessionId` | Unmount the tree and release the app when its last session closes. |
250
+
251
+ Events emitted before JavaScript subscribes are queued. Event delivery starts
252
+ after the runtime installs its first listener.
253
+
254
+ ## Brownfield integration checklist
255
+
256
+ The brownfield application must:
257
+
258
+ 1. retain one React Native runtime and the controller surface that renders the
259
+ session track;
260
+ 2. create a unique `sessionId` for every native destination screen;
261
+ 3. resolve `appName` and the incoming `scheme` at the native navigation
262
+ boundary;
263
+ 4. bind native screen lifecycle to the session APIs below;
264
+ 5. install a Portal destination with the same `sessionId`;
265
+ 6. provide `adapter.loadBundle()` and return an absolute verified bundle path;
266
+ 7. keep the React tree mounted until native teardown emits `closeApp`; and
267
+ 8. keep Granite's base brownfield view APIs, such as scheme resolution and
268
+ closing the current Granite view, in the Granite host integration. They are
269
+ not duplicated by this package.
270
+
271
+ The container-owned Brownfield Brick registry defines how the current native
272
+ view closes. JavaScript uses Granite's `closeView()` command. The session
273
+ lifecycle remains separate: native emits `closeApp(sessionId)` only when the
274
+ container actually tears down so JavaScript can unmount its React tree.
275
+
276
+ ## Android native API
277
+
278
+ All public Android APIs live in `run.granite.microfrontend`, except the Portal
279
+ destination views in `com.teleport.host`.
280
+
281
+ ### Session APIs
282
+
283
+ | API | Lifetime / behavior |
284
+ | --- | --- |
285
+ | `ActivitySessionBinding.bind(activity, sessionId, appName, scheme)` | Convenience binding for an `Activity`. Emits open, derives visibility from start/stop, and emits close on destroy. Bindings are keyed by `sessionId`, so a destroy caused by a configuration change keeps the session open and the next `bind()` with the same id rebinds the recreated instance instead of opening a second session. Use a `sessionId` that identifies the destination, not the Activity instance. Retain it for the Activity lifetime. |
286
+ | `GraniteMicroFrontendRuntimeHost.registerSession(sessionId)` | Register a custom native container and return a `GraniteMicroFrontendSessionRegistration`. `sessionId` must be unique for every live destination; reuse only after the previous registration is closed/invalidated. |
287
+ | `GraniteMicroFrontendSessionRegistration.openApp(appName, scheme)` | Emit `openApp` once. |
288
+ | `GraniteMicroFrontendSessionRegistration.setVisible(isVisible)` | Emit a visibility event only when the value changes. |
289
+ | `GraniteMicroFrontendSessionRegistration.closeApp()` | Emit `closeApp` once after open. |
290
+ | `GraniteMicroFrontendSessionRegistration.close()` | Unregister the native session. Call after `closeApp()`. |
291
+ | `GraniteMicroFrontendRuntimeHost.emitPreloadApp(appName)` | Fire-and-forget preload. |
292
+
293
+ ### Portal destination APIs
294
+
295
+ | API | Lifetime / behavior |
296
+ | --- | --- |
297
+ | `PortalHostView.setName(name)` | Register/unregister the destination name. Use `sessionId`. |
298
+ | `PortalHostView.cleanup()` | Permanently unregister the destination during teardown. |
299
+ | `PortalReactRootView(context, reactHost, surfaceId, moduleName)` | Detached Fabric root that forwards touch/pointer events through the retained `ReactHost`; it does not start another runtime or surface. |
300
+
301
+ `PortalHostView` also re-registers on window attachment and temporarily
302
+ unregisters while detached. `nextInsertionIndexForChildAt()` is renderer
303
+ plumbing, not an application integration API.
304
+
305
+ #### Props the Portal components do not apply
306
+
307
+ `Portal` and the Portal host component are renderer plumbing, not styleable
308
+ views. Their TypeScript props extend `ViewProps` because React Native's codegen
309
+ only accepts `ViewProps` as a base, so the type advertises more than the native
310
+ managers apply. The following are accepted by the type checker and silently
311
+ ignored on Android: `pointerEvents` (the native manager pins it to `box-none`
312
+ so touches reach the portalled tree), `hitSlop`, `focusable`, `accessible`,
313
+ `nativeBackgroundAndroid`, `nativeForegroundAndroid`, `borderStyle`,
314
+ `overflow`, `backfaceVisibility`, `collapsable`, `collapsableChildren`,
315
+ `needsOffscreenAlphaCompositing`, `background*` shorthands, `hasTVPreferredFocus`
316
+ and the `nextFocus*` family.
317
+
318
+ Wrap the Portal in your own `<View>` when you need any of these.
319
+
320
+ ### Activity example
321
+
322
+ ```kotlin
323
+ import android.os.Bundle
324
+ import android.widget.FrameLayout
325
+ import androidx.appcompat.app.AppCompatActivity
326
+ import com.facebook.react.ReactHost
327
+ import com.facebook.react.bridge.ReactApplicationContext
328
+ import com.facebook.react.uimanager.ThemedReactContext
329
+ import com.teleport.host.PortalHostView
330
+ import com.teleport.host.PortalReactRootView
331
+ import run.granite.microfrontend.ActivitySessionBinding
332
+
333
+ class CartActivity : AppCompatActivity() {
334
+ // The session id identifies the destination, not the Activity instance. A value generated per
335
+ // instance changes on every configuration change, so rotation would close and reopen the session
336
+ // and remount the React tree. Derive it from what the caller navigated to.
337
+ private val sessionId: String
338
+ get() = requireNotNull(intent.data?.host) {
339
+ "CartActivity requires the destination name as the URI host"
340
+ }
341
+
342
+ private lateinit var sessionBinding: ActivitySessionBinding
343
+ private var portalHostView: PortalHostView? = null
344
+
345
+ override fun onCreate(savedInstanceState: Bundle?) {
346
+ super.onCreate(savedInstanceState)
347
+ val scheme = requireNotNull(intent.data).toString()
348
+
349
+ sessionBinding = ActivitySessionBinding.bind(
350
+ activity = this,
351
+ sessionId = sessionId,
352
+ appName = "cart",
353
+ scheme = scheme,
354
+ )
355
+ }
356
+
357
+ fun installPortalHost(
358
+ reactContext: ReactApplicationContext,
359
+ reactHost: ReactHost,
360
+ controllerSurfaceId: Int,
361
+ controllerModuleName: String,
362
+ ) {
363
+ val themedContext = ThemedReactContext(
364
+ reactContext,
365
+ this,
366
+ controllerModuleName,
367
+ controllerSurfaceId,
368
+ )
369
+ val rootView = PortalReactRootView(
370
+ themedContext,
371
+ reactHost,
372
+ controllerSurfaceId,
373
+ controllerModuleName,
374
+ )
375
+ val hostView = PortalHostView(themedContext).apply { setName(sessionId) }
376
+
377
+ rootView.addView(
378
+ hostView,
379
+ FrameLayout.LayoutParams(
380
+ FrameLayout.LayoutParams.MATCH_PARENT,
381
+ FrameLayout.LayoutParams.MATCH_PARENT,
382
+ ),
383
+ )
384
+ portalHostView = hostView
385
+ setContentView(rootView)
386
+ }
387
+
388
+ override fun onDestroy() {
389
+ portalHostView?.cleanup()
390
+ portalHostView = null
391
+ super.onDestroy()
392
+ }
393
+ }
394
+ ```
395
+
396
+ Install the Portal host only after the retained `ReactHost` has an active
397
+ `ReactApplicationContext`. The brownfield runtime owner still forwards normal
398
+ resume, pause, destroy, and back events to its retained `ReactHost`.
399
+
400
+ Custom screen containers use `registerSession()` and drive `openApp()`,
401
+ `setVisible()`, `closeApp()`, and `close()` from their own lifecycle.
402
+
403
+ ## iOS native API
404
+
405
+ Import public APIs from `GraniteMicroFrontendRuntime`.
406
+
407
+ ```objc
408
+ #import <GraniteMicroFrontendRuntime/GraniteMicroFrontendRuntimeHost.h>
409
+ #import <GraniteMicroFrontendRuntime/PortalHostContainerView.h>
410
+ ```
411
+
412
+ ### Session APIs
413
+
414
+ | API | Lifetime / behavior |
415
+ | --- | --- |
416
+ | `+[GraniteMicroFrontendViewControllerSessionBinding bindViewController:sessionId:appName:scheme:]` | Convenience binding for a `UIViewController`. Emits open and derives visibility from `viewWillAppear` / `viewWillDisappear` combined with app foreground and background transitions. Returns `nil` if `sessionId` is already registered. Retain until teardown. |
417
+ | `-[GraniteMicroFrontendViewControllerSessionBinding invalidate]` | Emit close and detach lifecycle observation. |
418
+ | `+[GraniteMicroFrontendRuntimeHost registerSession:]` | Register a custom container and return a session registration, or `nil` if `sessionId` is already registered. Use a unique id per destination and call `invalidate` before reuse. |
419
+ | `-[GraniteMicroFrontendSessionRegistration openAppWithAppName:scheme:]` | Emit `openApp` once. |
420
+ | `-[GraniteMicroFrontendSessionRegistration setVisible:]` | Emit visibility only when it changes. |
421
+ | `-[GraniteMicroFrontendSessionRegistration closeApp]` | Emit `closeApp` once after open. |
422
+ | `-[GraniteMicroFrontendSessionRegistration invalidate]` | Unregister the session. Call after `closeApp`. |
423
+ | `+[GraniteMicroFrontendRuntimeHost emitPreloadApp:]` | Fire-and-forget preload. |
424
+
425
+ ### Portal destination APIs
426
+
427
+ | API | Lifetime / behavior |
428
+ | --- | --- |
429
+ | `-initWithFrame:` | Create and immediately activate a UIKit-owned Portal destination after React has booted. |
430
+ | `-initWithFrame:deferredActivation:` | Create before React boot without reading React feature flags. |
431
+ | `-setName:` | Register/unregister the destination name. Use `sessionId`. |
432
+ | `-activateIfNeeded` | Create the Fabric host, attach its touch handler, and apply the pending name. Main thread only, after React boot. |
433
+ | `isActivated` | Whether the underlying Fabric host exists. |
434
+ | `hasAttachedContent` | Whether teleported content is currently attached. This is readiness, not presentation visibility. |
435
+ | `onContentDidAttach` / `onContentDidDetach` | Main-thread readiness callbacks for the first attach and last detach. |
436
+ | `-invalidate` | Unregister the host and clear callbacks during teardown. |
437
+
438
+ ### UIViewController example
439
+
440
+ ```swift
441
+ import GraniteMicroFrontendRuntime
442
+ import UIKit
443
+
444
+ final class CartViewController: UIViewController {
445
+ /// Create a new id for every push/present. Reusing an id before the previous
446
+ /// binding is invalidated fails registration.
447
+ private let sessionId = UUID().uuidString
448
+ private var sessionBinding: GraniteMicroFrontendViewControllerSessionBinding?
449
+ private var portalHostView: PortalHostContainerView!
450
+
451
+ override func viewDidLoad() {
452
+ super.viewDidLoad()
453
+
454
+ portalHostView = PortalHostContainerView(
455
+ frame: view.bounds,
456
+ deferredActivation: true
457
+ )
458
+ portalHostView.setName(sessionId)
459
+ view.addSubview(portalHostView)
460
+
461
+ sessionBinding = GraniteMicroFrontendViewControllerSessionBinding.bindViewController(
462
+ self,
463
+ sessionId: sessionId,
464
+ appName: "cart",
465
+ scheme: "granite://cart/products/1"
466
+ )
467
+ }
468
+
469
+ func reactRuntimeDidStart() {
470
+ portalHostView.activateIfNeeded()
471
+ }
472
+
473
+ deinit {
474
+ sessionBinding?.invalidate()
475
+ portalHostView?.invalidate()
476
+ }
477
+ }
478
+ ```
479
+
480
+ Create, activate, and invalidate these objects on the main thread. Call
481
+ `invalidate` during teardown so the binding and Portal destination are released
482
+ with the controller.
483
+
484
+ ## Portal primitive and example
485
+
486
+ `Portal` remains public for a Portal-only integration or test fixture:
487
+
488
+ ```tsx
489
+ import { Portal } from '@granite-js/micro-frontend';
490
+
491
+ <Portal hostName="store">
492
+ <StoreNavigationContainer />
493
+ </Portal>;
494
+ ```
495
+
496
+ The React subtree keeps the same owner, context, and state while its native
497
+ views move to the named destination. Host names are application data; a
498
+ generic native destination should read the requested name at runtime.
499
+
500
+ See [examples/portal/README.md](examples/portal/README.md) for the retained
501
+ Portal-only cross-Activity / UIViewController example.
502
+
503
+ ## License and credit
504
+
505
+ Apache-2.0. The Portal implementation is based on
506
+ [react-native-teleport](https://github.com/kirillzyusko/react-native-teleport)
507
+ by Kiryl Ziusko; see [NOTICE](NOTICE) and
508
+ [LICENSE.react-native-teleport](LICENSE.react-native-teleport).
@@ -0,0 +1,34 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+ project(granite-micro-frontend)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_EXTENSIONS OFF)
6
+
7
+ # This file is the module's own externalNativeBuild entry point and builds only the library that
8
+ # ships inside the AAR. The codegen target lives in src/main/jni/CMakeLists.txt, which autolinking
9
+ # builds inside the consuming app.
10
+
11
+ add_library(
12
+ granite-micro-frontend
13
+ SHARED
14
+ src/main/cpp/BundleEvaluator.cpp
15
+ src/main/cpp/FileReader.cpp
16
+ src/main/cpp/onLoad.cpp
17
+ )
18
+
19
+ find_package(fbjni REQUIRED CONFIG)
20
+ find_package(ReactAndroid REQUIRED CONFIG)
21
+
22
+ target_include_directories(
23
+ granite-micro-frontend
24
+ PRIVATE
25
+ src/main/cpp
26
+ )
27
+
28
+ target_link_libraries(
29
+ granite-micro-frontend
30
+ android
31
+ fbjni::fbjni
32
+ ReactAndroid::jsi
33
+ ReactAndroid::reactnative
34
+ )
@@ -0,0 +1,74 @@
1
+ buildscript {
2
+ ext.safeExtGet = { name, fallback ->
3
+ rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', project.properties['GraniteMicroFrontendRuntime_kotlinVersion'])}"
14
+ }
15
+ }
16
+
17
+ apply plugin: 'com.android.library'
18
+ apply plugin: 'kotlin-android'
19
+ apply plugin: 'com.facebook.react'
20
+
21
+ def getIntegerProperty(name) {
22
+ return safeExtGet(name, project.properties["GraniteMicroFrontendRuntime_${name}"]).toInteger()
23
+ }
24
+
25
+ android {
26
+ namespace 'run.granite.microfrontend'
27
+ compileSdkVersion getIntegerProperty('compileSdkVersion')
28
+
29
+ defaultConfig {
30
+ minSdkVersion getIntegerProperty('minSdkVersion')
31
+ targetSdkVersion getIntegerProperty('targetSdkVersion')
32
+
33
+ externalNativeBuild {
34
+ cmake {
35
+ // ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES is what aligns the AAR's .so to a 16 KB page
36
+ // boundary. React Native passes it for the app build; a library build has to pass it
37
+ // itself, otherwise the .so this module ships stays 4 KB aligned.
38
+ arguments '-DANDROID_STL=c++_shared', '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON'
39
+ }
40
+ }
41
+ }
42
+
43
+ buildFeatures {
44
+ buildConfig true
45
+ prefab true
46
+ }
47
+
48
+ externalNativeBuild {
49
+ cmake {
50
+ path 'CMakeLists.txt'
51
+ }
52
+ }
53
+
54
+ compileOptions {
55
+ sourceCompatibility JavaVersion.VERSION_17
56
+ targetCompatibility JavaVersion.VERSION_17
57
+ }
58
+
59
+ kotlinOptions {
60
+ jvmTarget = '17'
61
+ }
62
+
63
+ testOptions {
64
+ unitTests.returnDefaultValues = true
65
+ }
66
+ }
67
+
68
+ dependencies {
69
+ implementation 'com.facebook.react:react-android'
70
+ implementation 'com.facebook.fbjni:fbjni:0.7.0'
71
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', project.properties['GraniteMicroFrontendRuntime_kotlinVersion'])}"
72
+ testImplementation 'junit:junit:4.13.2'
73
+ testImplementation 'org.mockito:mockito-core:5.14.2'
74
+ }
@@ -0,0 +1,4 @@
1
+ GraniteMicroFrontendRuntime_kotlinVersion=2.0.21
2
+ GraniteMicroFrontendRuntime_compileSdkVersion=35
3
+ GraniteMicroFrontendRuntime_minSdkVersion=24
4
+ GraniteMicroFrontendRuntime_targetSdkVersion=35
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,64 @@
1
+ #include "BundleEvaluator.h"
2
+ #include "FileReader.h"
3
+
4
+ #include <jsi/jsi.h>
5
+
6
+ namespace granite::microfrontend {
7
+
8
+ using facebook::jni::JString;
9
+ using facebook::jni::alias_ref;
10
+ using facebook::jsi::Runtime;
11
+ using facebook::jsi::StringBuffer;
12
+
13
+ void BundleEvaluator::registerNatives() {
14
+ registerHybrid(
15
+ {makeNativeMethod("evaluateFileSync", BundleEvaluator::evaluateFileSync)});
16
+ }
17
+
18
+ void BundleEvaluator::evaluateFileSync(
19
+ alias_ref<jhybridobject>,
20
+ jlong runtimePointer,
21
+ alias_ref<JString> filePath,
22
+ alias_ref<JString> sourceUrl) {
23
+ if (runtimePointer == 0) {
24
+ facebook::jni::throwNewJavaException(
25
+ "java/lang/IllegalStateException", "JavaScript runtime is unavailable");
26
+ return;
27
+ }
28
+
29
+ const std::string path = filePath->toStdString();
30
+ std::string source;
31
+ try {
32
+ source = io::readFileToMemory(path);
33
+ } catch (const io::FileReaderError &error) {
34
+ switch (error.kind()) {
35
+ case io::FileReaderErrorKind::NotFound:
36
+ facebook::jni::throwNewJavaException(
37
+ "java/io/FileNotFoundException", "%s", error.what());
38
+ break;
39
+ case io::FileReaderErrorKind::StatFailed:
40
+ case io::FileReaderErrorKind::ReadFailed:
41
+ facebook::jni::throwNewJavaException(
42
+ "java/io/IOException", "%s", error.what());
43
+ break;
44
+ case io::FileReaderErrorKind::AllocationFailed:
45
+ facebook::jni::throwNewJavaException(
46
+ "java/lang/OutOfMemoryError", "%s", error.what());
47
+ break;
48
+ }
49
+ return;
50
+ }
51
+
52
+ if (source.empty()) {
53
+ facebook::jni::throwNewJavaException(
54
+ "java/io/IOException", "Bundle file is empty: %s", path.c_str());
55
+ return;
56
+ }
57
+
58
+ auto *runtime = reinterpret_cast<Runtime *>(runtimePointer);
59
+ runtime->evaluateJavaScript(
60
+ std::make_shared<StringBuffer>(std::move(source)),
61
+ sourceUrl->toStdString());
62
+ }
63
+
64
+ } // namespace granite::microfrontend