@granite-js/micro-frontend 2.0.1 → 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 +20 -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/ios/GraniteMicroFrontendRuntimeModule.mm +6 -1
- 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
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ResolvedPendingHostComponent,
|
|
15
15
|
ValidateParams,
|
|
16
16
|
} from './types';
|
|
17
|
+
import { getMicroFrontendGlobalContext, MicroFrontendGlobalContextCompatibilityError } from '../runtime/globalContext';
|
|
17
18
|
|
|
18
19
|
interface PendingHostComponentRouteEntry {
|
|
19
20
|
readonly id: string;
|
|
@@ -40,34 +41,54 @@ interface PendingHostComponentStore {
|
|
|
40
41
|
version: number;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
hideSharedPendingHostComponent?: () => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getGlobalObject() {
|
|
49
|
-
return globalThis as typeof globalThis & GlobalWithPendingHostComponentStore;
|
|
50
|
-
}
|
|
44
|
+
const PENDING_STORE_KEY = 'pendingHostComponentStore';
|
|
45
|
+
const HIDE_SHARED_PENDING_HOST_COMPONENT_KEY = 'hideSharedPendingHostComponent';
|
|
51
46
|
|
|
52
|
-
function getPendingHostComponentStore() {
|
|
53
|
-
const
|
|
47
|
+
function getPendingHostComponentStore(): PendingHostComponentStore {
|
|
48
|
+
const context = getMicroFrontendGlobalContext();
|
|
49
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(context, PENDING_STORE_KEY);
|
|
50
|
+
if (descriptor != null) {
|
|
51
|
+
if (!isPendingHostComponentStore(descriptor.value)) {
|
|
52
|
+
throw new MicroFrontendGlobalContextCompatibilityError('canonical-global-is-not-adoptable');
|
|
53
|
+
}
|
|
54
|
+
return descriptor.value;
|
|
55
|
+
}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
const store = createPendingHostComponentStore();
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
!Reflect.defineProperty(context, PENDING_STORE_KEY, {
|
|
61
|
+
configurable: true,
|
|
62
|
+
enumerable: false,
|
|
63
|
+
value: store,
|
|
64
|
+
writable: true,
|
|
65
|
+
})
|
|
66
|
+
) {
|
|
67
|
+
throw new MicroFrontendGlobalContextCompatibilityError('canonical-global-is-not-adoptable');
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
return
|
|
70
|
+
return store;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createPendingHostComponentStore(): PendingHostComponentStore {
|
|
74
|
+
return { entries: [], hidden: false, listeners: new Set(), version: 0 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPendingHostComponentStore(value: unknown): value is PendingHostComponentStore {
|
|
78
|
+
return (
|
|
79
|
+
typeof value === 'object' &&
|
|
80
|
+
value != null &&
|
|
81
|
+
Array.isArray(Reflect.get(value, 'entries')) &&
|
|
82
|
+
typeof Reflect.get(value, 'hidden') === 'boolean' &&
|
|
83
|
+
Reflect.get(value, 'listeners') instanceof Set &&
|
|
84
|
+
typeof Reflect.get(value, 'version') === 'number'
|
|
85
|
+
);
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
function emitPendingHostComponentStoreChange() {
|
|
68
89
|
const store = getPendingHostComponentStore();
|
|
69
90
|
store.version += 1;
|
|
70
|
-
store.listeners.forEach(listener => listener());
|
|
91
|
+
store.listeners.forEach((listener) => listener());
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
export function subscribePendingHostComponentStore(listener: () => void) {
|
|
@@ -99,8 +120,11 @@ function setPendingHostComponentHidden(hidden: boolean) {
|
|
|
99
120
|
export function hidePendingHostComponent() {
|
|
100
121
|
setPendingHostComponentHidden(true);
|
|
101
122
|
|
|
102
|
-
const hideSharedPendingHostComponent =
|
|
103
|
-
if (
|
|
123
|
+
const hideSharedPendingHostComponent = Reflect.get(globalThis, HIDE_SHARED_PENDING_HOST_COMPONENT_KEY);
|
|
124
|
+
if (
|
|
125
|
+
typeof hideSharedPendingHostComponent === 'function' &&
|
|
126
|
+
hideSharedPendingHostComponent !== hidePendingHostComponent
|
|
127
|
+
) {
|
|
104
128
|
hideSharedPendingHostComponent();
|
|
105
129
|
}
|
|
106
130
|
}
|
|
@@ -110,12 +134,13 @@ export function resetPendingHostComponent() {
|
|
|
110
134
|
}
|
|
111
135
|
|
|
112
136
|
export function installPendingHostComponentBridge() {
|
|
113
|
-
|
|
137
|
+
Reflect.set(globalThis, HIDE_SHARED_PENDING_HOST_COMPONENT_KEY, hidePendingHostComponent);
|
|
114
138
|
}
|
|
115
139
|
|
|
116
|
-
export function registerPendingHostComponentRoute<
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
export function registerPendingHostComponentRoute<TParams extends Readonly<object> | undefined = undefined>(
|
|
141
|
+
routePath: string,
|
|
142
|
+
options: RegisterPendingHostComponentRouteOptions<TParams>
|
|
143
|
+
) {
|
|
119
144
|
const store = getPendingHostComponentStore();
|
|
120
145
|
const normalizedRoutePath = normalizeRoutePath(routePath);
|
|
121
146
|
const appName = normalizePendingHostComponentAppName(options.app.name);
|
|
@@ -130,7 +155,7 @@ export function registerPendingHostComponentRoute<
|
|
|
130
155
|
parserParams: options.parserParams,
|
|
131
156
|
validateParams: options.validateParams,
|
|
132
157
|
};
|
|
133
|
-
const existingIndex = store.entries.findIndex(item => item.id === id);
|
|
158
|
+
const existingIndex = store.entries.findIndex((item) => item.id === id);
|
|
134
159
|
|
|
135
160
|
if (existingIndex >= 0) {
|
|
136
161
|
store.entries[existingIndex] = entry;
|
|
@@ -144,7 +169,7 @@ export function registerPendingHostComponentRoute<
|
|
|
144
169
|
export function removePendingHostComponentRoutes(appName: string) {
|
|
145
170
|
const store = getPendingHostComponentStore();
|
|
146
171
|
const normalizedAppName = normalizePendingHostComponentAppName(appName);
|
|
147
|
-
const remainingEntries = store.entries.filter(entry => entry.appName !== normalizedAppName);
|
|
172
|
+
const remainingEntries = store.entries.filter((entry) => entry.appName !== normalizedAppName);
|
|
148
173
|
|
|
149
174
|
if (remainingEntries.length === store.entries.length) {
|
|
150
175
|
return;
|
|
@@ -154,13 +179,17 @@ export function removePendingHostComponentRoutes(appName: string) {
|
|
|
154
179
|
emitPendingHostComponentStoreChange();
|
|
155
180
|
}
|
|
156
181
|
|
|
157
|
-
export function resolvePendingHostComponent(
|
|
182
|
+
export function resolvePendingHostComponent(
|
|
183
|
+
request: PendingHostComponentRouteRequest | string
|
|
184
|
+
): ResolvedPendingHostComponent | null {
|
|
158
185
|
const matched = typeof request === 'string' ? findRouteByUrl(request) : findRouteByRequest(request);
|
|
159
186
|
|
|
160
187
|
return resolveMatchedPendingHostComponent(matched);
|
|
161
188
|
}
|
|
162
189
|
|
|
163
|
-
function resolveMatchedPendingHostComponent(
|
|
190
|
+
function resolveMatchedPendingHostComponent(
|
|
191
|
+
matched: MatchedPendingHostComponentRoute | null
|
|
192
|
+
): ResolvedPendingHostComponent | null {
|
|
164
193
|
if (matched == null) {
|
|
165
194
|
return null;
|
|
166
195
|
}
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export {
|
|
|
36
36
|
export type { MicroFrontendSession, MicroFrontendSessionProviderProps } from './session/MicroFrontendSessionContext';
|
|
37
37
|
export { useMicroFrontendSessions } from './session/useMicroFrontendSessions';
|
|
38
38
|
export type { MicroFrontendSessionState } from './session/useMicroFrontendSessions';
|
|
39
|
+
export type { AppDisposeCallback, MicroFrontendRuntimeContext } from './runtime/registry';
|
|
39
40
|
export { default as Portal } from './specs/PortalViewNativeComponent';
|
|
40
41
|
export type {
|
|
41
42
|
AppRequest,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PluginObj } from '@babel/core';
|
|
2
|
+
import type * as BabelTypes from '@babel/types';
|
|
3
|
+
|
|
4
|
+
export interface DisposeOwnershipPluginOptions {
|
|
5
|
+
appName?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function injectDisposeAppName(
|
|
9
|
+
{ types: babelTypes }: { types: typeof BabelTypes },
|
|
10
|
+
options: DisposeOwnershipPluginOptions
|
|
11
|
+
): PluginObj {
|
|
12
|
+
return {
|
|
13
|
+
name: 'inject-micro-frontend-dispose-app-name',
|
|
14
|
+
visitor: {
|
|
15
|
+
CallExpression(path) {
|
|
16
|
+
if (options.appName == null || path.node.arguments.length !== 1) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const runtimeIdentifier = getRuntimeIdentifier(path.node.callee, babelTypes);
|
|
21
|
+
if (runtimeIdentifier == null || path.scope.getBinding(runtimeIdentifier) != null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
path.node.arguments.unshift(babelTypes.stringLiteral(options.appName));
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getRuntimeIdentifier(
|
|
32
|
+
callee: BabelTypes.Expression | BabelTypes.V8IntrinsicIdentifier,
|
|
33
|
+
babelTypes: typeof BabelTypes
|
|
34
|
+
): string | null {
|
|
35
|
+
// - `global.__MICRO_FRONTEND__.dispose()`
|
|
36
|
+
// - `globalThis.__MICRO_FRONTEND__.dispose()`
|
|
37
|
+
if (
|
|
38
|
+
!babelTypes.isMemberExpression(callee) ||
|
|
39
|
+
callee.computed ||
|
|
40
|
+
!babelTypes.isIdentifier(callee.property, { name: 'dispose' }) ||
|
|
41
|
+
!babelTypes.isMemberExpression(callee.object) ||
|
|
42
|
+
callee.object.computed ||
|
|
43
|
+
!babelTypes.isIdentifier(callee.object.property, { name: '__MICRO_FRONTEND__' }) ||
|
|
44
|
+
(!babelTypes.isIdentifier(callee.object.object, { name: 'global' }) &&
|
|
45
|
+
!babelTypes.isIdentifier(callee.object.object, { name: 'globalThis' }))
|
|
46
|
+
) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return callee.object.object.name;
|
|
51
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as babel from '@babel/core';
|
|
2
|
+
import { injectDisposeAppName, type DisposeOwnershipPluginOptions } from './disposeBabelPlugin';
|
|
3
|
+
|
|
4
|
+
export function shouldTransformDispose(code: string): boolean {
|
|
5
|
+
return code.includes('globalThis.__MICRO_FRONTEND__.dispose') || code.includes('global.__MICRO_FRONTEND__.dispose');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function transformDisposeOwnership(id: string, code: string, options: DisposeOwnershipPluginOptions): string {
|
|
9
|
+
if (options.appName == null || !shouldTransformDispose(code)) {
|
|
10
|
+
return code;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
babel.transformSync(code, {
|
|
15
|
+
ast: false,
|
|
16
|
+
babelrc: false,
|
|
17
|
+
configFile: false,
|
|
18
|
+
filename: id,
|
|
19
|
+
parserOpts: {
|
|
20
|
+
plugins: getParserPlugins(id),
|
|
21
|
+
sourceType: 'unambiguous',
|
|
22
|
+
},
|
|
23
|
+
plugins: [[injectDisposeAppName, options]],
|
|
24
|
+
retainLines: true,
|
|
25
|
+
})?.code ?? code
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getParserPlugins(id: string): babel.ParserOptions['plugins'] {
|
|
30
|
+
const plugins: babel.ParserOptions['plugins'] = [];
|
|
31
|
+
|
|
32
|
+
if (/\.[cm]?tsx?$/.test(id)) {
|
|
33
|
+
plugins.push('typescript');
|
|
34
|
+
} else if (/\.[cm]?jsx?$/.test(id)) {
|
|
35
|
+
plugins.push('flow');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (/\.[cm]?[jt]sx$/.test(id)) {
|
|
39
|
+
plugins.push('jsx');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return plugins;
|
|
43
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export const globalContextScript = String.raw`
|
|
2
|
+
(function bootstrapMicroFrontendGlobal(globalObject) {
|
|
3
|
+
var canonicalKey = '__MICRO_FRONTEND__';
|
|
4
|
+
var instancesKey = '__INSTANCES__';
|
|
5
|
+
var sharedKey = '__SHARED__';
|
|
6
|
+
var containersKey = '__CONTAINERS__';
|
|
7
|
+
|
|
8
|
+
function failure(reason) {
|
|
9
|
+
return new Error('Cannot establish the micro-frontend global context: ' + reason);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === 'object' && value != null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasKeysInOrder(value, expectedKeys) {
|
|
17
|
+
var actualKeys = Object.keys(value);
|
|
18
|
+
return actualKeys.length === expectedKeys.length && actualKeys.every(function (key, index) {
|
|
19
|
+
return key === expectedKeys[index];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasExactKeySet(value, expectedKeys) {
|
|
24
|
+
var actualKeys = Object.keys(value);
|
|
25
|
+
return actualKeys.length === expectedKeys.length && expectedKeys.every(function (key) {
|
|
26
|
+
return actualKeys.indexOf(key) !== -1;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isCanonicalContext(value) {
|
|
31
|
+
return isRecord(value) &&
|
|
32
|
+
hasKeysInOrder(value, [instancesKey, sharedKey, containersKey]) &&
|
|
33
|
+
Array.isArray(value[instancesKey]) &&
|
|
34
|
+
isRecord(value[sharedKey]) &&
|
|
35
|
+
isRecord(value[containersKey]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLegacyContext(value) {
|
|
39
|
+
return isRecord(value) &&
|
|
40
|
+
hasExactKeySet(value, [instancesKey, sharedKey]) &&
|
|
41
|
+
Array.isArray(value[instancesKey]) &&
|
|
42
|
+
isRecord(value[sharedKey]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function restoreProperty(target, key, descriptor) {
|
|
46
|
+
var current = Object.getOwnPropertyDescriptor(target, key) || {};
|
|
47
|
+
var previous = descriptor || {};
|
|
48
|
+
var currentKeys = Object.keys(current);
|
|
49
|
+
var descriptorsMatch = currentKeys.length === Object.keys(previous).length && currentKeys.every(function (property) {
|
|
50
|
+
return Object.is(Reflect.get(current, property), Reflect.get(previous, property));
|
|
51
|
+
});
|
|
52
|
+
if (descriptorsMatch) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!(descriptor == null ? Reflect.deleteProperty(target, key) : Reflect.defineProperty(target, key, descriptor))) {
|
|
56
|
+
throw failure('canonical-global-is-not-adoptable');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var canonicalValue = globalObject[canonicalKey];
|
|
61
|
+
if (canonicalValue != null && !isCanonicalContext(canonicalValue) && !isLegacyContext(canonicalValue)) {
|
|
62
|
+
throw failure('canonical-global-is-not-adoptable');
|
|
63
|
+
}
|
|
64
|
+
if (isCanonicalContext(canonicalValue)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var canonicalDescriptor = Object.getOwnPropertyDescriptor(globalObject, canonicalKey);
|
|
69
|
+
var legacyHasCanonicalOrder = isLegacyContext(canonicalValue) &&
|
|
70
|
+
hasKeysInOrder(canonicalValue, [instancesKey, sharedKey]);
|
|
71
|
+
if (canonicalDescriptor == null && !Object.isExtensible(globalObject)) {
|
|
72
|
+
throw failure('global-object-is-not-extensible');
|
|
73
|
+
}
|
|
74
|
+
if (canonicalDescriptor != null && !legacyHasCanonicalOrder && canonicalDescriptor.configurable !== true) {
|
|
75
|
+
throw failure('canonical-global-is-locked');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (legacyHasCanonicalOrder) {
|
|
79
|
+
var containersDescriptor = Object.getOwnPropertyDescriptor(canonicalValue, containersKey);
|
|
80
|
+
if (!Object.isExtensible(canonicalValue)) {
|
|
81
|
+
throw failure('canonical-global-is-not-adoptable');
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
if (!Reflect.defineProperty(canonicalValue, containersKey, {
|
|
85
|
+
configurable: true,
|
|
86
|
+
enumerable: true,
|
|
87
|
+
value: {},
|
|
88
|
+
writable: true,
|
|
89
|
+
}) || !isCanonicalContext(canonicalValue)) {
|
|
90
|
+
throw failure('canonical-global-is-not-adoptable');
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
restoreProperty(canonicalValue, containersKey, containersDescriptor);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var context = {
|
|
100
|
+
__INSTANCES__: canonicalValue == null ? [] : canonicalValue[instancesKey],
|
|
101
|
+
__SHARED__: canonicalValue == null ? {} : canonicalValue[sharedKey],
|
|
102
|
+
__CONTAINERS__: {},
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
if (!Reflect.defineProperty(globalObject, canonicalKey, {
|
|
106
|
+
configurable: true,
|
|
107
|
+
enumerable: true,
|
|
108
|
+
value: context,
|
|
109
|
+
writable: true,
|
|
110
|
+
})) {
|
|
111
|
+
throw failure('canonical-global-is-not-adoptable');
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
restoreProperty(globalObject, canonicalKey, canonicalDescriptor);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
})(global);
|
|
118
|
+
`;
|
|
@@ -2,6 +2,8 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import type { GranitePluginCore } from '@granite-js/plugin-core';
|
|
4
4
|
import { prepareLocalDirectory } from '@granite-js/utils';
|
|
5
|
+
import type { DisposeOwnershipPluginOptions } from './disposeBabelPlugin';
|
|
6
|
+
import { transformDisposeOwnership } from './disposeTransform';
|
|
5
7
|
import { intoShared } from './intoShared';
|
|
6
8
|
import { getPreludeConfig } from './prelude';
|
|
7
9
|
import { createSharedResolverConfig } from './resolver';
|
|
@@ -15,11 +17,16 @@ export async function microFrontend(options: MicroFrontendPluginOptions = {}): P
|
|
|
15
17
|
};
|
|
16
18
|
const nonEagerEntries = Object.entries(shared ?? {}).filter(([, config]) => config.eager !== true);
|
|
17
19
|
const localDirectory = prepareLocalDirectory(process.cwd());
|
|
18
|
-
|
|
20
|
+
// Keep this path distinct from @granite-js/plugin-micro-frontend's generated prelude.
|
|
21
|
+
// Both plugins can coexist while consumers migrate, and sharing a path makes the
|
|
22
|
+
// later plugin overwrite the earlier plugin's runtime registrations.
|
|
23
|
+
const preludePath = path.join(localDirectory, 'granite-micro-frontend-runtime.js');
|
|
19
24
|
const prelude = getPreludeConfig(normalizedOptions);
|
|
20
25
|
const resolver = createSharedResolverConfig(nonEagerEntries);
|
|
26
|
+
const disposeOwnershipOptions: DisposeOwnershipPluginOptions = {};
|
|
21
27
|
|
|
22
28
|
function writePrelude(appName?: string) {
|
|
29
|
+
disposeOwnershipOptions.appName = appName;
|
|
23
30
|
const config = getPreludeConfig(normalizedOptions, appName);
|
|
24
31
|
fs.writeFileSync(preludePath, config.preludeScript, 'utf8');
|
|
25
32
|
}
|
|
@@ -48,6 +55,11 @@ export async function microFrontend(options: MicroFrontendPluginOptions = {}): P
|
|
|
48
55
|
}
|
|
49
56
|
: undefined,
|
|
50
57
|
resolver,
|
|
58
|
+
transformer: {
|
|
59
|
+
transformSync(id, code) {
|
|
60
|
+
return transformDisposeOwnership(id, code, disposeOwnershipOptions);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
51
63
|
esbuild: {
|
|
52
64
|
prelude: [preludePath],
|
|
53
65
|
banner: { js: prelude.banner },
|
package/src/plugin/prelude.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { globalContextScript } from './globalContextScript';
|
|
2
3
|
import type { MicroFrontendPluginOptions } from './types';
|
|
4
|
+
import { CONTAINER_PAIRING_KEY } from '../runtime/containerPairing';
|
|
3
5
|
|
|
4
6
|
export interface MicroFrontendPreludeConfig {
|
|
5
7
|
readonly banner: string;
|
|
6
8
|
readonly preludeScript: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function getPreludeConfig(
|
|
10
|
-
options: MicroFrontendPluginOptions,
|
|
11
|
-
appName?: string
|
|
12
|
-
): MicroFrontendPreludeConfig {
|
|
11
|
+
export function getPreludeConfig(options: MicroFrontendPluginOptions, appName?: string): MicroFrontendPreludeConfig {
|
|
13
12
|
const eagerSharedModules = Object.entries(options.shared ?? {}).filter(
|
|
14
13
|
([, config]) => !Array.isArray(options.shared) && config.eager === true
|
|
15
14
|
);
|
|
@@ -32,36 +31,177 @@ export function getPreludeConfig(
|
|
|
32
31
|
|
|
33
32
|
return {
|
|
34
33
|
banner: [
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
34
|
+
globalContextScript,
|
|
35
|
+
'(function installMicroFrontendLifecycle(context) {',
|
|
36
|
+
' const callbacksDescriptor = Object.getOwnPropertyDescriptor(context, "disposeCallbacksByApp");',
|
|
37
|
+
' const disposeDescriptor = Object.getOwnPropertyDescriptor(context, "dispose");',
|
|
38
|
+
' const disposeCallbacksByApp = callbacksDescriptor == null ? {} : callbacksDescriptor.value;',
|
|
39
|
+
' if (typeof disposeCallbacksByApp !== "object" || disposeCallbacksByApp == null || Array.isArray(disposeCallbacksByApp)) {',
|
|
40
|
+
' throw new Error("Cannot install micro-frontend lifecycle: invalid-dispose-callback-registry");',
|
|
41
|
+
' }',
|
|
42
|
+
' const dispose = disposeDescriptor == null ? function dispose(appName, callback) {',
|
|
43
|
+
' if (typeof appName !== "string" || typeof callback !== "function") {',
|
|
44
|
+
' throw new Error("dispose() must be compiled with the microFrontend plugin");',
|
|
45
|
+
' }',
|
|
46
|
+
' const callbacks = disposeCallbacksByApp[appName] ||= new Set();',
|
|
47
|
+
' callbacks.add(callback);',
|
|
48
|
+
' let isRegistered = true;',
|
|
49
|
+
' return function unregisterDispose() {',
|
|
50
|
+
' if (!isRegistered) { return; }',
|
|
51
|
+
' isRegistered = false;',
|
|
52
|
+
' callbacks.delete(callback);',
|
|
53
|
+
' };',
|
|
54
|
+
' } : disposeDescriptor.value;',
|
|
55
|
+
' if (typeof dispose !== "function") {',
|
|
56
|
+
' throw new Error("Cannot install micro-frontend lifecycle: invalid-dispose-registrar");',
|
|
57
|
+
' }',
|
|
58
|
+
' try {',
|
|
59
|
+
' if (!Reflect.defineProperty(context, "disposeCallbacksByApp", {',
|
|
60
|
+
' configurable: true, enumerable: false, value: disposeCallbacksByApp, writable: true,',
|
|
61
|
+
' }) || !Reflect.defineProperty(context, "dispose", {',
|
|
62
|
+
' configurable: true, enumerable: false, value: dispose, writable: true,',
|
|
63
|
+
' })) {',
|
|
64
|
+
' throw new Error("Cannot install micro-frontend lifecycle: canonical-context-is-locked");',
|
|
65
|
+
' }',
|
|
66
|
+
' } catch (error) {',
|
|
67
|
+
' if (callbacksDescriptor == null) {',
|
|
68
|
+
' Reflect.deleteProperty(context, "disposeCallbacksByApp");',
|
|
69
|
+
' } else {',
|
|
70
|
+
' Reflect.defineProperty(context, "disposeCallbacksByApp", callbacksDescriptor);',
|
|
71
|
+
' }',
|
|
72
|
+
' if (disposeDescriptor == null) {',
|
|
73
|
+
' Reflect.deleteProperty(context, "dispose");',
|
|
74
|
+
' } else {',
|
|
75
|
+
' Reflect.defineProperty(context, "dispose", disposeDescriptor);',
|
|
76
|
+
' }',
|
|
77
|
+
' throw error;',
|
|
78
|
+
' }',
|
|
79
|
+
'})(global.__MICRO_FRONTEND__);',
|
|
39
80
|
].join('\n'),
|
|
40
81
|
preludeScript: [
|
|
82
|
+
`const __containerPairSymbol = Symbol.for(${JSON.stringify(CONTAINER_PAIRING_KEY)});`,
|
|
83
|
+
'const __legacyContainers = new WeakMap();',
|
|
41
84
|
'function createContainer(appName, config) {',
|
|
42
|
-
' const
|
|
43
|
-
'
|
|
44
|
-
'
|
|
85
|
+
' const context = global.__MICRO_FRONTEND__;',
|
|
86
|
+
' const containers = context.__CONTAINERS__;',
|
|
87
|
+
' const instances = context.__INSTANCES__;',
|
|
88
|
+
' if (containers[appName] != null || typeof instances[appName] === "number") {',
|
|
89
|
+
" throw new Error(`App container '${appName}' is already registered`);",
|
|
90
|
+
' }',
|
|
91
|
+
' if (!Object.isExtensible(containers) || !Object.isExtensible(instances)) {',
|
|
92
|
+
' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
|
|
93
|
+
' }',
|
|
94
|
+
' const modernContainer = { appName, config, exposedModules: {} };',
|
|
95
|
+
' const legacyContainer = { name: appName, config, exposeMap: {} };',
|
|
96
|
+
' try {',
|
|
97
|
+
' if (!Reflect.defineProperty(instances, appName, {',
|
|
98
|
+
' configurable: true, enumerable: false, value: instances.length, writable: false,',
|
|
99
|
+
' })) {',
|
|
100
|
+
' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
|
|
101
|
+
' }',
|
|
102
|
+
' instances.push(legacyContainer);',
|
|
103
|
+
' if (!Reflect.set(containers, appName, modernContainer)) {',
|
|
104
|
+
' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
|
|
105
|
+
' }',
|
|
106
|
+
' if (!Reflect.defineProperty(modernContainer, __containerPairSymbol, {',
|
|
107
|
+
' configurable: true, enumerable: false, value: legacyContainer, writable: false,',
|
|
108
|
+
' }) || !Reflect.defineProperty(legacyContainer, __containerPairSymbol, {',
|
|
109
|
+
' configurable: true, enumerable: false, value: modernContainer, writable: false,',
|
|
110
|
+
' })) {',
|
|
111
|
+
' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
|
|
112
|
+
' }',
|
|
113
|
+
' } catch (error) {',
|
|
114
|
+
' if (Object.is(instances[instances.length - 1], legacyContainer)) {',
|
|
115
|
+
' instances.pop();',
|
|
116
|
+
' }',
|
|
117
|
+
' Reflect.deleteProperty(instances, appName);',
|
|
118
|
+
' Reflect.deleteProperty(containers, appName);',
|
|
119
|
+
' Reflect.deleteProperty(modernContainer, __containerPairSymbol);',
|
|
120
|
+
' Reflect.deleteProperty(legacyContainer, __containerPairSymbol);',
|
|
121
|
+
' throw error;',
|
|
45
122
|
' }',
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
' return container;',
|
|
123
|
+
' __legacyContainers.set(modernContainer, legacyContainer);',
|
|
124
|
+
' return modernContainer;',
|
|
49
125
|
'}',
|
|
50
126
|
'function exposeModule(container, exposedModule, moduleValue) {',
|
|
51
127
|
" const normalizedModule = exposedModule.startsWith('./') ? exposedModule : `./${exposedModule}`;",
|
|
52
|
-
|
|
53
|
-
'
|
|
128
|
+
" const legacyModule = exposedModule.startsWith('./') ? exposedModule.slice(2) : exposedModule;",
|
|
129
|
+
' const legacyContainer = __legacyContainers.get(container);',
|
|
130
|
+
' if (Reflect.has(container.exposedModules, normalizedModule) ||',
|
|
131
|
+
' (legacyContainer != null && Reflect.has(legacyContainer.exposeMap, legacyModule))) {',
|
|
132
|
+
" throw new Error(`Exposed module '${normalizedModule}' is already registered in app container '${container.appName}'`);",
|
|
133
|
+
' }',
|
|
134
|
+
' const ownsModernContainer = Object.is(',
|
|
135
|
+
' global.__MICRO_FRONTEND__.__CONTAINERS__[container.appName],',
|
|
136
|
+
' container',
|
|
137
|
+
' );',
|
|
138
|
+
' let legacyModuleValue = moduleValue;',
|
|
139
|
+
' if (legacyContainer != null &&',
|
|
140
|
+
' ((typeof moduleValue === "object" && moduleValue != null) || typeof moduleValue === "function") &&',
|
|
141
|
+
' moduleValue.__esModule !== true) {',
|
|
142
|
+
' legacyModuleValue = typeof moduleValue === "function"',
|
|
143
|
+
' ? function (...args) { return Reflect.apply(moduleValue, this, args); }',
|
|
144
|
+
' : {};',
|
|
145
|
+
' for (const exportName of Reflect.ownKeys(moduleValue)) {',
|
|
146
|
+
' if (exportName === "__esModule" || exportName === "default") {',
|
|
147
|
+
' continue;',
|
|
148
|
+
' }',
|
|
149
|
+
' const sourceDescriptor = Reflect.getOwnPropertyDescriptor(moduleValue, exportName);',
|
|
150
|
+
' const facadeDescriptor = Reflect.getOwnPropertyDescriptor(legacyModuleValue, exportName);',
|
|
151
|
+
' if (sourceDescriptor == null || (facadeDescriptor != null && facadeDescriptor.configurable === false)) {',
|
|
152
|
+
' // Callable facades keep intrinsic non-configurable slots such as prototype.',
|
|
153
|
+
' continue;',
|
|
154
|
+
' }',
|
|
155
|
+
' if (!Reflect.defineProperty(legacyModuleValue, exportName, {',
|
|
156
|
+
' enumerable: sourceDescriptor.enumerable,',
|
|
157
|
+
' get: function () { return Reflect.get(moduleValue, exportName); },',
|
|
158
|
+
' })) {',
|
|
159
|
+
' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
|
|
160
|
+
' }',
|
|
161
|
+
' }',
|
|
162
|
+
' const hasDefaultExport = Reflect.getOwnPropertyDescriptor(moduleValue, "default") != null;',
|
|
163
|
+
' Object.defineProperties(legacyModuleValue, {',
|
|
164
|
+
' __esModule: { value: true },',
|
|
165
|
+
' default: {',
|
|
166
|
+
' enumerable: true,',
|
|
167
|
+
' get: hasDefaultExport',
|
|
168
|
+
' ? function () { return Reflect.get(moduleValue, "default"); }',
|
|
169
|
+
' : function () { return moduleValue; },',
|
|
170
|
+
' },',
|
|
171
|
+
' });',
|
|
172
|
+
' }',
|
|
173
|
+
' try {',
|
|
174
|
+
' if (ownsModernContainer && !Reflect.set(container.exposedModules, normalizedModule, moduleValue)) {',
|
|
175
|
+
' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
|
|
176
|
+
' }',
|
|
177
|
+
' if (legacyContainer != null) {',
|
|
178
|
+
' Object.defineProperty(legacyContainer.exposeMap, legacyModule, {',
|
|
179
|
+
' configurable: true,',
|
|
180
|
+
' enumerable: true,',
|
|
181
|
+
' get: function () { return legacyModuleValue; },',
|
|
182
|
+
' });',
|
|
183
|
+
' }',
|
|
184
|
+
' } catch (error) {',
|
|
185
|
+
' if (ownsModernContainer) {',
|
|
186
|
+
' Reflect.deleteProperty(container.exposedModules, normalizedModule);',
|
|
187
|
+
' }',
|
|
188
|
+
' if (legacyContainer != null) {',
|
|
189
|
+
' Reflect.deleteProperty(legacyContainer.exposeMap, legacyModule);',
|
|
190
|
+
' }',
|
|
191
|
+
' throw error;',
|
|
54
192
|
' }',
|
|
55
|
-
' container.exposedModules[normalizedModule] = moduleValue;',
|
|
56
193
|
'}',
|
|
57
194
|
'function registerShared(moduleName, moduleValue) {',
|
|
58
|
-
' const sharedModules = global.
|
|
195
|
+
' const sharedModules = global.__MICRO_FRONTEND__.__SHARED__;',
|
|
59
196
|
' const existingModule = sharedModules[moduleName];',
|
|
60
197
|
' if (existingModule != null) {',
|
|
61
|
-
' if (
|
|
198
|
+
' if (typeof existingModule === "object" &&',
|
|
199
|
+
' typeof existingModule.get === "function" &&',
|
|
200
|
+
' typeof existingModule.loaded === "boolean" &&',
|
|
201
|
+
' Object.is(existingModule.get(), moduleValue)) {',
|
|
62
202
|
' return;',
|
|
63
203
|
' }',
|
|
64
|
-
|
|
204
|
+
" throw new Error(`Shared module '${moduleName}' is already registered`);",
|
|
65
205
|
' }',
|
|
66
206
|
' sharedModules[moduleName] = { get: () => moduleValue, loaded: true };',
|
|
67
207
|
'}',
|
package/src/plugin/resolver.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function createSharedResolverConfig(
|
|
|
24
24
|
return {
|
|
25
25
|
loader: 'js',
|
|
26
26
|
contents: [
|
|
27
|
-
`var sharedModule = global.
|
|
27
|
+
`var sharedModule = global.__MICRO_FRONTEND__.__SHARED__[${moduleName}];`,
|
|
28
28
|
'if (sharedModule == null) {',
|
|
29
29
|
` throw new Error('Shared module ' + ${moduleName} + ' is not registered');`,
|
|
30
30
|
'}',
|