Skip to content

Commit 254692d

Browse files
authored
fix: loading sync remotes (#86)
1 parent 89e9761 commit 254692d

File tree

8 files changed

+101
-43
lines changed

8 files changed

+101
-43
lines changed

apps/example-host/src/App.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ import {
88
View,
99
} from 'react-native';
1010

11+
// @ts-ignore
12+
import NestedMiniInfo from 'nestedMini/nestedMiniInfo';
1113
import Card from './Card';
1214

1315
// @ts-ignore
1416
const Info = React.lazy(() => import('mini/info'));
1517

16-
// @ts-ignore
17-
const NestedMiniInfo = React.lazy(() => import('nestedMini/nestedMiniInfo'));
18-
1918
function App(): React.JSX.Element {
2019
const [shouldLoadMini, setShouldLoadMini] = useState(false);
2120
const [lodashVersion, setLodashVersion] = useState<string>();
@@ -76,15 +75,7 @@ function App(): React.JSX.Element {
7675
title="Nested Federated Remote"
7776
description="Dynamically loaded nested module"
7877
>
79-
<React.Suspense
80-
fallback={
81-
<View>
82-
<ActivityIndicator size="large" color="#8b5cf6" />
83-
</View>
84-
}
85-
>
86-
<NestedMiniInfo />
87-
</React.Suspense>
78+
<NestedMiniInfo />
8879
</Card>
8980
</View>
9081
</View>

apps/example-nested-mini/src/nested-mini-info.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { VERSION } from 'lodash';
2+
// @ts-ignore
3+
import Info from 'mini/info';
24
import React from 'react';
35
import { View } from 'react-native';
46

5-
// @ts-ignore
6-
const Info = React.lazy(() => import('mini/info'));
7-
87
export default function NestedMiniInfo() {
98
return (
109
<View testID="nested-mini">

packages/core/src/modules/asyncRequire.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,39 @@ function joinComponents(prefix: string, suffix: string) {
66

77
// get the public path from the url
88
// e.g. http://host:8081/a/b.bundle -> http://host:8081/a
9-
function getPublicPath(url: string) {
10-
return url.split('/').slice(0, -1).join('/');
11-
}
12-
13-
// get bundle id from the url path
14-
// e.g. /a/b.bundle?platform=ios -> a/b
15-
function getBundleId(urlPath: string) {
16-
const [bundlePath] = urlPath.split('?');
17-
return bundlePath.slice(1).replace('.bundle', '');
9+
function getPublicPath(url?: string) {
10+
return url?.split('/').slice(0, -1).join('/');
1811
}
1912

2013
function isUrl(url: string) {
2114
return url.match(/^https?:\/\//);
2215
}
2316

24-
function isSameOrigin(url: string, origin?: string) {
25-
return origin && url.startsWith(origin);
17+
// get bundle id from the bundle path
18+
// e.g. /a/b.bundle?platform=ios -> a/b
19+
// e.g. http://host:8081/a/b.bundle -> a/b
20+
function getBundleId(bundlePath: string, publicPath: string) {
21+
let path = bundlePath;
22+
// remove the public path if it's an url
23+
if (isUrl(path)) {
24+
path = path.replace(publicPath, '');
25+
}
26+
// remove the leading slash
27+
if (path.startsWith('/')) {
28+
path = path.slice(1);
29+
}
30+
// remove the query params
31+
path = path.split('?')[0];
32+
// remove the bundle extension
33+
return path.replace('.bundle', '');
34+
}
35+
36+
function isSameOrigin(url: string, originPublicPath?: string) {
37+
// if it's not a fully qualified url, we assume it's the same origin
38+
if (!isUrl(url)) {
39+
return true;
40+
}
41+
return !!originPublicPath && url.startsWith(originPublicPath);
2642
}
2743

2844
// prefix the bundle path with the public path
@@ -42,7 +58,7 @@ function getBundlePath(bundlePath: string, bundleOrigin?: string) {
4258
if (!bundleOrigin) {
4359
return bundlePath;
4460
}
45-
return joinComponents(getPublicPath(bundleOrigin), bundlePath);
61+
return joinComponents(bundleOrigin, bundlePath);
4662
}
4763

4864
function buildLoadBundleAsyncWrapper() {
@@ -58,7 +74,11 @@ function buildLoadBundleAsyncWrapper() {
5874
return async (originalBundlePath: string) => {
5975
const scope = globalThis.__FEDERATION__.__NATIVE__[__METRO_GLOBAL_PREFIX__];
6076

61-
const bundlePath = getBundlePath(originalBundlePath, scope.origin);
77+
// entry is always in the root directory of assets associated with remote
78+
// based on that, we extract the public path from the origin URL
79+
// e.g. http://example.com/a/b/c/mf-manfiest.json -> http://example.com/a/b/c
80+
const publicPath = getPublicPath(scope.origin);
81+
const bundlePath = getBundlePath(originalBundlePath, publicPath);
6282

6383
// ../../node_modules/ -> ..%2F..%2Fnode_modules/ so that it's not automatically sanitized
6484
const encodedBundlePath = bundlePath.replaceAll('../', '..%2F');
@@ -67,13 +87,14 @@ function buildLoadBundleAsyncWrapper() {
6787

6888
// when the origin is not the same, it means we are loading a remote container
6989
// we can return early since dependencies are processed differently for entry bundles
70-
if (!isSameOrigin(bundlePath, scope.origin)) {
90+
if (!isSameOrigin(bundlePath, publicPath)) {
7191
return result;
7292
}
7393

7494
// at this point the code in the bundle has been evaluated
7595
// but not yet executed through metroRequire
76-
const bundleId = getBundleId(originalBundlePath);
96+
// note: at this point, public path is always defined
97+
const bundleId = getBundleId(bundlePath, publicPath!);
7798
const shared = scope.deps.shared[bundleId];
7899
const remotes = scope.deps.remotes[bundleId];
79100

packages/core/src/plugin/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
import { VirtualModuleManager } from '../utils';
1010
import { createBabelTransformer } from './babel-transformer';
1111
import {
12+
isUsingMFBundleCommand,
1213
isUsingMFCommand,
1314
prepareTmpDir,
1415
replaceExtension,
@@ -126,7 +127,10 @@ function augmentConfig(
126127
...config,
127128
serializer: {
128129
...config.serializer,
129-
customSerializer: getModuleFederationSerializer(options),
130+
customSerializer: getModuleFederationSerializer(
131+
options,
132+
isUsingMFBundleCommand()
133+
),
130134
getModulesRunBeforeMainModule: () => {
131135
return isHost ? [initHostPath] : [];
132136
},

packages/core/src/plugin/serializer.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import baseJSBundle from 'metro/src/DeltaBundler/Serializers/baseJSBundle';
55
import CountingSet from 'metro/src/lib/CountingSet';
66
import bundleToString from 'metro/src/lib/bundleToString';
77
import type { ModuleFederationConfigNormalized, Shared } from '../types';
8+
import { ConfigError } from '../utils/errors';
89

910
type CustomSerializer = SerializerConfigT['customSerializer'];
1011

1112
export function getModuleFederationSerializer(
12-
mfConfig: ModuleFederationConfigNormalized
13+
mfConfig: ModuleFederationConfigNormalized,
14+
isUsingMFBundleCommand: boolean
1315
): CustomSerializer {
1416
return async (entryPoint, preModules, graph, options) => {
1517
const syncRemoteModules = collectSyncRemoteModules(graph, mfConfig.remotes);
@@ -25,11 +27,17 @@ export function getModuleFederationSerializer(
2527
}
2628

2729
// skip non-project source like node_modules
30+
// this includes handling of shared modules!
2831
if (!isProjectSource(entryPoint, options.projectRoot)) {
2932
return getBundleCode(entryPoint, preModules, graph, options);
3033
}
3134

32-
const bundlePath = getBundlePath(entryPoint, options.projectRoot);
35+
const bundlePath = getBundlePath(
36+
entryPoint,
37+
options.projectRoot,
38+
mfConfig.exposes,
39+
isUsingMFBundleCommand
40+
);
3341
const finalPreModules = [
3442
getSyncShared(syncSharedModules, bundlePath, mfConfig.name),
3543
getSyncRemotes(syncRemoteModules, bundlePath, mfConfig.name),
@@ -156,10 +164,40 @@ function isProjectSource(entryPoint: string, projectRoot: string) {
156164
);
157165
}
158166

159-
function getBundlePath(entryPoint: string, projectRoot: string) {
160-
const relativePath = path.relative(projectRoot, entryPoint);
161-
const { dir, name } = path.parse(relativePath);
162-
return path.format({ dir, name, ext: '' });
167+
function getBundlePath(
168+
entryPoint: string,
169+
projectRoot: string,
170+
exposes: ModuleFederationConfigNormalized['exposes'],
171+
isUsingMFBundleCommand: boolean
172+
) {
173+
const relativeEntryPath = path.relative(projectRoot, entryPoint);
174+
if (!isUsingMFBundleCommand) {
175+
const { dir, name } = path.parse(relativeEntryPath);
176+
return path.format({ dir, name, ext: '' });
177+
}
178+
179+
// try to match with an exposed module first
180+
const exposedMatchedKey = Object.keys(exposes).find((exposeKey) =>
181+
exposes[exposeKey].match(relativeEntryPath)
182+
);
183+
184+
if (exposedMatchedKey) {
185+
// handle as exposed module
186+
let exposedName = exposedMatchedKey;
187+
// Remove './' prefix
188+
if (exposedName.startsWith('./')) {
189+
exposedName = exposedName.slice(2);
190+
}
191+
return `exposed/${exposedName}`;
192+
}
193+
194+
throw new ConfigError(
195+
`Unable to handle entry point: ${relativeEntryPath}. ` +
196+
'Expected to match an entrypoint with one of the exposed keys, but failed. ' +
197+
'This is most likely a configuration error. ' +
198+
'If you believe this is not a configuration issue, please report it as a bug. ' +
199+
`Debug info: entryPoint="${entryPoint}", projectRoot="${projectRoot}", exposesKeys=[${Object.keys(exposes).join(', ')}]`
200+
);
163201
}
164202

165203
function getBundleCode(

packages/core/src/runtime/init-host.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,19 @@ globalThis.__FEDERATION__.__NATIVE__[name].deps ??= {
2929
remotes: {},
3030
};
3131

32-
globalThis.__FEDERATION__.__NATIVE__[name].init = Promise.all([
32+
globalThis.__FEDERATION__.__NATIVE__[name].init = Promise.all(
3333
instance.initializeSharing(shareScopeName, {
3434
strategy: shareStrategy,
3535
from: 'build',
3636
initScope: [],
37-
}),
38-
]).then(() =>
37+
})
38+
).then(() =>
3939
Promise.all([
4040
...Object.keys(usedShared).map(loadSharedToRegistry),
4141
...__EARLY_REMOTES__.map(loadRemoteToRegistry),
4242
])
4343
);
4444

45+
// IMPORTANT: load early shared deps immediately without
46+
// waiting for the async part of initializeSharing to resolve
4547
__EARLY_SHARED__.forEach(loadSharedToRegistry);

packages/core/src/runtime/remote-entry.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,20 @@ async function init(shared = {}, initScope = []) {
4646
initScope.push(initToken);
4747
instance.initShareScopeMap(shareScopeName, shared);
4848

49-
await Promise.all(
49+
const initSharingPromise = Promise.all(
5050
instance.initializeSharing(shareScopeName, {
5151
strategy: shareStrategy,
5252
from: 'build',
5353
initScope,
5454
})
5555
);
5656

57-
// load early shared deps
57+
// IMPORTANT: load early shared deps immediately without
58+
// waiting for the async part of initializeSharing to resolve
5859
__EARLY_SHARED__.forEach(loadSharedToRegistry);
5960

61+
await initSharingPromise;
62+
6063
// setup HMR client after the initializing sync shared deps
6164
if (__DEV__ && !hmrInitialized) {
6265
const hmr = require('mf:remote-hmr');

packages/core/src/runtime/remote-module-registry.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export async function loadRemoteToRegistry(id) {
3636
const remoteModule = await loadRemote(id);
3737
cloneModule(remoteModule, registry[id]);
3838
})();
39-
return loading[id];
39+
await loading[id];
4040
}
4141
}
4242

0 commit comments

Comments
 (0)