Skip to content

Commit 260278b

Browse files
committed
feat(rsc-mf): add remote expose-asset fallback middleware
1 parent 18a8193 commit 260278b

3 files changed

Lines changed: 468 additions & 1 deletion

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import {
2+
type MiddlewareHandler,
3+
defineServerConfig,
4+
} from '@modern-js/server-runtime';
5+
6+
const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback';
7+
const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json';
8+
const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i;
9+
10+
interface RemoteManifestAssetEntry {
11+
assets?: {
12+
js?: {
13+
sync?: string[];
14+
async?: string[];
15+
};
16+
css?: {
17+
sync?: string[];
18+
async?: string[];
19+
};
20+
};
21+
}
22+
23+
interface RemoteManifestShape {
24+
shared?: RemoteManifestAssetEntry[];
25+
exposes?: RemoteManifestAssetEntry[];
26+
}
27+
28+
const isExposeAssetRequestPath = (pathname: string) =>
29+
pathname.includes('__federation_expose_') &&
30+
(pathname.endsWith('.js') || pathname.endsWith('.css'));
31+
32+
const toCanonicalChunkName = (filePath: string) =>
33+
filePath
34+
.replace(/\/+$/, '')
35+
.split('/')
36+
.pop()
37+
?.replace(/\.(js|css)$/i, '')
38+
.replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, '');
39+
40+
const toNormalizedManifestAssetPath = (assetPath: string) => {
41+
try {
42+
return new URL(assetPath).pathname.replace(/^\/+/, '');
43+
} catch {
44+
return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0];
45+
}
46+
};
47+
48+
const collectManifestAssetPaths = (manifest: RemoteManifestShape) => {
49+
const entries = [...(manifest.shared || []), ...(manifest.exposes || [])];
50+
const assetPaths = new Set<string>();
51+
for (const entry of entries) {
52+
const jsSyncAssets = entry.assets?.js?.sync || [];
53+
const jsAsyncAssets = entry.assets?.js?.async || [];
54+
const cssSyncAssets = entry.assets?.css?.sync || [];
55+
const cssAsyncAssets = entry.assets?.css?.async || [];
56+
for (const assetPath of [
57+
...jsSyncAssets,
58+
...jsAsyncAssets,
59+
...cssSyncAssets,
60+
...cssAsyncAssets,
61+
]) {
62+
assetPaths.add(assetPath);
63+
}
64+
}
65+
return [...assetPaths];
66+
};
67+
68+
const resolveManifestFallbackAssetPath = (
69+
pathname: string,
70+
manifest: RemoteManifestShape,
71+
) => {
72+
if (!isExposeAssetRequestPath(pathname)) {
73+
return undefined;
74+
}
75+
76+
const canonicalRequestedChunkName = toCanonicalChunkName(pathname);
77+
if (!canonicalRequestedChunkName) {
78+
return undefined;
79+
}
80+
81+
const requestedAssetDirectory = pathname.includes('/static/css/async/')
82+
? 'static/css/async/'
83+
: 'static/js/async/';
84+
const manifestAssets = collectManifestAssetPaths(manifest);
85+
return manifestAssets.find(assetPath => {
86+
const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath);
87+
if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) {
88+
return false;
89+
}
90+
return (
91+
toCanonicalChunkName(normalizedAssetPath) === canonicalRequestedChunkName
92+
);
93+
});
94+
};
95+
96+
const createManifestFallbackAssetUrl = ({
97+
remoteOrigin,
98+
fallbackAssetPath,
99+
requestSearch,
100+
}: {
101+
remoteOrigin: string;
102+
fallbackAssetPath: string;
103+
requestSearch: string;
104+
}) => {
105+
let fallbackAssetUrl: URL;
106+
try {
107+
fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOrigin}/`);
108+
} catch {
109+
return undefined;
110+
}
111+
112+
if (fallbackAssetUrl.origin !== remoteOrigin) {
113+
return undefined;
114+
}
115+
116+
if (!requestSearch) {
117+
return fallbackAssetUrl.toString();
118+
}
119+
120+
const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search);
121+
const requestSearchParams = new URLSearchParams(requestSearch);
122+
for (const [key, value] of requestSearchParams.entries()) {
123+
mergedSearchParams.set(key, value);
124+
}
125+
const mergedSearch = mergedSearchParams.toString();
126+
fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : '';
127+
128+
return fallbackAssetUrl.toString();
129+
};
130+
131+
const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async (
132+
c,
133+
next,
134+
) => {
135+
const reqUrl = new URL(c.req.url);
136+
const pathname = reqUrl.pathname;
137+
if (!isExposeAssetRequestPath(pathname)) {
138+
await next();
139+
return;
140+
}
141+
142+
const requestHeaders = c.req.headers;
143+
const isInternalFallbackFetch =
144+
requestHeaders?.get?.(INTERNAL_FALLBACK_HEADER) === '1';
145+
if (isInternalFallbackFetch) {
146+
await next();
147+
return;
148+
}
149+
150+
const remoteOrigin = reqUrl.origin;
151+
const manifestResponse = await fetch(
152+
`${remoteOrigin}${REMOTE_MANIFEST_PATH}`,
153+
{
154+
headers: {
155+
[INTERNAL_FALLBACK_HEADER]: '1',
156+
},
157+
},
158+
).catch((): undefined => undefined);
159+
if (!manifestResponse?.ok) {
160+
await next();
161+
return;
162+
}
163+
164+
const manifest = (await manifestResponse
165+
.json()
166+
.catch((): undefined => undefined)) as RemoteManifestShape | undefined;
167+
if (!manifest) {
168+
await next();
169+
return;
170+
}
171+
172+
const fallbackAssetPath = resolveManifestFallbackAssetPath(
173+
pathname,
174+
manifest,
175+
);
176+
if (!fallbackAssetPath) {
177+
await next();
178+
return;
179+
}
180+
181+
const fallbackAssetUrl = createManifestFallbackAssetUrl({
182+
remoteOrigin,
183+
fallbackAssetPath,
184+
requestSearch: reqUrl.search,
185+
});
186+
if (!fallbackAssetUrl || fallbackAssetUrl === reqUrl.toString()) {
187+
await next();
188+
return;
189+
}
190+
191+
const fallbackAssetResponse = await fetch(fallbackAssetUrl, {
192+
headers: {
193+
[INTERNAL_FALLBACK_HEADER]: '1',
194+
},
195+
}).catch((): undefined => undefined);
196+
if (!fallbackAssetResponse?.ok) {
197+
await next();
198+
return;
199+
}
200+
201+
c.res = new Response(await fallbackAssetResponse.arrayBuffer(), {
202+
status: fallbackAssetResponse.status,
203+
headers: fallbackAssetResponse.headers,
204+
});
205+
};
206+
207+
export default defineServerConfig({
208+
middlewares: [
209+
{
210+
name: 'recover-remote-federation-expose-asset',
211+
handler: recoverRemoteExposeAssetMiddleware,
212+
order: 'pre',
213+
before: ['server-static'],
214+
},
215+
],
216+
});

0 commit comments

Comments
 (0)