@granite-js/micro-frontend 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GraniteMicroFrontendRuntime.podspec +40 -0
- package/LICENSE +202 -0
- package/LICENSE.react-native-teleport +21 -0
- package/NOTICE +17 -0
- package/README.md +508 -0
- package/android/CMakeLists.txt +34 -0
- package/android/build.gradle +74 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/BundleEvaluator.cpp +64 -0
- package/android/src/main/cpp/BundleEvaluator.h +20 -0
- package/android/src/main/cpp/FileReader.cpp +92 -0
- package/android/src/main/cpp/FileReader.h +27 -0
- package/android/src/main/cpp/onLoad.cpp +9 -0
- package/android/src/main/java/com/teleport/extensions/Float.kt +12 -0
- package/android/src/main/java/com/teleport/extensions/View.kt +10 -0
- package/android/src/main/java/com/teleport/global/PortalRegistry.kt +117 -0
- package/android/src/main/java/com/teleport/host/PortalHostView.kt +97 -0
- package/android/src/main/java/com/teleport/host/PortalHostViewManager.kt +41 -0
- package/android/src/main/java/com/teleport/host/PortalReactRootView.kt +116 -0
- package/android/src/main/java/com/teleport/managers/TeleportViewManager.kt +27 -0
- package/android/src/main/java/com/teleport/portal/PortalLayoutStateController.kt +107 -0
- package/android/src/main/java/com/teleport/portal/PortalView.kt +309 -0
- package/android/src/main/java/com/teleport/portal/PortalViewManager.kt +51 -0
- package/android/src/main/java/run/granite/microfrontend/ActivitySessionBinding.kt +87 -0
- package/android/src/main/java/run/granite/microfrontend/BundleEvaluator.kt +46 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendEvent.kt +62 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouter.kt +186 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeHost.kt +42 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeModule.kt +56 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimePackage.kt +35 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendSessionStore.kt +116 -0
- package/android/src/main/jni/CMakeLists.txt +64 -0
- package/android/src/test/cpp/CMakeLists.txt +35 -0
- package/android/src/test/cpp/FileReaderTest.cpp +44 -0
- package/android/src/test/java/com/teleport/host/PortalReactRootViewContractTest.kt +76 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouterTest.kt +216 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendSessionStoreTest.kt +190 -0
- package/android/test-cpp.sh +12 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/ComponentDescriptors.h +24 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.cpp +31 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.h +29 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewComponentDescriptor.h +22 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.h +43 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewState.h +21 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewComponentDescriptor.h +54 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.h +51 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewState.h +43 -0
- package/dist/module/createMicroFrontendRuntime.js +18 -0
- package/dist/module/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/createRoute.js +43 -0
- package/dist/module/createRoute.js.map +1 -0
- package/dist/module/host/PendingHostComponent.js +70 -0
- package/dist/module/host/PendingHostComponent.js.map +1 -0
- package/dist/module/host/index.js +6 -0
- package/dist/module/host/index.js.map +1 -0
- package/dist/module/host/pendingHostComponentStore.js +164 -0
- package/dist/module/host/pendingHostComponentStore.js.map +1 -0
- package/dist/module/host/resolveParams.js +60 -0
- package/dist/module/host/resolveParams.js.map +1 -0
- package/dist/module/host/routeMatcher.js +92 -0
- package/dist/module/host/routeMatcher.js.map +1 -0
- package/dist/module/host/types.js +4 -0
- package/dist/module/host/types.js.map +1 -0
- package/dist/module/index.js +12 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/package.json +1 -0
- package/dist/module/plugin/index.js +4 -0
- package/dist/module/plugin/index.js.map +1 -0
- package/dist/module/plugin/intoShared.js +32 -0
- package/dist/module/plugin/intoShared.js.map +1 -0
- package/dist/module/plugin/microFrontendPlugin.js +58 -0
- package/dist/module/plugin/microFrontendPlugin.js.map +1 -0
- package/dist/module/plugin/prelude.js +23 -0
- package/dist/module/plugin/prelude.js.map +1 -0
- package/dist/module/plugin/resolver.js +25 -0
- package/dist/module/plugin/resolver.js.map +1 -0
- package/dist/module/plugin/types.js +4 -0
- package/dist/module/plugin/types.js.map +1 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js +79 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/runtime/errors.js +63 -0
- package/dist/module/runtime/errors.js.map +1 -0
- package/dist/module/runtime/parseAppRequest.js +16 -0
- package/dist/module/runtime/parseAppRequest.js.map +1 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js +54 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js.map +1 -0
- package/dist/module/runtime/registry.js +87 -0
- package/dist/module/runtime/registry.js.map +1 -0
- package/dist/module/session/MicroFrontendSessionContext.js +32 -0
- package/dist/module/session/MicroFrontendSessionContext.js.map +1 -0
- package/dist/module/session/useMicroFrontendSessions.js +47 -0
- package/dist/module/session/useMicroFrontendSessions.js.map +1 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js +21 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js.map +1 -0
- package/dist/module/specs/PortalHostViewNativeComponent.ts +9 -0
- package/dist/module/specs/PortalViewNativeComponent.ts +9 -0
- package/dist/module/types.js +2 -0
- package/dist/module/types.js.map +1 -0
- package/dist/typescript/createMicroFrontendRuntime.d.ts +6 -0
- package/dist/typescript/createRoute.d.ts +23 -0
- package/dist/typescript/host/PendingHostComponent.d.ts +14 -0
- package/dist/typescript/host/index.d.ts +5 -0
- package/dist/typescript/host/pendingHostComponentStore.d.ts +11 -0
- package/dist/typescript/host/resolveParams.d.ts +10 -0
- package/dist/typescript/host/routeMatcher.d.ts +7 -0
- package/dist/typescript/host/types.d.ts +32 -0
- package/dist/typescript/index.d.ts +16 -0
- package/dist/typescript/plugin/index.d.ts +2 -0
- package/dist/typescript/plugin/intoShared.d.ts +3 -0
- package/dist/typescript/plugin/microFrontendPlugin.d.ts +3 -0
- package/dist/typescript/plugin/prelude.d.ts +6 -0
- package/dist/typescript/plugin/resolver.d.ts +3 -0
- package/dist/typescript/plugin/types.d.ts +5 -0
- package/dist/typescript/runtime/createMicroFrontendRuntime.d.ts +31 -0
- package/dist/typescript/runtime/errors.d.ts +35 -0
- package/dist/typescript/runtime/parseAppRequest.d.ts +6 -0
- package/dist/typescript/runtime/parseNativeRuntimeEvent.d.ts +3 -0
- package/dist/typescript/runtime/registry.d.ts +35 -0
- package/dist/typescript/session/MicroFrontendSessionContext.d.ts +13 -0
- package/dist/typescript/session/useMicroFrontendSessions.d.ts +8 -0
- package/dist/typescript/specs/NativeGraniteMicroFrontendRuntime.d.ts +21 -0
- package/dist/typescript/specs/PortalHostViewNativeComponent.d.ts +6 -0
- package/dist/typescript/specs/PortalViewNativeComponent.d.ts +6 -0
- package/dist/typescript/types.d.ts +49 -0
- package/ios/GraniteMicroFrontendRuntimeHost+Internal.h +18 -0
- package/ios/GraniteMicroFrontendRuntimeHost.h +34 -0
- package/ios/GraniteMicroFrontendRuntimeHost.mm +485 -0
- package/ios/GraniteMicroFrontendRuntimeModule.h +6 -0
- package/ios/GraniteMicroFrontendRuntimeModule.mm +138 -0
- package/ios/PortalHostContainerView.h +56 -0
- package/ios/PortalHostContainerView.mm +133 -0
- package/ios/PortalHostView.h +39 -0
- package/ios/PortalHostView.mm +153 -0
- package/ios/PortalRegistry.h +38 -0
- package/ios/PortalRegistry.mm +228 -0
- package/ios/PortalView.h +32 -0
- package/ios/PortalView.mm +426 -0
- package/package.json +113 -0
- package/react-native.config.js +13 -0
- package/src/createMicroFrontendRuntime.ts +22 -0
- package/src/createRoute.ts +84 -0
- package/src/host/PendingHostComponent.tsx +92 -0
- package/src/host/index.ts +26 -0
- package/src/host/pendingHostComponentStore.ts +241 -0
- package/src/host/resolveParams.ts +70 -0
- package/src/host/routeMatcher.ts +117 -0
- package/src/host/types.ts +45 -0
- package/src/index.ts +57 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/intoShared.ts +50 -0
- package/src/plugin/microFrontendPlugin.ts +57 -0
- package/src/plugin/prelude.ts +73 -0
- package/src/plugin/resolver.ts +73 -0
- package/src/plugin/types.ts +6 -0
- package/src/runtime/createMicroFrontendRuntime.ts +115 -0
- package/src/runtime/errors.ts +86 -0
- package/src/runtime/parseAppRequest.ts +22 -0
- package/src/runtime/parseNativeRuntimeEvent.ts +54 -0
- package/src/runtime/registry.ts +131 -0
- package/src/session/MicroFrontendSessionContext.tsx +45 -0
- package/src/session/useMicroFrontendSessions.ts +73 -0
- package/src/specs/NativeGraniteMicroFrontendRuntime.ts +44 -0
- package/src/specs/PortalHostViewNativeComponent.ts +9 -0
- package/src/specs/PortalViewNativeComponent.ts +9 -0
- package/src/types.ts +55 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MicroFrontendPluginOptions } from './types';
|
|
2
|
+
import type { SharedConfig, SharedModuleConfig } from '../runtime/registry';
|
|
3
|
+
|
|
4
|
+
const SHARED_PRESETS = {
|
|
5
|
+
react: ['react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
6
|
+
'react-native': [
|
|
7
|
+
'react-native/Libraries/BatchedBridge/BatchedBridge',
|
|
8
|
+
'react-native/Libraries/BatchedBridge/NativeModules',
|
|
9
|
+
'react-native/Libraries/BatchedBridge/MessageQueue',
|
|
10
|
+
'react-native/Libraries/NativeComponent/NativeComponentRegistry',
|
|
11
|
+
'react-native/Libraries/NativeComponent/NativeComponentRegistryUnstable',
|
|
12
|
+
'react-native/Libraries/NativeComponent/ViewConfigIgnore',
|
|
13
|
+
'react-native/Libraries/ReactNative/RendererProxy',
|
|
14
|
+
'react-native/Libraries/StyleSheet/PlatformColorValueTypes',
|
|
15
|
+
'react-native/Libraries/StyleSheet/normalizeColor',
|
|
16
|
+
'react-native/Libraries/StyleSheet/processColor',
|
|
17
|
+
'react-native/Libraries/TurboModule/TurboModuleRegistry',
|
|
18
|
+
'react-native/Libraries/Utilities/NativePlatformConstantsIOS',
|
|
19
|
+
'react-native/Libraries/Utilities/Platform',
|
|
20
|
+
'react-native/Libraries/Utilities/defineLazyObjectProperty',
|
|
21
|
+
],
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export function intoShared(shared: MicroFrontendPluginOptions['shared']): SharedConfig | undefined {
|
|
25
|
+
if (shared == null) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalized: Record<string, SharedModuleConfig> = {};
|
|
30
|
+
if (Array.isArray(shared)) {
|
|
31
|
+
for (const moduleName of shared) {
|
|
32
|
+
normalized[moduleName] = {};
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
Object.assign(normalized, shared);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const [moduleName, subpaths] of Object.entries(SHARED_PRESETS)) {
|
|
39
|
+
const moduleConfig = normalized[moduleName];
|
|
40
|
+
if (moduleConfig == null) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const subpath of subpaths) {
|
|
45
|
+
normalized[subpath] ??= { ...moduleConfig };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type { GranitePluginCore } from '@granite-js/plugin-core';
|
|
4
|
+
import { prepareLocalDirectory } from '@granite-js/utils';
|
|
5
|
+
import { intoShared } from './intoShared';
|
|
6
|
+
import { getPreludeConfig } from './prelude';
|
|
7
|
+
import { createSharedResolverConfig } from './resolver';
|
|
8
|
+
import type { MicroFrontendPluginOptions } from './types';
|
|
9
|
+
|
|
10
|
+
export async function microFrontend(options: MicroFrontendPluginOptions = {}): Promise<GranitePluginCore> {
|
|
11
|
+
const shared = intoShared(options.shared);
|
|
12
|
+
const normalizedOptions: MicroFrontendPluginOptions = {
|
|
13
|
+
...options,
|
|
14
|
+
shared,
|
|
15
|
+
};
|
|
16
|
+
const nonEagerEntries = Object.entries(shared ?? {}).filter(([, config]) => config.eager !== true);
|
|
17
|
+
const localDirectory = prepareLocalDirectory(process.cwd());
|
|
18
|
+
const preludePath = path.join(localDirectory, 'micro-frontend-runtime.js');
|
|
19
|
+
const prelude = getPreludeConfig(normalizedOptions);
|
|
20
|
+
const resolver = createSharedResolverConfig(nonEagerEntries);
|
|
21
|
+
|
|
22
|
+
function writePrelude(appName?: string) {
|
|
23
|
+
const config = getPreludeConfig(normalizedOptions, appName);
|
|
24
|
+
fs.writeFileSync(preludePath, config.preludeScript, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
writePrelude();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: 'micro-frontend',
|
|
31
|
+
dev: {
|
|
32
|
+
order: 'pre',
|
|
33
|
+
handler({ appName }) {
|
|
34
|
+
writePrelude(appName);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
build: {
|
|
38
|
+
order: 'pre',
|
|
39
|
+
handler({ appName }) {
|
|
40
|
+
writePrelude(appName);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
config: {
|
|
44
|
+
extra: nonEagerEntries.some(([moduleName]) => moduleName === 'react-native')
|
|
45
|
+
? {
|
|
46
|
+
skipReactNativeInitializeCore: true,
|
|
47
|
+
skipReactNativePolyfills: true,
|
|
48
|
+
}
|
|
49
|
+
: undefined,
|
|
50
|
+
resolver,
|
|
51
|
+
esbuild: {
|
|
52
|
+
prelude: [preludePath],
|
|
53
|
+
banner: { js: prelude.banner },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import type { MicroFrontendPluginOptions } from './types';
|
|
3
|
+
|
|
4
|
+
export interface MicroFrontendPreludeConfig {
|
|
5
|
+
readonly banner: string;
|
|
6
|
+
readonly preludeScript: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getPreludeConfig(
|
|
10
|
+
options: MicroFrontendPluginOptions,
|
|
11
|
+
appName?: string
|
|
12
|
+
): MicroFrontendPreludeConfig {
|
|
13
|
+
const eagerSharedModules = Object.entries(options.shared ?? {}).filter(
|
|
14
|
+
([, config]) => !Array.isArray(options.shared) && config.eager === true
|
|
15
|
+
);
|
|
16
|
+
const registerStatements = eagerSharedModules.map(([moduleName], index) => {
|
|
17
|
+
const identifier = `__shared${index}`;
|
|
18
|
+
return [
|
|
19
|
+
`import * as ${identifier} from ${JSON.stringify(moduleName)};`,
|
|
20
|
+
`registerShared(${JSON.stringify(moduleName)}, ${identifier});`,
|
|
21
|
+
].join('\n');
|
|
22
|
+
});
|
|
23
|
+
const exposeStatements = Object.entries(options.exposes ?? {}).map(([exposedModule, modulePath], index) => {
|
|
24
|
+
const identifier = `__expose${index}`;
|
|
25
|
+
return [
|
|
26
|
+
`import * as ${identifier} from ${JSON.stringify(path.resolve(modulePath))};`,
|
|
27
|
+
`exposeModule(__container, ${JSON.stringify(exposedModule)}, ${identifier});`,
|
|
28
|
+
].join('\n');
|
|
29
|
+
});
|
|
30
|
+
const containerConfig = JSON.stringify({ shared: options.shared });
|
|
31
|
+
const containerName = appName == null ? 'global.__granite.app.name' : JSON.stringify(appName);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
banner: [
|
|
35
|
+
'global._graniteMicroFrontend = global._graniteMicroFrontend || {',
|
|
36
|
+
' containers: {},',
|
|
37
|
+
' sharedModules: {},',
|
|
38
|
+
'};',
|
|
39
|
+
].join('\n'),
|
|
40
|
+
preludeScript: [
|
|
41
|
+
'function createContainer(appName, config) {',
|
|
42
|
+
' const containers = global._graniteMicroFrontend.containers;',
|
|
43
|
+
' if (containers[appName] != null) {',
|
|
44
|
+
' throw new Error(`App container \'${appName}\' is already registered`);',
|
|
45
|
+
' }',
|
|
46
|
+
' const container = { appName, config, exposedModules: {} };',
|
|
47
|
+
' containers[appName] = container;',
|
|
48
|
+
' return container;',
|
|
49
|
+
'}',
|
|
50
|
+
'function exposeModule(container, exposedModule, moduleValue) {',
|
|
51
|
+
" const normalizedModule = exposedModule.startsWith('./') ? exposedModule : `./${exposedModule}`;",
|
|
52
|
+
' if (container.exposedModules[normalizedModule] != null) {',
|
|
53
|
+
' throw new Error(`Exposed module \'${normalizedModule}\' is already registered in app container \'${container.appName}\'`);',
|
|
54
|
+
' }',
|
|
55
|
+
' container.exposedModules[normalizedModule] = moduleValue;',
|
|
56
|
+
'}',
|
|
57
|
+
'function registerShared(moduleName, moduleValue) {',
|
|
58
|
+
' const sharedModules = global._graniteMicroFrontend.sharedModules;',
|
|
59
|
+
' const existingModule = sharedModules[moduleName];',
|
|
60
|
+
' if (existingModule != null) {',
|
|
61
|
+
' if (Object.is(existingModule.get(), moduleValue)) {',
|
|
62
|
+
' return;',
|
|
63
|
+
' }',
|
|
64
|
+
' throw new Error(`Shared module \'${moduleName}\' is already registered`);',
|
|
65
|
+
' }',
|
|
66
|
+
' sharedModules[moduleName] = { get: () => moduleValue, loaded: true };',
|
|
67
|
+
'}',
|
|
68
|
+
`const __container = createContainer(${containerName}, ${containerConfig});`,
|
|
69
|
+
...registerStatements,
|
|
70
|
+
...exposeStatements,
|
|
71
|
+
].join('\n'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ResolverConfig } from '@granite-js/plugin-core';
|
|
2
|
+
import type { SharedModuleConfig } from '../runtime/registry';
|
|
3
|
+
|
|
4
|
+
const VIRTUAL_SHARED_PROTOCOL = 'granite-micro-frontend-shared';
|
|
5
|
+
|
|
6
|
+
export function createSharedResolverConfig(
|
|
7
|
+
moduleEntries: readonly (readonly [string, SharedModuleConfig])[]
|
|
8
|
+
): Pick<ResolverConfig, 'alias' | 'protocols'> {
|
|
9
|
+
const alias: ResolverConfig['alias'] = moduleEntries.map(([moduleName]) => ({
|
|
10
|
+
exact: true,
|
|
11
|
+
from: moduleName,
|
|
12
|
+
to: `${VIRTUAL_SHARED_PROTOCOL}:${moduleName}`,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
alias,
|
|
17
|
+
protocols:
|
|
18
|
+
alias.length === 0
|
|
19
|
+
? undefined
|
|
20
|
+
: {
|
|
21
|
+
[VIRTUAL_SHARED_PROTOCOL]: {
|
|
22
|
+
load(args: { readonly path: string }) {
|
|
23
|
+
const moduleName = JSON.stringify(args.path);
|
|
24
|
+
return {
|
|
25
|
+
loader: 'js',
|
|
26
|
+
contents: [
|
|
27
|
+
`var sharedModule = global._graniteMicroFrontend.sharedModules[${moduleName}];`,
|
|
28
|
+
'if (sharedModule == null) {',
|
|
29
|
+
` throw new Error('Shared module ' + ${moduleName} + ' is not registered');`,
|
|
30
|
+
'}',
|
|
31
|
+
'var sharedExports = sharedModule.get();',
|
|
32
|
+
'var moduleExports = {};',
|
|
33
|
+
'if (sharedExports != null) {',
|
|
34
|
+
' var sharedKeys = Reflect.ownKeys(sharedExports);',
|
|
35
|
+
' for (var i = 0; i < sharedKeys.length; i++) {',
|
|
36
|
+
' var sharedKey = sharedKeys[i];',
|
|
37
|
+
' var sharedDescriptor = Object.getOwnPropertyDescriptor(sharedExports, sharedKey);',
|
|
38
|
+
' if (sharedDescriptor == null || sharedDescriptor.enumerable !== true) {',
|
|
39
|
+
' continue;',
|
|
40
|
+
' }',
|
|
41
|
+
' (function (key) {',
|
|
42
|
+
' Object.defineProperty(moduleExports, key, {',
|
|
43
|
+
' enumerable: true,',
|
|
44
|
+
' configurable: true,',
|
|
45
|
+
' get: function () {',
|
|
46
|
+
' return sharedExports[key];',
|
|
47
|
+
' },',
|
|
48
|
+
' set: function (value) {',
|
|
49
|
+
' Object.defineProperty(moduleExports, key, {',
|
|
50
|
+
' enumerable: true,',
|
|
51
|
+
' configurable: true,',
|
|
52
|
+
' writable: true,',
|
|
53
|
+
' value: value,',
|
|
54
|
+
' });',
|
|
55
|
+
' },',
|
|
56
|
+
' });',
|
|
57
|
+
' })(sharedKey);',
|
|
58
|
+
' }',
|
|
59
|
+
'}',
|
|
60
|
+
"Object.defineProperty(moduleExports, '__esModule', {",
|
|
61
|
+
' enumerable: true,',
|
|
62
|
+
' configurable: true,',
|
|
63
|
+
' writable: true,',
|
|
64
|
+
' value: true,',
|
|
65
|
+
'});',
|
|
66
|
+
'module.exports = moduleExports;',
|
|
67
|
+
].join('\n'),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppRequest,
|
|
3
|
+
MicroFrontendAdapter,
|
|
4
|
+
MicroFrontendRuntimeApi,
|
|
5
|
+
MicroFrontendRuntimeEvent,
|
|
6
|
+
MicroFrontendRuntimeEventSubscription,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { AppContainerNotFoundError, InvalidAppNameError } from './errors';
|
|
9
|
+
import { parseAppRequest } from './parseAppRequest';
|
|
10
|
+
|
|
11
|
+
export interface NativeMicroFrontendRuntimeEvent {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly params: {
|
|
14
|
+
readonly appName?: string;
|
|
15
|
+
readonly sessionId?: string;
|
|
16
|
+
readonly scheme?: string;
|
|
17
|
+
readonly isVisible?: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NativeMicroFrontendRuntime {
|
|
22
|
+
readonly evaluateScript: (request: { readonly filePath: string }) => Promise<void>;
|
|
23
|
+
readonly startEventDelivery: () => void;
|
|
24
|
+
readonly onEvent: (
|
|
25
|
+
listener: (event: NativeMicroFrontendRuntimeEvent) => void
|
|
26
|
+
) => MicroFrontendRuntimeEventSubscription;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MicroFrontendModuleRegistry {
|
|
30
|
+
readonly hasContainer: (appName: string) => boolean;
|
|
31
|
+
readonly removeContainer: (appName: string) => void;
|
|
32
|
+
readonly importModule: <TModule>(request: AppRequest) => TModule;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateMicroFrontendRuntimeDependencies {
|
|
36
|
+
readonly adapter: MicroFrontendAdapter;
|
|
37
|
+
readonly nativeRuntime: NativeMicroFrontendRuntime;
|
|
38
|
+
readonly onPreloadError: (error: unknown) => void;
|
|
39
|
+
readonly registry: MicroFrontendModuleRegistry;
|
|
40
|
+
readonly removePendingHostComponentRoutes: (appName: string) => void;
|
|
41
|
+
readonly parseEvent: (event: NativeMicroFrontendRuntimeEvent) => MicroFrontendRuntimeEvent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createMicroFrontendRuntimeWithDependencies(
|
|
45
|
+
dependencies: CreateMicroFrontendRuntimeDependencies
|
|
46
|
+
): MicroFrontendRuntimeApi {
|
|
47
|
+
// A fulfilled promise is the evaluated-state cache. Rejected evaluations remove themselves.
|
|
48
|
+
const appEvaluations = new Map<string, Promise<void>>();
|
|
49
|
+
|
|
50
|
+
function resetFailedEvaluation(appName: string) {
|
|
51
|
+
appEvaluations.delete(appName);
|
|
52
|
+
dependencies.registry.removeContainer(appName);
|
|
53
|
+
dependencies.removePendingHostComponentRoutes(appName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function preloadApp(appName: string): Promise<void> {
|
|
57
|
+
if (appName.length === 0) {
|
|
58
|
+
return Promise.reject(new InvalidAppNameError(appName));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const existingEvaluation = appEvaluations.get(appName);
|
|
62
|
+
if (existingEvaluation != null) {
|
|
63
|
+
return existingEvaluation;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const evaluation = evaluateApp(appName);
|
|
67
|
+
appEvaluations.set(appName, evaluation);
|
|
68
|
+
return evaluation;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function evaluateApp(appName: string): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
const { filePath } = await dependencies.adapter.loadBundle({ appName });
|
|
74
|
+
await dependencies.nativeRuntime.evaluateScript({ filePath });
|
|
75
|
+
|
|
76
|
+
if (!dependencies.registry.hasContainer(appName)) {
|
|
77
|
+
throw new AppContainerNotFoundError(appName);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
resetFailedEvaluation(appName);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
evaluateScript: (filePath) => dependencies.nativeRuntime.evaluateScript({ filePath }),
|
|
87
|
+
preloadApp,
|
|
88
|
+
async importApp<TModule>(request: AppRequest): Promise<TModule> {
|
|
89
|
+
const { appName } = parseAppRequest(request);
|
|
90
|
+
await preloadApp(appName);
|
|
91
|
+
return dependencies.registry.importModule<TModule>(request);
|
|
92
|
+
},
|
|
93
|
+
onEvent(listener) {
|
|
94
|
+
const subscription = dependencies.nativeRuntime.onEvent((event) => {
|
|
95
|
+
const parsedEvent = dependencies.parseEvent(event);
|
|
96
|
+
switch (parsedEvent.name) {
|
|
97
|
+
case 'preloadApp':
|
|
98
|
+
void preloadApp(parsedEvent.params.appName).catch(dependencies.onPreloadError);
|
|
99
|
+
return;
|
|
100
|
+
case 'openApp':
|
|
101
|
+
case 'closeApp':
|
|
102
|
+
case 'sessionVisibilityChanged':
|
|
103
|
+
listener(parsedEvent);
|
|
104
|
+
return;
|
|
105
|
+
default: {
|
|
106
|
+
const exhaustiveEvent: never = parsedEvent;
|
|
107
|
+
return exhaustiveEvent;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
dependencies.nativeRuntime.startEventDelivery();
|
|
112
|
+
return subscription;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export class InvalidAppRequestError extends Error {
|
|
2
|
+
readonly request: string;
|
|
3
|
+
|
|
4
|
+
constructor(request: string) {
|
|
5
|
+
super(`Invalid micro-frontend app request: '${request}'`);
|
|
6
|
+
this.name = 'InvalidAppRequestError';
|
|
7
|
+
this.request = request;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class InvalidAppNameError extends Error {
|
|
12
|
+
readonly appName: string;
|
|
13
|
+
|
|
14
|
+
constructor(appName: string) {
|
|
15
|
+
super('Micro-frontend app name must not be empty');
|
|
16
|
+
this.name = 'InvalidAppNameError';
|
|
17
|
+
this.appName = appName;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AppContainerNotFoundError extends Error {
|
|
22
|
+
readonly appName: string;
|
|
23
|
+
|
|
24
|
+
constructor(appName: string) {
|
|
25
|
+
super(`App container '${appName}' was not registered after bundle evaluation`);
|
|
26
|
+
this.name = 'AppContainerNotFoundError';
|
|
27
|
+
this.appName = appName;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AppContainerAlreadyRegisteredError extends Error {
|
|
32
|
+
readonly appName: string;
|
|
33
|
+
|
|
34
|
+
constructor(appName: string) {
|
|
35
|
+
super(`App container '${appName}' is already registered`);
|
|
36
|
+
this.name = 'AppContainerAlreadyRegisteredError';
|
|
37
|
+
this.appName = appName;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ExposedModuleNotFoundError extends Error {
|
|
42
|
+
readonly appName: string;
|
|
43
|
+
readonly exposedModule: string;
|
|
44
|
+
|
|
45
|
+
constructor(appName: string, exposedModule: string) {
|
|
46
|
+
super(`Could not resolve '${exposedModule}' from app container '${appName}'`);
|
|
47
|
+
this.name = 'ExposedModuleNotFoundError';
|
|
48
|
+
this.appName = appName;
|
|
49
|
+
this.exposedModule = exposedModule;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ExposedModuleAlreadyRegisteredError extends Error {
|
|
54
|
+
readonly appName: string;
|
|
55
|
+
readonly exposedModule: string;
|
|
56
|
+
|
|
57
|
+
constructor(appName: string, exposedModule: string) {
|
|
58
|
+
super(`Exposed module '${exposedModule}' is already registered in app container '${appName}'`);
|
|
59
|
+
this.name = 'ExposedModuleAlreadyRegisteredError';
|
|
60
|
+
this.appName = appName;
|
|
61
|
+
this.exposedModule = exposedModule;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class SharedModuleAlreadyRegisteredError extends Error {
|
|
66
|
+
readonly moduleName: string;
|
|
67
|
+
|
|
68
|
+
constructor(moduleName: string) {
|
|
69
|
+
super(`Shared module '${moduleName}' is already registered`);
|
|
70
|
+
this.name = 'SharedModuleAlreadyRegisteredError';
|
|
71
|
+
this.moduleName = moduleName;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class InvalidNativeRuntimeEventError extends Error {
|
|
76
|
+
readonly eventName: string;
|
|
77
|
+
readonly fieldName?: string;
|
|
78
|
+
|
|
79
|
+
constructor(eventName: string, fieldName?: string) {
|
|
80
|
+
const detail = fieldName == null ? '' : `: missing or invalid '${fieldName}'`;
|
|
81
|
+
super(`Invalid native micro-frontend event '${eventName}'${detail}`);
|
|
82
|
+
this.name = 'InvalidNativeRuntimeEventError';
|
|
83
|
+
this.eventName = eventName;
|
|
84
|
+
this.fieldName = fieldName;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AppRequest } from '../types';
|
|
2
|
+
import { InvalidAppRequestError } from './errors';
|
|
3
|
+
|
|
4
|
+
export interface ParsedAppRequest {
|
|
5
|
+
readonly appName: string;
|
|
6
|
+
readonly exposedModule: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseAppRequest(request: AppRequest): ParsedAppRequest {
|
|
10
|
+
const separatorIndex = request.indexOf('/');
|
|
11
|
+
const appName = request.slice(0, separatorIndex);
|
|
12
|
+
const moduleName = request.slice(separatorIndex + 1);
|
|
13
|
+
|
|
14
|
+
if (separatorIndex <= 0 || moduleName.length === 0) {
|
|
15
|
+
throw new InvalidAppRequestError(request);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
appName,
|
|
20
|
+
exposedModule: moduleName.startsWith('./') ? moduleName : `./${moduleName}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MicroFrontendRuntimeEvent } from '../types';
|
|
2
|
+
import type { NativeMicroFrontendRuntimeEvent } from './createMicroFrontendRuntime';
|
|
3
|
+
import { InvalidNativeRuntimeEventError } from './errors';
|
|
4
|
+
|
|
5
|
+
function requireString(event: NativeMicroFrontendRuntimeEvent, fieldName: 'appName' | 'scheme' | 'sessionId'): string {
|
|
6
|
+
const value = event.params[fieldName];
|
|
7
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
8
|
+
throw new InvalidNativeRuntimeEventError(event.name, fieldName);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseNativeRuntimeEvent(event: NativeMicroFrontendRuntimeEvent): MicroFrontendRuntimeEvent {
|
|
14
|
+
switch (event.name) {
|
|
15
|
+
case 'preloadApp':
|
|
16
|
+
return {
|
|
17
|
+
name: 'preloadApp',
|
|
18
|
+
params: { appName: requireString(event, 'appName') },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
case 'openApp':
|
|
22
|
+
return {
|
|
23
|
+
name: 'openApp',
|
|
24
|
+
params: {
|
|
25
|
+
appName: requireString(event, 'appName'),
|
|
26
|
+
scheme: requireString(event, 'scheme'),
|
|
27
|
+
sessionId: requireString(event, 'sessionId'),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
case 'closeApp':
|
|
32
|
+
return {
|
|
33
|
+
name: 'closeApp',
|
|
34
|
+
params: { sessionId: requireString(event, 'sessionId') },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
case 'sessionVisibilityChanged': {
|
|
38
|
+
const isVisible = event.params.isVisible;
|
|
39
|
+
if (typeof isVisible !== 'boolean') {
|
|
40
|
+
throw new InvalidNativeRuntimeEventError(event.name, 'isVisible');
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
name: 'sessionVisibilityChanged',
|
|
44
|
+
params: {
|
|
45
|
+
isVisible,
|
|
46
|
+
sessionId: requireString(event, 'sessionId'),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
default:
|
|
52
|
+
throw new InvalidNativeRuntimeEventError(event.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { AppRequest } from '../types';
|
|
2
|
+
import type { MicroFrontendModuleRegistry } from './createMicroFrontendRuntime';
|
|
3
|
+
import {
|
|
4
|
+
AppContainerAlreadyRegisteredError,
|
|
5
|
+
ExposedModuleAlreadyRegisteredError,
|
|
6
|
+
ExposedModuleNotFoundError,
|
|
7
|
+
SharedModuleAlreadyRegisteredError,
|
|
8
|
+
} from './errors';
|
|
9
|
+
import { parseAppRequest } from './parseAppRequest';
|
|
10
|
+
|
|
11
|
+
export interface SharedModuleConfig {
|
|
12
|
+
readonly eager?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SharedConfig = Readonly<Record<string, SharedModuleConfig>>;
|
|
16
|
+
|
|
17
|
+
export interface AppContainerConfig {
|
|
18
|
+
readonly shared?: SharedConfig;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AppContainer {
|
|
22
|
+
readonly appName: string;
|
|
23
|
+
readonly config: AppContainerConfig;
|
|
24
|
+
readonly exposedModules: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SharedModule {
|
|
28
|
+
readonly get: () => unknown;
|
|
29
|
+
readonly loaded: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MicroFrontendRuntimeContext {
|
|
33
|
+
readonly containers: Record<string, AppContainer>;
|
|
34
|
+
readonly sharedModules: Record<string, SharedModule>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare global {
|
|
38
|
+
var _graniteMicroFrontend: MicroFrontendRuntimeContext | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getMicroFrontendRuntimeContext(): MicroFrontendRuntimeContext {
|
|
42
|
+
const existingContext = globalThis._graniteMicroFrontend;
|
|
43
|
+
if (existingContext != null) {
|
|
44
|
+
return existingContext;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const context: MicroFrontendRuntimeContext = {
|
|
48
|
+
containers: {},
|
|
49
|
+
sharedModules: {},
|
|
50
|
+
};
|
|
51
|
+
globalThis._graniteMicroFrontend = context;
|
|
52
|
+
return context;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createContainer(appName: string, config: AppContainerConfig = {}): AppContainer {
|
|
56
|
+
const containers = getMicroFrontendRuntimeContext().containers;
|
|
57
|
+
if (containers[appName] != null) {
|
|
58
|
+
throw new AppContainerAlreadyRegisteredError(appName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const container: AppContainer = {
|
|
62
|
+
appName,
|
|
63
|
+
config,
|
|
64
|
+
exposedModules: {},
|
|
65
|
+
};
|
|
66
|
+
containers[appName] = container;
|
|
67
|
+
return container;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function exposeModule(container: AppContainer, exposedModule: string, module: unknown): void {
|
|
71
|
+
const normalizedModule = normalizeExposedModule(exposedModule);
|
|
72
|
+
if (container.exposedModules[normalizedModule] != null) {
|
|
73
|
+
throw new ExposedModuleAlreadyRegisteredError(container.appName, normalizedModule);
|
|
74
|
+
}
|
|
75
|
+
container.exposedModules[normalizedModule] = module;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function registerShared(moduleName: string, module: unknown): void {
|
|
79
|
+
const sharedModules = getMicroFrontendRuntimeContext().sharedModules;
|
|
80
|
+
const existingModule = sharedModules[moduleName];
|
|
81
|
+
if (existingModule != null) {
|
|
82
|
+
if (Object.is(existingModule.get(), module)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw new SharedModuleAlreadyRegisteredError(moduleName);
|
|
86
|
+
}
|
|
87
|
+
sharedModules[moduleName] = {
|
|
88
|
+
get: () => module,
|
|
89
|
+
loaded: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getContainer(appName: string): AppContainer | null {
|
|
94
|
+
return getMicroFrontendRuntimeContext().containers[appName] ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function removeContainer(appName: string): void {
|
|
98
|
+
const containers = getMicroFrontendRuntimeContext().containers;
|
|
99
|
+
const container = containers[appName];
|
|
100
|
+
if (container == null) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const exposedModule of Object.keys(container.exposedModules)) {
|
|
105
|
+
Reflect.deleteProperty(container.exposedModules, exposedModule);
|
|
106
|
+
}
|
|
107
|
+
Reflect.deleteProperty(containers, appName);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function hasContainer(appName: string): boolean {
|
|
111
|
+
return getContainer(appName) != null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function importModule<TModule>(request: AppRequest): TModule {
|
|
115
|
+
const { appName, exposedModule } = parseAppRequest(request);
|
|
116
|
+
const module = getContainer(appName)?.exposedModules[exposedModule];
|
|
117
|
+
if (module == null) {
|
|
118
|
+
throw new ExposedModuleNotFoundError(appName, exposedModule);
|
|
119
|
+
}
|
|
120
|
+
return module as TModule;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeExposedModule(exposedModule: string): string {
|
|
124
|
+
return exposedModule.startsWith('./') ? exposedModule : `./${exposedModule}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const microFrontendModuleRegistry: MicroFrontendModuleRegistry = {
|
|
128
|
+
hasContainer,
|
|
129
|
+
importModule,
|
|
130
|
+
removeContainer,
|
|
131
|
+
};
|