@granite-js/micro-frontend 2.0.2 → 2.1.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/CHANGELOG.md +11 -0
- package/README.md +124 -55
- package/dist/module/host/pendingHostComponentStore.js +33 -14
- package/dist/module/host/pendingHostComponentStore.js.map +1 -1
- package/dist/module/index.js.map +1 -1
- package/dist/module/plugin/disposeBabelPlugin.js +38 -0
- package/dist/module/plugin/disposeBabelPlugin.js.map +1 -0
- package/dist/module/plugin/disposeTransform.js +37 -0
- package/dist/module/plugin/disposeTransform.js.map +1 -0
- package/dist/module/plugin/globalContextScript.js +121 -0
- package/dist/module/plugin/globalContextScript.js.map +1 -0
- package/dist/module/plugin/microFrontendPlugin.js +12 -1
- package/dist/module/plugin/microFrontendPlugin.js.map +1 -1
- package/dist/module/plugin/prelude.js +4 -2
- package/dist/module/plugin/prelude.js.map +1 -1
- package/dist/module/plugin/resolver.js +1 -1
- package/dist/module/plugin/resolver.js.map +1 -1
- package/dist/module/runtime/containerModuleExposure.js +79 -0
- package/dist/module/runtime/containerModuleExposure.js.map +1 -0
- package/dist/module/runtime/containerPairing.js +86 -0
- package/dist/module/runtime/containerPairing.js.map +1 -0
- package/dist/module/runtime/containerRegistry.js +190 -0
- package/dist/module/runtime/containerRegistry.js.map +1 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js +9 -6
- package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -1
- package/dist/module/runtime/globalContext.js +89 -0
- package/dist/module/runtime/globalContext.js.map +1 -0
- package/dist/module/runtime/registry.js +103 -45
- package/dist/module/runtime/registry.js.map +1 -1
- package/dist/module/session/useMicroFrontendSessions.js +23 -1
- package/dist/module/session/useMicroFrontendSessions.js.map +1 -1
- package/dist/typescript/index.d.ts +1 -0
- package/dist/typescript/plugin/disposeBabelPlugin.d.ts +8 -0
- package/dist/typescript/plugin/disposeTransform.d.ts +3 -0
- package/dist/typescript/plugin/globalContextScript.d.ts +1 -0
- package/dist/typescript/runtime/containerModuleExposure.d.ts +2 -0
- package/dist/typescript/runtime/containerPairing.d.ts +18 -0
- package/dist/typescript/runtime/containerRegistry.d.ts +6 -0
- package/dist/typescript/runtime/globalContext.d.ts +17 -0
- package/dist/typescript/runtime/registry.d.ts +10 -8
- package/package.json +7 -4
- package/src/host/pendingHostComponentStore.ts +58 -29
- package/src/index.ts +1 -0
- package/src/plugin/disposeBabelPlugin.ts +51 -0
- package/src/plugin/disposeTransform.ts +43 -0
- package/src/plugin/globalContextScript.ts +118 -0
- package/src/plugin/microFrontendPlugin.ts +13 -1
- package/src/plugin/prelude.ts +160 -20
- package/src/plugin/resolver.ts +1 -1
- package/src/runtime/containerModuleExposure.ts +92 -0
- package/src/runtime/containerPairing.ts +120 -0
- package/src/runtime/containerRegistry.ts +239 -0
- package/src/runtime/createMicroFrontendRuntime.ts +8 -2
- package/src/runtime/globalContext.ts +164 -0
- package/src/runtime/registry.ts +151 -58
- package/src/session/useMicroFrontendSessions.ts +26 -16
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getLegacyContainerPair } from './containerPairing';
|
|
2
|
+
import { ExposedModuleAlreadyRegisteredError } from './errors';
|
|
3
|
+
import { getMicroFrontendGlobalContext, MicroFrontendGlobalContextCompatibilityError } from './globalContext';
|
|
4
|
+
import type { AppContainer } from './registry';
|
|
5
|
+
|
|
6
|
+
export function exposeContainerModule(container: AppContainer, exposedModule: string, module: unknown): void {
|
|
7
|
+
const normalizedModule = normalizeExposedModule(exposedModule);
|
|
8
|
+
const legacyModule = normalizeLegacyModule(exposedModule);
|
|
9
|
+
const legacyContainer = getLegacyContainerPair(container);
|
|
10
|
+
if (
|
|
11
|
+
Reflect.has(container.exposedModules, normalizedModule) ||
|
|
12
|
+
(legacyContainer != null && Reflect.has(legacyContainer.exposeMap, legacyModule))
|
|
13
|
+
) {
|
|
14
|
+
throw new ExposedModuleAlreadyRegisteredError(container.appName, normalizedModule);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ownsModernContainer = Object.is(
|
|
18
|
+
Reflect.get(getMicroFrontendGlobalContext().__CONTAINERS__, container.appName),
|
|
19
|
+
container
|
|
20
|
+
);
|
|
21
|
+
const legacyModuleValue = legacyContainer == null ? module : toLegacyEsm(module);
|
|
22
|
+
try {
|
|
23
|
+
if (ownsModernContainer && !Reflect.set(container.exposedModules, normalizedModule, module)) {
|
|
24
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
25
|
+
}
|
|
26
|
+
if (
|
|
27
|
+
legacyContainer != null &&
|
|
28
|
+
!Reflect.defineProperty(legacyContainer.exposeMap, legacyModule, {
|
|
29
|
+
configurable: true,
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: () => legacyModuleValue,
|
|
32
|
+
})
|
|
33
|
+
) {
|
|
34
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (ownsModernContainer) {
|
|
38
|
+
Reflect.deleteProperty(container.exposedModules, normalizedModule);
|
|
39
|
+
}
|
|
40
|
+
if (legacyContainer != null) {
|
|
41
|
+
Reflect.deleteProperty(legacyContainer.exposeMap, legacyModule);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toLegacyEsm(module: unknown): unknown {
|
|
48
|
+
if ((typeof module !== 'object' || module == null) && typeof module !== 'function') {
|
|
49
|
+
return module;
|
|
50
|
+
}
|
|
51
|
+
if (Reflect.get(module, '__esModule') === true) {
|
|
52
|
+
return module;
|
|
53
|
+
}
|
|
54
|
+
const legacyModule =
|
|
55
|
+
typeof module === 'function'
|
|
56
|
+
? function (this: unknown, ...args: unknown[]): unknown {
|
|
57
|
+
return Reflect.apply(module, this, args);
|
|
58
|
+
}
|
|
59
|
+
: {};
|
|
60
|
+
for (const exportName of Reflect.ownKeys(module)) {
|
|
61
|
+
if (exportName === '__esModule' || exportName === 'default') {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const sourceDescriptor = Reflect.getOwnPropertyDescriptor(module, exportName);
|
|
65
|
+
const facadeDescriptor = Reflect.getOwnPropertyDescriptor(legacyModule, exportName);
|
|
66
|
+
if (sourceDescriptor == null || facadeDescriptor?.configurable === false) {
|
|
67
|
+
// Callable facades keep intrinsic non-configurable slots such as prototype.
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
Reflect.defineProperty(legacyModule, exportName, {
|
|
71
|
+
enumerable: sourceDescriptor.enumerable,
|
|
72
|
+
get: () => Reflect.get(module, exportName),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const hasDefaultExport = Reflect.getOwnPropertyDescriptor(module, 'default') != null;
|
|
76
|
+
Object.defineProperties(legacyModule, {
|
|
77
|
+
__esModule: { value: true },
|
|
78
|
+
default: {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
get: hasDefaultExport ? () => Reflect.get(module, 'default') : () => module,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return legacyModule;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeExposedModule(exposedModule: string): string {
|
|
87
|
+
return exposedModule.startsWith('./') ? exposedModule : `./${exposedModule}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeLegacyModule(exposedModule: string): string {
|
|
91
|
+
return exposedModule.startsWith('./') ? exposedModule.slice(2) : exposedModule;
|
|
92
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { AppContainer } from './registry';
|
|
2
|
+
|
|
3
|
+
export const CONTAINER_PAIRING_KEY = 'granite.micro-frontend.container-pair' as const;
|
|
4
|
+
|
|
5
|
+
const containerPairingSymbol = Symbol.for(CONTAINER_PAIRING_KEY);
|
|
6
|
+
const pairedLegacyByModern = new WeakMap<AppContainer, LegacyAppContainer>();
|
|
7
|
+
const pairedModernByLegacy = new WeakMap<LegacyAppContainer, AppContainer>();
|
|
8
|
+
const legacyAdapters = new WeakSet<AppContainer>();
|
|
9
|
+
|
|
10
|
+
export interface LegacyAppContainer {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly config: unknown;
|
|
13
|
+
readonly exposeMap: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function defineContainerPair(first: object, second: object): boolean {
|
|
17
|
+
try {
|
|
18
|
+
if (!definePairMarker(first, second) || !definePairMarker(second, first)) {
|
|
19
|
+
clearContainerPair(first, second);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
clearContainerPair(first, second);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isContainerPair(first: object, second: object): boolean {
|
|
30
|
+
return hasPairMarker(first, second) && hasPairMarker(second, first);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getContainerPair(container: object): object | null {
|
|
34
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(container, containerPairingSymbol);
|
|
35
|
+
if (!isPairDescriptor(descriptor) || !isObject(descriptor.value)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return hasPairMarker(descriptor.value, container) ? descriptor.value : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clearContainerPair(first: object, second: object): void {
|
|
42
|
+
Reflect.deleteProperty(first, containerPairingSymbol);
|
|
43
|
+
Reflect.deleteProperty(second, containerPairingSymbol);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function rememberContainerPair(modern: AppContainer, legacy: LegacyAppContainer): void {
|
|
47
|
+
pairedLegacyByModern.set(modern, legacy);
|
|
48
|
+
pairedModernByLegacy.set(legacy, modern);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function rememberLegacyAdapterPair(modern: AppContainer, legacy: LegacyAppContainer): void {
|
|
52
|
+
rememberContainerPair(modern, legacy);
|
|
53
|
+
legacyAdapters.add(modern);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getLegacyContainerPair(modern: AppContainer): LegacyAppContainer | undefined {
|
|
57
|
+
const markedPair = getContainerPair(modern);
|
|
58
|
+
if (isLegacyAppContainer(markedPair) && markedPair.name === modern.appName) {
|
|
59
|
+
rememberContainerPair(modern, markedPair);
|
|
60
|
+
return markedPair;
|
|
61
|
+
}
|
|
62
|
+
return legacyAdapters.has(modern) ? pairedLegacyByModern.get(modern) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getModernContainerPair(legacy: LegacyAppContainer): AppContainer | undefined {
|
|
66
|
+
return pairedModernByLegacy.get(legacy);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function forgetContainerPair(modern: AppContainer | null, legacy: LegacyAppContainer | null): void {
|
|
70
|
+
if (modern != null) {
|
|
71
|
+
pairedLegacyByModern.delete(modern);
|
|
72
|
+
legacyAdapters.delete(modern);
|
|
73
|
+
}
|
|
74
|
+
if (legacy != null) {
|
|
75
|
+
pairedModernByLegacy.delete(legacy);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isLegacyAppContainer(value: unknown): value is LegacyAppContainer {
|
|
80
|
+
return (
|
|
81
|
+
isObjectRecord(value) &&
|
|
82
|
+
typeof Reflect.get(value, 'name') === 'string' &&
|
|
83
|
+
isObjectRecord(Reflect.get(value, 'config')) &&
|
|
84
|
+
isObjectRecord(Reflect.get(value, 'exposeMap'))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function definePairMarker(container: object, pair: object): boolean {
|
|
89
|
+
return Reflect.defineProperty(container, containerPairingSymbol, {
|
|
90
|
+
configurable: true,
|
|
91
|
+
enumerable: false,
|
|
92
|
+
value: pair,
|
|
93
|
+
writable: false,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasPairMarker(container: object, pair: object): boolean {
|
|
98
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(container, containerPairingSymbol);
|
|
99
|
+
return isPairDescriptor(descriptor) && Object.is(descriptor.value, pair);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isPairDescriptor(descriptor: PropertyDescriptor | undefined): descriptor is PropertyDescriptor & {
|
|
103
|
+
readonly value: unknown;
|
|
104
|
+
} {
|
|
105
|
+
return (
|
|
106
|
+
descriptor != null &&
|
|
107
|
+
descriptor.configurable === true &&
|
|
108
|
+
descriptor.enumerable === false &&
|
|
109
|
+
descriptor.writable === false &&
|
|
110
|
+
Reflect.has(descriptor, 'value')
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
115
|
+
return typeof value === 'object' && value != null && !Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isObject(value: unknown): value is object {
|
|
119
|
+
return (typeof value === 'object' && value != null) || typeof value === 'function';
|
|
120
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearContainerPair,
|
|
3
|
+
defineContainerPair,
|
|
4
|
+
forgetContainerPair,
|
|
5
|
+
getLegacyContainerPair,
|
|
6
|
+
getModernContainerPair,
|
|
7
|
+
isLegacyAppContainer,
|
|
8
|
+
isContainerPair,
|
|
9
|
+
isObjectRecord,
|
|
10
|
+
rememberContainerPair,
|
|
11
|
+
rememberLegacyAdapterPair,
|
|
12
|
+
type LegacyAppContainer,
|
|
13
|
+
} from './containerPairing';
|
|
14
|
+
import { AppContainerAlreadyRegisteredError } from './errors';
|
|
15
|
+
import { getMicroFrontendGlobalContext, MicroFrontendGlobalContextCompatibilityError } from './globalContext';
|
|
16
|
+
import type { AppContainer, AppContainerConfig } from './registry';
|
|
17
|
+
|
|
18
|
+
export { exposeContainerModule as exposeModule } from './containerModuleExposure';
|
|
19
|
+
|
|
20
|
+
export function createContainer(appName: string, config: AppContainerConfig = {}): AppContainer {
|
|
21
|
+
const context = getMicroFrontendGlobalContext();
|
|
22
|
+
const canonicalValue: unknown = Reflect.get(context.__CONTAINERS__, appName);
|
|
23
|
+
const legacyIndex: unknown = Reflect.get(context.__INSTANCES__, appName);
|
|
24
|
+
if (canonicalValue != null || typeof legacyIndex === 'number') {
|
|
25
|
+
throw new AppContainerAlreadyRegisteredError(appName);
|
|
26
|
+
}
|
|
27
|
+
if (!Reflect.isExtensible(context.__CONTAINERS__) || !Reflect.isExtensible(context.__INSTANCES__)) {
|
|
28
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const modernContainer: AppContainer = { appName, config, exposedModules: {} };
|
|
32
|
+
const legacyContainer: LegacyAppContainer = { name: appName, config, exposeMap: {} };
|
|
33
|
+
const containerIndex = context.__INSTANCES__.length;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (
|
|
37
|
+
!Reflect.defineProperty(context.__INSTANCES__, appName, {
|
|
38
|
+
configurable: true,
|
|
39
|
+
enumerable: false,
|
|
40
|
+
value: containerIndex,
|
|
41
|
+
writable: false,
|
|
42
|
+
})
|
|
43
|
+
) {
|
|
44
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
45
|
+
}
|
|
46
|
+
context.__INSTANCES__.push(legacyContainer);
|
|
47
|
+
if (
|
|
48
|
+
!Reflect.set(context.__CONTAINERS__, appName, modernContainer) ||
|
|
49
|
+
!defineContainerPair(modernContainer, legacyContainer)
|
|
50
|
+
) {
|
|
51
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (Object.is(context.__INSTANCES__[context.__INSTANCES__.length - 1], legacyContainer)) {
|
|
55
|
+
context.__INSTANCES__.pop();
|
|
56
|
+
}
|
|
57
|
+
Reflect.deleteProperty(context.__INSTANCES__, appName);
|
|
58
|
+
Reflect.deleteProperty(context.__CONTAINERS__, appName);
|
|
59
|
+
clearContainerPair(modernContainer, legacyContainer);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
rememberContainerPair(modernContainer, legacyContainer);
|
|
64
|
+
return modernContainer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getContainer(appName: string): AppContainer | null {
|
|
68
|
+
const context = getMicroFrontendGlobalContext();
|
|
69
|
+
const canonicalValue: unknown = Reflect.get(context.__CONTAINERS__, appName);
|
|
70
|
+
const modernContainer = isAppContainer(canonicalValue) ? canonicalValue : null;
|
|
71
|
+
const legacyContainer = getLegacyContainer(context.__INSTANCES__, appName);
|
|
72
|
+
|
|
73
|
+
if (modernContainer != null && legacyContainer != null) {
|
|
74
|
+
if (!isContainerPair(modernContainer, legacyContainer)) {
|
|
75
|
+
throw new AppContainerAlreadyRegisteredError(appName);
|
|
76
|
+
}
|
|
77
|
+
return modernContainer;
|
|
78
|
+
}
|
|
79
|
+
if (modernContainer != null) {
|
|
80
|
+
return modernContainer;
|
|
81
|
+
}
|
|
82
|
+
if (legacyContainer != null) {
|
|
83
|
+
return getLegacyAdapter(legacyContainer);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getExposedModule(container: AppContainer, exposedModule: string): unknown {
|
|
89
|
+
const module: unknown = Reflect.get(container.exposedModules, normalizeExposedModule(exposedModule));
|
|
90
|
+
const legacyContainer = getLegacyContainerPair(container);
|
|
91
|
+
return (
|
|
92
|
+
module ??
|
|
93
|
+
(legacyContainer == null ? undefined : Reflect.get(legacyContainer.exposeMap, normalizeLegacyModule(exposedModule)))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function removeContainer(appName: string): void {
|
|
98
|
+
const context = getMicroFrontendGlobalContext();
|
|
99
|
+
const canonicalValue: unknown = Reflect.get(context.__CONTAINERS__, appName);
|
|
100
|
+
const modernContainer = isAppContainer(canonicalValue) ? canonicalValue : null;
|
|
101
|
+
const legacyContainer = getLegacyContainer(context.__INSTANCES__, appName);
|
|
102
|
+
|
|
103
|
+
if (modernContainer != null && legacyContainer != null && !isContainerPair(modernContainer, legacyContainer)) {
|
|
104
|
+
throw new AppContainerAlreadyRegisteredError(appName);
|
|
105
|
+
}
|
|
106
|
+
if (modernContainer == null && legacyContainer == null) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const nextInstances = createLegacyInstancesWithout(context.__INSTANCES__, legacyContainer);
|
|
111
|
+
if (legacyContainer != null && !Reflect.set(context, '__INSTANCES__', nextInstances)) {
|
|
112
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (modernContainer != null) {
|
|
116
|
+
clearRegistry(modernContainer.exposedModules);
|
|
117
|
+
Reflect.deleteProperty(context.__CONTAINERS__, appName);
|
|
118
|
+
}
|
|
119
|
+
if (legacyContainer != null) {
|
|
120
|
+
clearLegacyModules(legacyContainer);
|
|
121
|
+
}
|
|
122
|
+
if (modernContainer != null) {
|
|
123
|
+
if (legacyContainer != null) {
|
|
124
|
+
clearContainerPair(modernContainer, legacyContainer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
forgetContainerPair(modernContainer, legacyContainer);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getLegacyAdapter(legacyContainer: LegacyAppContainer): AppContainer {
|
|
131
|
+
const existingAdapter = getModernContainerPair(legacyContainer);
|
|
132
|
+
if (existingAdapter != null) {
|
|
133
|
+
return existingAdapter;
|
|
134
|
+
}
|
|
135
|
+
const adapter: AppContainer = {
|
|
136
|
+
appName: legacyContainer.name,
|
|
137
|
+
config: isAppContainerConfig(legacyContainer.config) ? legacyContainer.config : {},
|
|
138
|
+
exposedModules: createNormalizedExposedModules(legacyContainer),
|
|
139
|
+
};
|
|
140
|
+
rememberLegacyAdapterPair(adapter, legacyContainer);
|
|
141
|
+
return adapter;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createNormalizedExposedModules(container: LegacyAppContainer): Record<string, unknown> {
|
|
145
|
+
return new Proxy(
|
|
146
|
+
{},
|
|
147
|
+
{
|
|
148
|
+
get: (_target, property, receiver) =>
|
|
149
|
+
Reflect.get(
|
|
150
|
+
container.exposeMap,
|
|
151
|
+
typeof property === 'string' ? normalizeLegacyModule(property) : property,
|
|
152
|
+
receiver
|
|
153
|
+
),
|
|
154
|
+
ownKeys: () =>
|
|
155
|
+
Reflect.ownKeys(container.exposeMap).map((key) =>
|
|
156
|
+
typeof key === 'string' ? normalizeExposedModule(key) : key
|
|
157
|
+
),
|
|
158
|
+
getOwnPropertyDescriptor: (_target, property) => {
|
|
159
|
+
const moduleName = typeof property === 'string' ? normalizeLegacyModule(property) : property;
|
|
160
|
+
return Reflect.has(container.exposeMap, moduleName)
|
|
161
|
+
? { configurable: true, enumerable: true, value: Reflect.get(container.exposeMap, moduleName) }
|
|
162
|
+
: undefined;
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getLegacyContainer(instances: unknown[], appName: string): LegacyAppContainer | null {
|
|
169
|
+
const index: unknown = Reflect.get(instances, appName);
|
|
170
|
+
if (typeof index !== 'number' || !Number.isInteger(index) || index < 0) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const value: unknown = instances[index];
|
|
174
|
+
return isLegacyAppContainer(value) && value.name === appName ? value : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createLegacyInstancesWithout(instances: unknown[], removed: LegacyAppContainer | null): unknown[] {
|
|
178
|
+
const nextInstances =
|
|
179
|
+
removed == null ? Array.from(instances) : instances.filter((entry) => !Object.is(entry, removed));
|
|
180
|
+
const registeredNames = new Set<string>();
|
|
181
|
+
for (const [index, entry] of nextInstances.entries()) {
|
|
182
|
+
if (!isLegacyAppContainer(entry)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (registeredNames.has(entry.name) || typeof Reflect.get(nextInstances, entry.name) === 'number') {
|
|
186
|
+
throw new AppContainerAlreadyRegisteredError(entry.name);
|
|
187
|
+
}
|
|
188
|
+
registeredNames.add(entry.name);
|
|
189
|
+
if (
|
|
190
|
+
!Reflect.defineProperty(nextInstances, entry.name, {
|
|
191
|
+
configurable: true,
|
|
192
|
+
enumerable: false,
|
|
193
|
+
value: index,
|
|
194
|
+
writable: false,
|
|
195
|
+
})
|
|
196
|
+
) {
|
|
197
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return nextInstances;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function clearRegistry(registry: Record<string, unknown>): void {
|
|
204
|
+
for (const key of Object.keys(registry)) {
|
|
205
|
+
Reflect.deleteProperty(registry, key);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function clearLegacyModules(container: LegacyAppContainer): void {
|
|
210
|
+
clearRegistry(container.exposeMap);
|
|
211
|
+
if (Object.keys(container.exposeMap).length > 0 && !Reflect.set(container, 'exposeMap', {})) {
|
|
212
|
+
throw new MicroFrontendGlobalContextCompatibilityError('registry-is-not-extensible');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeExposedModule(exposedModule: string): string {
|
|
217
|
+
return exposedModule.startsWith('./') ? exposedModule : `./${exposedModule}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeLegacyModule(exposedModule: string): string {
|
|
221
|
+
return exposedModule.startsWith('./') ? exposedModule.slice(2) : exposedModule;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isAppContainer(value: unknown): value is AppContainer {
|
|
225
|
+
return (
|
|
226
|
+
isObjectRecord(value) &&
|
|
227
|
+
typeof Reflect.get(value, 'appName') === 'string' &&
|
|
228
|
+
isAppContainerConfig(Reflect.get(value, 'config')) &&
|
|
229
|
+
isObjectRecord(Reflect.get(value, 'exposedModules'))
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isAppContainerConfig(value: unknown): value is AppContainerConfig {
|
|
234
|
+
if (!isObjectRecord(value)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const shared: unknown = Reflect.get(value, 'shared');
|
|
238
|
+
return shared == null || isObjectRecord(shared);
|
|
239
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
MicroFrontendRuntimeEventSubscription,
|
|
7
7
|
} from '../types';
|
|
8
8
|
import { AppContainerNotFoundError, InvalidAppNameError } from './errors';
|
|
9
|
+
import { getMicroFrontendGlobalContext } from './globalContext';
|
|
9
10
|
import { parseAppRequest } from './parseAppRequest';
|
|
10
11
|
|
|
11
12
|
export interface NativeMicroFrontendRuntimeEvent {
|
|
@@ -68,10 +69,15 @@ export function createMicroFrontendRuntimeWithDependencies(
|
|
|
68
69
|
return evaluation;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function evaluateScript(filePath: string): Promise<void> {
|
|
73
|
+
getMicroFrontendGlobalContext();
|
|
74
|
+
return dependencies.nativeRuntime.evaluateScript({ filePath });
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
async function evaluateApp(appName: string): Promise<void> {
|
|
72
78
|
try {
|
|
73
79
|
const { filePath } = await dependencies.adapter.loadBundle({ appName });
|
|
74
|
-
await
|
|
80
|
+
await evaluateScript(filePath);
|
|
75
81
|
|
|
76
82
|
if (!dependencies.registry.hasContainer(appName)) {
|
|
77
83
|
throw new AppContainerNotFoundError(appName);
|
|
@@ -83,7 +89,7 @@ export function createMicroFrontendRuntimeWithDependencies(
|
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
return {
|
|
86
|
-
evaluateScript
|
|
92
|
+
evaluateScript,
|
|
87
93
|
preloadApp,
|
|
88
94
|
async importApp<TModule>(request: AppRequest): Promise<TModule> {
|
|
89
95
|
const { appName } = parseAppRequest(request);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { AppContainer } from './registry';
|
|
2
|
+
|
|
3
|
+
const CANONICAL_GLOBAL_KEY = '__MICRO_FRONTEND__';
|
|
4
|
+
type ObjectRecord = Record<string, unknown>;
|
|
5
|
+
type SharedModule = {
|
|
6
|
+
readonly get: () => unknown;
|
|
7
|
+
readonly loaded: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type MicroFrontendGlobalContext = {
|
|
11
|
+
readonly __INSTANCES__: unknown[];
|
|
12
|
+
readonly __SHARED__: Record<string, SharedModule>;
|
|
13
|
+
readonly __CONTAINERS__: Record<string, AppContainer>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type LegacyGlobalContext = {
|
|
17
|
+
readonly __INSTANCES__: unknown[];
|
|
18
|
+
readonly __SHARED__: Record<string, SharedModule>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CompatibilityFailureReason =
|
|
22
|
+
| 'canonical-global-is-not-adoptable'
|
|
23
|
+
| 'canonical-global-is-locked'
|
|
24
|
+
| 'global-object-is-not-extensible'
|
|
25
|
+
| 'registry-is-not-extensible';
|
|
26
|
+
|
|
27
|
+
export class MicroFrontendGlobalContextCompatibilityError extends Error {
|
|
28
|
+
public readonly reason: CompatibilityFailureReason;
|
|
29
|
+
|
|
30
|
+
public constructor(reason: CompatibilityFailureReason) {
|
|
31
|
+
super(`Cannot establish the micro-frontend global context: ${reason}`);
|
|
32
|
+
this.name = 'MicroFrontendGlobalContextCompatibilityError';
|
|
33
|
+
this.reason = reason;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertCompatible(condition: boolean, reason: CompatibilityFailureReason): asserts condition {
|
|
38
|
+
if (!condition) {
|
|
39
|
+
throw new MicroFrontendGlobalContextCompatibilityError(reason);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getMicroFrontendGlobalContext(globalObject: object = globalThis): MicroFrontendGlobalContext {
|
|
44
|
+
const canonicalValue: unknown = Reflect.get(globalObject, CANONICAL_GLOBAL_KEY);
|
|
45
|
+
assertCompatible(
|
|
46
|
+
canonicalValue == null || isMicroFrontendGlobalContext(canonicalValue) || isLegacyGlobalContext(canonicalValue),
|
|
47
|
+
'canonical-global-is-not-adoptable'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (isMicroFrontendGlobalContext(canonicalValue)) {
|
|
51
|
+
return canonicalValue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const canonicalDescriptor = Object.getOwnPropertyDescriptor(globalObject, CANONICAL_GLOBAL_KEY);
|
|
55
|
+
const legacyHasCanonicalOrder =
|
|
56
|
+
isLegacyGlobalContext(canonicalValue) && hasKeysInOrder(canonicalValue, ['__INSTANCES__', '__SHARED__']);
|
|
57
|
+
|
|
58
|
+
assertCompatible(
|
|
59
|
+
canonicalDescriptor != null || Reflect.isExtensible(globalObject),
|
|
60
|
+
'global-object-is-not-extensible'
|
|
61
|
+
);
|
|
62
|
+
assertCompatible(
|
|
63
|
+
canonicalDescriptor == null || legacyHasCanonicalOrder || canonicalDescriptor.configurable === true,
|
|
64
|
+
'canonical-global-is-locked'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (legacyHasCanonicalOrder) {
|
|
68
|
+
return addContainersRegistry(canonicalValue);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const context: MicroFrontendGlobalContext = {
|
|
72
|
+
__INSTANCES__: canonicalValue?.__INSTANCES__ ?? [],
|
|
73
|
+
__SHARED__: canonicalValue?.__SHARED__ ?? {},
|
|
74
|
+
__CONTAINERS__: {},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
assertCompatible(
|
|
79
|
+
Reflect.defineProperty(globalObject, CANONICAL_GLOBAL_KEY, {
|
|
80
|
+
configurable: true,
|
|
81
|
+
enumerable: true,
|
|
82
|
+
value: context,
|
|
83
|
+
writable: true,
|
|
84
|
+
}),
|
|
85
|
+
'canonical-global-is-not-adoptable'
|
|
86
|
+
);
|
|
87
|
+
return context;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
restoreProperty(globalObject, CANONICAL_GLOBAL_KEY, canonicalDescriptor);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function addContainersRegistry(context: LegacyGlobalContext): MicroFrontendGlobalContext {
|
|
95
|
+
const containersDescriptor = Object.getOwnPropertyDescriptor(context, '__CONTAINERS__');
|
|
96
|
+
assertCompatible(Reflect.isExtensible(context), 'canonical-global-is-not-adoptable');
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
assertCompatible(
|
|
100
|
+
Reflect.defineProperty(context, '__CONTAINERS__', {
|
|
101
|
+
configurable: true,
|
|
102
|
+
enumerable: true,
|
|
103
|
+
value: {},
|
|
104
|
+
writable: true,
|
|
105
|
+
}),
|
|
106
|
+
'canonical-global-is-not-adoptable'
|
|
107
|
+
);
|
|
108
|
+
assertCompatible(isMicroFrontendGlobalContext(context), 'canonical-global-is-not-adoptable');
|
|
109
|
+
return context;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
restoreProperty(context, '__CONTAINERS__', containersDescriptor);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function restoreProperty(target: object, key: PropertyKey, descriptor: PropertyDescriptor | undefined): void {
|
|
117
|
+
const current = Object.getOwnPropertyDescriptor(target, key) ?? {};
|
|
118
|
+
const previous = descriptor ?? {};
|
|
119
|
+
const currentKeys = Object.keys(current);
|
|
120
|
+
const descriptorsMatch =
|
|
121
|
+
currentKeys.length === Object.keys(previous).length &&
|
|
122
|
+
currentKeys.every((property) => Object.is(Reflect.get(current, property), Reflect.get(previous, property)));
|
|
123
|
+
|
|
124
|
+
if (descriptorsMatch) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
assertCompatible(
|
|
129
|
+
descriptor == null ? Reflect.deleteProperty(target, key) : Reflect.defineProperty(target, key, descriptor),
|
|
130
|
+
'canonical-global-is-not-adoptable'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isMicroFrontendGlobalContext(value: unknown): value is MicroFrontendGlobalContext {
|
|
135
|
+
return (
|
|
136
|
+
isObjectRecord(value) &&
|
|
137
|
+
hasKeysInOrder(value, ['__INSTANCES__', '__SHARED__', '__CONTAINERS__']) &&
|
|
138
|
+
Array.isArray(Reflect.get(value, '__INSTANCES__')) &&
|
|
139
|
+
isObjectRecord(Reflect.get(value, '__SHARED__')) &&
|
|
140
|
+
isObjectRecord(Reflect.get(value, '__CONTAINERS__'))
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isLegacyGlobalContext(value: unknown): value is LegacyGlobalContext {
|
|
145
|
+
return (
|
|
146
|
+
isObjectRecord(value) &&
|
|
147
|
+
hasExactKeySet(value, ['__INSTANCES__', '__SHARED__']) &&
|
|
148
|
+
Array.isArray(Reflect.get(value, '__INSTANCES__')) &&
|
|
149
|
+
isObjectRecord(Reflect.get(value, '__SHARED__'))
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isObjectRecord = (value: unknown): value is ObjectRecord =>
|
|
154
|
+
typeof value === 'object' && value != null && !Array.isArray(value);
|
|
155
|
+
|
|
156
|
+
function hasExactKeySet(value: object, expectedKeys: readonly string[]): boolean {
|
|
157
|
+
const actualKeys = Object.keys(value);
|
|
158
|
+
return actualKeys.length === expectedKeys.length && expectedKeys.every((key) => actualKeys.includes(key));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function hasKeysInOrder(value: object, expectedKeys: readonly string[]): boolean {
|
|
162
|
+
const actualKeys = Object.keys(value);
|
|
163
|
+
return actualKeys.length === expectedKeys.length && actualKeys.every((key, index) => key === expectedKeys[index]);
|
|
164
|
+
}
|