diff --git a/.changeset/chatty-days-march.md b/.changeset/chatty-days-march.md new file mode 100644 index 00000000000..3f2fab4c3e7 --- /dev/null +++ b/.changeset/chatty-days-march.md @@ -0,0 +1,5 @@ +--- +'@module-federation/bridge-react': patch +--- + +feat(bridge-react): enable custom createRoot in bridge-react diff --git a/.changeset/neat-types-fix.md b/.changeset/neat-types-fix.md new file mode 100644 index 00000000000..ae5d8908f24 --- /dev/null +++ b/.changeset/neat-types-fix.md @@ -0,0 +1,5 @@ +--- +'@module-federation/bridge-react': patch +--- + +refactor(bridge-react): centralize type definitions into a single file for better maintainability and consistency diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge.mdx b/apps/website-new/docs/en/practice/bridge/react-bridge.mdx index 14eef4889ec..f39eaa2c8c8 100644 --- a/apps/website-new/docs/en/practice/bridge/react-bridge.mdx +++ b/apps/website-new/docs/en/practice/bridge/react-bridge.mdx @@ -1,11 +1,12 @@ # React Bridge `@module-federation/bridge-react` provides bridge utility functions for React applications: -- `createBridgeComponent`: Used for exporting application-level modules, suitable for producers to wrap modules exported as application types. -- `createRemoteComponent`: Used for loading application-level modules, suitable for consumers to load modules as application types. +- `createBridgeComponent`: Used for exporting application-level modules, suitable for producers to wrap modules exported as application types +- `createRemoteComponent`: Used for loading application-level modules, suitable for consumers to load modules as application types [View Demo](https://github.com/module-federation/core/tree/main/apps/router-demo) + ### Installation import { PackageManagerTabs } from '@theme'; @@ -41,7 +42,7 @@ export default createBridgeComponent({ }); ``` -- Step 2: In the rsbuild.config.ts configuration file, we need to export `export-app.tsx` as an application type module +- Step 2: In the `rsbuild.config.ts` configuration file, we need to export `export-app.tsx` as an application type module ```ts // rsbuild.config.ts @@ -60,7 +61,6 @@ export default defineConfig({ ], }); ``` - At this point, we have completed the export of the application type module. :::info @@ -77,7 +77,7 @@ Why do application type modules need to be wrapped with `createBridgeComponent`? > Host -- Step 1: In the rsbuild.config.ts configuration, we need to register remote modules, which is no different from other Module Federation configurations. +- Step 1: In the `rsbuild.config.ts` configuration, we need to register remote modules, which is no different from other Module Federation configurations. ```ts // rsbuild.config.ts @@ -100,64 +100,63 @@ export default defineConfig({ // ./src/App.tsx import React from 'react'; import { createRemoteComponent } from '@module-federation/bridge-react'; -import { loadRemote } from '@module-federation/enhanced/runtime' import styles from './index.module.less'; -// define FallbackErrorComp Component -const FallbackErrorComp = (info: any) => { +// Define FallbackErrorComp component +const FallbackErrorComp = ({ error, resetErrorBoundary }) => { return (

This is ErrorBoundary Component

Something went wrong:

-
{info?.error.message}
-
); }; -// define FallbackLoading Component +// Define FallbackLoading component const FallbackComp =
loading...
; -// use createRemoteComponent to export remote component +// Use createRemoteComponent to create remote component const Remote1App = createRemoteComponent({ - // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app') + // loader is used to load remote modules, e.g.: loadRemote('remote1/export-app'), import('remote1/export-app') loader: () => loadRemote('remote1/export-app'), - // fallback 用于在加载远程模块失败时展示的组件 - // fallback is for error handling + // fallback is used for displaying components when remote module loading fails fallback: FallbackErrorComp, - // loading is for loading state + // loading is used for displaying components when loading remote modules loading: FallbackComp, }); const App = () => { - return ( + return ( + - ( )} /> - ) + + ); }; ``` - -::: - At this point, we have completed loading the application type module. :::info @@ -173,126 +172,141 @@ making the user experience almost equivalent to using local components. ::: -### Methods + +### API Reference #### createBridgeComponent +`createBridgeComponent` is used to wrap React components into remotely loadable modules. + ```tsx -export declare function createBridgeComponent(bridgeInfo: ProviderFnParams): () => { - render(info: RenderFnParams): Promise; - destroy(info: { - dom: HTMLElement; - }): Promise; +/** + * Create a remotely loadable React component + * @param bridgeInfo - Bridge Component Config information + * @returns Returns a function that returns an object containing render and destroy methods + */ +function createBridgeComponent( + bridgeInfo: ProviderFnParams +): () => { + render(info: RenderFnParams): Promise; + destroy(info: DestroyParams): Promise; }; -type ProviderFnParams = { +/** + * Bridge component configuration information + */ +interface ProviderFnParams { + /** Root component to be remotely loaded */ rootComponent: React.ComponentType; + + /** + * Custom render function for custom rendering logic + * @param App - React element + * @param id - DOM element or string ID + * @returns React root element or Promise + */ render?: ( App: React.ReactElement, id?: HTMLElement | string, ) => RootType | Promise; -}; - -export declare interface RenderFnParams extends ProviderParams { - dom: HTMLElement; + + /** + * Custom function to create React root node + * @param container - Container element + * @param options - Options for creating root node + * @returns React root node + */ + createRoot?: ( + container: Element | DocumentFragment, + options?: CreateRootOptions, + ) => Root; } -export declare interface ProviderParams { - moduleName?: string; - basename?: string; - memoryRoute?: { - entryPath: string; - }; - style?: React.CSSProperties; - className?: string; +/** + * Options for creating React root node + */ +interface CreateRootOptions { + /** Add prefix to generated React IDs to avoid ID conflicts */ + identifierPrefix?: string; + + /** Callback function to handle recoverable errors during React rendering */ + onRecoverableError?: (error: unknown) => void; + + /** Transition callbacks for React 18 concurrent features */ + transitionCallbacks?: TransitionCallbacks; } - ``` -* `bridgeInfo` - * type: -```tsx -type ProviderFnParams = { - rootComponent: React.ComponentType; - render?: ( - App: React.ReactElement, - id?: HTMLElement | string, - ) => RootType | Promise; -}; -``` - - * Purpose: Used to pass the root component - * ReturnType - * type: - - ```tsx - () => { - render(info: { - moduleName?: string; - basename?: string; - memoryRoute?: { - entryPath: string; - }; - style?: React.CSSProperties; - className?: string; - dom?: HTMLElement; - }): Promise; - destroy(info: { dom: HTMLElement}): Promise; - } - ``` - #### createRemoteComponent -```tsx -import { createRemoteComponent } from '@module-federation/bridge-react'; -import type { ProviderParams } from '@module-federation/bridge-react'; - -function createRemoteComponent( - options: { - // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app') - loader: () => Promise, - // default is default, used to specify the export of the module - export?: E; - // loading is for loading state - loading: React.ReactNode; - // fallback is for error handling - fallback: ComponentType<{ error: any; }>; - } -): (props: { - basename?: ProviderParams['basename']; - memoryRoute?: { entryPath: string }; -} & RawComponentType) => React.JSX.Element; -``` - -* `options` - * `loader` - * type: `() => Promise` - * Purpose: Used to load remote modules, for example: `loadRemote('remote1/export-app')`, `import('remote1/export-app')` +`createRemoteComponent` is used to load remote React components. ```tsx -const Remote1App = createRemoteComponent({ - // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app') - loader: () => loadRemote('remote1/export-app'), - // fallback is for error handling - fallback: FallbackErrorComp, - // loading is for loading state - loading: FallbackComp, -}); +/** + * Create remote React component + * @param options - Remote component configuration options + * @returns Returns a React component that can receive props and render remote component + */ +function createRemoteComponent( + options: RemoteComponentParams +): React.ForwardRefExoticComponent< + React.PropsWithoutRef & React.RefAttributes +>; + +/** + * Remote component configuration parameters + */ +interface RemoteComponentParams< + T = Record, + E extends keyof T = keyof T +> { + /** + * Function to load remote module + * Example: () => loadRemote('remote1/export-app') or () => import('remote1/export-app') + */ + loader: () => Promise; + + /** Component displayed when loading remote module */ + loading: React.ReactNode; + + /** Error component displayed when loading or rendering remote module fails */ + fallback: React.ComponentType<{ error: Error }>; + + /** + * Specify module export name + * Default is 'default' + */ + export?: E; +} -const Remote2App = createRemoteComponent({ - // loader is for loading remote module, for example: loadRemote('remote2/export-app')、import('remote2/export-app') - loader: () => import('remote2/export-app'), - // fallback is for error handling - fallback: FallbackErrorComp, - // loading is for loading state - loading: FallbackComp, -}); +/** + * Remote component properties + */ +interface RemoteComponentProps> { + /** Properties passed to remote component */ + props?: T; + + /** + * Memory route configuration, used to control child application routing as memoryRouter + * Will not directly display URL in browser address bar + */ + memoryRoute?: { entryPath: string }; + + /** Base path name */ + basename?: string; + + /** Style */ + style?: React.CSSProperties; + + /** Class name */ + className?: string; +} +``` +### Usage Examples + +#### Using export to specify module export -``` - * `export` - * type: `string` - * Purpose: Can specify the export of the module ```tsx // remote export const provider = createBridgeComponent({ @@ -302,48 +316,36 @@ export const provider = createBridgeComponent({ // host const Remote1App = createRemoteComponent({ loader: () => loadRemote('remote1/export-app'), - export: 'provider' -}); -``` - * `loading` - * type: `React.ReactNode` - * Purpose: Component displayed when loading remote modules - * `fallback` - * type: `ComponentType<{ error: any; }>` - * Purpose: Component displayed when loading, rendering remote modules - -* `ReturnType` - * type: `(props: PropsInfo)=> React.JSX.Element` - * Purpose: Used to render remote module components - -```tsx -const Remote1App = createRemoteComponent({ - // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app') - loader: () => loadRemote('remote1/export-app'), - // fallback is for error handling + export: 'provider', // Specify to use provider export fallback: FallbackErrorComp, - // loading is for loading state loading: FallbackComp, }); +``` +#### Using memoryRoute to control routing +```tsx function App() { - return ( - - + + ( )} /> - - ) + + + ); } -``` diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx b/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx index a2dcfad63ac..145679952ef 100644 --- a/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx +++ b/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx @@ -26,7 +26,6 @@ import { PackageManagerTabs } from '@theme'; :::danger 请注意:使用 `@module-federation/bridge-react` 后不能将 `react-router-dom` 设置成 shared,否则构建工具将会提示异常。这是因为 `@module-federation/bridge-react` 通过代理 `react-router-dom` 实现了对路由的控制,以保证应用间路由能够正常协同工作。 - ::: > 在生产者项目中,假设我们需要将应用通过 `@module-federation/bridge-react` 导出为一个应用类型模块,应用入口为 App.tsx 文件 @@ -43,7 +42,7 @@ export default createBridgeComponent({ }); ``` -- Step2: 在 rsbuild.config.ts 配置文件中,我们需要将 `export-app.tsx` 作为应用类型模块导出 +- Step2: 在 `rsbuild.config.ts` 配置文件中,我们需要将 `export-app.tsx` 作为应用类型模块导出 ```ts // rsbuild.config.ts @@ -78,7 +77,7 @@ export default defineConfig({ > Host -- Step1: 在 rsbuild.config.ts 配置中,我们需要注册远程模块,这点与其它 Module Federation 配置无异。 +- Step1: 在 `rsbuild.config.ts` 配置中,我们需要注册远程模块,这点与其它 Module Federation 配置无异。 ```ts // rsbuild.config.ts @@ -104,13 +103,13 @@ import { createRemoteComponent } from '@module-federation/bridge-react'; import styles from './index.module.less'; // 定义 FallbackErrorComp 组件 -const FallbackErrorComp = (info: any) => { +const FallbackErrorComp = ({ error, resetErrorBoundary }) => { return (

This is ErrorBoundary Component

Something went wrong:

-
{info?.error.message}
-
@@ -120,7 +119,7 @@ const FallbackErrorComp = (info: any) => { // 定义 FallbackLoading 组件 const FallbackComp =
loading...
; -// 使用 createRemoteComponent 导出远程组件 +// 使用 createRemoteComponent 创建远程组件 const Remote1App = createRemoteComponent({ // loader 用于加载远程模块,例如:loadRemote('remote1/export-app')、import('remote1/export-app') loader: () => loadRemote('remote1/export-app'), @@ -131,10 +130,11 @@ const Remote1App = createRemoteComponent({ }); const App = () => { - return ( + return ( + - ( @@ -147,11 +147,14 @@ const App = () => { age={12} // 可设置 ref, 将自动转发到远程组件,可获取 ref 对象操作 dom ref={ref} + // 通过 memoryRoute 来将子应用路由控制为 memoryRouter,将不会直接将 url 展示在浏览器地址上 + memoryRoute={{ entryPath: '/detail' }} /> )} /> - ) + + ); }; ``` 至此,我们完成了应用类型模块的加载。 @@ -170,124 +173,140 @@ const App = () => { ::: -### 方法 +### API 参考 #### createBridgeComponent +`createBridgeComponent` 用于将 React 组件包装成可远程加载的模块。 + ```tsx -export declare function createBridgeComponent(bridgeInfo: ProviderFnParams): () => { - render(info: RenderFnParams): Promise; - destroy(info: { - dom: HTMLElement; - }): Promise; +/** + * 创建一个可远程加载的 React 组件 + * @param bridgeInfo - Bridge Component Config information + * @returns 返回一个函数,该函数返回包含 render 和 destroy 方法的对象 + */ +function createBridgeComponent( + bridgeInfo: ProviderFnParams +): () => { + render(info: RenderFnParams): Promise; + destroy(info: DestroyParams): Promise; }; -type ProviderFnParams = { +/** + * 桥接组件配置信息 + */ +interface ProviderFnParams { + /** 需要被远程加载的根组件 */ rootComponent: React.ComponentType; + + /** + * 自定义渲染函数,用于自定义渲染逻辑 + * @param App - React 元素 + * @param id - DOM 元素或字符串 ID + * @returns React 根元素或 Promise + */ render?: ( App: React.ReactElement, id?: HTMLElement | string, ) => RootType | Promise; -}; - -export declare interface RenderFnParams extends ProviderParams { - dom: HTMLElement; + + /** + * 自定义创建 React 根节点的函数 + * @param container - 容器元素 + * @param options - 创建根节点的选项 + * @returns React 根节点 + */ + createRoot?: ( + container: Element | DocumentFragment, + options?: CreateRootOptions, + ) => Root; } -export declare interface ProviderParams { - moduleName?: string; - basename?: string; - memoryRoute?: { - entryPath: string; - }; - style?: React.CSSProperties; - className?: string; +/** + * 创建 React 根节点的选项 + */ +interface CreateRootOptions { + /** 为生成的 React ID 添加前缀,用于避免 ID 冲突 */ + identifierPrefix?: string; + + /** 处理 React 在渲染过程中可恢复的错误的回调函数 */ + onRecoverableError?: (error: unknown) => void; + + /** 过渡回调函数,用于 React 18 的并发特性 */ + transitionCallbacks?: TransitionCallbacks; } - -``` - -* `bridgeInfo` - * type: -```tsx -type ProviderFnParams = { - rootComponent: React.ComponentType; - render?: ( - App: React.ReactElement, - id?: HTMLElement | string, - ) => RootType | Promise; -}; ``` - * 作用: 用于传递根组件 - * ReturnType - * type: - - ```tsx - () => { - render(info: { - moduleName?: string; - basename?: string; - memoryRoute?: { - entryPath: string; - }; - style?: React.CSSProperties; - className?: string; - dom?: HTMLElement; - }): Promise; - destroy(info: { dom: HTMLElement}): Promise; - } - ``` - #### createRemoteComponent -```tsx -import { createRemoteComponent } from '@module-federation/bridge-react'; -import type { ProviderParams } from '@module-federation/bridge-react'; - -function createRemoteComponent( - options: { - // 加载远程应用的函数,例如:loadRemote('remote1/export-app')、import('remote1/export-app') - loader: () => Promise, - // 默认为 default,用于指定模块的 export - export?: E; - loading: React.ReactNode; - fallback: ComponentType<{ error: any; }>; - } -): (props: { - basename?: ProviderParams['basename']; - memoryRoute?: { entryPath: string }; -} & RawComponentType) => React.JSX.Element; -``` - -* `options` - * `loader` - * type: `() => Promise` - * 作用: 用于加载远程模块的函数,例如:`loadRemote('remote1/export-app')`、`import('remote1/export-app')` +`createRemoteComponent` 用于加载远程 React 组件。 ```tsx -const Remote1App = createRemoteComponent({ - // loader 用于加载远程模块,例如:loadRemote('remote1/export-app')、import('remote1/export-app') - loader: () => loadRemote('remote1/export-app'), - // fallback 用于在加载远程模块失败时展示的组件 - fallback: FallbackErrorComp, - // loading 用于在加载远程模块时展示的组件 - loading: FallbackComp, -}); +/** + * 创建远程 React 组件 + * @param options - 远程组件配置选项 + * @returns 返回一个 React 组件,可以接收 props 并渲染远程组件 + */ +function createRemoteComponent( + options: RemoteComponentParams +): React.ForwardRefExoticComponent< + React.PropsWithoutRef & React.RefAttributes +>; + +/** + * 远程组件配置参数 + */ +interface RemoteComponentParams< + T = Record, + E extends keyof T = keyof T +> { + /** + * 加载远程模块的函数 + * 例如:() => loadRemote('remote1/export-app') 或 () => import('remote1/export-app') + */ + loader: () => Promise; + + /** 加载远程模块时显示的组件 */ + loading: React.ReactNode; + + /** 加载或渲染远程模块失败时显示的错误组件 */ + fallback: React.ComponentType<{ error: Error }>; + + /** + * 指定模块的导出名称 + * 默认为 'default' + */ + export?: E; +} -const Remote2App = createRemoteComponent({ - // loader 用于加载远程模块,例如:loadRemote('remote2/export-app')、import('remote2/export-app') - loader: () => import('remote2/export-app'), - // fallback 用于在加载远程模块失败时展示的组件 - fallback: FallbackErrorComp, - // loading 用于在加载远程模块时展示的组件 - loading: FallbackComp, -}); +/** + * 远程组件属性 + */ +interface RemoteComponentProps> { + /** 传递给远程组件的属性 */ + props?: T; + + /** + * 内存路由配置,用于将子应用路由控制为 memoryRouter + * 将不会直接将 URL 展示在浏览器地址栏上 + */ + memoryRoute?: { entryPath: string }; + + /** 基础路径名 */ + basename?: string; + + /** 样式 */ + style?: React.CSSProperties; + + /** 类名 */ + className?: string; +} +``` +### 使用示例 + +#### 使用 export 指定模块导出 -``` - * `export` - * type: `string` - * 作用: 可以指定模块的 export ```tsx // remote export const provider = createBridgeComponent({ @@ -297,48 +316,36 @@ export const provider = createBridgeComponent({ // host const Remote1App = createRemoteComponent({ loader: () => loadRemote('remote1/export-app'), - export: 'provider' -}); -``` - * `loading` - * type: `React.ReactNode` - * 作用: 加载远程模块时显示的组件 - * `fallback` - * type: `ComponentType<{ error: any; }>` - * 作用: 加载、渲染远程模块过程中展示的错误 - -* `ReturnType` - * type: `(props: PropsInfo)=> React.JSX.Element` - * 作用: 用于渲染远程模块组件 - -```tsx -const Remote1App = createRemoteComponent({ - // loader 用于加载远程模块,例如:loadRemote('remote1/export-app')、import('remote1/export-app') - loader: () => loadRemote('remote1/export-app'), - // fallback 用于在加载远程模块失败时展示的组件 + export: 'provider', // 指定使用 provider 导出 fallback: FallbackErrorComp, - // loading 用于在加载远程模块时展示的组件 loading: FallbackComp, }); +``` +#### 使用 memoryRoute 控制路由 +```tsx function App() { - return ( - - + + ( )} /> - - ) + + + ); } -``` diff --git a/packages/bridge/bridge-react/__tests__/bridge.spec.tsx b/packages/bridge/bridge-react/__tests__/bridge.spec.tsx index 786a33432ab..b09fbdf6314 100644 --- a/packages/bridge/bridge-react/__tests__/bridge.spec.tsx +++ b/packages/bridge/bridge-react/__tests__/bridge.spec.tsx @@ -102,4 +102,36 @@ describe('bridge', () => { expect(getHtml(container)).toMatch('hello world'); expect(ref.current).not.toBeNull(); }); + + it('createRemoteComponent with custom createRoot prop', async () => { + const renderMock = vi.fn(); + + function Component({ props }: { props?: Record }) { + return
life cycle render {props?.msg}
; + } + const BridgeComponent = createBridgeComponent({ + rootComponent: Component, + createRoot: () => { + return { + render: renderMock, + unmount: vi.fn(), + }; + }, + }); + const RemoteComponent = createRemoteComponent({ + loader: async () => { + return { + default: BridgeComponent, + }; + }, + fallback: () =>
, + loading:
loading
, + }); + + const { container } = render(); + expect(getHtml(container)).toMatch('loading'); + + await sleep(200); + expect(renderMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/bridge/bridge-react/src/index.ts b/packages/bridge/bridge-react/src/index.ts index 146de283507..6f61382ce25 100644 --- a/packages/bridge/bridge-react/src/index.ts +++ b/packages/bridge/bridge-react/src/index.ts @@ -1,6 +1,3 @@ export { createRemoteComponent } from './remote/create'; export { createBridgeComponent } from './provider/create'; -export type { - ProviderParams, - RenderFnParams, -} from '@module-federation/bridge-shared'; +export type { ProviderParams, RenderFnParams } from './types'; diff --git a/packages/bridge/bridge-react/src/provider/compat.ts b/packages/bridge/bridge-react/src/provider/compat.ts index e09c34c1aa1..815fec41de4 100644 --- a/packages/bridge/bridge-react/src/provider/compat.ts +++ b/packages/bridge/bridge-react/src/provider/compat.ts @@ -1,15 +1,5 @@ import ReactDOM from 'react-dom'; - -interface CreateRootOptions { - identifierPrefix?: string; - onRecoverableError?: (error: unknown) => void; - transitionCallbacks?: unknown; -} - -interface Root { - render(children: React.ReactNode): void; - unmount(): void; -} +import { CreateRootOptions, Root } from '../types'; const isReact18 = ReactDOM.version.startsWith('18'); @@ -29,7 +19,6 @@ export function createRoot( // For React 16/17, simulate the new root API using render/unmountComponentAtNode return { render(children: React.ReactNode) { - // @ts-ignore - React 17's render method is deprecated but still functional ReactDOM.render(children, container); }, unmount() { @@ -52,11 +41,16 @@ export function hydrateRoot( return (ReactDOM as any).hydrateRoot(container, initialChildren, options); } - // For React 16/17, simulate the new root API using hydrate + // For React 16/17, simulate the new root API using hydrate/unmountComponentAtNode return { render(children: React.ReactNode) { - // @ts-ignore - React 17's hydrate method is deprecated but still functional - ReactDOM.hydrate(children, container); + // For the initial render, use hydrate + if (children === initialChildren) { + ReactDOM.hydrate(children, container); + } else { + // For subsequent renders, use regular render + ReactDOM.render(children, container); + } }, unmount() { ReactDOM.unmountComponentAtNode(container); diff --git a/packages/bridge/bridge-react/src/provider/context.tsx b/packages/bridge/bridge-react/src/provider/context.tsx index 6af19e1b765..f6843a5f8ee 100644 --- a/packages/bridge/bridge-react/src/provider/context.tsx +++ b/packages/bridge/bridge-react/src/provider/context.tsx @@ -1,4 +1,4 @@ import React from 'react'; -import { ProviderParams } from '@module-federation/bridge-shared'; +import { ProviderParams } from '../types'; export const RouterContext = React.createContext(null); diff --git a/packages/bridge/bridge-react/src/provider/create.tsx b/packages/bridge/bridge-react/src/provider/create.tsx index 64e4592e774..366f7cc4d1f 100644 --- a/packages/bridge/bridge-react/src/provider/create.tsx +++ b/packages/bridge/bridge-react/src/provider/create.tsx @@ -2,32 +2,21 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import type { ProviderParams, - RenderFnParams, -} from '@module-federation/bridge-shared'; + ProviderFnParams, + RootType, + DestroyParams, + RenderParams, +} from '../types'; import { ErrorBoundary } from 'react-error-boundary'; import { RouterContext } from './context'; import { LoggerInstance } from '../utils'; import { federationRuntime } from './plugin'; -import { createRoot } from './compat'; +import { createRoot as defaultCreateRoot } from './compat'; -type RenderParams = RenderFnParams & { - [key: string]: unknown; -}; -type DestroyParams = { - moduleName: string; - dom: HTMLElement; -}; -type RootType = HTMLElement | ReturnType; - -export type ProviderFnParams = { - rootComponent: React.ComponentType; - render?: ( - App: React.ReactElement, - id?: HTMLElement | string, - ) => RootType | Promise; -}; - -export function createBridgeComponent(bridgeInfo: ProviderFnParams) { +export function createBridgeComponent({ + createRoot = defaultCreateRoot, + ...bridgeInfo +}: ProviderFnParams) { return () => { const rootMap = new Map(); const instance = federationRuntime.instance; @@ -80,34 +69,37 @@ export function createBridgeComponent(bridgeInfo: ProviderFnParams) { ); - if (bridgeInfo?.render) { - // in case bridgeInfo?.render is an async function, resolve this to promise - Promise.resolve( - bridgeInfo?.render(rootComponentWithErrorBoundary, dom), - ).then((root: RootType) => rootMap.set(info.dom, root)); + if (bridgeInfo.render) { + await Promise.resolve( + bridgeInfo.render(rootComponentWithErrorBoundary, dom), + ).then((root: RootType) => rootMap.set(dom, root)); } else { - let root = rootMap.get(info.dom); + let root = rootMap.get(dom); // do not call createRoot multiple times if (!root) { - root = createRoot(info.dom); - rootMap.set(info.dom, root); + root = createRoot(dom); + rootMap.set(dom, root); + } + + if ('render' in root) { + root.render(rootComponentWithErrorBoundary); } - root.render(rootComponentWithErrorBoundary); } instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(info) || {}; }, destroy(info: DestroyParams) { + const { dom } = info; LoggerInstance.debug(`createBridgeComponent destroy Info`, info); - const root = rootMap.get(info.dom); + const root = rootMap.get(dom); if (root) { if ('unmount' in root) { root.unmount(); } else { ReactDOM.unmountComponentAtNode(root as HTMLElement); } - rootMap.delete(info.dom); + rootMap.delete(dom); } instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit(info); }, diff --git a/packages/bridge/bridge-react/src/remote/component.tsx b/packages/bridge/bridge-react/src/remote/component.tsx index 2a5ce1810ac..9112d9bb650 100644 --- a/packages/bridge/bridge-react/src/remote/component.tsx +++ b/packages/bridge/bridge-react/src/remote/component.tsx @@ -6,38 +6,13 @@ import React, { forwardRef, } from 'react'; import * as ReactRouterDOM from 'react-router-dom'; -import type { ProviderParams } from '@module-federation/bridge-shared'; import { dispatchPopstateEnv } from '@module-federation/bridge-shared'; -import { ErrorBoundaryPropsWithComponent } from 'react-error-boundary'; import { LoggerInstance, pathJoin, getRootDomDefaultClassName } from '../utils'; import { federationRuntime } from '../provider/plugin'; - -declare const __APP_VERSION__: string; -export interface RenderFnParams extends ProviderParams { - dom?: any; - fallback: ErrorBoundaryPropsWithComponent['FallbackComponent']; -} - -interface RemoteModule { - provider: () => { - render: ( - info: ProviderParams & { - dom: any; - }, - ) => void; - destroy: (info: { dom: any }) => void; - }; -} - -interface RemoteAppParams { - moduleName: string; - providerInfo: NonNullable; - exportName: string | number | symbol; - fallback: ErrorBoundaryPropsWithComponent['FallbackComponent']; -} +import { RemoteComponentProps, RemoteAppParams } from '../types'; const RemoteAppWrapper = forwardRef(function ( - props: RemoteAppParams & RenderFnParams, + props: RemoteAppParams & RemoteComponentProps, ref, ) { const { diff --git a/packages/bridge/bridge-react/src/remote/create.tsx b/packages/bridge/bridge-react/src/remote/create.tsx index fe138b55ca3..ec0f179ab1d 100644 --- a/packages/bridge/bridge-react/src/remote/create.tsx +++ b/packages/bridge/bridge-react/src/remote/create.tsx @@ -1,33 +1,19 @@ import React, { forwardRef } from 'react'; -import { - ErrorBoundary, - ErrorBoundaryPropsWithComponent, -} from 'react-error-boundary'; +import { ErrorBoundary } from 'react-error-boundary'; import { LoggerInstance } from '../utils'; import RemoteApp from './component'; -import type { ProviderParams } from '@module-federation/bridge-shared'; - -export interface RenderFnParams extends ProviderParams { - dom?: any; -} - -interface RemoteModule { - provider: () => { - render: (info: RenderFnParams) => void; - destroy: (info: { dom: any }) => void; - }; -} +import { + RemoteComponentParams, + RemoteComponentProps, + RemoteModule, +} from '../types'; -type LazyRemoteComponentInfo = { - loader: () => Promise; - loading: React.ReactNode; - fallback: ErrorBoundaryPropsWithComponent['FallbackComponent']; - export?: E; -}; +type LazyRemoteComponentInfo = RemoteComponentParams; -function createLazyRemoteComponent( - info: LazyRemoteComponentInfo, -) { +function createLazyRemoteComponent< + T = Record, + E extends keyof T = keyof T, +>(info: LazyRemoteComponentInfo) { const exportName = info?.export || 'default'; return React.lazy(async () => { LoggerInstance.debug(`createRemoteComponent LazyComponent create >>>`, { @@ -49,10 +35,7 @@ function createLazyRemoteComponent( if (exportName in m && typeof exportFn === 'function') { const RemoteAppComponent = forwardRef< HTMLDivElement, - { - basename?: ProviderParams['basename']; - memoryRoute?: ProviderParams['memoryRoute']; - } + RemoteComponentProps >((props, ref) => { return ( ( }); } -export function createRemoteComponent( - info: LazyRemoteComponentInfo, -) { - type ExportType = T[E] extends (...args: any) => any - ? ReturnType - : never; - - type RawComponentType = '__BRIDGE_FN__' extends keyof ExportType - ? ExportType['__BRIDGE_FN__'] extends (...args: any) => any - ? Parameters[0] - : {} - : {}; - +export function createRemoteComponent< + T = Record, + E extends keyof T = keyof T, +>(info: LazyRemoteComponentInfo) { const LazyComponent = createLazyRemoteComponent(info); - return forwardRef( - (props, ref) => { - return ( - - - - - - ); - }, - ); + return forwardRef((props, ref) => { + return ( + + + + + + ); + }); } diff --git a/packages/bridge/bridge-react/src/types.ts b/packages/bridge/bridge-react/src/types.ts new file mode 100644 index 00000000000..77e1c63314e --- /dev/null +++ b/packages/bridge/bridge-react/src/types.ts @@ -0,0 +1,128 @@ +import * as React from 'react'; +import { ErrorBoundaryPropsWithComponent } from 'react-error-boundary'; + +/** + * Options for creating a React root + */ +export interface CreateRootOptions { + identifierPrefix?: string; + onRecoverableError?: (error: unknown) => void; + transitionCallbacks?: unknown; +} + +/** + * Interface for a React root object + */ +export interface Root { + render(children: React.ReactNode): void; + unmount(): void; +} + +/** + * Type for a root element, which can be either an HTMLElement or a React root + */ +export type RootType = HTMLElement | Root; + +/** + * Parameters for the render function + */ +export interface RenderParams { + moduleName?: string; + basename?: string; + memoryRoute?: { + entryPath: string; + initialState?: Record; + }; + dom: HTMLElement; + [key: string]: unknown; +} + +/** + * Parameters for the destroy function + */ +export interface DestroyParams { + moduleName: string; + dom: HTMLElement; +} + +/** + * Parameters for the provider function + */ +export interface ProviderParams { + moduleName?: string; + basename?: string; + memoryRoute?: { + entryPath: string; + initialState?: Record; + }; + style?: React.CSSProperties; + className?: string; +} + +/** + * Parameters for the render function, extending ProviderParams + */ +export interface RenderFnParams extends ProviderParams { + dom: HTMLElement; + fallback?: React.ComponentType<{ error: Error }>; + [key: string]: unknown; +} + +/** + * Parameters for the provider function + */ +export interface ProviderFnParams { + rootComponent: React.ComponentType; + render?: ( + App: React.ReactElement, + id?: HTMLElement | string, + ) => RootType | Promise; + createRoot?: ( + container: Element | DocumentFragment, + options?: CreateRootOptions, + ) => Root; +} + +/** + * Parameters for the remote component + */ +export interface RemoteComponentProps> { + props?: T; + fallback?: React.ComponentType<{ error: Error }>; + loading?: React.ReactNode; + [key: string]: unknown; +} + +/** + * Parameters for the remote component loader + */ +export interface RemoteComponentParams< + T = Record, + E extends keyof T = keyof T, +> { + loader: () => Promise; + loading: React.ReactNode; + fallback: React.ComponentType<{ error: Error }>; + export?: E; + props?: T; +} + +/** + * Interface for a remote module provider + */ +export interface RemoteModule { + provider: () => { + render: (info: RenderFnParams) => void; + destroy: (info: { dom: any }) => void; + }; +} + +/** + * Parameters for a remote app component + */ +export interface RemoteAppParams extends ProviderParams { + moduleName: string; + providerInfo: NonNullable; + exportName: string | number | symbol; + fallback: ErrorBoundaryPropsWithComponent['FallbackComponent']; +}