@granite-js/micro-frontend 2.5.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +20 -6
  3. package/dist/module/host/PendingHostComponent.js.map +1 -1
  4. package/dist/module/host/index.js.map +1 -1
  5. package/dist/module/host/resolveParams.js.map +1 -1
  6. package/dist/module/index.js.map +1 -1
  7. package/dist/module/next/README.md +15 -0
  8. package/dist/module/next/index.js +109 -0
  9. package/dist/module/next/index.js.map +1 -0
  10. package/dist/module/next/types.js +4 -0
  11. package/dist/module/next/types.js.map +1 -0
  12. package/dist/module/plugin/microFrontendPlugin.js +4 -5
  13. package/dist/module/plugin/microFrontendPlugin.js.map +1 -1
  14. package/dist/module/plugin-utils/disposeBabelPlugin.js.map +1 -0
  15. package/dist/module/plugin-utils/disposeTransform.js.map +1 -0
  16. package/dist/module/{plugin → plugin-utils}/globalContextScript.js.map +1 -1
  17. package/dist/module/plugin-utils/intoShared.js.map +1 -0
  18. package/dist/module/{plugin → plugin-utils}/prelude.js +4 -2
  19. package/dist/module/plugin-utils/prelude.js.map +1 -0
  20. package/dist/module/runtime/containerModuleExposure.js +1 -37
  21. package/dist/module/runtime/containerModuleExposure.js.map +1 -1
  22. package/dist/module/runtime/legacyEsm.js +41 -0
  23. package/dist/module/runtime/legacyEsm.js.map +1 -0
  24. package/dist/module/runtime/registry.js +3 -12
  25. package/dist/module/runtime/registry.js.map +1 -1
  26. package/dist/module/runtime/sharedModule.js +22 -0
  27. package/dist/module/runtime/sharedModule.js.map +1 -0
  28. package/dist/module/session/MicroFrontendSessionContext.js.map +1 -1
  29. package/dist/module/specs/PortalHostViewNativeComponent.ts +2 -2
  30. package/dist/module/specs/PortalViewNativeComponent.ts +2 -2
  31. package/dist/typescript/host/index.d.ts +1 -1
  32. package/dist/typescript/index.d.ts +1 -1
  33. package/dist/typescript/next/index.d.ts +4 -0
  34. package/dist/typescript/next/types.d.ts +6 -0
  35. package/dist/typescript/plugin-utils/intoShared.d.ts +2 -0
  36. package/dist/typescript/plugin-utils/prelude.d.ts +11 -0
  37. package/dist/typescript/runtime/legacyEsm.d.ts +1 -0
  38. package/dist/typescript/runtime/sharedModule.d.ts +4 -0
  39. package/dist/typescript/specs/PortalHostViewNativeComponent.d.ts +1 -1
  40. package/dist/typescript/specs/PortalViewNativeComponent.d.ts +1 -1
  41. package/package.json +14 -5
  42. package/react-native.config.js +1 -4
  43. package/src/host/PendingHostComponent.tsx +10 -2
  44. package/src/host/index.ts +1 -4
  45. package/src/host/resolveParams.ts +1 -1
  46. package/src/host/types.ts +1 -3
  47. package/src/index.ts +1 -4
  48. package/src/next/README.md +15 -0
  49. package/src/next/index.ts +107 -0
  50. package/src/next/types.ts +7 -0
  51. package/src/plugin/microFrontendPlugin.ts +5 -6
  52. package/src/{plugin → plugin-utils}/intoShared.ts +1 -2
  53. package/src/{plugin → plugin-utils}/prelude.ts +14 -47
  54. package/src/runtime/containerModuleExposure.ts +1 -39
  55. package/src/runtime/legacyEsm.ts +39 -0
  56. package/src/runtime/registry.ts +3 -18
  57. package/src/runtime/sharedModule.ts +26 -0
  58. package/src/session/MicroFrontendSessionContext.tsx +1 -3
  59. package/src/specs/PortalHostViewNativeComponent.ts +2 -2
  60. package/src/specs/PortalViewNativeComponent.ts +2 -2
  61. package/dist/module/plugin/disposeBabelPlugin.js.map +0 -1
  62. package/dist/module/plugin/disposeTransform.js.map +0 -1
  63. package/dist/module/plugin/intoShared.js.map +0 -1
  64. package/dist/module/plugin/prelude.js.map +0 -1
  65. package/dist/typescript/plugin/intoShared.d.ts +0 -3
  66. package/dist/typescript/plugin/prelude.d.ts +0 -6
  67. /package/dist/module/{plugin → plugin-utils}/disposeBabelPlugin.js +0 -0
  68. /package/dist/module/{plugin → plugin-utils}/disposeTransform.js +0 -0
  69. /package/dist/module/{plugin → plugin-utils}/globalContextScript.js +0 -0
  70. /package/dist/module/{plugin → plugin-utils}/intoShared.js +0 -0
  71. /package/dist/typescript/{plugin → plugin-utils}/disposeBabelPlugin.d.ts +0 -0
  72. /package/dist/typescript/{plugin → plugin-utils}/disposeTransform.d.ts +0 -0
  73. /package/dist/typescript/{plugin → plugin-utils}/globalContextScript.d.ts +0 -0
  74. /package/src/{plugin → plugin-utils}/disposeBabelPlugin.ts +0 -0
  75. /package/src/{plugin → plugin-utils}/disposeTransform.ts +0 -0
  76. /package/src/{plugin → plugin-utils}/globalContextScript.ts +0 -0
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ // Keep this helper self-contained so runtime and generated registrations stay identical.
4
+ export function registerSharedValue(sharedModules, moduleName, module, normalize) {
5
+ // Preserve original identity across independently bundled runtimes.
6
+ const sourceKey = Symbol.for('granite.microFrontend.sharedModuleSource');
7
+ const existingModule = sharedModules[moduleName];
8
+ if (existingModule != null) {
9
+ return typeof existingModule === 'object' && typeof existingModule.get === 'function' && typeof existingModule.loaded === 'boolean' && (Object.is(existingModule.get(), module) || Object.prototype.hasOwnProperty.call(existingModule, sourceKey) && Object.is(Reflect.get(existingModule, sourceKey), module));
10
+ }
11
+ const value = normalize(module);
12
+ const entry = {
13
+ get: () => value,
14
+ loaded: true
15
+ };
16
+ Object.defineProperty(entry, sourceKey, {
17
+ value: module
18
+ });
19
+ sharedModules[moduleName] = entry;
20
+ return true;
21
+ }
22
+ //# sourceMappingURL=sharedModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["registerSharedValue","sharedModules","moduleName","module","normalize","sourceKey","Symbol","for","existingModule","get","loaded","Object","is","prototype","hasOwnProperty","call","Reflect","value","entry","defineProperty"],"sourceRoot":"../../../src","sources":["runtime/sharedModule.ts"],"mappings":";;AAAA;AACA,OAAO,SAASA,mBAAmBA,CACjCC,aAAwF,EACxFC,UAAkB,EAClBC,MAAe,EACfC,SAAsC,EAC7B;EACT;EACA,MAAMC,SAAS,GAAGC,MAAM,CAACC,GAAG,CAAC,0CAA0C,CAAC;EACxE,MAAMC,cAAc,GAAGP,aAAa,CAACC,UAAU,CAAC;EAChD,IAAIM,cAAc,IAAI,IAAI,EAAE;IAC1B,OACE,OAAOA,cAAc,KAAK,QAAQ,IAClC,OAAOA,cAAc,CAACC,GAAG,KAAK,UAAU,IACxC,OAAOD,cAAc,CAACE,MAAM,KAAK,SAAS,KACzCC,MAAM,CAACC,EAAE,CAACJ,cAAc,CAACC,GAAG,CAAC,CAAC,EAAEN,MAAM,CAAC,IACrCQ,MAAM,CAACE,SAAS,CAACC,cAAc,CAACC,IAAI,CAACP,cAAc,EAAEH,SAAS,CAAC,IAC9DM,MAAM,CAACC,EAAE,CAACI,OAAO,CAACP,GAAG,CAACD,cAAc,EAAEH,SAAS,CAAC,EAAEF,MAAM,CAAE,CAAC;EAEnE;EACA,MAAMc,KAAK,GAAGb,SAAS,CAACD,MAAM,CAAC;EAC/B,MAAMe,KAAK,GAAG;IAAET,GAAG,EAAEA,CAAA,KAAMQ,KAAK;IAAEP,MAAM,EAAE;EAAK,CAAC;EAChDC,MAAM,CAACQ,cAAc,CAACD,KAAK,EAAEb,SAAS,EAAE;IAAEY,KAAK,EAAEd;EAAO,CAAC,CAAC;EAC1DF,aAAa,CAACC,UAAU,CAAC,GAAGgB,KAAK;EACjC,OAAO,IAAI;AACb","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"names":["VisibilityChangedProvider","createContext","useContext","useMemo","jsx","_jsx","MicroFrontendSessionContext","MissingMicroFrontendSessionError","Error","constructor","name","MicroFrontendSessionProvider","props","value","sessionId","Provider","children","isVisible","presentationVisibility","useMicroFrontendSession","session"],"sourceRoot":"../../../src","sources":["session/MicroFrontendSessionContext.tsx"],"mappings":";;AAAA,SAASA,yBAAyB,QAAQ,0BAA0B;AACpE,SAASC,aAAa,EAAEC,UAAU,EAAEC,OAAO,QAA2C,OAAO;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAW9F,MAAMC,2BAA2B,gBAAGL,aAAa,CAA8B,IAAI,CAAC;AAEpF,OAAO,MAAMM,gCAAgC,SAASC,KAAK,CAAC;EAC1DC,WAAWA,CAAA,EAAG;IACZ,KAAK,CAAC,0EAA0E,CAAC;IACjF,IAAI,CAACC,IAAI,GAAG,kCAAkC;EAChD;AACF;AAEA,OAAO,SAASC,4BAA4BA,CAACC,KAAwC,EAAgB;EACnG,MAAMC,KAAK,GAAGV,OAAO,CACnB,OAAO;IACLW,SAAS,EAAEF,KAAK,CAACE;EACnB,CAAC,CAAC,EACF,CAACF,KAAK,CAACE,SAAS,CAClB,CAAC;EAED,oBACET,IAAA,CAACC,2BAA2B,CAACS,QAAQ;IAACF,KAAK,EAAEA,KAAM;IAAAG,QAAA,eACjDX,IAAA,CAACL,yBAAyB;MAACiB,SAAS,EAAEL,KAAK,CAACM,sBAAuB;MAAAF,QAAA,EAChEJ,KAAK,CAACI;IAAQ,CACU;EAAC,CACQ,CAAC;AAE3C;AAEA,OAAO,SAASG,uBAAuBA,CAAA,EAAyB;EAC9D,MAAMC,OAAO,GAAGlB,UAAU,CAACI,2BAA2B,CAAC;EACvD,IAAIc,OAAO,IAAI,IAAI,EAAE;IACnB,MAAM,IAAIb,gCAAgC,CAAC,CAAC;EAC9C;EACA,OAAOa,OAAO;AAChB","ignoreList":[]}
1
+ {"version":3,"names":["VisibilityChangedProvider","createContext","useContext","useMemo","jsx","_jsx","MicroFrontendSessionContext","MissingMicroFrontendSessionError","Error","constructor","name","MicroFrontendSessionProvider","props","value","sessionId","Provider","children","isVisible","presentationVisibility","useMicroFrontendSession","session"],"sourceRoot":"../../../src","sources":["session/MicroFrontendSessionContext.tsx"],"mappings":";;AAAA,SAASA,yBAAyB,QAAQ,0BAA0B;AACpE,SAASC,aAAa,EAAEC,UAAU,EAAEC,OAAO,QAA2C,OAAO;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAW9F,MAAMC,2BAA2B,gBAAGL,aAAa,CAA8B,IAAI,CAAC;AAEpF,OAAO,MAAMM,gCAAgC,SAASC,KAAK,CAAC;EAC1DC,WAAWA,CAAA,EAAG;IACZ,KAAK,CAAC,0EAA0E,CAAC;IACjF,IAAI,CAACC,IAAI,GAAG,kCAAkC;EAChD;AACF;AAEA,OAAO,SAASC,4BAA4BA,CAACC,KAAwC,EAAgB;EACnG,MAAMC,KAAK,GAAGV,OAAO,CACnB,OAAO;IACLW,SAAS,EAAEF,KAAK,CAACE;EACnB,CAAC,CAAC,EACF,CAACF,KAAK,CAACE,SAAS,CAClB,CAAC;EAED,oBACET,IAAA,CAACC,2BAA2B,CAACS,QAAQ;IAACF,KAAK,EAAEA,KAAM;IAAAG,QAAA,eACjDX,IAAA,CAACL,yBAAyB;MAACiB,SAAS,EAAEL,KAAK,CAACM,sBAAuB;MAAAF,QAAA,EAAEJ,KAAK,CAACI;IAAQ,CAA4B;EAAC,CAC5E,CAAC;AAE3C;AAEA,OAAO,SAASG,uBAAuBA,CAAA,EAAyB;EAC9D,MAAMC,OAAO,GAAGlB,UAAU,CAACI,2BAA2B,CAAC;EACvD,IAAIc,OAAO,IAAI,IAAI,EAAE;IACnB,MAAM,IAAIb,gCAAgC,CAAC,CAAC;EAC9C;EACA,OAAOa,OAAO;AAChB","ignoreList":[]}
@@ -1,9 +1,9 @@
1
- import { codegenNativeComponent, type ViewProps } from "react-native";
1
+ import { codegenNativeComponent, type ViewProps } from 'react-native';
2
2
 
3
3
  interface NativeProps extends ViewProps {
4
4
  readonly name?: string;
5
5
  }
6
6
 
7
- export default codegenNativeComponent<NativeProps>("PortalHostView", {
7
+ export default codegenNativeComponent<NativeProps>('PortalHostView', {
8
8
  interfaceOnly: true,
9
9
  });
@@ -1,9 +1,9 @@
1
- import { codegenNativeComponent, type ViewProps } from "react-native";
1
+ import { codegenNativeComponent, type ViewProps } from 'react-native';
2
2
 
3
3
  interface NativeProps extends ViewProps {
4
4
  readonly hostName: string;
5
5
  }
6
6
 
7
- export default codegenNativeComponent<NativeProps>("PortalView", {
7
+ export default codegenNativeComponent<NativeProps>('PortalView', {
8
8
  interfaceOnly: true,
9
9
  });
@@ -1,5 +1,5 @@
1
1
  export type { PendingHostComponentController, PendingHostComponentProps } from './PendingHostComponent';
2
2
  export { PendingHostComponent, usePendingHostComponentController, useIsPendingHostComponentHidden, useResolvedPendingHostComponent, } from './PendingHostComponent';
3
3
  export type { PendingHostComponentAppConfig, PendingHostComponentRenderer, PendingHostComponentParams, PendingHostComponentRouteRequest, RegisterPendingHostComponentRouteOptions, ResolvedPendingHostComponent, } from './types';
4
- export { createPendingHostComponentRoutePrefix, normalizeRoutePath, } from './routeMatcher';
4
+ export { createPendingHostComponentRoutePrefix, normalizeRoutePath } from './routeMatcher';
5
5
  export { hidePendingHostComponent, installPendingHostComponentBridge, registerPendingHostComponentRoute, resetPendingHostComponent, resolvePendingHostComponent, } from './pendingHostComponentStore';
@@ -6,7 +6,7 @@ export { findAppNameBySourceURL, getAppName } from './runtime/getAppName';
6
6
  export type { PendingHostComponentController, PendingHostComponentProps } from './host/PendingHostComponent';
7
7
  export { PendingHostComponent, usePendingHostComponentController, useIsPendingHostComponentHidden, useResolvedPendingHostComponent, } from './host/PendingHostComponent';
8
8
  export type { PendingHostComponentAppConfig, PendingHostComponentRenderer, PendingHostComponentParams, PendingHostComponentRouteRequest, RegisterPendingHostComponentRouteOptions, ResolvedPendingHostComponent, } from './host/types';
9
- export { createPendingHostComponentRoutePrefix, normalizeRoutePath, } from './host/routeMatcher';
9
+ export { createPendingHostComponentRoutePrefix, normalizeRoutePath } from './host/routeMatcher';
10
10
  export { hidePendingHostComponent, installPendingHostComponentBridge, registerPendingHostComponentRoute, resetPendingHostComponent, resolvePendingHostComponent, } from './host/pendingHostComponentStore';
11
11
  export { MicroFrontendSessionProvider, MissingMicroFrontendSessionError, useMicroFrontendSession, } from './session/MicroFrontendSessionContext';
12
12
  export type { MicroFrontendSession, MicroFrontendSessionProviderProps } from './session/MicroFrontendSessionContext';
@@ -0,0 +1,4 @@
1
+ import type { PluginOption } from 'rollipop';
2
+ import type { MicroFrontendPluginOptions } from './types';
3
+ export declare function microFrontend(options: MicroFrontendPluginOptions): PluginOption;
4
+ export type { MicroFrontendPluginOptions } from './types';
@@ -0,0 +1,6 @@
1
+ import type { SharedConfig } from '../runtime/registry';
2
+ export interface MicroFrontendPluginOptions {
3
+ appName: string;
4
+ shared?: SharedConfig | readonly string[];
5
+ exposes?: Readonly<Record<string, string>>;
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { SharedConfig } from '../runtime/registry';
2
+ export declare function intoShared(shared: SharedConfig | readonly string[] | undefined): SharedConfig | undefined;
@@ -0,0 +1,11 @@
1
+ import type { SharedConfig } from '../runtime/registry';
2
+ interface MicroFrontendPreludeOptions {
3
+ readonly shared?: SharedConfig | readonly string[];
4
+ readonly exposes?: Readonly<Record<string, string>>;
5
+ }
6
+ export interface MicroFrontendPreludeConfig {
7
+ readonly banner: string;
8
+ readonly preludeScript: string;
9
+ }
10
+ export declare function getPreludeConfig(options: MicroFrontendPreludeOptions, appName?: string): MicroFrontendPreludeConfig;
11
+ export {};
@@ -0,0 +1 @@
1
+ export declare function toLegacyEsm(module: unknown): unknown;
@@ -0,0 +1,4 @@
1
+ export declare function registerSharedValue(sharedModules: Record<string, {
2
+ readonly get: () => unknown;
3
+ readonly loaded: boolean;
4
+ }>, moduleName: string, module: unknown, normalize: (value: unknown) => unknown): boolean;
@@ -1,4 +1,4 @@
1
- import { type ViewProps } from "react-native";
1
+ import { type ViewProps } from 'react-native';
2
2
  interface NativeProps extends ViewProps {
3
3
  readonly name?: string;
4
4
  }
@@ -1,4 +1,4 @@
1
- import { type ViewProps } from "react-native";
1
+ import { type ViewProps } from 'react-native';
2
2
  interface NativeProps extends ViewProps {
3
3
  readonly hostName: string;
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granite-js/micro-frontend",
3
- "version": "2.5.5",
3
+ "version": "3.0.0",
4
4
  "description": "Micro-frontend build, runtime, session, and Portal contracts for Granite brownfield applications",
5
5
  "main": "./dist/module/index.js",
6
6
  "types": "./dist/typescript/index.d.ts",
@@ -12,6 +12,10 @@
12
12
  "default": "./dist/module/index.js"
13
13
  },
14
14
  "./plugin": {
15
+ "types": "./dist/typescript/next/index.d.ts",
16
+ "default": "./dist/module/next/index.js"
17
+ },
18
+ "./plugin-mpack": {
15
19
  "types": "./dist/typescript/plugin/index.d.ts",
16
20
  "default": "./dist/module/plugin/index.js"
17
21
  },
@@ -56,24 +60,28 @@
56
60
  },
57
61
  "dependencies": {
58
62
  "@babel/core": "7.28.5",
59
- "@granite-js/utils": "2.5.5",
63
+ "@granite-js/utils": "3.0.0",
60
64
  "@standard-schema/spec": "^1.0.0"
61
65
  },
62
66
  "peerDependencies": {
63
67
  "@granite-js/plugin-core": "*",
64
68
  "@granite-js/react-native": "*",
65
69
  "react": "*",
66
- "react-native": "*"
70
+ "react-native": "*",
71
+ "rollipop": "*"
67
72
  },
68
73
  "peerDependenciesMeta": {
69
74
  "@granite-js/plugin-core": {
70
75
  "optional": true
76
+ },
77
+ "rollipop": {
78
+ "optional": true
71
79
  }
72
80
  },
73
81
  "devDependencies": {
74
82
  "@babel/types": "7.28.5",
75
- "@granite-js/plugin-core": "2.5.5",
76
- "@granite-js/react-native": "2.5.5",
83
+ "@granite-js/plugin-core": "3.0.0",
84
+ "@granite-js/react-native": "3.0.0",
77
85
  "@types/babel__core": "^7.20.5",
78
86
  "@types/node": "24.10.12",
79
87
  "@types/react": "19.2.0",
@@ -82,6 +90,7 @@
82
90
  "react-native": "0.84.0",
83
91
  "react-native-builder-bob": "0.40.17",
84
92
  "react-test-renderer": "19.2.3",
93
+ "rollipop": "1.0.0-alpha.32",
85
94
  "typescript": "5.9.3",
86
95
  "vitest": "^4.0.12"
87
96
  },
@@ -2,10 +2,7 @@ module.exports = {
2
2
  dependency: {
3
3
  platforms: {
4
4
  android: {
5
- componentDescriptors: [
6
- 'PortalViewComponentDescriptor',
7
- 'PortalHostViewComponentDescriptor',
8
- ],
5
+ componentDescriptors: ['PortalViewComponentDescriptor', 'PortalHostViewComponentDescriptor'],
9
6
  cmakeListsPath: '../android/src/main/jni/CMakeLists.txt',
10
7
  },
11
8
  },
@@ -21,11 +21,19 @@ export interface PendingHostComponentController {
21
21
  }
22
22
 
23
23
  export function useIsPendingHostComponentHidden() {
24
- return useSyncExternalStore(subscribePendingHostComponentStore, getIsPendingHostComponentHidden, getIsPendingHostComponentHidden);
24
+ return useSyncExternalStore(
25
+ subscribePendingHostComponentStore,
26
+ getIsPendingHostComponentHidden,
27
+ getIsPendingHostComponentHidden
28
+ );
25
29
  }
26
30
 
27
31
  function usePendingHostComponentStoreVersion() {
28
- return useSyncExternalStore(subscribePendingHostComponentStore, getPendingHostComponentStoreVersion, getPendingHostComponentStoreVersion);
32
+ return useSyncExternalStore(
33
+ subscribePendingHostComponentStore,
34
+ getPendingHostComponentStoreVersion,
35
+ getPendingHostComponentStoreVersion
36
+ );
29
37
  }
30
38
 
31
39
  export function useResolvedPendingHostComponent(url: string | null | undefined) {
package/src/host/index.ts CHANGED
@@ -13,10 +13,7 @@ export type {
13
13
  RegisterPendingHostComponentRouteOptions,
14
14
  ResolvedPendingHostComponent,
15
15
  } from './types';
16
- export {
17
- createPendingHostComponentRoutePrefix,
18
- normalizeRoutePath,
19
- } from './routeMatcher';
16
+ export { createPendingHostComponentRoutePrefix, normalizeRoutePath } from './routeMatcher';
20
17
  export {
21
18
  hidePendingHostComponent,
22
19
  installPendingHostComponentBridge,
@@ -27,7 +27,7 @@ export function resolvePendingHostComponentParams(
27
27
  if (result.issues != null) {
28
28
  return {
29
29
  status: 'invalid',
30
- cause: new Error(result.issues.map(issue => issue.message).join(', ')),
30
+ cause: new Error(result.issues.map((issue) => issue.message).join(', ')),
31
31
  };
32
32
  }
33
33
 
package/src/host/types.ts CHANGED
@@ -20,9 +20,7 @@ export interface PendingHostComponentAppConfig {
20
20
  readonly host?: string | null;
21
21
  }
22
22
 
23
- export interface RegisterPendingHostComponentRouteOptions<
24
- TParams extends Readonly<object> | undefined = undefined,
25
- > {
23
+ export interface RegisterPendingHostComponentRouteOptions<TParams extends Readonly<object> | undefined = undefined> {
26
24
  readonly component: PendingHostComponentRenderer<TParams>;
27
25
  readonly parserParams?: ParserParams;
28
26
  readonly validateParams?: ValidateParams<TParams>;
package/src/index.ts CHANGED
@@ -18,10 +18,7 @@ export type {
18
18
  RegisterPendingHostComponentRouteOptions,
19
19
  ResolvedPendingHostComponent,
20
20
  } from './host/types';
21
- export {
22
- createPendingHostComponentRoutePrefix,
23
- normalizeRoutePath,
24
- } from './host/routeMatcher';
21
+ export { createPendingHostComponentRoutePrefix, normalizeRoutePath } from './host/routeMatcher';
25
22
  export {
26
23
  hidePendingHostComponent,
27
24
  installPendingHostComponentBridge,
@@ -0,0 +1,15 @@
1
+ # Rollipop
2
+
3
+ ```ts
4
+ import { microFrontend } from '@granite-js/micro-frontend/plugin';
5
+
6
+ const plugins = [
7
+ microFrontend({
8
+ appName: 'my-service',
9
+ shared: ['react', 'react-native'],
10
+ exposes: { './App': './src/App.tsx' },
11
+ }),
12
+ ];
13
+ ```
14
+
15
+ Hosts use `shared: { react: { eager: true } }` to register bundled modules. Remotes resolve non-eager modules from the shared registry. Shared exports remain live, and disposal callbacks are scoped to the application.
@@ -0,0 +1,107 @@
1
+ import * as path from 'node:path';
2
+ import type { Plugin, PluginOption } from 'rollipop';
3
+ import { code, include } from 'rollipop/filter';
4
+ import type { MicroFrontendPluginOptions } from './types';
5
+ import { transformDisposeOwnership } from '../plugin-utils/disposeTransform';
6
+ import { intoShared } from '../plugin-utils/intoShared';
7
+ import { getPreludeConfig } from '../plugin-utils/prelude';
8
+ import type { SharedConfig } from '../runtime/registry';
9
+
10
+ const PRELUDE = '\0granite:micro-frontend';
11
+ const SHARED = '\0granite:shared:';
12
+
13
+ export function microFrontend(options: MicroFrontendPluginOptions): PluginOption {
14
+ const shared = intoShared(options.shared);
15
+ const external = new Set(
16
+ Object.entries(shared ?? {})
17
+ .filter(([, value]) => value.eager !== true)
18
+ .map(([name]) => name)
19
+ );
20
+ return [prelude(options, shared), external.size > 0 && sharedModules(external), dispose(options.appName)];
21
+ }
22
+
23
+ function prelude(options: MicroFrontendPluginOptions, shared: SharedConfig | undefined): Plugin {
24
+ let prelude = '';
25
+
26
+ return {
27
+ name: 'granite:micro-frontend:prelude',
28
+ config(config) {
29
+ const root = config.root ?? process.cwd();
30
+ const exposes = Object.fromEntries(
31
+ Object.entries(options.exposes ?? {}).map(([name, file]) => [name, path.resolve(root, file)])
32
+ );
33
+ const generated = getPreludeConfig({ ...options, shared, exposes }, options.appName);
34
+ prelude = generated.preludeScript;
35
+ // Must run before the native-module proxy and before any exposed app module.
36
+ config.polyfills = [
37
+ { type: 'plain', code: `var global = globalThis;\n${generated.banner}` },
38
+ ...(config.polyfills ?? []),
39
+ ];
40
+ return { prelude: [PRELUDE] };
41
+ },
42
+ resolveId: {
43
+ filter: { id: /^\0granite:micro-frontend$/ },
44
+ handler() {
45
+ return PRELUDE;
46
+ },
47
+ },
48
+ load: {
49
+ filter: { id: /^\0granite:micro-frontend$/ },
50
+ handler() {
51
+ return prelude;
52
+ },
53
+ },
54
+ };
55
+ }
56
+
57
+ function sharedModules(external: ReadonlySet<string>): Plugin {
58
+ const externalPattern = new RegExp(
59
+ `^(?:${[...external].map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})$`
60
+ );
61
+
62
+ return {
63
+ name: 'granite:micro-frontend:shared-modules',
64
+ resolveId: {
65
+ filter: { id: externalPattern },
66
+ handler(source) {
67
+ return external.has(source) ? `${SHARED}${source}` : undefined;
68
+ },
69
+ },
70
+ load: {
71
+ filter: { id: /^\0granite:shared:/ },
72
+ handler(id) {
73
+ const name = JSON.stringify(id.slice(SHARED.length));
74
+ return `
75
+ var shared = globalThis.__MICRO_FRONTEND__.__SHARED__[${name}];
76
+ if (!shared) throw new Error('Shared module ' + ${name} + ' is not registered');
77
+ var original = shared.get();
78
+ var facade = {};
79
+ if (original != null) for (const key of Reflect.ownKeys(original)) {
80
+ if (key === '__esModule' || !Object.getOwnPropertyDescriptor(original, key)?.enumerable) continue;
81
+ Object.defineProperty(facade, key, { enumerable: true, configurable: true,
82
+ get() { return original[key]; },
83
+ set(value) { Object.defineProperty(facade, key, { value, writable: true, enumerable: true, configurable: true }); }
84
+ });
85
+ }
86
+ Object.defineProperty(facade, '__esModule', { value: true, writable: true, enumerable: true, configurable: true });
87
+ module.exports = facade;
88
+ `;
89
+ },
90
+ },
91
+ };
92
+ }
93
+
94
+ function dispose(appName: string): Plugin {
95
+ return {
96
+ name: 'granite:micro-frontend:dispose',
97
+ transform: {
98
+ filter: [include(code(/(?:global|globalThis)\.__MICRO_FRONTEND__\.dispose/))],
99
+ handler(source, id) {
100
+ const transformed = transformDisposeOwnership(id, source, { appName });
101
+ return source === transformed ? null : { code: transformed, map: null };
102
+ },
103
+ },
104
+ };
105
+ }
106
+
107
+ export type { MicroFrontendPluginOptions } from './types';
@@ -0,0 +1,7 @@
1
+ import type { SharedConfig } from '../runtime/registry';
2
+
3
+ export interface MicroFrontendPluginOptions {
4
+ appName: string;
5
+ shared?: SharedConfig | readonly string[];
6
+ exposes?: Readonly<Record<string, string>>;
7
+ }
@@ -2,12 +2,12 @@ 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';
7
- import { intoShared } from './intoShared';
8
- import { getPreludeConfig } from './prelude';
9
5
  import { createSharedResolverConfig } from './resolver';
10
6
  import type { MicroFrontendPluginOptions } from './types';
7
+ import type { DisposeOwnershipPluginOptions } from '../plugin-utils/disposeBabelPlugin';
8
+ import { transformDisposeOwnership } from '../plugin-utils/disposeTransform';
9
+ import { intoShared } from '../plugin-utils/intoShared';
10
+ import { getPreludeConfig } from '../plugin-utils/prelude';
11
11
 
12
12
  export async function microFrontend(options: MicroFrontendPluginOptions = {}): Promise<GranitePluginCore> {
13
13
  const shared = intoShared(options.shared);
@@ -18,8 +18,7 @@ export async function microFrontend(options: MicroFrontendPluginOptions = {}): P
18
18
  const nonEagerEntries = Object.entries(shared ?? {}).filter(([, config]) => config.eager !== true);
19
19
  const localDirectory = prepareLocalDirectory(process.cwd());
20
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.
21
+ // Sharing a path would let one plugin overwrite the other's runtime registrations.
23
22
  const preludePath = path.join(localDirectory, 'granite-micro-frontend-runtime.js');
24
23
  const prelude = getPreludeConfig(normalizedOptions);
25
24
  const resolver = createSharedResolverConfig(nonEagerEntries);
@@ -1,4 +1,3 @@
1
- import type { MicroFrontendPluginOptions } from './types';
2
1
  import type { SharedConfig, SharedModuleConfig } from '../runtime/registry';
3
2
 
4
3
  const SHARED_PRESETS = {
@@ -21,7 +20,7 @@ const SHARED_PRESETS = {
21
20
  ],
22
21
  } as const;
23
22
 
24
- export function intoShared(shared: MicroFrontendPluginOptions['shared']): SharedConfig | undefined {
23
+ export function intoShared(shared: SharedConfig | readonly string[] | undefined): SharedConfig | undefined {
25
24
  if (shared == null) {
26
25
  return undefined;
27
26
  }
@@ -1,15 +1,22 @@
1
- import path from 'path';
1
+ import * as path from 'node:path';
2
2
  import { globalContextScript } from './globalContextScript';
3
- import type { MicroFrontendPluginOptions } from './types';
4
3
  import { CONTAINER_PAIRING_KEY } from '../runtime/containerPairing';
4
+ import { toLegacyEsm } from '../runtime/legacyEsm';
5
+ import type { SharedConfig } from '../runtime/registry';
5
6
  import { captureStackFrames, resolveCurrentSourceURL } from '../runtime/runtimeSourceURL';
7
+ import { registerSharedValue } from '../runtime/sharedModule';
8
+
9
+ interface MicroFrontendPreludeOptions {
10
+ readonly shared?: SharedConfig | readonly string[];
11
+ readonly exposes?: Readonly<Record<string, string>>;
12
+ }
6
13
 
7
14
  export interface MicroFrontendPreludeConfig {
8
15
  readonly banner: string;
9
16
  readonly preludeScript: string;
10
17
  }
11
18
 
12
- export function getPreludeConfig(options: MicroFrontendPluginOptions, appName?: string): MicroFrontendPreludeConfig {
19
+ export function getPreludeConfig(options: MicroFrontendPreludeOptions, appName?: string): MicroFrontendPreludeConfig {
13
20
  const eagerSharedModules = Object.entries(options.shared ?? {}).filter(
14
21
  ([, config]) => !Array.isArray(options.shared) && config.eager === true
15
22
  );
@@ -140,41 +147,7 @@ export function getPreludeConfig(options: MicroFrontendPluginOptions, appName?:
140
147
  ' global.__MICRO_FRONTEND__.__CONTAINERS__[container.appName],',
141
148
  ' container',
142
149
  ' );',
143
- ' let legacyModuleValue = moduleValue;',
144
- ' if (legacyContainer != null &&',
145
- ' ((typeof moduleValue === "object" && moduleValue != null) || typeof moduleValue === "function") &&',
146
- ' moduleValue.__esModule !== true) {',
147
- ' legacyModuleValue = typeof moduleValue === "function"',
148
- ' ? function (...args) { return Reflect.apply(moduleValue, this, args); }',
149
- ' : {};',
150
- ' for (const exportName of Reflect.ownKeys(moduleValue)) {',
151
- ' if (exportName === "__esModule" || exportName === "default") {',
152
- ' continue;',
153
- ' }',
154
- ' const sourceDescriptor = Reflect.getOwnPropertyDescriptor(moduleValue, exportName);',
155
- ' const facadeDescriptor = Reflect.getOwnPropertyDescriptor(legacyModuleValue, exportName);',
156
- ' if (sourceDescriptor == null || (facadeDescriptor != null && facadeDescriptor.configurable === false)) {',
157
- ' // Callable facades keep intrinsic non-configurable slots such as prototype.',
158
- ' continue;',
159
- ' }',
160
- ' if (!Reflect.defineProperty(legacyModuleValue, exportName, {',
161
- ' enumerable: sourceDescriptor.enumerable,',
162
- ' get: function () { return Reflect.get(moduleValue, exportName); },',
163
- ' })) {',
164
- ' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
165
- ' }',
166
- ' }',
167
- ' const hasDefaultExport = Reflect.getOwnPropertyDescriptor(moduleValue, "default") != null;',
168
- ' Object.defineProperties(legacyModuleValue, {',
169
- ' __esModule: { value: true },',
170
- ' default: {',
171
- ' enumerable: true,',
172
- ' get: hasDefaultExport',
173
- ' ? function () { return Reflect.get(moduleValue, "default"); }',
174
- ' : function () { return moduleValue; },',
175
- ' },',
176
- ' });',
177
- ' }',
150
+ ' const legacyModuleValue = legacyContainer == null ? moduleValue : toLegacyEsm(moduleValue);',
178
151
  ' try {',
179
152
  ' if (ownsModernContainer && !Reflect.set(container.exposedModules, normalizedModule, moduleValue)) {',
180
153
  ' throw new Error("Cannot establish the micro-frontend global context: registry-is-not-extensible");',
@@ -196,19 +169,13 @@ export function getPreludeConfig(options: MicroFrontendPluginOptions, appName?:
196
169
  ' throw error;',
197
170
  ' }',
198
171
  '}',
172
+ `const toLegacyEsm = ${toLegacyEsm.toString()};`,
173
+ `const registerSharedValue = ${registerSharedValue.toString()};`,
199
174
  'function registerShared(moduleName, moduleValue) {',
200
175
  ' const sharedModules = global.__MICRO_FRONTEND__.__SHARED__;',
201
- ' const existingModule = sharedModules[moduleName];',
202
- ' if (existingModule != null) {',
203
- ' if (typeof existingModule === "object" &&',
204
- ' typeof existingModule.get === "function" &&',
205
- ' typeof existingModule.loaded === "boolean" &&',
206
- ' Object.is(existingModule.get(), moduleValue)) {',
207
- ' return;',
208
- ' }',
176
+ ' if (!registerSharedValue(sharedModules, moduleName, moduleValue, toLegacyEsm)) {',
209
177
  " throw new Error(`Shared module '${moduleName}' is already registered`);",
210
178
  ' }',
211
- ' sharedModules[moduleName] = { get: () => moduleValue, loaded: true };',
212
179
  '}',
213
180
  `const __container = createContainer(${containerName}, ${containerConfig});`,
214
181
  ...registerStatements,
@@ -1,6 +1,7 @@
1
1
  import { getLegacyContainerPair } from './containerPairing';
2
2
  import { ExposedModuleAlreadyRegisteredError } from './errors';
3
3
  import { getMicroFrontendGlobalContext, MicroFrontendGlobalContextCompatibilityError } from './globalContext';
4
+ import { toLegacyEsm } from './legacyEsm';
4
5
  import type { AppContainer } from './registry';
5
6
 
6
7
  export function exposeContainerModule(container: AppContainer, exposedModule: string, module: unknown): void {
@@ -44,45 +45,6 @@ export function exposeContainerModule(container: AppContainer, exposedModule: st
44
45
  }
45
46
  }
46
47
 
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
48
  function normalizeExposedModule(exposedModule: string): string {
87
49
  return exposedModule.startsWith('./') ? exposedModule : `./${exposedModule}`;
88
50
  }
@@ -0,0 +1,39 @@
1
+ // Keep this helper self-contained: the build plugin serializes it into the prelude.
2
+ export function toLegacyEsm(module: unknown): unknown {
3
+ if ((typeof module !== 'object' || module == null) && typeof module !== 'function') {
4
+ return module;
5
+ }
6
+ if (Reflect.get(module, '__esModule') === true) {
7
+ return module;
8
+ }
9
+ const legacyModule =
10
+ typeof module === 'function'
11
+ ? function (this: unknown, ...args: unknown[]): unknown {
12
+ return Reflect.apply(module, this, args);
13
+ }
14
+ : {};
15
+ for (const exportName of Reflect.ownKeys(module)) {
16
+ if (exportName === '__esModule' || exportName === 'default') {
17
+ continue;
18
+ }
19
+ const sourceDescriptor = Reflect.getOwnPropertyDescriptor(module, exportName);
20
+ const facadeDescriptor = Reflect.getOwnPropertyDescriptor(legacyModule, exportName);
21
+ if (sourceDescriptor == null || facadeDescriptor?.configurable === false) {
22
+ // Callable facades keep intrinsic non-configurable slots such as prototype.
23
+ continue;
24
+ }
25
+ Reflect.defineProperty(legacyModule, exportName, {
26
+ enumerable: sourceDescriptor.enumerable,
27
+ get: () => Reflect.get(module, exportName),
28
+ });
29
+ }
30
+ const hasDefaultExport = Reflect.getOwnPropertyDescriptor(module, 'default') != null;
31
+ Object.defineProperties(legacyModule, {
32
+ __esModule: { value: true },
33
+ default: {
34
+ enumerable: true,
35
+ get: hasDefaultExport ? () => Reflect.get(module, 'default') : () => module,
36
+ },
37
+ });
38
+ return legacyModule;
39
+ }