Skip to content

Commit aa058ca

Browse files
committed
chore(react): export createRemoteComponent and related react utils
1 parent 4f6a097 commit aa058ca

38 files changed

+2128
-296
lines changed

.changeset/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"@module-federation/inject-external-runtime-core-plugin",
2828
"@module-federation/runtime-core",
2929
"create-module-federation",
30-
"@module-federation/cli"
30+
"@module-federation/cli",
31+
"@module-federation/react"
3132
]
3233
],
3334
"ignorePatterns": ["^alpha|^beta"],

.changeset/spicy-parents-greet.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
'@module-federation/bridge-react': patch
2+
'@module-federation/react': patch
33
'@module-federation/modern-js': patch
44
---
55

6-
chore(bridge-react): export createRemoteComponent and related utils
6+
chore(react): export createRemoteComponent and related react utils

apps/modern-component-data-fetch/provider-csr/module-federation.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createModuleFederationConfig } from '@module-federation/modern-js';
1+
import { createModuleFederationConfig } from '@module-federation/rsbuild-plugin';
22

33
export default createModuleFederationConfig({
44
name: 'provider_csr',

packages/bridge/bridge-react/package.json

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"url": "https://github.com/module-federation/core",
1212
"directory": "packages/bridge-react"
1313
},
14-
"type": "module",
1514
"main": "./dist/index.cjs.js",
1615
"module": "./dist/index.es.js",
1716
"types": "./dist/index.d.ts",
@@ -61,10 +60,10 @@
6160
"import": "./dist/data-fetch-utils.es.js",
6261
"require": "./dist/data-fetch-utils.cjs.js"
6362
},
64-
"./data-fetch-constant": {
65-
"types": "./dist/module/constant.d.ts",
66-
"import": "./dist/data-fetch-constant.es.js",
67-
"require": "./dist/data-fetch-constant.cjs.js"
63+
"./data-fetch-server-middleware": {
64+
"types": "./dist/module/data-fetch/data-fetch-server-middleware.d.ts",
65+
"import": "./dist/data-fetch-server-middleware.es.js",
66+
"require": "./dist/data-fetch-server-middleware.cjs.js"
6867
},
6968
"./*": "./*"
7069
},
@@ -97,8 +96,8 @@
9796
"data-fetch-utils": [
9897
"./dist/module/utils.d.ts"
9998
],
100-
"data-fetch-constant": [
101-
"./dist/module/constant.d.ts"
99+
"data-fetch-server-middleware": [
100+
"./dist/module/data-fetch/data-fetch-server-middleware.d.ts"
102101
]
103102
}
104103
},
@@ -115,13 +114,7 @@
115114
"peerDependencies": {
116115
"react": ">=16.9.0",
117116
"react-dom": ">=16.9.0",
118-
"react-router-dom": "^4 || ^5 || ^6 || ^7",
119-
"@module-federation/runtime": ">=0.15.0"
120-
},
121-
"peerDependenciesMeta": {
122-
"@module-federation/runtime": {
123-
"optional": true
124-
}
117+
"react-router-dom": "^4 || ^5 || ^6 || ^7"
125118
},
126119
"devDependencies": {
127120
"@testing-library/react": "15.0.7",
@@ -137,6 +130,7 @@
137130
"typescript": "^5.2.2",
138131
"vite": "^5.4.18",
139132
"vite-plugin-dts": "^4.5.4",
140-
"@module-federation/runtime": "workspace:*"
133+
"@module-federation/runtime": "workspace:*",
134+
"hono": "3.12.12"
141135
}
142136
}

packages/bridge/bridge-react/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ export type {
1414
} from './types';
1515

1616
export { kit, ERROR_TYPE, autoFetchDataPlugin } from './module';
17-
export type { DataFetchParams, NoSSRRemoteInfo } from './module';
17+
export type {
18+
DataFetchParams,
19+
NoSSRRemoteInfo,
20+
CollectSSRAssetsOptions,
21+
CreateRemoteComponentOptions,
22+
} from './module';

packages/bridge/bridge-react/src/module/createRemoteComponent.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { ReactNode, useEffect, useState } from 'react';
22
import logger from './logger';
3-
import { getInstance } from '@module-federation/runtime';
43
import { AwaitDataFetch, transformError } from './AwaitDataFetch';
54
import {
65
fetchData,
@@ -21,11 +20,13 @@ import {
2120

2221
import type { ErrorInfo } from './AwaitDataFetch';
2322
import type { DataFetchParams, NoSSRRemoteInfo } from './types';
23+
import type { FederationHost } from '@module-federation/runtime';
2424

25-
type IProps = {
25+
export type IProps = {
2626
id: string;
2727
injectScript?: boolean;
2828
injectLink?: boolean;
29+
runtime: typeof import('@module-federation/runtime');
2930
};
3031

3132
export type CreateRemoteComponentOptions<T, E extends keyof T> = {
@@ -35,12 +36,12 @@ export type CreateRemoteComponentOptions<T, E extends keyof T> = {
3536
export?: E;
3637
dataFetchParams?: DataFetchParams;
3738
noSSR?: boolean;
39+
runtime: typeof import('@module-federation/runtime');
3840
};
3941

4042
type ReactKey = { key?: React.Key | null };
4143

42-
function getTargetModuleInfo(id: string) {
43-
const instance = getInstance();
44+
function getTargetModuleInfo(id: string, instance?: FederationHost) {
4445
if (!instance) {
4546
return;
4647
}
@@ -88,12 +89,12 @@ export function collectSSRAssets(options: IProps) {
8889
} = typeof options === 'string' ? { id: options } : options;
8990
const links: React.ReactNode[] = [];
9091
const scripts: React.ReactNode[] = [];
91-
const instance = getInstance();
92+
const instance = options.runtime.getInstance();
9293
if (!instance || (!injectLink && !injectScript)) {
9394
return [...scripts, ...links];
9495
}
9596

96-
const moduleAndPublicPath = getTargetModuleInfo(id);
97+
const moduleAndPublicPath = getTargetModuleInfo(id, instance);
9798
if (!moduleAndPublicPath) {
9899
return [...scripts, ...links];
99100
}
@@ -191,6 +192,12 @@ function getServerNeedRemoteInfo(
191192
export function createRemoteComponent<T, E extends keyof T>(
192193
options: CreateRemoteComponentOptions<T, E>,
193194
) {
195+
const { runtime } = options;
196+
if (!runtime?.getInstance) {
197+
throw new Error(
198+
'runtime is required if used in "@module-federation/bridge-react"!',
199+
);
200+
}
194201
type ComponentType = T[E] extends (...args: any) => any
195202
? Parameters<T[E]>[0] extends undefined
196203
? ReactKey
@@ -212,7 +219,7 @@ export function createRemoteComponent<T, E extends keyof T>(
212219
const getData = async (noSSR?: boolean) => {
213220
let loadedRemoteInfo: ReturnType<typeof getLoadedRemoteInfos>;
214221
let moduleId: string;
215-
const instance = getInstance();
222+
const instance = runtime.getInstance();
216223
try {
217224
const m = await callLoader();
218225
moduleId = m && m[Symbol.for('mf_module_id')];
@@ -264,7 +271,7 @@ export function createRemoteComponent<T, E extends keyof T>(
264271
const LazyComponent = React.lazy(async () => {
265272
const m = await callLoader();
266273
const moduleId = m && m[Symbol.for('mf_module_id')];
267-
const instance = getInstance()!;
274+
const instance = runtime.getInstance()!;
268275
const loadedRemoteInfo = getLoadedRemoteInfos(moduleId, instance);
269276
loadedRemoteInfo?.snapshot;
270277
const dataFetchMapKey = loadedRemoteInfo
@@ -282,6 +289,7 @@ export function createRemoteComponent<T, E extends keyof T>(
282289

283290
const assets = collectSSRAssets({
284291
id: moduleId,
292+
runtime,
285293
});
286294

287295
const Com = m[exportName] as React.FC<ComponentType>;
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { DATA_FETCH_QUERY, MF_DATA_FETCH_STATUS } from '../constant';
2+
import {
3+
getDataFetchMap,
4+
fetchData,
5+
initDataFetchMap,
6+
loadDataFetchModule,
7+
} from '../utils';
8+
import { SEPARATOR, MANIFEST_EXT } from '@module-federation/sdk';
9+
import logger from '../logger';
10+
11+
import type { NoSSRRemoteInfo } from '../types';
12+
import type { MiddlewareHandler } from 'hono';
13+
14+
function wrapSetTimeout(
15+
targetPromise: Promise<unknown>,
16+
delay = 20000,
17+
id: string,
18+
) {
19+
if (targetPromise && typeof targetPromise.then === 'function') {
20+
return new Promise((resolve, reject) => {
21+
const timeoutId = setTimeout(() => {
22+
logger.warn(`Data fetch for ID ${id} timed out after 20 seconds.`);
23+
reject(new Error(`Data fetch for ID ${id} timed out after 20 seconds`));
24+
}, delay);
25+
26+
targetPromise
27+
.then((value: any) => {
28+
clearTimeout(timeoutId);
29+
resolve(value);
30+
})
31+
.catch((err: any) => {
32+
clearTimeout(timeoutId);
33+
reject(err);
34+
});
35+
});
36+
}
37+
}
38+
39+
function addProtocol(url: string) {
40+
if (url.startsWith('//')) {
41+
return 'https:' + url;
42+
}
43+
return url;
44+
}
45+
46+
const getDecodeQuery = (url: URL, name: string) => {
47+
const res = url.searchParams.get(name);
48+
if (!res) {
49+
return null;
50+
}
51+
return decodeURIComponent(res);
52+
};
53+
54+
const dataFetchServerMiddleware: MiddlewareHandler = async (ctx, next) => {
55+
let url: URL;
56+
let dataFetchId: string | null;
57+
let params: Record<string, unknown>;
58+
let remoteInfo: NoSSRRemoteInfo;
59+
try {
60+
url = new URL(ctx.req.url);
61+
dataFetchId = getDecodeQuery(url, DATA_FETCH_QUERY);
62+
params = JSON.parse(getDecodeQuery(url, 'params') || '{}');
63+
const remoteInfoQuery = getDecodeQuery(url, 'remoteInfo');
64+
remoteInfo = remoteInfoQuery ? JSON.parse(remoteInfoQuery) : null;
65+
} catch (e) {
66+
logger.error('fetch data from server, error: ', e);
67+
return next();
68+
}
69+
70+
if (!dataFetchId) {
71+
return next();
72+
}
73+
logger.log('fetch data from server, dataFetchId: ', dataFetchId);
74+
logger.debug(
75+
'fetch data from server, moduleInfo: ',
76+
globalThis.__FEDERATION__?.moduleInfo,
77+
);
78+
try {
79+
const dataFetchMap = getDataFetchMap();
80+
if (!dataFetchMap) {
81+
initDataFetchMap();
82+
}
83+
const fetchDataPromise = dataFetchMap[dataFetchId]?.[1];
84+
logger.debug(
85+
'fetch data from server, fetchDataPromise: ',
86+
fetchDataPromise,
87+
);
88+
if (
89+
fetchDataPromise &&
90+
dataFetchMap[dataFetchId]?.[2] !== MF_DATA_FETCH_STATUS.ERROR
91+
) {
92+
const targetPromise = fetchDataPromise[0];
93+
// Ensure targetPromise is thenable
94+
const wrappedPromise = wrapSetTimeout(targetPromise, 20000, dataFetchId);
95+
if (wrappedPromise) {
96+
const res = await wrappedPromise;
97+
logger.log('fetch data from server, fetchDataPromise res: ', res);
98+
return ctx.json(res);
99+
}
100+
logger.error(
101+
`Expected a Promise from fetchDataPromise[0] for dataFetchId ${dataFetchId}, but received:`,
102+
targetPromise,
103+
'Will try call new dataFetch again...',
104+
);
105+
}
106+
107+
if (remoteInfo) {
108+
try {
109+
const hostInstance = globalThis.__FEDERATION__.__INSTANCES__[0];
110+
const remoteEntry = `${addProtocol(remoteInfo.ssrPublicPath) + remoteInfo.ssrRemoteEntry}`;
111+
if (!hostInstance) {
112+
throw new Error('host instance not found!');
113+
}
114+
const remote = hostInstance.options.remotes.find(
115+
(remote) => remote.name === remoteInfo.name,
116+
);
117+
logger.debug('find remote: ', JSON.stringify(remote));
118+
if (!remote) {
119+
hostInstance.registerRemotes([
120+
{
121+
name: remoteInfo.name,
122+
entry: remoteEntry,
123+
entryGlobalName: remoteInfo.globalName,
124+
},
125+
]);
126+
} else if (
127+
!('entry' in remote) ||
128+
!remote.entry.includes(MANIFEST_EXT)
129+
) {
130+
const { hostGlobalSnapshot, remoteSnapshot } =
131+
hostInstance.snapshotHandler.getGlobalRemoteInfo(remoteInfo);
132+
logger.debug(
133+
'find hostGlobalSnapshot: ',
134+
JSON.stringify(hostGlobalSnapshot),
135+
);
136+
logger.debug('find remoteSnapshot: ', JSON.stringify(remoteSnapshot));
137+
138+
if (!hostGlobalSnapshot || !remoteSnapshot) {
139+
if ('version' in remote) {
140+
// @ts-ignore
141+
delete remote.version;
142+
}
143+
// @ts-ignore
144+
remote.entry = remoteEntry;
145+
remote.entryGlobalName = remoteInfo.globalName;
146+
}
147+
}
148+
} catch (e) {
149+
ctx.status(500);
150+
return ctx.text(
151+
`failed to fetch ${remoteInfo.name} data, error:\n ${e}`,
152+
);
153+
}
154+
}
155+
156+
const dataFetchItem = dataFetchMap[dataFetchId];
157+
logger.debug('fetch data from server, dataFetchItem: ', dataFetchItem);
158+
if (dataFetchItem) {
159+
const callFetchDataPromise = fetchData(dataFetchId, {
160+
...params,
161+
isDowngrade: !remoteInfo,
162+
});
163+
const wrappedPromise = wrapSetTimeout(
164+
callFetchDataPromise,
165+
20000,
166+
dataFetchId,
167+
);
168+
if (wrappedPromise) {
169+
const res = await wrappedPromise;
170+
logger.log('fetch data from server, dataFetchItem res: ', res);
171+
return ctx.json(res);
172+
}
173+
}
174+
175+
const remoteId = dataFetchId.split(SEPARATOR)[0];
176+
const hostInstance = globalThis.__FEDERATION__.__INSTANCES__[0];
177+
if (!hostInstance) {
178+
throw new Error('host instance not found!');
179+
}
180+
const dataFetchFn = await loadDataFetchModule(hostInstance, remoteId);
181+
const data = await dataFetchFn({ ...params, isDowngrade: !remoteInfo });
182+
logger.log('fetch data from server, loadDataFetchModule res: ', data);
183+
return ctx.json(data);
184+
} catch (e) {
185+
logger.error('server plugin data fetch error: ', e);
186+
ctx.status(500);
187+
return ctx.text(`failed to fetch ${remoteInfo.name} data, error:\n ${e}`);
188+
}
189+
};
190+
191+
export default dataFetchServerMiddleware;

packages/bridge/bridge-react/src/module/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import autoFetchDataPlugin from './data-fetch/runtime-plugin';
22

33
export type { DataFetchParams, NoSSRRemoteInfo } from './types';
44
export { ERROR_TYPE } from './constant';
5+
export type {
6+
CreateRemoteComponentOptions,
7+
IProps as CollectSSRAssetsOptions,
8+
} from './createRemoteComponent';
59

610
// avoid import react directly https://github.com/web-infra-dev/modern.js/issues/7096
711
export const kit = {

packages/bridge/bridge-react/vite.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ export default defineConfig({
3131
'src/module/data-fetch/runtime-plugin.ts',
3232
),
3333
'data-fetch-utils': path.resolve(__dirname, 'src/module/utils.ts'),
34-
'data-fetch-constant': path.resolve(
34+
'data-fetch-server-middleware': path.resolve(
3535
__dirname,
36-
'src/module/constant.ts',
36+
'src/module/data-fetch/data-fetch-server-middleware.ts',
3737
),
3838
},
3939
formats: ['cjs', 'es'],

0 commit comments

Comments
 (0)