Skip to content

Commit 7410cb8

Browse files
committed
feat(rsc-mf): auto-resolve remote public paths from federation state
1 parent 438da69 commit 7410cb8

2 files changed

Lines changed: 313 additions & 9 deletions

File tree

tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts

Lines changed: 207 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,223 @@
11
import type { ModuleFederationRuntimePlugin } from '@module-federation/modern-js-v3';
22

3-
const getRemotePublicPath = (entry: string) => {
3+
const TARGET_REMOTE_ALIAS = 'rscRemote';
4+
const SSR_BUNDLES_SEGMENT = '/bundles/';
5+
6+
interface FederationRemoteConfig {
7+
alias?: string;
8+
name?: string;
9+
entry?: string;
10+
}
11+
12+
interface FederationInstanceLike {
13+
options?: {
14+
remotes?: Record<string, string | FederationRemoteConfig> | unknown[];
15+
};
16+
}
17+
18+
interface FederationModuleInfoLike {
19+
alias?: string;
20+
name?: string;
21+
metaData?: {
22+
publicPath?: string;
23+
ssrPublicPath?: string;
24+
};
25+
}
26+
27+
interface FederationGlobalLike {
28+
__INSTANCES__?: FederationInstanceLike[];
29+
moduleInfo?: Record<string, FederationModuleInfoLike>;
30+
}
31+
32+
const getRemoteOriginPublicPath = (entry: string) => {
433
try {
534
return `${new URL(entry).origin}/`;
635
} catch {
736
return undefined;
837
}
938
};
1039

40+
const getNormalizedAbsolutePublicPath = (value: string) => {
41+
try {
42+
const publicPathUrl = new URL(value);
43+
publicPathUrl.search = '';
44+
publicPathUrl.hash = '';
45+
publicPathUrl.pathname = publicPathUrl.pathname.endsWith('/')
46+
? publicPathUrl.pathname
47+
: `${publicPathUrl.pathname}/`;
48+
return publicPathUrl.toString();
49+
} catch {
50+
return undefined;
51+
}
52+
};
53+
54+
const getPublicPathFromSsrPublicPath = (ssrPublicPath: string) => {
55+
const normalizedSsrPublicPath =
56+
getNormalizedAbsolutePublicPath(ssrPublicPath);
57+
if (!normalizedSsrPublicPath) {
58+
return undefined;
59+
}
60+
try {
61+
const ssrPublicPathUrl = new URL(normalizedSsrPublicPath);
62+
if (!ssrPublicPathUrl.pathname.endsWith(SSR_BUNDLES_SEGMENT)) {
63+
return undefined;
64+
}
65+
ssrPublicPathUrl.pathname = ssrPublicPathUrl.pathname.replace(
66+
/\/bundles\/$/,
67+
'/',
68+
);
69+
return ssrPublicPathUrl.toString();
70+
} catch {
71+
return undefined;
72+
}
73+
};
74+
75+
const getGlobalFederationState = () =>
76+
(globalThis as typeof globalThis & { __FEDERATION__?: FederationGlobalLike })
77+
.__FEDERATION__;
78+
79+
const getRemoteEntryFromRemoteConfig = (
80+
remoteConfig: string | FederationRemoteConfig | undefined,
81+
) => {
82+
if (!remoteConfig) {
83+
return undefined;
84+
}
85+
if (typeof remoteConfig === 'string') {
86+
const atIndex = remoteConfig.indexOf('@');
87+
return atIndex >= 0 ? remoteConfig.slice(atIndex + 1) : remoteConfig;
88+
}
89+
if (typeof remoteConfig.entry === 'string') {
90+
return remoteConfig.entry;
91+
}
92+
return undefined;
93+
};
94+
95+
const getRemoteEntryFromFederationInstances = (remoteAlias: string) => {
96+
const federationState = getGlobalFederationState();
97+
if (!federationState?.__INSTANCES__) {
98+
return undefined;
99+
}
100+
for (const instance of federationState.__INSTANCES__) {
101+
const remotes = instance?.options?.remotes;
102+
if (!remotes) {
103+
continue;
104+
}
105+
if (!Array.isArray(remotes) && typeof remotes === 'object') {
106+
const remoteConfig = remotes[remoteAlias] as
107+
| string
108+
| FederationRemoteConfig
109+
| undefined;
110+
const remoteEntry = getRemoteEntryFromRemoteConfig(remoteConfig);
111+
if (remoteEntry) {
112+
return remoteEntry;
113+
}
114+
for (const remotesEntry of Object.values(remotes)) {
115+
if (
116+
remotesEntry &&
117+
typeof remotesEntry === 'object' &&
118+
'alias' in remotesEntry &&
119+
remotesEntry.alias === remoteAlias
120+
) {
121+
return getRemoteEntryFromRemoteConfig(
122+
remotesEntry as FederationRemoteConfig,
123+
);
124+
}
125+
}
126+
continue;
127+
}
128+
if (!Array.isArray(remotes)) {
129+
continue;
130+
}
131+
for (const remoteConfig of remotes) {
132+
if (!remoteConfig || typeof remoteConfig !== 'object') {
133+
continue;
134+
}
135+
const alias = 'alias' in remoteConfig ? remoteConfig.alias : undefined;
136+
const name = 'name' in remoteConfig ? remoteConfig.name : undefined;
137+
if (alias !== remoteAlias && name !== remoteAlias) {
138+
continue;
139+
}
140+
const remoteEntry = getRemoteEntryFromRemoteConfig(
141+
remoteConfig as FederationRemoteConfig,
142+
);
143+
if (remoteEntry) {
144+
return remoteEntry;
145+
}
146+
}
147+
}
148+
return undefined;
149+
};
150+
151+
const getRemoteModuleInfoFromFederationState = (remoteAlias: string) => {
152+
const federationState = getGlobalFederationState();
153+
if (!federationState?.moduleInfo) {
154+
return undefined;
155+
}
156+
const directRemoteModuleInfo = federationState.moduleInfo[remoteAlias];
157+
if (directRemoteModuleInfo) {
158+
return directRemoteModuleInfo;
159+
}
160+
return Object.values(federationState.moduleInfo).find(
161+
moduleInfo =>
162+
moduleInfo?.alias === remoteAlias || moduleInfo?.name === remoteAlias,
163+
);
164+
};
165+
166+
const resolveRemotePublicPaths = ({
167+
remoteAlias,
168+
remoteEntry,
169+
}: {
170+
remoteAlias: string;
171+
remoteEntry?: string;
172+
}) => {
173+
const preferredRemoteEntry =
174+
remoteEntry || getRemoteEntryFromFederationInstances(remoteAlias);
175+
const remotePublicPath = preferredRemoteEntry
176+
? getRemoteOriginPublicPath(preferredRemoteEntry)
177+
: undefined;
178+
if (remotePublicPath) {
179+
return {
180+
remotePublicPath,
181+
remoteSsrPublicPath: `${remotePublicPath}bundles/`,
182+
};
183+
}
184+
185+
const remoteModuleInfo = getRemoteModuleInfoFromFederationState(remoteAlias);
186+
const remoteModuleInfoPublicPath = remoteModuleInfo?.metaData?.publicPath
187+
? getNormalizedAbsolutePublicPath(remoteModuleInfo.metaData.publicPath)
188+
: undefined;
189+
const remoteModuleInfoSsrPublicPath = remoteModuleInfo?.metaData
190+
?.ssrPublicPath
191+
? getNormalizedAbsolutePublicPath(remoteModuleInfo.metaData.ssrPublicPath)
192+
: undefined;
193+
const fallbackRemotePublicPath =
194+
remoteModuleInfoPublicPath ||
195+
(remoteModuleInfoSsrPublicPath
196+
? getPublicPathFromSsrPublicPath(remoteModuleInfoSsrPublicPath)
197+
: undefined);
198+
const fallbackRemoteSsrPublicPath =
199+
remoteModuleInfoSsrPublicPath ||
200+
(fallbackRemotePublicPath
201+
? `${fallbackRemotePublicPath}bundles/`
202+
: undefined);
203+
return {
204+
remotePublicPath: fallbackRemotePublicPath,
205+
remoteSsrPublicPath: fallbackRemoteSsrPublicPath,
206+
};
207+
};
208+
11209
const forceRemotePublicPath = (): ModuleFederationRuntimePlugin => ({
12210
name: 'rsc-mf-force-remote-public-path',
13211
loadRemoteSnapshot(args: any) {
14212
const { remoteInfo, remoteSnapshot } = args;
15-
if (remoteInfo?.alias !== 'rscRemote' || !remoteSnapshot) {
16-
return args;
17-
}
18-
19-
const entry = remoteInfo?.entry;
20-
if (!entry || typeof entry !== 'string') {
213+
if (remoteInfo?.alias !== TARGET_REMOTE_ALIAS || !remoteSnapshot) {
21214
return args;
22215
}
23-
const remotePublicPath = getRemotePublicPath(entry);
216+
const { remotePublicPath, remoteSsrPublicPath } = resolveRemotePublicPaths({
217+
remoteAlias: remoteInfo.alias,
218+
remoteEntry:
219+
typeof remoteInfo?.entry === 'string' ? remoteInfo.entry : undefined,
220+
});
24221
if (!remotePublicPath) {
25222
return args;
26223
}
@@ -32,7 +229,8 @@ const forceRemotePublicPath = (): ModuleFederationRuntimePlugin => ({
32229
remoteSnapshot.metaData.publicPath = remotePublicPath;
33230
}
34231
if (remoteSnapshot.metaData && 'ssrPublicPath' in remoteSnapshot.metaData) {
35-
remoteSnapshot.metaData.ssrPublicPath = `${remotePublicPath}bundles/`;
232+
remoteSnapshot.metaData.ssrPublicPath =
233+
remoteSsrPublicPath || `${remotePublicPath}bundles/`;
36234
}
37235

38236
return args;

tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
import forceRemotePublicPath from '../host/runtime/forceRemotePublicPath';
22

33
describe('host forceRemotePublicPath runtime plugin', () => {
4+
const originalFederation = (
5+
globalThis as typeof globalThis & { __FEDERATION__?: unknown }
6+
).__FEDERATION__;
7+
8+
afterEach(() => {
9+
if (typeof originalFederation === 'undefined') {
10+
delete (globalThis as typeof globalThis & { __FEDERATION__?: unknown })
11+
.__FEDERATION__;
12+
return;
13+
}
14+
15+
(
16+
globalThis as typeof globalThis & {
17+
__FEDERATION__?: unknown;
18+
}
19+
).__FEDERATION__ = originalFederation;
20+
});
21+
422
it('keeps plugin name stable', () => {
523
const plugin = forceRemotePublicPath();
624
expect(plugin.name).toBe('rsc-mf-force-remote-public-path');
@@ -55,6 +73,94 @@ describe('host forceRemotePublicPath runtime plugin', () => {
5573
);
5674
});
5775

76+
it('resolves remote public paths from __FEDERATION__ remotes when entry is missing', () => {
77+
(
78+
globalThis as typeof globalThis & {
79+
__FEDERATION__?: unknown;
80+
}
81+
).__FEDERATION__ = {
82+
__INSTANCES__: [
83+
{
84+
options: {
85+
remotes: {
86+
rscRemote:
87+
'rscRemote@https://federation-runtime.example.com/static/mf-manifest.json',
88+
},
89+
},
90+
},
91+
],
92+
} as any;
93+
const plugin = forceRemotePublicPath();
94+
const args = {
95+
remoteInfo: {
96+
alias: 'rscRemote',
97+
},
98+
remoteSnapshot: {
99+
publicPath: 'http://stale.example.com/',
100+
metaData: {
101+
publicPath: 'http://stale.example.com/',
102+
ssrPublicPath: 'http://stale.example.com/bundles/',
103+
},
104+
},
105+
};
106+
107+
plugin.loadRemoteSnapshot?.(args as any);
108+
109+
expect(args.remoteSnapshot.publicPath).toBe(
110+
'https://federation-runtime.example.com/',
111+
);
112+
expect(args.remoteSnapshot.metaData.publicPath).toBe(
113+
'https://federation-runtime.example.com/',
114+
);
115+
expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe(
116+
'https://federation-runtime.example.com/bundles/',
117+
);
118+
});
119+
120+
it('resolves remote public paths from __FEDERATION__ module metadata fallback', () => {
121+
(
122+
globalThis as typeof globalThis & {
123+
__FEDERATION__?: unknown;
124+
}
125+
).__FEDERATION__ = {
126+
moduleInfo: {
127+
rscRemote: {
128+
metaData: {
129+
publicPath:
130+
'https://federation-metadata.example.com/assets?cache=1#hash',
131+
ssrPublicPath:
132+
'https://federation-metadata.example.com/assets/bundles?cache=1#hash',
133+
},
134+
},
135+
},
136+
} as any;
137+
const plugin = forceRemotePublicPath();
138+
const args = {
139+
remoteInfo: {
140+
alias: 'rscRemote',
141+
},
142+
remoteSnapshot: {
143+
publicPath: 'http://stale.example.com/',
144+
metaData: {
145+
publicPath: 'http://stale.example.com/',
146+
ssrPublicPath: 'http://stale.example.com/bundles/',
147+
},
148+
},
149+
};
150+
151+
plugin.loadRemoteSnapshot?.(args as any);
152+
153+
expect(args.remoteSnapshot.publicPath).toBe(
154+
'https://federation-metadata.example.com/assets/',
155+
);
156+
expect(args.remoteSnapshot.metaData.publicPath).toBe(
157+
'https://federation-metadata.example.com/assets/',
158+
);
159+
expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe(
160+
'https://federation-metadata.example.com/assets/bundles/',
161+
);
162+
});
163+
58164
it('does not mutate when entry is not a valid URL', () => {
59165
const plugin = forceRemotePublicPath();
60166
const args = {

0 commit comments

Comments
 (0)