Skip to content

Commit 76615bd

Browse files
committed
fix(rsc-mf): proxy unresolved host actions to remote
1 parent b2a1d3b commit 76615bd

5 files changed

Lines changed: 130 additions & 112 deletions

File tree

packages/server/core/src/adapters/node/plugins/resource.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'path';
2+
import vm from 'vm';
23
import { fileReader } from '@modern-js/runtime-utils/fileReader';
34
import type { Monitors, ServerRoute } from '@modern-js/types';
45
import {
@@ -20,6 +21,68 @@ import type {
2021
} from '../../../types';
2122
import { uniqueKeyByRoute } from '../../../utils';
2223

24+
const ASYNC_NODE_STARTUP_CALL =
25+
'var __webpack_exports__ = __webpack_require__.x();';
26+
27+
const isPromiseLike = (value: unknown): value is Promise<unknown> =>
28+
Boolean(value) &&
29+
(typeof value === 'object' || typeof value === 'function') &&
30+
'then' in (value as Promise<unknown>) &&
31+
typeof (value as Promise<unknown>).then === 'function';
32+
33+
const loadPatchedAsyncNodeBundle = async (
34+
filepath: string,
35+
): Promise<unknown | undefined> => {
36+
const bundleCode = await fs.readFile(filepath, 'utf-8');
37+
38+
if (
39+
!bundleCode.includes(ASYNC_NODE_STARTUP_CALL) ||
40+
!bundleCode.includes('__webpack_require__.mfAsyncStartup')
41+
) {
42+
return undefined;
43+
}
44+
45+
const patchedCode = bundleCode.replace(
46+
ASYNC_NODE_STARTUP_CALL,
47+
'var __webpack_exports__ = __webpack_require__.x({}, []);',
48+
);
49+
50+
const localModule: { exports: unknown } = { exports: {} };
51+
const wrapped = `(function(exports, require, module, __filename, __dirname){${patchedCode}\n})`;
52+
const runBundle = vm.runInThisContext(wrapped, { filename: filepath }) as (
53+
exports: unknown,
54+
require: NodeJS.Require,
55+
module: { exports: unknown },
56+
__filename: string,
57+
__dirname: string,
58+
) => void;
59+
60+
runBundle(
61+
localModule.exports,
62+
require,
63+
localModule,
64+
filepath,
65+
path.dirname(filepath),
66+
);
67+
68+
if (isPromiseLike(localModule.exports)) {
69+
return await localModule.exports;
70+
}
71+
72+
return localModule.exports;
73+
};
74+
75+
const loadBundleModule = async (filepath: string): Promise<unknown> => {
76+
try {
77+
return require(filepath);
78+
} catch (err: any) {
79+
if (err?.code === 'ERR_REQUIRE_ESM') {
80+
return compatibleRequire(filepath, false);
81+
}
82+
throw err;
83+
}
84+
};
85+
2386
export async function getHtmlTemplates(pwd: string, routes: ServerRoute[]) {
2487
// Only process routes with entryName, which are HTML template routes.
2588
// Public static file routes don't have entryName and shouldn't be processed here.
@@ -59,14 +122,26 @@ export function injectTemplates(
59122
};
60123
}
61124

62-
const loadBundle = async (filepath: string, monitors?: Monitors) => {
125+
const loadBundle = async (
126+
filepath: string,
127+
monitors?: Monitors,
128+
): Promise<any> => {
63129
if (!(await fs.pathExists(filepath))) {
64130
return undefined;
65131
}
66132

67133
try {
68-
const module = await compatibleRequire(filepath, false);
69-
return module;
134+
const module = await loadBundleModule(filepath);
135+
if (!isPromiseLike(module)) {
136+
return module;
137+
}
138+
139+
const resolvedModule = await module;
140+
if (resolvedModule) {
141+
return resolvedModule;
142+
}
143+
144+
return loadPatchedAsyncNodeBundle(filepath);
70145
} catch (e) {
71146
if (monitors) {
72147
monitors.error(

tests/integration/rsc-mf/host/module-federation.config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,6 @@ export default createModuleFederationConfig({
107107
rscRemote: `rscRemote@http://127.0.0.1:${REMOTE_PORT}/static/mf-manifest.json`,
108108
},
109109
shared: sharedByScope as any,
110-
runtimePlugins: [
111-
path.resolve(__dirname, './runtime/forceRemotePublicPath.ts'),
112-
],
113110
dts: false,
114111
experiments: {
115112
asyncStartup: true,

tests/integration/rsc-mf/host/server/modern.server.ts

Lines changed: 52 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import fs from 'node:fs/promises';
2-
import path from 'node:path';
31
import {
42
type MiddlewareHandler,
53
defineServerConfig,
64
} from '@modern-js/server-runtime';
5+
import { loadServerAction } from 'react-server-dom-rspack/server.node';
76

87
const shouldProxyRemoteAsset = (pathname: string) => {
98
if (pathname.startsWith('/static/js/async/')) {
@@ -27,60 +26,55 @@ const REMOTE_COUNTER_SOURCE_MODULE = './src/components/RemoteClientCounter.tsx';
2726
const createRemoteNestedMixedAliasChunk = () =>
2827
`\n;(globalThis["chunk_rscHost"] = globalThis["chunk_rscHost"] || []).push([["__federation_expose_RemoteNestedMixed_alias"],{"${REMOTE_COUNTER_ALIAS_MODULE}":function(module,__unused,__webpack_require__){module.exports=__webpack_require__("${REMOTE_COUNTER_SOURCE_MODULE}");}}]);`;
2928

30-
const REMOTE_ACTION_ID_TO_PROXY_EXPORT = {
31-
'606c30f35d74d843171a8a71358eda595991e4ee16270e9f052af3faef57a19999':
32-
'proxyIncrementRemoteCount',
33-
'40e41a2ee9d9de373b364dcf2a0201701057c8502037bf9ef2cd26bb2a1259dabd':
34-
'proxyRemoteActionEcho',
35-
'408da81ddb8214f8cb98a83552cb70c4d17b27b6fd36d972cac89e7030a4874fd4':
36-
'proxyNestedRemoteAction',
37-
'40fd3fd0c01e4d21630b7f6f902c1ddc49d3b05418ca1da003c4fdc6c6272e0bf2':
38-
'proxyDefaultRemoteAction',
39-
} as const;
40-
const ACTION_EXPOSE_PATHS = new Set([
41-
'/static/js/async/__federation_expose_actions.js',
42-
'/static/js/async/__federation_expose_nestedActions.js',
43-
'/static/js/async/__federation_expose_defaultAction.js',
44-
]);
45-
46-
let cachedProxyActionIds:
47-
| Partial<
48-
Record<
49-
(typeof REMOTE_ACTION_ID_TO_PROXY_EXPORT)[keyof typeof REMOTE_ACTION_ID_TO_PROXY_EXPORT],
50-
string
51-
>
52-
>
53-
| undefined;
54-
55-
const getProxyActionIds = async () => {
56-
if (cachedProxyActionIds) {
57-
return cachedProxyActionIds;
29+
const proxyRemoteRscAction: MiddlewareHandler = async (c, next) => {
30+
const request = c.req.raw;
31+
if (request.method !== 'POST') {
32+
await next();
33+
return;
5834
}
5935

60-
const serverBundlePath = path.resolve(
61-
__dirname,
62-
'../dist/bundles/server-component-root.js',
63-
);
64-
const serverBundleCode = await fs.readFile(serverBundlePath, 'utf-8');
65-
66-
const actionIdMap: Partial<
67-
Record<
68-
(typeof REMOTE_ACTION_ID_TO_PROXY_EXPORT)[keyof typeof REMOTE_ACTION_ID_TO_PROXY_EXPORT],
69-
string
70-
>
71-
> = {};
72-
Object.values(REMOTE_ACTION_ID_TO_PROXY_EXPORT).forEach(exportName => {
73-
const pattern = new RegExp(
74-
`registerServerReference\\\\(${exportName},\\\\s*"([^"]+)"`,
75-
);
76-
const match = serverBundleCode.match(pattern);
77-
if (match?.[1]) {
78-
actionIdMap[exportName] = match[1];
36+
const actionId = request.headers.get('x-rsc-action');
37+
if (!actionId) {
38+
await next();
39+
return;
40+
}
41+
42+
try {
43+
const localAction = loadServerAction(actionId);
44+
if (typeof localAction === 'function') {
45+
await next();
46+
return;
7947
}
80-
});
81-
cachedProxyActionIds = actionIdMap;
48+
} catch (_error) {
49+
// Unknown host action ID: forward to remote server below.
50+
}
51+
52+
const remotePort = process.env.RSC_MF_REMOTE_PORT;
53+
if (!remotePort) {
54+
await next();
55+
return;
56+
}
8257

83-
return actionIdMap;
58+
const reqUrl = new URL(c.req.url);
59+
const remoteUrl = `http://127.0.0.1:${remotePort}${reqUrl.pathname}${reqUrl.search}`;
60+
const headers = new Headers(request.headers);
61+
headers.delete('host');
62+
const body = await request.arrayBuffer();
63+
const upstream = await fetch(remoteUrl, {
64+
method: 'POST',
65+
headers,
66+
body,
67+
}).catch(() => undefined);
68+
69+
if (!upstream) {
70+
await next();
71+
return;
72+
}
73+
74+
c.res = new Response(await upstream.arrayBuffer(), {
75+
status: upstream.status,
76+
headers: upstream.headers,
77+
});
8478
};
8579

8680
const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => {
@@ -106,28 +100,11 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => {
106100
return;
107101
}
108102

109-
const shouldPatchActionChunk = ACTION_EXPOSE_PATHS.has(pathname);
110103
const shouldPatchNestedMixed =
111104
pathname === '/static/js/async/__federation_expose_RemoteNestedMixed.js';
112-
if (shouldPatchActionChunk || shouldPatchNestedMixed) {
105+
if (shouldPatchNestedMixed) {
113106
let chunkText = await upstream.text();
114-
115-
if (shouldPatchActionChunk) {
116-
const proxyActionIds = await getProxyActionIds().catch(() => ({}));
117-
Object.entries(REMOTE_ACTION_ID_TO_PROXY_EXPORT).forEach(
118-
([remoteActionId, proxyExport]) => {
119-
const proxyActionId = proxyActionIds[proxyExport];
120-
if (!proxyActionId) {
121-
return;
122-
}
123-
chunkText = chunkText.split(remoteActionId).join(proxyActionId);
124-
},
125-
);
126-
}
127-
128-
if (shouldPatchNestedMixed) {
129-
chunkText = `${chunkText}${createRemoteNestedMixedAliasChunk()}`;
130-
}
107+
chunkText = `${chunkText}${createRemoteNestedMixedAliasChunk()}`;
131108

132109
c.res = new Response(chunkText, {
133110
status: upstream.status,
@@ -146,6 +123,10 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => {
146123

147124
export default defineServerConfig({
148125
middlewares: [
126+
{
127+
name: 'proxy-remote-rsc-action',
128+
handler: proxyRemoteRscAction,
129+
},
149130
{
150131
name: 'proxy-remote-federation-asset',
151132
handler: proxyRemoteFederationAsset,

tests/integration/rsc-mf/host/src/server-component-root/App.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,8 @@ import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly';
88
import getServerOnlyDefaultInfo from 'rscRemote/remoteServerOnlyDefault';
99
import styles from './App.module.less';
1010
import HostRemoteActionRunner from './HostRemoteActionRunner';
11-
import {
12-
proxyDefaultRemoteAction,
13-
proxyIncrementRemoteCount,
14-
proxyNestedRemoteAction,
15-
proxyRemoteActionEcho,
16-
} from './remoteActionProxy';
1711

1812
const App = () => {
19-
// Ensure host action manifest includes proxy action IDs.
20-
void proxyIncrementRemoteCount;
21-
void proxyRemoteActionEcho;
22-
void proxyNestedRemoteAction;
23-
void proxyDefaultRemoteAction;
2413
const remoteServerOnlyInfo = getServerOnlyInfo();
2514
const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo();
2615
const remoteMetaLabel = getRemoteMetaLabel();

tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)