@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +124 -55
  3. package/dist/module/host/pendingHostComponentStore.js +33 -14
  4. package/dist/module/host/pendingHostComponentStore.js.map +1 -1
  5. package/dist/module/index.js.map +1 -1
  6. package/dist/module/plugin/disposeBabelPlugin.js +38 -0
  7. package/dist/module/plugin/disposeBabelPlugin.js.map +1 -0
  8. package/dist/module/plugin/disposeTransform.js +37 -0
  9. package/dist/module/plugin/disposeTransform.js.map +1 -0
  10. package/dist/module/plugin/globalContextScript.js +121 -0
  11. package/dist/module/plugin/globalContextScript.js.map +1 -0
  12. package/dist/module/plugin/microFrontendPlugin.js +12 -1
  13. package/dist/module/plugin/microFrontendPlugin.js.map +1 -1
  14. package/dist/module/plugin/prelude.js +4 -2
  15. package/dist/module/plugin/prelude.js.map +1 -1
  16. package/dist/module/plugin/resolver.js +1 -1
  17. package/dist/module/plugin/resolver.js.map +1 -1
  18. package/dist/module/runtime/containerModuleExposure.js +79 -0
  19. package/dist/module/runtime/containerModuleExposure.js.map +1 -0
  20. package/dist/module/runtime/containerPairing.js +86 -0
  21. package/dist/module/runtime/containerPairing.js.map +1 -0
  22. package/dist/module/runtime/containerRegistry.js +190 -0
  23. package/dist/module/runtime/containerRegistry.js.map +1 -0
  24. package/dist/module/runtime/createMicroFrontendRuntime.js +9 -6
  25. package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -1
  26. package/dist/module/runtime/globalContext.js +89 -0
  27. package/dist/module/runtime/globalContext.js.map +1 -0
  28. package/dist/module/runtime/registry.js +103 -45
  29. package/dist/module/runtime/registry.js.map +1 -1
  30. package/dist/module/session/useMicroFrontendSessions.js +23 -1
  31. package/dist/module/session/useMicroFrontendSessions.js.map +1 -1
  32. package/dist/typescript/index.d.ts +1 -0
  33. package/dist/typescript/plugin/disposeBabelPlugin.d.ts +8 -0
  34. package/dist/typescript/plugin/disposeTransform.d.ts +3 -0
  35. package/dist/typescript/plugin/globalContextScript.d.ts +1 -0
  36. package/dist/typescript/runtime/containerModuleExposure.d.ts +2 -0
  37. package/dist/typescript/runtime/containerPairing.d.ts +18 -0
  38. package/dist/typescript/runtime/containerRegistry.d.ts +6 -0
  39. package/dist/typescript/runtime/globalContext.d.ts +17 -0
  40. package/dist/typescript/runtime/registry.d.ts +10 -8
  41. package/package.json +7 -4
  42. package/src/host/pendingHostComponentStore.ts +58 -29
  43. package/src/index.ts +1 -0
  44. package/src/plugin/disposeBabelPlugin.ts +51 -0
  45. package/src/plugin/disposeTransform.ts +43 -0
  46. package/src/plugin/globalContextScript.ts +118 -0
  47. package/src/plugin/microFrontendPlugin.ts +13 -1
  48. package/src/plugin/prelude.ts +160 -20
  49. package/src/plugin/resolver.ts +1 -1
  50. package/src/runtime/containerModuleExposure.ts +92 -0
  51. package/src/runtime/containerPairing.ts +120 -0
  52. package/src/runtime/containerRegistry.ts +239 -0
  53. package/src/runtime/createMicroFrontendRuntime.ts +8 -2
  54. package/src/runtime/globalContext.ts +164 -0
  55. package/src/runtime/registry.ts +151 -58
  56. 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
- interface GlobalWithPendingHostComponentStore {
44
- __graniteMicroFrontendPendingHostComponentStore?: PendingHostComponentStore;
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 globalObject = getGlobalObject();
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
- if (globalObject.__graniteMicroFrontendPendingHostComponentStore == null) {
56
- globalObject.__graniteMicroFrontendPendingHostComponentStore = {
57
- entries: [],
58
- hidden: false,
59
- listeners: new Set(),
60
- version: 0,
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 globalObject.__graniteMicroFrontendPendingHostComponentStore;
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 = getGlobalObject().hideSharedPendingHostComponent;
103
- if (typeof hideSharedPendingHostComponent === 'function' && hideSharedPendingHostComponent !== hidePendingHostComponent) {
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
- getGlobalObject().hideSharedPendingHostComponent = hidePendingHostComponent;
137
+ Reflect.set(globalThis, HIDE_SHARED_PENDING_HOST_COMPONENT_KEY, hidePendingHostComponent);
114
138
  }
115
139
 
116
- export function registerPendingHostComponentRoute<
117
- TParams extends Readonly<object> | undefined = undefined,
118
- >(routePath: string, options: RegisterPendingHostComponentRouteOptions<TParams>) {
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(request: PendingHostComponentRouteRequest | string): ResolvedPendingHostComponent | null {
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(matched: MatchedPendingHostComponentRoute | null): ResolvedPendingHostComponent | null {
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
- const preludePath = path.join(localDirectory, 'micro-frontend-runtime.js');
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 },
@@ -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
- 'global._graniteMicroFrontend = global._graniteMicroFrontend || {',
36
- ' containers: {},',
37
- ' sharedModules: {},',
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 containers = global._graniteMicroFrontend.containers;',
43
- ' if (containers[appName] != null) {',
44
- ' throw new Error(`App container \'${appName}\' is already registered`);',
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
- ' const container = { appName, config, exposedModules: {} };',
47
- ' containers[appName] = container;',
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
- ' if (container.exposedModules[normalizedModule] != null) {',
53
- ' throw new Error(`Exposed module \'${normalizedModule}\' is already registered in app container \'${container.appName}\'`);',
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._graniteMicroFrontend.sharedModules;',
195
+ ' const sharedModules = global.__MICRO_FRONTEND__.__SHARED__;',
59
196
  ' const existingModule = sharedModules[moduleName];',
60
197
  ' if (existingModule != null) {',
61
- ' if (Object.is(existingModule.get(), moduleValue)) {',
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
- ' throw new Error(`Shared module \'${moduleName}\' is already registered`);',
204
+ " throw new Error(`Shared module '${moduleName}' is already registered`);",
65
205
  ' }',
66
206
  ' sharedModules[moduleName] = { get: () => moduleValue, loaded: true };',
67
207
  '}',
@@ -24,7 +24,7 @@ export function createSharedResolverConfig(
24
24
  return {
25
25
  loader: 'js',
26
26
  contents: [
27
- `var sharedModule = global._graniteMicroFrontend.sharedModules[${moduleName}];`,
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
  '}',