@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,92 @@
1
+ import { useCallback, useState, useSyncExternalStore } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import {
4
+ getPendingHostComponentStoreVersion,
5
+ getIsPendingHostComponentHidden,
6
+ resetPendingHostComponent,
7
+ resolvePendingHostComponent,
8
+ subscribePendingHostComponentStore,
9
+ } from './pendingHostComponentStore';
10
+
11
+ export interface PendingHostComponentProps {
12
+ readonly url: string | null | undefined;
13
+ }
14
+
15
+ export interface PendingHostComponentController {
16
+ readonly url: string | null;
17
+ readonly isPresent: boolean;
18
+ readonly isHidden: boolean;
19
+ readonly present: (url: string | null) => void;
20
+ readonly dismiss: () => void;
21
+ }
22
+
23
+ export function useIsPendingHostComponentHidden() {
24
+ return useSyncExternalStore(subscribePendingHostComponentStore, getIsPendingHostComponentHidden, getIsPendingHostComponentHidden);
25
+ }
26
+
27
+ function usePendingHostComponentStoreVersion() {
28
+ return useSyncExternalStore(subscribePendingHostComponentStore, getPendingHostComponentStoreVersion, getPendingHostComponentStoreVersion);
29
+ }
30
+
31
+ export function useResolvedPendingHostComponent(url: string | null | undefined) {
32
+ usePendingHostComponentStoreVersion();
33
+
34
+ if (url == null) {
35
+ return null;
36
+ }
37
+
38
+ return resolvePendingHostComponent(url);
39
+ }
40
+
41
+ export function PendingHostComponent({ url }: PendingHostComponentProps) {
42
+ const resolvedPendingHostComponent = useResolvedPendingHostComponent(url);
43
+ const isHidden = useIsPendingHostComponentHidden();
44
+
45
+ if (resolvedPendingHostComponent == null || isHidden) {
46
+ return null;
47
+ }
48
+
49
+ const Component = resolvedPendingHostComponent.component;
50
+
51
+ return (
52
+ <View
53
+ accessibilityElementsHidden={true}
54
+ importantForAccessibility="no-hide-descendants"
55
+ pointerEvents="none"
56
+ style={styles.root}
57
+ >
58
+ <Component {...resolvedPendingHostComponent.params} />
59
+ </View>
60
+ );
61
+ }
62
+
63
+ export function usePendingHostComponentController(): PendingHostComponentController {
64
+ const [url, setUrl] = useState<string | null>(null);
65
+ const [isPresent, setIsPresent] = useState(false);
66
+ const isHidden = useIsPendingHostComponentHidden();
67
+
68
+ const present = useCallback((nextUrl: string | null) => {
69
+ setIsPresent((wasPresent) => {
70
+ if (wasPresent) {
71
+ resetPendingHostComponent();
72
+ }
73
+
74
+ return true;
75
+ });
76
+ setUrl(nextUrl);
77
+ }, []);
78
+
79
+ const dismiss = useCallback(() => {
80
+ resetPendingHostComponent();
81
+ setUrl(null);
82
+ setIsPresent(false);
83
+ }, []);
84
+
85
+ return { url, isPresent, isHidden, present, dismiss };
86
+ }
87
+
88
+ const styles = StyleSheet.create({
89
+ root: {
90
+ ...StyleSheet.absoluteFillObject,
91
+ },
92
+ });
@@ -0,0 +1,26 @@
1
+ export type { PendingHostComponentController, PendingHostComponentProps } from './PendingHostComponent';
2
+ export {
3
+ PendingHostComponent,
4
+ usePendingHostComponentController,
5
+ useIsPendingHostComponentHidden,
6
+ useResolvedPendingHostComponent,
7
+ } from './PendingHostComponent';
8
+ export type {
9
+ PendingHostComponentAppConfig,
10
+ PendingHostComponentRenderer,
11
+ PendingHostComponentParams,
12
+ PendingHostComponentRouteRequest,
13
+ RegisterPendingHostComponentRouteOptions,
14
+ ResolvedPendingHostComponent,
15
+ } from './types';
16
+ export {
17
+ createPendingHostComponentRoutePrefix,
18
+ normalizeRoutePath,
19
+ } from './routeMatcher';
20
+ export {
21
+ hidePendingHostComponent,
22
+ installPendingHostComponentBridge,
23
+ registerPendingHostComponentRoute,
24
+ resetPendingHostComponent,
25
+ resolvePendingHostComponent,
26
+ } from './pendingHostComponentStore';
@@ -0,0 +1,241 @@
1
+ import { resolvePendingHostComponentParams } from './resolveParams';
2
+ import {
3
+ createPendingHostComponentRoutePrefix,
4
+ getQueryParamsFromUrl,
5
+ getRoutePathFromUrl,
6
+ matchRoutePath,
7
+ normalizePendingHostComponentAppName,
8
+ normalizeRoutePath,
9
+ } from './routeMatcher';
10
+ import type {
11
+ PendingHostComponentRenderer,
12
+ PendingHostComponentRouteRequest,
13
+ RegisterPendingHostComponentRouteOptions,
14
+ ResolvedPendingHostComponent,
15
+ ValidateParams,
16
+ } from './types';
17
+
18
+ interface PendingHostComponentRouteEntry {
19
+ readonly id: string;
20
+ readonly routePath: string;
21
+ readonly routePrefix: string;
22
+ readonly appName: string;
23
+ readonly component: PendingHostComponentRenderer<Readonly<object>>;
24
+ readonly parserParams?: (params: Record<string, unknown>) => Record<string, unknown>;
25
+ readonly validateParams?: ValidateParams<Readonly<object> | undefined>;
26
+ }
27
+
28
+ interface MatchedPendingHostComponentRoute {
29
+ readonly entry: PendingHostComponentRouteEntry;
30
+ readonly pathParams: Record<string, string>;
31
+ readonly params: Record<string, unknown>;
32
+ readonly routePath: string;
33
+ readonly url: string | null;
34
+ }
35
+
36
+ interface PendingHostComponentStore {
37
+ entries: PendingHostComponentRouteEntry[];
38
+ hidden: boolean;
39
+ readonly listeners: Set<() => void>;
40
+ version: number;
41
+ }
42
+
43
+ interface GlobalWithPendingHostComponentStore {
44
+ __graniteMicroFrontendPendingHostComponentStore?: PendingHostComponentStore;
45
+ hideSharedPendingHostComponent?: () => void;
46
+ }
47
+
48
+ function getGlobalObject() {
49
+ return globalThis as typeof globalThis & GlobalWithPendingHostComponentStore;
50
+ }
51
+
52
+ function getPendingHostComponentStore() {
53
+ const globalObject = getGlobalObject();
54
+
55
+ if (globalObject.__graniteMicroFrontendPendingHostComponentStore == null) {
56
+ globalObject.__graniteMicroFrontendPendingHostComponentStore = {
57
+ entries: [],
58
+ hidden: false,
59
+ listeners: new Set(),
60
+ version: 0,
61
+ };
62
+ }
63
+
64
+ return globalObject.__graniteMicroFrontendPendingHostComponentStore;
65
+ }
66
+
67
+ function emitPendingHostComponentStoreChange() {
68
+ const store = getPendingHostComponentStore();
69
+ store.version += 1;
70
+ store.listeners.forEach(listener => listener());
71
+ }
72
+
73
+ export function subscribePendingHostComponentStore(listener: () => void) {
74
+ const store = getPendingHostComponentStore();
75
+ store.listeners.add(listener);
76
+
77
+ return () => store.listeners.delete(listener);
78
+ }
79
+
80
+ export function getPendingHostComponentStoreVersion() {
81
+ return getPendingHostComponentStore().version;
82
+ }
83
+
84
+ export function getIsPendingHostComponentHidden() {
85
+ return getPendingHostComponentStore().hidden;
86
+ }
87
+
88
+ function setPendingHostComponentHidden(hidden: boolean) {
89
+ const store = getPendingHostComponentStore();
90
+
91
+ if (store.hidden === hidden) {
92
+ return;
93
+ }
94
+
95
+ store.hidden = hidden;
96
+ emitPendingHostComponentStoreChange();
97
+ }
98
+
99
+ export function hidePendingHostComponent() {
100
+ setPendingHostComponentHidden(true);
101
+
102
+ const hideSharedPendingHostComponent = getGlobalObject().hideSharedPendingHostComponent;
103
+ if (typeof hideSharedPendingHostComponent === 'function' && hideSharedPendingHostComponent !== hidePendingHostComponent) {
104
+ hideSharedPendingHostComponent();
105
+ }
106
+ }
107
+
108
+ export function resetPendingHostComponent() {
109
+ setPendingHostComponentHidden(false);
110
+ }
111
+
112
+ export function installPendingHostComponentBridge() {
113
+ getGlobalObject().hideSharedPendingHostComponent = hidePendingHostComponent;
114
+ }
115
+
116
+ export function registerPendingHostComponentRoute<
117
+ TParams extends Readonly<object> | undefined = undefined,
118
+ >(routePath: string, options: RegisterPendingHostComponentRouteOptions<TParams>) {
119
+ const store = getPendingHostComponentStore();
120
+ const normalizedRoutePath = normalizeRoutePath(routePath);
121
+ const appName = normalizePendingHostComponentAppName(options.app.name);
122
+ const routePrefix = createPendingHostComponentRoutePrefix(options.app);
123
+ const id = `${routePrefix}:${normalizedRoutePath}`;
124
+ const entry: PendingHostComponentRouteEntry = {
125
+ id,
126
+ routePath: normalizedRoutePath,
127
+ routePrefix,
128
+ appName,
129
+ component: options.component,
130
+ parserParams: options.parserParams,
131
+ validateParams: options.validateParams,
132
+ };
133
+ const existingIndex = store.entries.findIndex(item => item.id === id);
134
+
135
+ if (existingIndex >= 0) {
136
+ store.entries[existingIndex] = entry;
137
+ } else {
138
+ store.entries.push(entry);
139
+ }
140
+
141
+ emitPendingHostComponentStoreChange();
142
+ }
143
+
144
+ export function removePendingHostComponentRoutes(appName: string) {
145
+ const store = getPendingHostComponentStore();
146
+ const normalizedAppName = normalizePendingHostComponentAppName(appName);
147
+ const remainingEntries = store.entries.filter(entry => entry.appName !== normalizedAppName);
148
+
149
+ if (remainingEntries.length === store.entries.length) {
150
+ return;
151
+ }
152
+
153
+ store.entries = remainingEntries;
154
+ emitPendingHostComponentStoreChange();
155
+ }
156
+
157
+ export function resolvePendingHostComponent(request: PendingHostComponentRouteRequest | string): ResolvedPendingHostComponent | null {
158
+ const matched = typeof request === 'string' ? findRouteByUrl(request) : findRouteByRequest(request);
159
+
160
+ return resolveMatchedPendingHostComponent(matched);
161
+ }
162
+
163
+ function resolveMatchedPendingHostComponent(matched: MatchedPendingHostComponentRoute | null): ResolvedPendingHostComponent | null {
164
+ if (matched == null) {
165
+ return null;
166
+ }
167
+
168
+ const params = resolvePendingHostComponentParams(
169
+ { ...matched.pathParams, ...matched.params },
170
+ matched.entry.parserParams,
171
+ matched.entry.validateParams
172
+ );
173
+
174
+ if (params.status === 'invalid') {
175
+ return null;
176
+ }
177
+
178
+ return {
179
+ component: matched.entry.component,
180
+ params: params.value,
181
+ url: matched.url,
182
+ routePath: matched.routePath,
183
+ appName: matched.entry.appName,
184
+ };
185
+ }
186
+
187
+ function findRouteByUrl(url: string): MatchedPendingHostComponentRoute | null {
188
+ const entries = getPendingHostComponentStore().entries;
189
+
190
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
191
+ const entry = entries[index];
192
+ if (entry == null) {
193
+ continue;
194
+ }
195
+
196
+ const routePath = getRoutePathFromUrl(url, entry.routePrefix);
197
+ const pathParams = routePath == null ? null : matchRoutePath(entry.routePath, routePath);
198
+ if (routePath == null || pathParams == null) {
199
+ continue;
200
+ }
201
+
202
+ return { entry, pathParams, params: getQueryParamsFromUrl(url), routePath, url };
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ function findRouteByRequest(request: PendingHostComponentRouteRequest): MatchedPendingHostComponentRoute | null {
209
+ const appName = normalizePendingHostComponentAppName(request.appName);
210
+
211
+ for (let index = getPendingHostComponentStore().entries.length - 1; index >= 0; index -= 1) {
212
+ const entry = getPendingHostComponentStore().entries[index];
213
+ if (entry == null || entry.appName !== appName) {
214
+ continue;
215
+ }
216
+
217
+ const routePath = normalizeRoutePath(request.routePath);
218
+ const pathParams = matchRoutePath(entry.routePath, routePath);
219
+ if (pathParams == null) {
220
+ continue;
221
+ }
222
+
223
+ return {
224
+ entry,
225
+ pathParams,
226
+ params: request.params ?? {},
227
+ routePath,
228
+ url: request.url ?? null,
229
+ } satisfies MatchedPendingHostComponentRoute;
230
+ }
231
+
232
+ return null;
233
+ }
234
+
235
+ export function resetPendingHostComponentStoreForTest() {
236
+ const store = getPendingHostComponentStore();
237
+ store.entries = [];
238
+ store.hidden = false;
239
+ store.version = 0;
240
+ emitPendingHostComponentStoreChange();
241
+ }
@@ -0,0 +1,70 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { ParserParams, ValidateParams } from './types';
3
+
4
+ type ResolvedParams =
5
+ | { readonly status: 'resolved'; readonly value: Readonly<object> }
6
+ | { readonly status: 'invalid'; readonly cause?: Error };
7
+
8
+ export function resolvePendingHostComponentParams(
9
+ rawParams: Record<string, unknown>,
10
+ parserParams: ParserParams | undefined,
11
+ validateParams: ValidateParams<Readonly<object> | undefined> | undefined
12
+ ): ResolvedParams {
13
+ const parsedParams = (parserParams ?? defaultParserParams)(rawParams);
14
+
15
+ if (validateParams == null) {
16
+ return { status: 'resolved', value: parsedParams };
17
+ }
18
+
19
+ try {
20
+ if (isStandardSchema(validateParams)) {
21
+ const result = validateParams['~standard'].validate(parsedParams);
22
+
23
+ if (result instanceof Promise) {
24
+ return { status: 'invalid', cause: new Error('Async pending host component validation is not supported') };
25
+ }
26
+
27
+ if (result.issues != null) {
28
+ return {
29
+ status: 'invalid',
30
+ cause: new Error(result.issues.map(issue => issue.message).join(', ')),
31
+ };
32
+ }
33
+
34
+ return { status: 'resolved', value: result.value ?? {} };
35
+ }
36
+
37
+ return { status: 'resolved', value: validateParams(parsedParams) ?? {} };
38
+ } catch (error) {
39
+ return {
40
+ status: 'invalid',
41
+ cause: error instanceof Error ? error : new Error(String(error)),
42
+ };
43
+ }
44
+ }
45
+
46
+ function defaultParserParams(params: Record<string, unknown>): Record<string, unknown> {
47
+ return Object.fromEntries(
48
+ Object.entries(params).map(([key, value]) => {
49
+ if (typeof value !== 'string') {
50
+ return [key, value];
51
+ }
52
+
53
+ try {
54
+ return [key, JSON.parse(value)];
55
+ } catch (error) {
56
+ if (!(error instanceof SyntaxError)) {
57
+ throw error;
58
+ }
59
+
60
+ return [key, value];
61
+ }
62
+ })
63
+ );
64
+ }
65
+
66
+ function isStandardSchema<TParams extends Readonly<object> | undefined>(
67
+ value: ValidateParams<TParams>
68
+ ): value is StandardSchemaV1<unknown, TParams> {
69
+ return typeof value === 'object' && value !== null && '~standard' in value;
70
+ }
@@ -0,0 +1,117 @@
1
+ import type { PendingHostComponentAppConfig } from './types';
2
+
3
+ export function normalizeRoutePath(routePath: string) {
4
+ const pathname = routePath.split('#')[0]?.split('?')[0] ?? '/';
5
+ const pathWithLeadingSlash = pathname.startsWith('/') ? pathname : `/${pathname}`;
6
+ const normalizedPathname = pathWithLeadingSlash.replace(/\/+/g, '/');
7
+
8
+ return normalizedPathname === '/' ? '/' : normalizedPathname.replace(/\/+$/g, '');
9
+ }
10
+
11
+ export function createPendingHostComponentRoutePrefix(app: PendingHostComponentAppConfig) {
12
+ const scheme = app.scheme.replace(/:$/g, '');
13
+ const appName = normalizePendingHostComponentAppName(app.name);
14
+ const host = app.host == null ? '' : app.host.replace(/^\/+|\/+$/g, '');
15
+
16
+ return host.length > 0 ? `${scheme}://${host}/${appName}` : `${scheme}://${appName}`;
17
+ }
18
+
19
+ export function getRoutePathFromUrl(url: string, routePrefix: string) {
20
+ const normalizedPrefix = trimTrailingSlash(routePrefix);
21
+ const urlWithoutSearch = trimTrailingSlash(stripSearchAndHash(url));
22
+
23
+ if (urlWithoutSearch === normalizedPrefix) {
24
+ return '/';
25
+ }
26
+
27
+ if (!urlWithoutSearch.startsWith(`${normalizedPrefix}/`)) {
28
+ return null;
29
+ }
30
+
31
+ return normalizeRoutePath(urlWithoutSearch.slice(normalizedPrefix.length));
32
+ }
33
+
34
+ export function getQueryParamsFromUrl(url: string): Record<string, string> {
35
+ try {
36
+ return Object.fromEntries(new URL(url).searchParams.entries());
37
+ } catch (error) {
38
+ if (!(error instanceof TypeError)) {
39
+ throw error;
40
+ }
41
+
42
+ const questionMarkIndex = url.indexOf('?');
43
+ if (questionMarkIndex === -1) {
44
+ return {};
45
+ }
46
+
47
+ const fragmentIndex = url.indexOf('#', questionMarkIndex + 1);
48
+ const queryEndIndex = fragmentIndex === -1 ? url.length : fragmentIndex;
49
+
50
+ return Object.fromEntries(new URLSearchParams(url.slice(questionMarkIndex + 1, queryEndIndex)).entries());
51
+ }
52
+ }
53
+
54
+ export function matchRoutePath(pattern: string, routePath: string) {
55
+ const patternSegments = splitRoutePath(pattern);
56
+ const routeSegments = splitRoutePath(routePath);
57
+
58
+ if (patternSegments.length !== routeSegments.length) {
59
+ return null;
60
+ }
61
+
62
+ const params: Record<string, string> = {};
63
+
64
+ for (let index = 0; index < patternSegments.length; index += 1) {
65
+ const patternSegment = patternSegments[index];
66
+ const routeSegment = routeSegments[index];
67
+
68
+ if (patternSegment == null || routeSegment == null) {
69
+ return null;
70
+ }
71
+
72
+ if (patternSegment.startsWith(':')) {
73
+ const paramName = patternSegment.slice(1);
74
+ if (paramName.length === 0) {
75
+ return null;
76
+ }
77
+
78
+ params[paramName] = decodeRouteSegment(routeSegment);
79
+ continue;
80
+ }
81
+
82
+ if (patternSegment !== routeSegment) {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ return params;
88
+ }
89
+
90
+ export function normalizePendingHostComponentAppName(appName: string) {
91
+ return appName.replace(/^\/+|\/+$/g, '');
92
+ }
93
+
94
+ function splitRoutePath(routePath: string) {
95
+ const normalizedRoutePath = normalizeRoutePath(routePath);
96
+ return normalizedRoutePath === '/' ? [] : normalizedRoutePath.slice(1).split('/');
97
+ }
98
+
99
+ function stripSearchAndHash(url: string) {
100
+ return url.split('#')[0]?.split('?')[0] ?? url;
101
+ }
102
+
103
+ function trimTrailingSlash(value: string) {
104
+ return value.length > 1 ? value.replace(/\/+$/g, '') : value;
105
+ }
106
+
107
+ function decodeRouteSegment(value: string) {
108
+ try {
109
+ return decodeURIComponent(value);
110
+ } catch (error) {
111
+ if (!(error instanceof URIError)) {
112
+ throw error;
113
+ }
114
+
115
+ return value;
116
+ }
117
+ }
@@ -0,0 +1,45 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { ReactNode } from 'react';
3
+
4
+ export type PendingHostComponentParams<TParams extends Readonly<object> | undefined = undefined> =
5
+ TParams extends Readonly<object> ? TParams : Readonly<Record<string, never>>;
6
+
7
+ export type PendingHostComponentRenderer<TParams extends Readonly<object> | undefined = undefined> = {
8
+ bivarianceHack(params: PendingHostComponentParams<TParams>): ReactNode;
9
+ }['bivarianceHack'];
10
+
11
+ export type ParserParams = (params: Record<string, unknown>) => Record<string, unknown>;
12
+
13
+ export type ValidateParams<TParams extends Readonly<object> | undefined> =
14
+ | ((params: Readonly<object> | undefined) => TParams)
15
+ | StandardSchemaV1<unknown, TParams>;
16
+
17
+ export interface PendingHostComponentAppConfig {
18
+ readonly name: string;
19
+ readonly scheme: string;
20
+ readonly host?: string | null;
21
+ }
22
+
23
+ export interface RegisterPendingHostComponentRouteOptions<
24
+ TParams extends Readonly<object> | undefined = undefined,
25
+ > {
26
+ readonly component: PendingHostComponentRenderer<TParams>;
27
+ readonly parserParams?: ParserParams;
28
+ readonly validateParams?: ValidateParams<TParams>;
29
+ readonly app: PendingHostComponentAppConfig;
30
+ }
31
+
32
+ export interface PendingHostComponentRouteRequest {
33
+ readonly routePath: string;
34
+ readonly params?: Record<string, unknown>;
35
+ readonly appName: string;
36
+ readonly url?: string | null;
37
+ }
38
+
39
+ export interface ResolvedPendingHostComponent {
40
+ readonly component: PendingHostComponentRenderer<Readonly<object>>;
41
+ readonly params: Readonly<object>;
42
+ readonly url: string | null;
43
+ readonly routePath: string;
44
+ readonly appName: string | null;
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ export { createMicroFrontendRuntime } from './createMicroFrontendRuntime';
2
+ export type { CreateMicroFrontendRuntimeOptions } from './createMicroFrontendRuntime';
3
+ export { createRoute } from './createRoute';
4
+ export type { MicroFrontendRouteOptions } from './createRoute';
5
+ export type { PendingHostComponentController, PendingHostComponentProps } from './host/PendingHostComponent';
6
+ export {
7
+ PendingHostComponent,
8
+ usePendingHostComponentController,
9
+ useIsPendingHostComponentHidden,
10
+ useResolvedPendingHostComponent,
11
+ } from './host/PendingHostComponent';
12
+ export type {
13
+ PendingHostComponentAppConfig,
14
+ PendingHostComponentRenderer,
15
+ PendingHostComponentParams,
16
+ PendingHostComponentRouteRequest,
17
+ RegisterPendingHostComponentRouteOptions,
18
+ ResolvedPendingHostComponent,
19
+ } from './host/types';
20
+ export {
21
+ createPendingHostComponentRoutePrefix,
22
+ normalizeRoutePath,
23
+ } from './host/routeMatcher';
24
+ export {
25
+ hidePendingHostComponent,
26
+ installPendingHostComponentBridge,
27
+ registerPendingHostComponentRoute,
28
+ resetPendingHostComponent,
29
+ resolvePendingHostComponent,
30
+ } from './host/pendingHostComponentStore';
31
+ export {
32
+ MicroFrontendSessionProvider,
33
+ MissingMicroFrontendSessionError,
34
+ useMicroFrontendSession,
35
+ } from './session/MicroFrontendSessionContext';
36
+ export type { MicroFrontendSession, MicroFrontendSessionProviderProps } from './session/MicroFrontendSessionContext';
37
+ export { useMicroFrontendSessions } from './session/useMicroFrontendSessions';
38
+ export type { MicroFrontendSessionState } from './session/useMicroFrontendSessions';
39
+ export { default as Portal } from './specs/PortalViewNativeComponent';
40
+ export type {
41
+ AppRequest,
42
+ MicroFrontendAppProps,
43
+ MicroFrontendAdapter,
44
+ MicroFrontendBundle,
45
+ MicroFrontendBundleRequest,
46
+ MicroFrontendRuntimeApi,
47
+ MicroFrontendRuntimeEvent,
48
+ MicroFrontendRuntimeEventSubscription,
49
+ MicroFrontendSessionEvent,
50
+ } from './types';
51
+ export {
52
+ AppContainerNotFoundError,
53
+ ExposedModuleNotFoundError,
54
+ InvalidAppNameError,
55
+ InvalidAppRequestError,
56
+ InvalidNativeRuntimeEventError,
57
+ } from './runtime/errors';
@@ -0,0 +1,2 @@
1
+ export { microFrontend } from './microFrontendPlugin';
2
+ export type { MicroFrontendPluginOptions } from './types';