Skip to content

feat(react): add cache api #3847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"@module-federation/inject-external-runtime-core-plugin",
"@module-federation/runtime-core",
"create-module-federation",
"@module-federation/cli"
"@module-federation/cli",
"@module-federation/react"
]
],
"ignorePatterns": ["^alpha|^beta"],
Expand Down
6 changes: 6 additions & 0 deletions .changeset/spicy-parents-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@module-federation/react': patch
'@module-federation/modern-js': patch
---

chore(react): export createRemoteComponent and related react utils
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
run: nproc

- name: Warm Nx Cache
run: npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4
run: npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4 --skip-nx-cache

- name: Run Build for All
run: npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4 --skip-nx-cache
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/devtools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
run: pnpm install

- name: Run Affected Build
run: npx nx run-many --targets=build --projects=tag:type:pkg
run: npx nx run-many --targets=build --projects=tag:type:pkg --skip-nx-cache

- name: Configuration xvfb
shell: bash
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createModuleFederationConfig } from '@module-federation/modern-js';
import { createModuleFederationConfig } from '@module-federation/rsbuild-plugin';

export default createModuleFederationConfig({
name: 'provider_csr',
Expand Down
9 changes: 1 addition & 8 deletions packages/modernjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@
"import": "./dist/esm/cli/mfRuntimePlugins/resolve-entry-ipv4.js",
"require": "./dist/esm/cli/mfRuntimePlugins/resolve-entry-ipv4.js"
},
"./auto-fetch-data": {
"types": "./dist/types/cli/mfRuntimePlugins/auto-fetch-data.d.ts",
"import": "./dist/esm/cli/mfRuntimePlugins/auto-fetch-data.js",
"require": "./dist/esm/cli/mfRuntimePlugins/auto-fetch-data.js"
},
"./inject-node-fetch": {
"types": "./dist/types/cli/mfRuntimePlugins/inject-node-fetch.d.ts",
"import": "./dist/esm/cli/mfRuntimePlugins/inject-node-fetch.js",
Expand Down Expand Up @@ -94,9 +89,6 @@
"resolve-entry-ipv4": [
"./dist/types/cli/mfRuntimePlugins/resolve-entry-ipv4.d.ts"
],
"auto-fetch-data": [
"./dist/types/cli/mfRuntimePlugins/auto-fetch-data.d.ts"
],
"inject-node-fetch": [
"./dist/types/cli/mfRuntimePlugins/inject-node-fetch.d.ts"
],
Expand All @@ -119,6 +111,7 @@
"@modern-js/utils": "2.67.5",
"@modern-js/node-bundle-require": "2.67.6",
"@module-federation/rsbuild-plugin": "workspace:*",
"@module-federation/react": "workspace:*",
"fs-extra": "11.3.0",
"lru-cache": "10.4.3",
"@module-federation/enhanced": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions packages/modernjs/src/cli/configPlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('patchMFConfig', async () => {
remoteType: 'script',
runtimePlugins: [
require.resolve('@module-federation/modern-js/shared-strategy'),
require.resolve('@module-federation/modern-js/auto-fetch-data'),
require.resolve('@module-federation/react/data-fetch-runtime-plugin'),
require.resolve('@module-federation/node/runtimePlugin'),
require.resolve('@module-federation/modern-js/inject-node-fetch'),
],
Expand Down Expand Up @@ -65,7 +65,7 @@ describe('patchMFConfig', async () => {
remoteType: 'script',
runtimePlugins: [
require.resolve('@module-federation/modern-js/shared-strategy'),
require.resolve('@module-federation/modern-js/auto-fetch-data'),
require.resolve('@module-federation/react/data-fetch-runtime-plugin'),
],
shared: {
react: {
Expand Down
2 changes: 1 addition & 1 deletion packages/modernjs/src/cli/configPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const patchMFConfig = (
);

injectRuntimePlugins(
require.resolve('@module-federation/modern-js/auto-fetch-data'),
require.resolve('@module-federation/react/data-fetch-runtime-plugin'),
runtimePlugins,
);

Expand Down
194 changes: 3 additions & 191 deletions packages/modernjs/src/cli/server/data-fetch-server-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,194 +1,6 @@
import { DATA_FETCH_QUERY, MF_DATA_FETCH_STATUS } from '../../constant';
import logger from '../../logger';
import { getDataFetchMap } from '../../utils';
import {
fetchData,
initDataFetchMap,
loadDataFetchModule,
} from '../../utils/dataFetch';
import { SEPARATOR, MANIFEST_EXT } from '@module-federation/sdk';
import type {
MiddlewareHandler,
ServerPlugin,
} from '@modern-js/server-runtime';
import type { NoSSRRemoteInfo } from '../../interfaces/global';
import dataFetchMiddleWare from '@module-federation/react/data-fetch-server-middleware';

function wrapSetTimeout(
targetPromise: Promise<unknown>,
delay = 20000,
id: string,
) {
if (targetPromise && typeof targetPromise.then === 'function') {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
logger.warn(`Data fetch for ID ${id} timed out after 20 seconds.`);
reject(new Error(`Data fetch for ID ${id} timed out after 20 seconds`));
}, delay);

targetPromise
.then((value: any) => {
clearTimeout(timeoutId);
resolve(value);
})
.catch((err: any) => {
clearTimeout(timeoutId);
reject(err);
});
});
}
}

function addProtocol(url: string) {
if (url.startsWith('//')) {
return 'https:' + url;
}
return url;
}

const getDecodeQuery = (url: URL, name: string) => {
const res = url.searchParams.get(name);
if (!res) {
return null;
}
return decodeURIComponent(res);
};

const middleware: MiddlewareHandler = async (ctx, next) => {
let url: URL;
let dataFetchId: string | null;
let params: Record<string, unknown>;
let remoteInfo: NoSSRRemoteInfo;
try {
url = new URL(ctx.req.url);
dataFetchId = getDecodeQuery(url, DATA_FETCH_QUERY);
params = JSON.parse(getDecodeQuery(url, 'params') || '{}');
const remoteInfoQuery = getDecodeQuery(url, 'remoteInfo');
remoteInfo = remoteInfoQuery ? JSON.parse(remoteInfoQuery) : null;
} catch (e) {
logger.error('fetch data from server, error: ', e);
return next();
}

if (!dataFetchId) {
return next();
}
logger.log('fetch data from server, dataFetchId: ', dataFetchId);
logger.debug(
'fetch data from server, moduleInfo: ',
globalThis.__FEDERATION__?.moduleInfo,
);
try {
const dataFetchMap = getDataFetchMap();
if (!dataFetchMap) {
initDataFetchMap();
}
const fetchDataPromise = dataFetchMap[dataFetchId]?.[1];
logger.debug(
'fetch data from server, fetchDataPromise: ',
fetchDataPromise,
);
if (
fetchDataPromise &&
dataFetchMap[dataFetchId]?.[2] !== MF_DATA_FETCH_STATUS.ERROR
) {
const targetPromise = fetchDataPromise[0];
// Ensure targetPromise is thenable
const wrappedPromise = wrapSetTimeout(targetPromise, 20000, dataFetchId);
if (wrappedPromise) {
const res = await wrappedPromise;
logger.log('fetch data from server, fetchDataPromise res: ', res);
return ctx.json(res);
}
logger.error(
`Expected a Promise from fetchDataPromise[0] for dataFetchId ${dataFetchId}, but received:`,
targetPromise,
'Will try call new dataFetch again...',
);
}

if (remoteInfo) {
try {
const hostInstance = globalThis.__FEDERATION__.__INSTANCES__[0];
const remoteEntry = `${addProtocol(remoteInfo.ssrPublicPath) + remoteInfo.ssrRemoteEntry}`;
if (!hostInstance) {
throw new Error('host instance not found!');
}
const remote = hostInstance.options.remotes.find(
(remote) => remote.name === remoteInfo.name,
);
logger.debug('find remote: ', JSON.stringify(remote));
if (!remote) {
hostInstance.registerRemotes([
{
name: remoteInfo.name,
entry: remoteEntry,
entryGlobalName: remoteInfo.globalName,
},
]);
} else if (
!('entry' in remote) ||
!remote.entry.includes(MANIFEST_EXT)
) {
const { hostGlobalSnapshot, remoteSnapshot } =
hostInstance.snapshotHandler.getGlobalRemoteInfo(remoteInfo);
logger.debug(
'find hostGlobalSnapshot: ',
JSON.stringify(hostGlobalSnapshot),
);
logger.debug('find remoteSnapshot: ', JSON.stringify(remoteSnapshot));

if (!hostGlobalSnapshot || !remoteSnapshot) {
if ('version' in remote) {
// @ts-ignore
delete remote.version;
}
// @ts-ignore
remote.entry = remoteEntry;
remote.entryGlobalName = remoteInfo.globalName;
}
}
} catch (e) {
ctx.status(500);
return ctx.text(
`failed to fetch ${remoteInfo.name} data, error:\n ${e}`,
);
}
}

const dataFetchItem = dataFetchMap[dataFetchId];
logger.debug('fetch data from server, dataFetchItem: ', dataFetchItem);
if (dataFetchItem) {
const callFetchDataPromise = fetchData(dataFetchId, {
...params,
isDowngrade: !remoteInfo,
});
const wrappedPromise = wrapSetTimeout(
callFetchDataPromise,
20000,
dataFetchId,
);
if (wrappedPromise) {
const res = await wrappedPromise;
logger.log('fetch data from server, dataFetchItem res: ', res);
return ctx.json(res);
}
}

const remoteId = dataFetchId.split(SEPARATOR)[0];
const hostInstance = globalThis.__FEDERATION__.__INSTANCES__[0];
if (!hostInstance) {
throw new Error('host instance not found!');
}
const dataFetchFn = await loadDataFetchModule(hostInstance, remoteId);
const data = await dataFetchFn({ ...params, isDowngrade: !remoteInfo });
logger.log('fetch data from server, loadDataFetchModule res: ', data);
return ctx.json(data);
} catch (e) {
logger.error('server plugin data fetch error: ', e);
ctx.status(500);
return ctx.text(`failed to fetch ${remoteInfo.name} data, error:\n ${e}`);
}
};
import type { ServerPlugin } from '@modern-js/server-runtime';

const dataFetchServePlugin = (): ServerPlugin => ({
name: 'mf-data-fetch-server-plugin',
Expand All @@ -198,7 +10,7 @@ const dataFetchServePlugin = (): ServerPlugin => ({
middlewares.push({
name: 'module-federation-serve-manifest',
// @ts-ignore type error
handler: middleware,
handler: dataFetchMiddleWare,
});
});
},
Expand Down
26 changes: 0 additions & 26 deletions packages/modernjs/src/constant.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,2 @@
export const LOCALHOST = 'localhost';
export const PLUGIN_IDENTIFIER = '[ Modern.js Module Federation ]';
export const DATA_FETCH_QUERY = 'x-mf-data-fetch';
export const DATA_FETCH_ERROR_PREFIX =
'caught the following error during dataFetch: ';
export const LOAD_REMOTE_ERROR_PREFIX =
'caught the following error during loadRemote: ';
export const DOWNGRADE_KEY = '_mfSSRDowngrade';
export const DATA_FETCH_MAP_KEY = '__MF_DATA_FETCH_MAP__';
export const DATA_FETCH_FUNCTION = '_mfDataFetch';
export const FS_HREF = '_mfFSHref';
export const ERROR_TYPE = {
DATA_FETCH: 1,
LOAD_REMOTE: 2,
UNKNOWN: 3,
};
export const WRAP_DATA_FETCH_ID_IDENTIFIER = 'wrap_dfip_identifier';
export const enum MF_DATA_FETCH_TYPE {
FETCH_SERVER = 1,
FETCH_CLIENT = 2,
}

export const enum MF_DATA_FETCH_STATUS {
LOADED = 1,
LOADING = 2,
AWAIT = 0,
ERROR = 3,
}
28 changes: 0 additions & 28 deletions packages/modernjs/src/interfaces/global.ts

This file was deleted.

Loading