diff --git a/.changeset/polite-toys-turn.md b/.changeset/polite-toys-turn.md new file mode 100644 index 00000000000..41f27c447d4 --- /dev/null +++ b/.changeset/polite-toys-turn.md @@ -0,0 +1,5 @@ +--- +'@module-federation/bridge-react-webpack-plugin': patch +--- + +feat: enchance react-bridge react-router-dom alias according to origin react-router-dom version diff --git a/apps/router-demo/router-host-2000/package.json b/apps/router-demo/router-host-2000/package.json index d82f0ba3bd8..eb6da59ab25 100644 --- a/apps/router-demo/router-host-2000/package.json +++ b/apps/router-demo/router-host-2000/package.json @@ -10,7 +10,8 @@ "dependencies": { "@ant-design/icons": "^5.3.6", "@module-federation/bridge-react": "workspace:*", - "@module-federation/enhanced": "workspace:*" + "@module-federation/enhanced": "workspace:*", + "react-router-dom": "6.24.1" }, "devDependencies": { "@rsbuild/core": "^0.6.15", diff --git a/apps/router-demo/router-remote1-2001/rsbuild.config.ts b/apps/router-demo/router-remote1-2001/rsbuild.config.ts index a91d0413220..6dade2aa306 100644 --- a/apps/router-demo/router-remote1-2001/rsbuild.config.ts +++ b/apps/router-demo/router-remote1-2001/rsbuild.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ react: path.resolve(__dirname, 'node_modules/react'), 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), // set `react-router-dom/` to reference react-router-dom v5 which shoule be find in node_modules/react-router-dom, otherwise it will cause app.tsx fail to work which in react-router-dom v5 mode. - 'react-router-dom/$': path.resolve( + 'react-router-dom': path.resolve( __dirname, 'node_modules/react-router-dom', ), diff --git a/apps/website-new/docs/en/practice/bridge/index.mdx b/apps/website-new/docs/en/practice/bridge/index.mdx index 2df6e422a46..47fa3fff47a 100644 --- a/apps/website-new/docs/en/practice/bridge/index.mdx +++ b/apps/website-new/docs/en/practice/bridge/index.mdx @@ -2,14 +2,16 @@ ## Introduction -The `MF bridge` is a utility function provided by `Module Federation` to help users load application-level modules through `Module Federation`. It automatically provides the necessary application lifecycle functions `render` and `destroy` required by the `provider` function and ensures compatibility with different framework versions. Additionally, it allows proper routing collaboration between applications. +`Bridge` is a utility function provided by `Module Federation` for loading application-level modules. +"Application-level modules" are modules that can run like applications, with framework rendering capabilities and routing abilities. +With `Bridge`, you can render your application in different frameworks and ensure that routing between applications works collaboratively. This is particularly useful for micro-frontend applications. :::tip -Before reading this chapter, it's assumed you are already familiar with: +Before reading this chapter, it's assumed you're familiar with: - [How to consume and export modules](../../guide/start/quick-start.mdx) - [Module Federation Builder plugin](../../guide/basic/rspack.mdx) -- [Features and capabilities of the Module Federation Runtime](../../guide/basic/runtime.mdx) +- [Characteristics and capabilities of Module Federation Runtime](../../guide/basic/runtime.mdx) ::: @@ -17,12 +19,33 @@ Before reading this chapter, it's assumed you are already familiar with: ### @module-federation/bridge-react -The `@module-federation/bridge-react` toolkit is a `bridge` utility function package provided by MF for React v18 applications. The exported `createBridgeComponent` can be used for exporting modules in React v18 applications. Usage examples of `@module-federation/bridge-react` can be found in the [Host demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-host-2000) and [Remote demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-remote2-2002). +`@module-federation/bridge-react` is suitable for React framework types, currently supporting React v16, v17, and v18 versions. + +This toolkit provides two APIs: + +- createBridgeComponent + +> Used to create React application-type module exports. If your application is React-based and you want it to be loaded as an application-type module by another MF application, you should use this to create standard-compliant exports for your application. + +- createRemoteComponent + +> Used to load application-type modules in a React application. The loaded module must be wrapped by `createBridgeComponent`. `createRemoteComponent` will automatically create a rendering context in your application to ensure the module works properly. + +For usage of `@module-federation/bridge-react`, see [Host demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-host-2000) and [Remote demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-remote2-2002). ### @module-federation/bridge-vue3 -The `@module-federation/bridge-vue3` toolkit is a `bridge` utility function package provided by MF for Vue v3 applications. The exported `createBridgeComponent` can be used for exporting modules in Vue v3 sub-applications. Usage examples of `@module-federation/bridge-vue3` can be found in the [Host demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-host-2100) and [Remote demo](https://github.com/module-federation/core/tree/main/apps/router-demo/router-remote3-2003). +`@module-federation/bridge-vue3` is suitable for Vue framework types, currently supporting Vue v3 version. + +This toolkit provides two APIs: +- createBridgeComponent + +> Used to create Vue application-type module exports. If your application is Vue v3-based and you want it to be loaded as an application-type module by another MF application, you should use this to create standard-compliant exports for your application. + +- createRemoteComponent + +> Used to load application-type modules in a Vue application. The loaded module must be wrapped by `createBridgeComponent`. `createRemoteComponent` will automatically create a rendering context in your application to ensure the module works properly. ## FAQ @@ -31,17 +54,17 @@ The `@module-federation/bridge-vue3` toolkit is a `bridge` utility function pack Bridge is mainly used to solve two problems: * Cross-application framework (React, Vue) loading and rendering -* Support for loading modules with routes (routes can work together properly) +* Support for loading modules with routing (routes can work well together) -These two problems are important features in the "micro-frontend framework". +These two issues are important features in "micro-frontend frameworks" -### How to solve it if the corresponding framework bridge is not provided? +### How to solve if there's no bridge provided for a specific framework? -Currently, `Module Federation` provides official bridge toolkits. If you need bridge toolkits for other frameworks, you can provide feedback via [issue](https://github.com/module-federation/core/issues) or refer to the existing [`Bridge`](https://github.com/module-federation/core/blob/34ba220bcee3d032e4083aae37f802d1ed20d61b/packages/bridge/bridge-react) to implement it yourself. +Currently, `Module Federation` provides official bridge toolkits. If you need bridge toolkits for other frameworks, you can provide feedback through [issues](https://github.com/module-federation/core/issues), or refer to the existing [`Bridge`](https://github.com/module-federation/core/blob/34ba220bcee3d032e4083aae37f802d1ed20d61b/packages/bridge/bridge-react) implementation. -The implementation of `Bridge` is very simple. The core is based on `DOM` rendering. Here is the pseudocode: +The implementation of `Bridge` is very simple, with the core based on `DOM` rendering. Here's some pseudocode: -> Export module +> Exporting module ```tsx export default function () { @@ -50,21 +73,24 @@ export default function () { render(info: { dom: HTMLElement; basename?: string; memoryRoute?: { entryPath: string; } }) { const root = ReactDOM.createRoot(info.dom); rootMap.set(info.dom, root); - root.render(); + root.render( + , + ); }, destroy(info: { dom: HTMLElement }) { const root = rootMap.get(info.dom); root?.unmount(); }, - }; + } } ``` -> Load module +> Loading module ```tsx -const LazyComponent = React.lazy(async () => { +const LazyComponent = React.lazy(async () => { const m = await loadRemote('remote1/export-app'); + const providerInfo = m.default; return { default: () => { const rootRef = useRef(null); @@ -82,15 +108,14 @@ const LazyComponent = React.lazy(async () => { }; }, []); return
; - }, + } }; }); -function Component() { - return ( - loading}> - - - ); +function Component () { + return (loading}> + +) } ``` + 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 513bf2fa445..bd53c45ead8 100644 --- a/apps/website-new/docs/en/practice/bridge/react-bridge.mdx +++ b/apps/website-new/docs/en/practice/bridge/react-bridge.mdx @@ -1,6 +1,12 @@ # React Bridge -`@module-federation/bridge-react` provides `bridge` utility functions for React applications. The `createBridgeComponent` function can be used to export application-level modules, while `createRemoteComponent` is used to load application-level modules. [Demo](https://github.com/module-federation/core/core/tree/main/apps/router-demo) +# 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. + +[View Demo](https://github.com/module-federation/core/tree/main/apps/router-demo) ### Installation @@ -14,14 +20,18 @@ import { PackageManagerTabs } from '@theme'; }} /> -### Example -:::danger -After using @module-federation/bridge-react, you should not set react-router-dom as shared; otherwise, the build tool will raise an exception. This is because @module-federation/bridge-react controls routing by proxying react-router-dom. +### Examples +#### Exporting Application Type Modules + +:::danger +Note: After using `@module-federation/bridge-react`, you cannot set `react-router-dom` as shared, otherwise the build tool will throw an exception. This is because `@module-federation/bridge-react` implements route control by proxying `react-router-dom` to ensure that inter-application routing works correctly. ::: -> Remote +> In the producer project, assuming we need to export the application as an application type module using `@module-federation/bridge-react`, with App.tsx as the application entry point. + +- Step 1: First, create a new file `export-app.tsx`, which will be the file exported as an application type module. We need to use `createBridgeComponent` to wrap the root component of the application. ```tsx // ./src/export-app.tsx @@ -33,6 +43,8 @@ export default createBridgeComponent({ }); ``` +- 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 export default defineConfig({ @@ -52,8 +64,24 @@ export default defineConfig({ }); ``` +At this point, we have completed the export of the application type module. + +:::info + +Why do application type modules need to be wrapped with `createBridgeComponent`? There are three main reasons: + +1. Support for cross-framework rendering. Components wrapped with `createBridgeComponent` will conform to the loading protocol of the application type consumer, making cross-framework rendering possible. +2. Automatic injection of `basename`. Components wrapped with `createBridgeComponent` will automatically inject `basename`, ensuring that the producer application works correctly under the consumer project. +3. Wrapping ErrorBoundary. Components wrapped with `createBridgeComponent` will be wrapped with ErrorBoundary to ensure that fallback logic is automatically entered when remote loading fails or rendering errors occur. + +::: + +#### Loading Application Type Modules + > Host +- 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 export default defineConfig({ @@ -73,40 +101,167 @@ export default defineConfig({ }); ``` +- Step 2: In the consumer project, we need to load the application type module. We use `createRemoteComponent` to load the application type module + ```tsx // ./src/App.tsx import React from 'react'; import { createRemoteComponent } from '@module-federation/bridge-react'; +import styles from './index.module.less'; + +// define FallbackErrorComp Component +const FallbackErrorComp = (info: any) => { + return ( +
+

This is ErrorBoundary Component

+

Something went wrong:

+
{info?.error.message}
+ +
+ ); +}; -const Remote1App = createRemoteComponent(() => loadRemote('remote2/export-app')); +// define FallbackLoading Component +const FallbackComp =
loading...
; + +// use createRemoteComponent to export remote component +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 用于在加载远程模块失败时展示的组件 + // fallback is for error handling + fallback: FallbackErrorComp, + // loading is for loading state + loading: FallbackComp, +}); const App = () => { - return ( - + return ( - loading} />} + // use Remote1App component, will be lazy loaded + Component={() => ( + + )} /> - - ); + ) }; ``` +::: + +At this point, we have completed loading the application type module. + +:::info + +1. The remote module exported by `createRemoteComponent` will automatically use the react-bridge loading protocol to load the module, +making cross-framework rendering of applications possible. + +2. Additionally, `createRemoteComponent` will automatically handle module loading, module destruction, error handling, loading, routing, and other logic, +allowing developers to focus solely on how to use the remote component. + +3. For remote modules exported through `createRemoteComponent`, you can use them like regular React components: passing className, style, props, ref, and other attributes will be automatically passed through to the remote component, +making the user experience almost equivalent to using local components. + +::: + ### Methods +#### createBridgeComponent + +```tsx +export declare function createBridgeComponent(bridgeInfo: ProviderFnParams): () => { + render(info: RenderFnParams): Promise; + destroy(info: { + dom: HTMLElement; + }): Promise; +}; + +type ProviderFnParams = { + rootComponent: React.ComponentType; + render?: ( + App: React.ReactElement, + id?: HTMLElement | string, + ) => RootType | Promise; +}; + +export declare interface RenderFnParams extends ProviderParams { + dom: HTMLElement; +} + +export declare interface ProviderParams { + moduleName?: string; + basename?: string; + memoryRoute?: { + entryPath: string; + }; + style?: React.CSSProperties; + className?: string; +} + +``` + +* `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: { - // Function to load the remote application, e.g., loadRemote('remote1/export-app'), import('remote1/export-app') + // 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 module's export + // 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: { @@ -118,15 +273,32 @@ function createRemoteComponent( * `options` * `loader` * type: `() => Promise` - * Purpose: Function to load the remote module, e.g., `loadRemote('remote1/export-app')`, `import('remote1/export-app')` + * Purpose: Used to load remote modules, for example: `loadRemote('remote1/export-app')`, `import('remote1/export-app')` ```tsx -const Remote1App = createRemoteComponent(() => loadRemote('remote1/export-app')); -const Remote2App = createRemoteComponent(() => import('remote2/export-app')); +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, +}); + +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, +}); + + ``` * `export` * type: `string` - * Purpose: Can specify the module's export + * Purpose: Can specify the export of the module ```tsx // remote export const provider = createBridgeComponent({ @@ -141,80 +313,43 @@ const Remote1App = createRemoteComponent({ ``` * `loading` * type: `React.ReactNode` - * Purpose: Component displayed while loading the remote module + * Purpose: Component displayed when loading remote modules * `fallback` * type: `ComponentType<{ error: any; }>` - * Purpose: Component displayed during loading/rendering errors of the remote module -* ReturnType - * type: `(props: PropsInfo) => React.JSX.Element` - * Purpose: Used to render the remote module component - -```tsx -const Remote1App = createRemoteComponent(() => loadRemote('remote1/export-app')); + * Purpose: Component displayed when loading, rendering remote modules -function App() { - return ( - - - loading} />} - /> - - - ); -} -``` +* `ReturnType` + * type: `(props: PropsInfo)=> React.JSX.Element` + * Purpose: Used to render remote module components ```tsx -const Remote1App = createRemoteComponent(() => loadRemote('remote1/export-app')); +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, +}); + function App() { - return ( - - - + + loading} memoryRoute={{ entryPath: '/detail' }} />} + Component={() => ( + + )} /> - - - ); -} -``` - -#### createBridgeComponent - -```tsx -function createBridgeComponent(bridgeInfo: { - rootComponent: React.ComponentType; -}): () => { - render(info: { - name?: string; - basename?: string; - memoryRoute?: { - entryPath: string; - }; - dom?: HTMLElement; -}): void; - destroy(info: { dom: HTMLElement }): void; + + ) } ``` - -* `bridgeInfo` - * type: `{ rootComponent: React.ComponentType; }` - * Purpose: Used to pass the root component -* ReturnType - * type: `() => { render: (info: RenderFnParams) => void; destroy: (info: { dom: HTMLElement }) => void; }` - -```tsx -// ./src/export-app.tsx -import React from 'react'; -import App from './src/App.tsx'; -import { createRemoteComponent } from '@module-federation/bridge-react'; - -export default createBridgeComponent({ - rootComponent: App -}); -``` 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 86a95b9d346..77eae43f85b 100644 --- a/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx +++ b/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx @@ -43,7 +43,7 @@ export default createBridgeComponent({ }); ``` -- Step2: 在 rsbuild 配置中,我们需要将 `export-app.tsx` 作为应用类型模块导出 +- Step2: 在 rsbuild.config.ts 配置文件中,我们需要将 `export-app.tsx` 作为应用类型模块导出 ```ts // rsbuild.config.ts @@ -79,10 +79,10 @@ export default defineConfig({ > Host -- Step1: 在 rsbuild 配置中,我们需要注册远程模块,这点与其它 Module Federation 配置无异。 +- Step1: 在 rsbuild.config.ts 配置中,我们需要注册远程模块,这点与其它 Module Federation 配置无异。 ```ts -//rsbuild.config.ts +// rsbuild.config.ts export default defineConfig({ tools: { rspack: (config, { appendPlugins }) => { @@ -166,7 +166,7 @@ const App = () => { 1. 通过 `createRemoteComponent` 导出的远程模块将会自动使用 react-bridge 加载协议加载模块, 这使得应用的跨框架渲染成为可能。 -2. 此外,createRemoteComponent 会自动处理模块加载、错误处理、loading、路由 等逻辑, +2. 此外,`createRemoteComponent` 会自动处理模块加载、模块销毁、错误处理、loading、路由 等逻辑, 开发者只需要关注如何使用远程组件即可。 3. 通过 `createRemoteComponent` 导出的远程模块,你可以像使用普通 React 组件一样使用远程组件:传递 className、style、props、ref 等属性均会自动透传到远程组件, @@ -187,6 +187,18 @@ export declare function createBridgeComponent(bridgeInfo: ProviderFnParams }): Promise; }; +type ProviderFnParams = { + rootComponent: React.ComponentType; + render?: ( + App: React.ReactElement, + id?: HTMLElement | string, + ) => RootType | Promise; +}; + +export declare interface RenderFnParams extends ProviderParams { + dom: HTMLElement; +} + export declare interface ProviderParams { moduleName?: string; basename?: string; @@ -197,10 +209,6 @@ export declare interface ProviderParams { className?: string; } -export declare interface RenderFnParams extends ProviderParams { - dom: HTMLElement; -} - ``` * `bridgeInfo` @@ -303,7 +311,8 @@ const Remote1App = createRemoteComponent({ * `fallback` * type: `ComponentType<{ error: any; }>` * 作用: 加载、渲染远程模块过程中展示的错误 -* ReturnType + +* `ReturnType` * type: `(props: PropsInfo)=> React.JSX.Element` * 作用: 用于渲染远程模块组件 @@ -321,34 +330,20 @@ const Remote1App = createRemoteComponent({ function App() { return ( - loading} />} - /> + ( + + )} + /> ) } ``` - -```tsx -const Remote1App = createRemoteComponent({ - // loader 用于加载远程模块,例如:loadRemote('remote1/export-app')、import('remote1/export-app') - loader: () => loadRemote('remote1/export-app'), - // fallback 用于在加载远程模块失败时展示的组件 - fallback: FallbackErrorComp, - // loading 用于在加载远程模块时展示的组件 - loading: FallbackComp, -}); - -function App() { - return ( - - loading} memoryRoute={{ entryPath: '/detail' }}/>} - /> - - ) -} -``` \ No newline at end of file diff --git a/packages/bridge/bridge-react-webpack-plugin/package.json b/packages/bridge/bridge-react-webpack-plugin/package.json index 813124ca9ba..b2446174f1b 100644 --- a/packages/bridge/bridge-react-webpack-plugin/package.json +++ b/packages/bridge/bridge-react-webpack-plugin/package.json @@ -22,7 +22,9 @@ "preview": "vite preview" }, "dependencies": { - "@module-federation/sdk": "workspace:*" + "@module-federation/sdk": "workspace:*", + "semver": "7.6.3", + "@types/semver": "7.5.8" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/packages/bridge/bridge-react-webpack-plugin/src/index.ts b/packages/bridge/bridge-react-webpack-plugin/src/index.ts index f3dd2ae09c4..da47bed04ba 100644 --- a/packages/bridge/bridge-react-webpack-plugin/src/index.ts +++ b/packages/bridge/bridge-react-webpack-plugin/src/index.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import type { moduleFederationPlugin } from '@module-federation/sdk'; +import { getBridgeRouterAlias } from './utis'; class ReactBridgeAliasChangerPlugin { alias: string; @@ -12,6 +13,7 @@ class ReactBridgeAliasChangerPlugin { this.moduleFederationOptions = info.moduleFederationOptions; this.alias = 'react-router-dom$'; this.targetFile = '@module-federation/bridge-react/dist/router.es.js'; + if (this.moduleFederationOptions.shared) { if (Array.isArray(this.moduleFederationOptions.shared)) { if (this.moduleFederationOptions.shared.includes('react-router-dom')) { @@ -42,7 +44,8 @@ class ReactBridgeAliasChangerPlugin { // Update alias const updatedAlias = { // allow `alias` can be override - [this.alias]: targetFilePath, + // [this.alias]: targetFilePath, + ...getBridgeRouterAlias(originalAlias['react-router-dom']), ...originalAlias, }; diff --git a/packages/bridge/bridge-react-webpack-plugin/src/utis.ts b/packages/bridge/bridge-react-webpack-plugin/src/utis.ts new file mode 100644 index 00000000000..cda7f6e5919 --- /dev/null +++ b/packages/bridge/bridge-react-webpack-plugin/src/utis.ts @@ -0,0 +1,89 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import semver from 'semver'; + +export const getBridgeRouterAlias = ( + originalAlias: string, +): Record => { + const userPackageJsonPath = path.resolve(process.cwd(), 'package.json'); + let userDependencies: Record = {}; + + if (fs.existsSync(userPackageJsonPath)) { + const userPackageJson = JSON.parse( + fs.readFileSync(userPackageJsonPath, 'utf-8'), + ); + userDependencies = { + ...userPackageJson.dependencies, + ...userPackageJson.devDependencies, + }; + } + + const hasBridgeReact = '@module-federation/bridge-react' in userDependencies; + + let bridgeRouterAlias = {}; + // user install @module-federation/bridge-react package or set bridgeReactRouterDomAlias + if (hasBridgeReact) { + // user install react-router-dom package + const reactRouterDomVersion = userDependencies['react-router-dom']; + let majorVersion = 0; + let reactRouterDomPath = ''; + + // if react-router-dom version is set, use the version in package.json + if (reactRouterDomVersion) { + majorVersion = semver.major( + semver.coerce(reactRouterDomVersion || '0.0.0') ?? '0.0.0', + ); + reactRouterDomPath = require.resolve('react-router-dom'); + } else { + // if react-router-dom version is not set, reslove react-router-dom to get the version + reactRouterDomPath = require.resolve('react-router-dom'); + const packageJsonPath = path.resolve( + reactRouterDomPath, + '../../package.json', + ); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + majorVersion = parseInt(packageJson.version.split('.')[0]); + } + + // if react-router-dom path has set alias by user, use the originalAlias + reactRouterDomPath = originalAlias || reactRouterDomPath; + + if (majorVersion === 5) { + bridgeRouterAlias = { + 'react-router-dom$': + '@module-federation/bridge-react/dist/router-v5.es.js', + }; + try { + require.resolve('react-router-dom/index.js'); + } catch (error) { + // if react-router-dom/index.js cannot be resolved, set the alias to origin reactRouterDomPath + bridgeRouterAlias = { + ...bridgeRouterAlias, + 'react-router-dom/index.js': reactRouterDomPath, + }; + } + } else if (majorVersion === 6) { + bridgeRouterAlias = { + 'react-router-dom$': + '@module-federation/bridge-react/dist/router-v6.es.js', + }; + + try { + require.resolve('react-router-dom/dist/index.js'); + } catch (error) { + // if react-router-dom/dist/index.js cannot be resolved, set the alias to origin reactRouterDomPath + bridgeRouterAlias = { + ...bridgeRouterAlias, + 'react-router-dom/dist/index.js': reactRouterDomPath, + }; + } + } else { + console.warn('react-router-dom version is not supported'); + } + } + console.log( + '<<<<<<<<<<<<< bridgeRouterAlias >>>>>>>>>>>>>', + bridgeRouterAlias, + ); + return bridgeRouterAlias; +}; diff --git a/packages/bridge/bridge-react/package.json b/packages/bridge/bridge-react/package.json index 8e5edc0b229..0e5c7738a70 100644 --- a/packages/bridge/bridge-react/package.json +++ b/packages/bridge/bridge-react/package.json @@ -21,14 +21,14 @@ "require": "./dist/router.cjs.js" }, "./router-v5": { - "types": "./dist/router.d.ts", - "import": "./dist/router.es.js", - "require": "./dist/router.cjs.js" + "types": "./dist/router-v5.d.ts", + "import": "./dist/router-v5.es.js", + "require": "./dist/router-v5.cjs.js" }, "./router-v6": { - "types": "./dist/router.d.ts", - "import": "./dist/router.es.js", - "require": "./dist/router.cjs.js" + "types": "./dist/router-v6.d.ts", + "import": "./dist/router-v6.es.js", + "require": "./dist/router-v6.cjs.js" }, "./*": "./*" }, diff --git a/packages/bridge/bridge-react/src/create.tsx b/packages/bridge/bridge-react/src/create.tsx index c3f664260e9..90f0ab51812 100644 --- a/packages/bridge/bridge-react/src/create.tsx +++ b/packages/bridge/bridge-react/src/create.tsx @@ -99,15 +99,16 @@ export function createRemoteComponent(info: { : {} : {}; - return forwardRef((props: ProviderParams & RawComponentType, ref) => { - const LazyComponent = createLazyRemoteComponent(info); - return ( - // set ErrorBoundary for LazyComponent rendering error, usually caused by inner bridge logic render process - - - - - - ); - }); + return forwardRef( + (props, ref) => { + const LazyComponent = createLazyRemoteComponent(info); + return ( + + + + + + ); + }, + ); } diff --git a/packages/bridge/bridge-react/src/remote/index.tsx b/packages/bridge/bridge-react/src/remote/index.tsx index 24f58faf6bf..fa0a6296da8 100644 --- a/packages/bridge/bridge-react/src/remote/index.tsx +++ b/packages/bridge/bridge-react/src/remote/index.tsx @@ -52,10 +52,11 @@ const RemoteAppWrapper = forwardRef(function ( ...resProps } = props; - const rootRef: React.MutableRefObject = + const rootRef: React.MutableRefObject = ref && 'current' in ref - ? (ref as React.MutableRefObject) + ? (ref as React.MutableRefObject) : useRef(null); + const renderDom: React.MutableRefObject = useRef(null); const providerInfoRef = useRef(null); diff --git a/packages/bridge/bridge-react/src/router-v5.tsx b/packages/bridge/bridge-react/src/router-v5.tsx new file mode 100644 index 00000000000..2216340e831 --- /dev/null +++ b/packages/bridge/bridge-react/src/router-v5.tsx @@ -0,0 +1,44 @@ +import React, { useContext } from 'react'; +// The upper alias react-router-dom$ into this file avoids the loop +// @ts-ignore +import * as ReactRouterDom from 'react-router-dom/index.js'; + +import { RouterContext } from './context'; +import { LoggerInstance } from './utils'; + +function WraperRouter( + props: + | Parameters[0] + | Parameters[0], +) { + const { basename, ...propsRes } = props; + const routerContextProps = useContext(RouterContext) || {}; + + LoggerInstance.log(`WraperRouter info >>>`, { + ...routerContextProps, + routerContextProps, + WraperRouterProps: props, + }); + + if (routerContextProps?.memoryRoute) { + return ( + + ); + } + return ( + + ); +} + +// @ts-ignore +// cause export directly from react-router-dom/index.js will cause build falied. +// it will be replace by react-router-dom/index.js in building phase +export * from 'react-router-dom/'; + +export { WraperRouter as BrowserRouter }; diff --git a/packages/bridge/bridge-react/src/router-v6.tsx b/packages/bridge/bridge-react/src/router-v6.tsx new file mode 100644 index 00000000000..64a012bbd89 --- /dev/null +++ b/packages/bridge/bridge-react/src/router-v6.tsx @@ -0,0 +1,73 @@ +import React, { useContext } from 'react'; +// The upper alias react-router-dom$ into this file avoids the loop +import * as ReactRouterDom from 'react-router-dom/dist/index.js'; + +import { RouterContext } from './context'; +import { LoggerInstance } from './utils'; + +function WraperRouter( + props: + | Parameters[0] + | Parameters[0], +) { + const { basename, ...propsRes } = props; + const routerContextProps = useContext(RouterContext) || {}; + + LoggerInstance.log(`WraperRouter info >>>`, { + ...routerContextProps, + routerContextProps, + WraperRouterProps: props, + }); + + if (routerContextProps?.memoryRoute) { + return ( + + ); + } + return ( + + ); +} + +function WraperRouterProvider( + props: Parameters[0], +) { + const { router, ...propsRes } = props; + const routerContextProps = useContext(RouterContext) || {}; + const routers = router.routes; + LoggerInstance.log(`WraperRouterProvider info >>>`, { + ...routerContextProps, + routerContextProps, + WraperRouterProviderProps: props, + router, + }); + const RouterProvider = (ReactRouterDom as any)['Router' + 'Provider']; + const createMemoryRouter = (ReactRouterDom as any)['create' + 'MemoryRouter']; + const createBrowserRouter = (ReactRouterDom as any)[ + 'create' + 'BrowserRouter' + ]; + + if (routerContextProps.memoryRoute) { + const MemeoryRouterInstance = createMemoryRouter(routers, { + initialEntries: [routerContextProps?.memoryRoute.entryPath], + }); + return ; + } else { + const BrowserRouterInstance = createBrowserRouter(routers, { + basename: routerContextProps.basename, + future: router.future, + window: router.window, + }); + return ; + } +} + +export * from 'react-router-dom/dist/index.js'; +export { WraperRouter as BrowserRouter }; +export { WraperRouterProvider as RouterProvider }; diff --git a/packages/bridge/bridge-react/vite.config.ts b/packages/bridge/bridge-react/vite.config.ts index 76b4c9d8fd0..469e140734e 100644 --- a/packages/bridge/bridge-react/vite.config.ts +++ b/packages/bridge/bridge-react/vite.config.ts @@ -22,8 +22,8 @@ export default defineConfig({ entry: { index: path.resolve(__dirname, 'src/index.ts'), router: path.resolve(__dirname, 'src/router.tsx'), - 'router-v5': path.resolve(__dirname, 'src/router.tsx'), - 'router-v6': path.resolve(__dirname, 'src/router.tsx'), + 'router-v5': path.resolve(__dirname, 'src/router-v5.tsx'), + 'router-v6': path.resolve(__dirname, 'src/router-v6.tsx'), }, formats: ['cjs', 'es'], fileName: (format, entryName) => `${entryName}.${format}.js`, @@ -43,13 +43,13 @@ export default defineConfig({ generateBundle(options, bundle) { for (const fileName in bundle) { const chunk = bundle[fileName]; - if (fileName.includes('router-v6') && chunk.type === 'chunk') { - chunk.code = chunk.code.replace( - // Match 'react-router-dom/' followed by single quotes, double quotes, or backticks, replacing only 'react-router-dom/' to react-router-v6 dist file structure - /react-router-dom\/(?=[\'\"\`])/g, - 'react-router-dom/dist/index.js', - ); - } + // if (fileName.includes('router-v6') && chunk.type === 'chunk') { + // chunk.code = chunk.code.replace( + // // Match 'react-router-dom/' followed by single quotes, double quotes, or backticks, replacing only 'react-router-dom/' to react-router-v6 dist file structure + // /react-router-dom\/(?=[\'\"\`])/g, + // 'react-router-dom/dist/index.js', + // ); + // } if (fileName.includes('router-v5') && chunk.type === 'chunk') { chunk.code = chunk.code.replace( diff --git a/packages/bridge/vue3-bridge/src/provider.ts b/packages/bridge/vue3-bridge/src/provider.ts index fcc78f46b76..7bfc3e1531d 100644 --- a/packages/bridge/vue3-bridge/src/provider.ts +++ b/packages/bridge/vue3-bridge/src/provider.ts @@ -29,7 +29,7 @@ export function createBridgeComponent(bridgeInfo: any) { }); LoggerInstance.log(`createBridgeComponent render router info>>>`, { - name: info.name, + name: info.moduleName, router, }); // memory route Initializes the route diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 145db2e89e3..5112a6f0315 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1496,6 +1496,9 @@ importers: '@module-federation/enhanced': specifier: workspace:* version: link:../../../packages/enhanced + react-router-dom: + specifier: 6.24.1 + version: 6.24.1(react-dom@18.3.1)(react@18.3.1) devDependencies: '@rsbuild/core': specifier: ^0.6.15 @@ -1922,6 +1925,12 @@ importers: '@module-federation/sdk': specifier: workspace:* version: link:../../sdk + '@types/semver': + specifier: 7.5.8 + version: 7.5.8 + semver: + specifier: 7.6.3 + version: 7.6.3 devDependencies: typescript: specifier: ^5.2.2 @@ -20084,7 +20093,6 @@ packages: /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - dev: true /@types/send@0.17.4: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}