@module-federation/bridge-react 0.3.5 → 0.5.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/src/provider.tsx CHANGED
@@ -8,65 +8,84 @@ import type {
8
8
  RenderFnParams,
9
9
  } from '@module-federation/bridge-shared';
10
10
  import { LoggerInstance, atLeastReact18 } from './utils';
11
+ import { ErrorBoundary } from 'react-error-boundary';
11
12
 
13
+ type RootType = HTMLElement | ReactDOMClient.Root;
12
14
  type ProviderFnParams<T> = {
13
15
  rootComponent: React.ComponentType<T>;
16
+ render?: (
17
+ App: React.ReactElement,
18
+ id?: HTMLElement | string,
19
+ ) => RootType | Promise<RootType>;
14
20
  };
15
21
 
16
22
  export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
17
23
  return () => {
18
- const rootMap = new Map<any, ReactDOMClient.Root>();
19
-
24
+ const rootMap = new Map<any, RootType>();
20
25
  const RawComponent = (info: { propsInfo: T; appInfo: ProviderParams }) => {
21
- const { appInfo, propsInfo } = info;
22
- const { name, memoryRoute, basename = '/' } = appInfo;
23
-
26
+ const { appInfo, propsInfo, ...restProps } = info;
27
+ const { moduleName, memoryRoute, basename = '/' } = appInfo;
24
28
  return (
25
- <RouterContext.Provider value={{ name, basename, memoryRoute }}>
26
- <bridgeInfo.rootComponent {...propsInfo} basename={basename} />
29
+ <RouterContext.Provider value={{ moduleName, basename, memoryRoute }}>
30
+ <bridgeInfo.rootComponent
31
+ {...propsInfo}
32
+ basename={basename}
33
+ {...restProps}
34
+ />
27
35
  </RouterContext.Provider>
28
36
  );
29
37
  };
30
38
 
31
39
  return {
32
- render(info: RenderFnParams & any) {
40
+ async render(info: RenderFnParams & any) {
33
41
  LoggerInstance.log(`createBridgeComponent render Info`, info);
34
- const { name, basename, memoryRoute, ...propsInfo } = info;
35
-
36
- if (atLeastReact18(React)) {
37
- const root = ReactDOMClient.createRoot(info.dom);
38
- rootMap.set(info.dom, root);
39
- root.render(
42
+ const {
43
+ moduleName,
44
+ dom,
45
+ basename,
46
+ memoryRoute,
47
+ fallback,
48
+ ...propsInfo
49
+ } = info;
50
+ const rootComponentWithErrorBoundary = (
51
+ // set ErrorBoundary for RawComponent rendering error, usually caused by user app rendering error
52
+ <ErrorBoundary FallbackComponent={fallback}>
40
53
  <RawComponent
41
- propsInfo={propsInfo}
42
54
  appInfo={{
43
- name,
55
+ moduleName,
44
56
  basename,
45
57
  memoryRoute,
46
58
  }}
47
- />,
48
- );
49
- } else {
50
- ReactDOM.render(
51
- <RawComponent
52
59
  propsInfo={propsInfo}
53
- appInfo={{
54
- name,
55
- basename,
56
- memoryRoute,
57
- }}
58
- />,
59
- info.dom,
60
- );
60
+ />
61
+ </ErrorBoundary>
62
+ );
63
+
64
+ if (atLeastReact18(React)) {
65
+ if (bridgeInfo?.render) {
66
+ // in case bridgeInfo?.render is an async function, resolve this to promise
67
+ Promise.resolve(
68
+ bridgeInfo?.render(rootComponentWithErrorBoundary, dom),
69
+ ).then((root: RootType) => rootMap.set(info.dom, root));
70
+ } else {
71
+ const root: RootType = ReactDOMClient.createRoot(info.dom);
72
+ root.render(rootComponentWithErrorBoundary);
73
+ rootMap.set(info.dom, root);
74
+ }
75
+ } else {
76
+ // react 17 render
77
+ const renderFn = bridgeInfo?.render || ReactDOM.render;
78
+ renderFn?.(rootComponentWithErrorBoundary, info.dom);
61
79
  }
62
80
  },
63
- destroy(info: { dom: HTMLElement }) {
81
+ async destroy(info: { dom: HTMLElement }) {
64
82
  LoggerInstance.log(`createBridgeComponent destroy Info`, {
65
83
  dom: info.dom,
66
84
  });
67
85
  if (atLeastReact18(React)) {
68
86
  const root = rootMap.get(info.dom);
69
- root?.unmount();
87
+ (root as ReactDOMClient.Root)?.unmount();
88
+ rootMap.delete(info.dom);
70
89
  } else {
71
90
  ReactDOM.unmountComponentAtNode(info.dom);
72
91
  }
@@ -84,5 +103,3 @@ export function ShadowRoot(info: { children: () => JSX.Element }) {
84
103
 
85
104
  return <div ref={domRef}>{root && <info.children />}</div>;
86
105
  }
87
-
88
- // function ShadowContent() {}
@@ -1,13 +1,20 @@
1
- import React, { useContext, useEffect, useRef, useState } from 'react';
1
+ import React, {
2
+ useContext,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ forwardRef,
7
+ } from 'react';
2
8
  import * as ReactRouterDOM from 'react-router-dom';
3
9
  import type { ProviderParams } from '@module-federation/bridge-shared';
4
10
  import { LoggerInstance, pathJoin } from '../utils';
5
11
  import { dispatchPopstateEnv } from '@module-federation/bridge-shared';
12
+ import { ErrorBoundaryPropsWithComponent } from 'react-error-boundary';
6
13
 
7
14
  declare const __APP_VERSION__: string;
8
-
9
15
  export interface RenderFnParams extends ProviderParams {
10
16
  dom?: any;
17
+ fallback: ErrorBoundaryPropsWithComponent['FallbackComponent'];
11
18
  }
12
19
 
13
20
  interface RemoteModule {
@@ -22,71 +29,96 @@ interface RemoteModule {
22
29
  }
23
30
 
24
31
  interface RemoteAppParams {
25
- name: string;
32
+ moduleName: string;
26
33
  providerInfo: NonNullable<RemoteModule['provider']>;
27
34
  exportName: string | number | symbol;
35
+ fallback: ErrorBoundaryPropsWithComponent['FallbackComponent'];
28
36
  }
29
37
 
30
- const RemoteApp = ({
31
- name,
32
- memoryRoute,
33
- basename,
34
- providerInfo,
35
- ...resProps
36
- }: RemoteAppParams & ProviderParams) => {
37
- const rootRef = useRef(null);
38
- const renderDom = useRef(null);
39
- const providerInfoRef = useRef<any>(null);
40
-
41
- useEffect(() => {
42
- const renderTimeout = setTimeout(() => {
43
- const providerReturn = providerInfo();
44
- providerInfoRef.current = providerReturn;
45
- const renderProps = {
46
- name,
47
- dom: rootRef.current,
48
- basename,
49
- memoryRoute,
50
- ...resProps,
51
- };
52
- renderDom.current = rootRef.current;
53
- LoggerInstance.log(
54
- `createRemoteComponent LazyComponent render >>>`,
55
- renderProps,
56
- );
57
- providerReturn.render(renderProps);
58
- });
59
-
60
- return () => {
61
- clearTimeout(renderTimeout);
62
- setTimeout(() => {
63
- if (providerInfoRef.current?.destroy) {
64
- LoggerInstance.log(
65
- `createRemoteComponent LazyComponent destroy >>>`,
66
- { name, basename, dom: renderDom.current },
67
- );
68
- providerInfoRef.current?.destroy({
69
- dom: renderDom.current,
70
- });
71
- }
38
+ const RemoteAppWrapper = forwardRef(function (
39
+ props: RemoteAppParams & RenderFnParams,
40
+ ref,
41
+ ) {
42
+ const RemoteApp = () => {
43
+ LoggerInstance.log(`RemoteAppWrapper RemoteApp props >>>`, { props });
44
+ const {
45
+ moduleName,
46
+ memoryRoute,
47
+ basename,
48
+ providerInfo,
49
+ className,
50
+ style,
51
+ fallback,
52
+ ...resProps
53
+ } = props;
54
+
55
+ const rootRef: React.MutableRefObject<HTMLElement | null> =
56
+ ref && 'current' in ref
57
+ ? (ref as React.MutableRefObject<HTMLElement | null>)
58
+ : useRef(null);
59
+ const renderDom: React.MutableRefObject<HTMLElement | null> = useRef(null);
60
+ const providerInfoRef = useRef<any>(null);
61
+
62
+ useEffect(() => {
63
+ const renderTimeout = setTimeout(() => {
64
+ const providerReturn = providerInfo();
65
+ providerInfoRef.current = providerReturn;
66
+
67
+ const renderProps = {
68
+ moduleName,
69
+ dom: rootRef.current,
70
+ basename,
71
+ memoryRoute,
72
+ fallback,
73
+ ...resProps,
74
+ };
75
+ renderDom.current = rootRef.current;
76
+ LoggerInstance.log(
77
+ `createRemoteComponent LazyComponent render >>>`,
78
+ renderProps,
79
+ );
80
+ providerReturn.render(renderProps);
72
81
  });
73
- };
74
- }, []);
75
82
 
76
- //@ts-ignore
77
- return <div ref={rootRef}></div>;
78
- };
83
+ return () => {
84
+ clearTimeout(renderTimeout);
85
+ setTimeout(() => {
86
+ if (providerInfoRef.current?.destroy) {
87
+ LoggerInstance.log(
88
+ `createRemoteComponent LazyComponent destroy >>>`,
89
+ { moduleName, basename, dom: renderDom.current },
90
+ );
91
+ providerInfoRef.current?.destroy({
92
+ dom: renderDom.current,
93
+ });
94
+ }
95
+ });
96
+ };
97
+ }, []);
98
+
99
+ return (
100
+ <div
101
+ className={props?.className}
102
+ style={props?.style}
103
+ ref={rootRef}
104
+ ></div>
105
+ );
106
+ };
79
107
 
80
- (RemoteApp as any)['__APP_VERSION__'] = __APP_VERSION__;
108
+ (RemoteApp as any)['__APP_VERSION__'] = __APP_VERSION__;
109
+ return <RemoteApp />;
110
+ });
81
111
 
82
112
  interface ExtraDataProps {
83
113
  basename?: string;
84
114
  }
85
115
 
86
- export function withRouterData<P extends Parameters<typeof RemoteApp>[0]>(
116
+ export function withRouterData<
117
+ P extends Parameters<typeof RemoteAppWrapper>[0],
118
+ >(
87
119
  WrappedComponent: React.ComponentType<P & ExtraDataProps>,
88
120
  ): React.FC<Omit<P, keyof ExtraDataProps>> {
89
- return (props: any) => {
121
+ const Component = forwardRef(function (props: any, ref) {
90
122
  let enableDispathPopstate = false;
91
123
  let routerContextVal: any;
92
124
  try {
@@ -158,8 +190,12 @@ export function withRouterData<P extends Parameters<typeof RemoteApp>[0]>(
158
190
  }, [location]);
159
191
  }
160
192
 
161
- return <WrappedComponent {...(props as P)} basename={basename} />;
162
- };
193
+ return <WrappedComponent {...(props as P)} basename={basename} ref={ref} />;
194
+ });
195
+
196
+ return forwardRef(function (props, ref) {
197
+ return <Component {...props} ref={ref} />;
198
+ }) as any;
163
199
  }
164
200
 
165
- export default withRouterData(RemoteApp);
201
+ export default withRouterData(RemoteAppWrapper);
package/src/router.tsx CHANGED
@@ -4,7 +4,7 @@ import * as ReactRouterDom from 'react-router-dom/';
4
4
  import { RouterContext } from './context';
5
5
  import { LoggerInstance } from './utils';
6
6
 
7
- function WraperRouter(
7
+ function WrapperRouter(
8
8
  props:
9
9
  | Parameters<typeof ReactRouterDom.BrowserRouter>[0]
10
10
  | Parameters<typeof ReactRouterDom.MemoryRouter>[0],
@@ -12,12 +12,11 @@ function WraperRouter(
12
12
  const { basename, ...propsRes } = props;
13
13
  const routerContextProps = useContext(RouterContext) || {};
14
14
 
15
- LoggerInstance.log(`WraperRouter info >>>`, {
15
+ LoggerInstance.log(`WrapperRouter info >>>`, {
16
16
  ...routerContextProps,
17
17
  routerContextProps,
18
- WraperRouterProps: props,
18
+ WrapperRouterProps: props,
19
19
  });
20
- if (!routerContextProps) return <ReactRouterDom.BrowserRouter {...props} />;
21
20
 
22
21
  if (routerContextProps?.memoryRoute) {
23
22
  return (
@@ -35,16 +34,16 @@ function WraperRouter(
35
34
  );
36
35
  }
37
36
 
38
- function WraperRouterProvider(
37
+ function WrapperRouterProvider(
39
38
  props: Parameters<typeof ReactRouterDom.RouterProvider>[0],
40
39
  ) {
41
40
  const { router, ...propsRes } = props;
42
41
  const routerContextProps = useContext(RouterContext) || {};
43
42
  const routers = router.routes;
44
- LoggerInstance.log(`WraperRouterProvider info >>>`, {
43
+ LoggerInstance.log(`WrapperRouterProvider info >>>`, {
45
44
  ...routerContextProps,
46
45
  routerContextProps,
47
- WraperRouterProviderProps: props,
46
+ WrapperRouterProviderProps: props,
48
47
  router,
49
48
  });
50
49
  const RouterProvider = (ReactRouterDom as any)['Router' + 'Provider'];
@@ -52,7 +51,6 @@ function WraperRouterProvider(
52
51
  const createBrowserRouter = (ReactRouterDom as any)[
53
52
  'create' + 'BrowserRouter'
54
53
  ];
55
- if (!routerContextProps) return <RouterProvider {...props} />;
56
54
 
57
55
  if (routerContextProps.memoryRoute) {
58
56
  const MemeoryRouterInstance = createMemoryRouter(routers, {
@@ -71,5 +69,5 @@ function WraperRouterProvider(
71
69
 
72
70
  export * from 'react-router-dom/';
73
71
 
74
- export { WraperRouter as BrowserRouter };
75
- export { WraperRouterProvider as RouterProvider };
72
+ export { WrapperRouter as BrowserRouter };
73
+ export { WrapperRouterProvider as RouterProvider };
package/vite.config.ts CHANGED
@@ -22,6 +22,8 @@ export default defineConfig({
22
22
  entry: {
23
23
  index: path.resolve(__dirname, 'src/index.ts'),
24
24
  router: path.resolve(__dirname, 'src/router.tsx'),
25
+ 'router-v5': path.resolve(__dirname, 'src/router.tsx'),
26
+ 'router-v6': path.resolve(__dirname, 'src/router.tsx'),
25
27
  },
26
28
  formats: ['cjs', 'es'],
27
29
  fileName: (format, entryName) => `${entryName}.${format}.js`,
@@ -32,6 +34,33 @@ export default defineConfig({
32
34
  '@remix-run/router',
33
35
  'react-router',
34
36
  'react-router-dom/',
37
+ 'react-router-dom/index.js',
38
+ 'react-router-dom/dist/index.js',
39
+ ],
40
+ plugins: [
41
+ {
42
+ name: 'modify-output-plugin',
43
+ generateBundle(options, bundle) {
44
+ for (const fileName in bundle) {
45
+ const chunk = bundle[fileName];
46
+ if (fileName.includes('router-v6') && chunk.type === 'chunk') {
47
+ chunk.code = chunk.code.replace(
48
+ // Match 'react-router-dom/' followed by single quotes, double quotes, or backticks, replacing only 'react-router-dom/' to react-router-v6 dist file structure
49
+ /react-router-dom\/(?=[\'\"\`])/g,
50
+ 'react-router-dom/dist/index.js',
51
+ );
52
+ }
53
+
54
+ if (fileName.includes('router-v5') && chunk.type === 'chunk') {
55
+ chunk.code = chunk.code.replace(
56
+ // Match 'react-router-dom/' followed by single quotes, double quotes, or backticks, replacing only 'react-router-dom/' to react-router-v5 dist file structure
57
+ /react-router-dom\/(?=[\'\"\`])/g,
58
+ 'react-router-dom/index.js',
59
+ );
60
+ }
61
+ }
62
+ },
63
+ },
35
64
  ],
36
65
  },
37
66
  minify: false,