From 3508e4c19d11b5deeef8360d301807d7ab8a46ff Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 17:31:51 +0200 Subject: [PATCH 01/11] fix: mark relative bundle paths as same origin --- packages/core/src/modules/asyncRequire.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/modules/asyncRequire.ts b/packages/core/src/modules/asyncRequire.ts index 7180d7a9..85ba734f 100644 --- a/packages/core/src/modules/asyncRequire.ts +++ b/packages/core/src/modules/asyncRequire.ts @@ -22,6 +22,11 @@ function isUrl(url: string) { } function isSameOrigin(url: string, origin?: string) { + // if it's not a fully qualified url, we assume it's the same origin + if (!isUrl(url)) { + return true; + } + return origin && url.startsWith(origin); } From bc1fb382c661efbd1a148dcf952d65480db62db1 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 18:36:58 +0200 Subject: [PATCH 02/11] fix: load early shared as soon as possible in remote entry --- packages/core/src/runtime/remote-entry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/runtime/remote-entry.js b/packages/core/src/runtime/remote-entry.js index 96d058d5..c22de261 100644 --- a/packages/core/src/runtime/remote-entry.js +++ b/packages/core/src/runtime/remote-entry.js @@ -46,7 +46,7 @@ async function init(shared = {}, initScope = []) { initScope.push(initToken); instance.initShareScopeMap(shareScopeName, shared); - await Promise.all( + const initSharingPromise = Promise.all( instance.initializeSharing(shareScopeName, { strategy: shareStrategy, from: 'build', @@ -57,6 +57,8 @@ async function init(shared = {}, initScope = []) { // load early shared deps __EARLY_SHARED__.forEach(loadSharedToRegistry); + await initSharingPromise; + // setup HMR client after the initializing sync shared deps if (__DEV__ && !hmrInitialized) { const hmr = require('mf:remote-hmr'); From 35d04fea9ef6c538aae1cd88b913d3ee0146c339 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 18:49:01 +0200 Subject: [PATCH 03/11] chore: add comments --- packages/core/src/runtime/init-host.js | 8 +++++--- packages/core/src/runtime/remote-entry.js | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/runtime/init-host.js b/packages/core/src/runtime/init-host.js index 78b0a11f..57fd52c6 100644 --- a/packages/core/src/runtime/init-host.js +++ b/packages/core/src/runtime/init-host.js @@ -29,17 +29,19 @@ globalThis.__FEDERATION__.__NATIVE__[name].deps ??= { remotes: {}, }; -globalThis.__FEDERATION__.__NATIVE__[name].init = Promise.all([ +globalThis.__FEDERATION__.__NATIVE__[name].init = Promise.all( instance.initializeSharing(shareScopeName, { strategy: shareStrategy, from: 'build', initScope: [], - }), -]).then(() => + }) +).then(() => Promise.all([ ...Object.keys(usedShared).map(loadSharedToRegistry), ...__EARLY_REMOTES__.map(loadRemoteToRegistry), ]) ); +// IMPORTANT: load early shared deps immediately without +// waiting for the async part of initializeSharing to resolve __EARLY_SHARED__.forEach(loadSharedToRegistry); diff --git a/packages/core/src/runtime/remote-entry.js b/packages/core/src/runtime/remote-entry.js index c22de261..b783517b 100644 --- a/packages/core/src/runtime/remote-entry.js +++ b/packages/core/src/runtime/remote-entry.js @@ -54,7 +54,8 @@ async function init(shared = {}, initScope = []) { }) ); - // load early shared deps + // IMPORTANT: load early shared deps immediately without + // waiting for the async part of initializeSharing to resolve __EARLY_SHARED__.forEach(loadSharedToRegistry); await initSharingPromise; From 0bc2ec3e8a2f436228f1a73e79ac0b42fe3dbe73 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Fri, 27 Jun 2025 21:04:47 +0200 Subject: [PATCH 04/11] feat: add sync import statements to example --- apps/example-host/src/App.tsx | 15 +++------------ apps/example-nested-mini/src/nested-mini-info.tsx | 5 ++--- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/apps/example-host/src/App.tsx b/apps/example-host/src/App.tsx index 1bc2ba7e..6537470a 100644 --- a/apps/example-host/src/App.tsx +++ b/apps/example-host/src/App.tsx @@ -8,14 +8,13 @@ import { View, } from 'react-native'; +// @ts-ignore +import NestedMiniInfo from 'nestedMini/nestedMiniInfo'; import Card from './Card'; // @ts-ignore const Info = React.lazy(() => import('mini/info')); -// @ts-ignore -const NestedMiniInfo = React.lazy(() => import('nestedMini/nestedMiniInfo')); - function App(): React.JSX.Element { const [shouldLoadMini, setShouldLoadMini] = useState(false); const [lodashVersion, setLodashVersion] = useState(); @@ -76,15 +75,7 @@ function App(): React.JSX.Element { title="Nested Federated Remote" description="Dynamically loaded nested module" > - - - - } - > - - + diff --git a/apps/example-nested-mini/src/nested-mini-info.tsx b/apps/example-nested-mini/src/nested-mini-info.tsx index 311a42e5..1b6aa5c3 100644 --- a/apps/example-nested-mini/src/nested-mini-info.tsx +++ b/apps/example-nested-mini/src/nested-mini-info.tsx @@ -1,10 +1,9 @@ import { VERSION } from 'lodash'; +// @ts-ignore +import Info from 'mini/info'; import React from 'react'; import { View } from 'react-native'; -// @ts-ignore -const Info = React.lazy(() => import('mini/info')); - export default function NestedMiniInfo() { return ( From c928d8cda7f0cd1ccd6ed1a5c739029728af0e26 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 30 Jun 2025 13:19:30 +0200 Subject: [PATCH 05/11] fix: bundleId from bundlePath --- packages/core/src/modules/asyncRequire.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/core/src/modules/asyncRequire.ts b/packages/core/src/modules/asyncRequire.ts index 85ba734f..4b67a41d 100644 --- a/packages/core/src/modules/asyncRequire.ts +++ b/packages/core/src/modules/asyncRequire.ts @@ -10,17 +10,22 @@ function getPublicPath(url: string) { return url.split('/').slice(0, -1).join('/'); } -// get bundle id from the url path -// e.g. /a/b.bundle?platform=ios -> a/b -function getBundleId(urlPath: string) { - const [bundlePath] = urlPath.split('?'); - return bundlePath.slice(1).replace('.bundle', ''); -} - function isUrl(url: string) { return url.match(/^https?:\/\//); } +// get bundle id from the bundle path +// e.g. /a/b.bundle?platform=ios -> a/b +// e.g. http://host:8081/a/b.bundle -> a/b +function getBundleId(bundlePath: string) { + // remove the public path and slash + const path = bundlePath.split('/').slice(1).join('/'); + // remove the query params + const [cleanPath] = path.split('?'); + // remove the .bundle extension + return cleanPath.replace('.bundle', ''); +} + function isSameOrigin(url: string, origin?: string) { // if it's not a fully qualified url, we assume it's the same origin if (!isUrl(url)) { @@ -78,7 +83,7 @@ function buildLoadBundleAsyncWrapper() { // at this point the code in the bundle has been evaluated // but not yet executed through metroRequire - const bundleId = getBundleId(originalBundlePath); + const bundleId = getBundleId(bundlePath); const shared = scope.deps.shared[bundleId]; const remotes = scope.deps.remotes[bundleId]; From 8b4abce1648190806b4eeca2ada81fa4ce9b812e Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 30 Jun 2025 13:29:09 +0200 Subject: [PATCH 06/11] fix: use the original approach --- packages/core/src/modules/asyncRequire.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/core/src/modules/asyncRequire.ts b/packages/core/src/modules/asyncRequire.ts index 4b67a41d..7d8a389f 100644 --- a/packages/core/src/modules/asyncRequire.ts +++ b/packages/core/src/modules/asyncRequire.ts @@ -18,12 +18,19 @@ function isUrl(url: string) { // e.g. /a/b.bundle?platform=ios -> a/b // e.g. http://host:8081/a/b.bundle -> a/b function getBundleId(bundlePath: string) { - // remove the public path and slash - const path = bundlePath.split('/').slice(1).join('/'); + let path = bundlePath; + // remove the public path if it's an url + if (isUrl(path)) { + path = path.replace(getPublicPath(path), ''); + } + // remove the leading slash + if (path.startsWith('/')) { + path = path.slice(1); + } // remove the query params - const [cleanPath] = path.split('?'); - // remove the .bundle extension - return cleanPath.replace('.bundle', ''); + path = path.split('?')[0]; + // remove the bundle extension + return path.replace('.bundle', ''); } function isSameOrigin(url: string, origin?: string) { From 092d24007c08855bc76fc5c6e9fb9632b5188075 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Mon, 30 Jun 2025 15:42:43 +0200 Subject: [PATCH 07/11] fix: remote names in prod builds --- packages/core/src/plugin/index.ts | 6 ++++- packages/core/src/plugin/serializer.ts | 32 ++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/core/src/plugin/index.ts b/packages/core/src/plugin/index.ts index da6d7191..e5da65fd 100644 --- a/packages/core/src/plugin/index.ts +++ b/packages/core/src/plugin/index.ts @@ -9,6 +9,7 @@ import type { import { VirtualModuleManager } from '../utils'; import { createBabelTransformer } from './babel-transformer'; import { + isUsingMFBundleCommand, isUsingMFCommand, prepareTmpDir, replaceExtension, @@ -126,7 +127,10 @@ function augmentConfig( ...config, serializer: { ...config.serializer, - customSerializer: getModuleFederationSerializer(options), + customSerializer: getModuleFederationSerializer( + options, + isUsingMFBundleCommand() + ), getModulesRunBeforeMainModule: () => { return isHost ? [initHostPath] : []; }, diff --git a/packages/core/src/plugin/serializer.ts b/packages/core/src/plugin/serializer.ts index ea24b330..868cf2a7 100644 --- a/packages/core/src/plugin/serializer.ts +++ b/packages/core/src/plugin/serializer.ts @@ -9,7 +9,8 @@ import type { ModuleFederationConfigNormalized, Shared } from '../types'; type CustomSerializer = SerializerConfigT['customSerializer']; export function getModuleFederationSerializer( - mfConfig: ModuleFederationConfigNormalized + mfConfig: ModuleFederationConfigNormalized, + isUsingMFBundleCommand: boolean ): CustomSerializer { return async (entryPoint, preModules, graph, options) => { const syncRemoteModules = collectSyncRemoteModules(graph, mfConfig.remotes); @@ -29,7 +30,12 @@ export function getModuleFederationSerializer( return getBundleCode(entryPoint, preModules, graph, options); } - const bundlePath = getBundlePath(entryPoint, options.projectRoot); + const bundlePath = getBundlePath( + entryPoint, + options.projectRoot, + mfConfig.exposes, + isUsingMFBundleCommand + ); const finalPreModules = [ getSyncShared(syncSharedModules, bundlePath, mfConfig.name), getSyncRemotes(syncRemoteModules, bundlePath, mfConfig.name), @@ -156,10 +162,26 @@ function isProjectSource(entryPoint: string, projectRoot: string) { ); } -function getBundlePath(entryPoint: string, projectRoot: string) { +function getBundlePath( + entryPoint: string, + projectRoot: string, + exposes: ModuleFederationConfigNormalized['exposes'], + isUsingMFBundleCommand: boolean +) { const relativePath = path.relative(projectRoot, entryPoint); - const { dir, name } = path.parse(relativePath); - return path.format({ dir, name, ext: '' }); + const exposeKeyName = Object.keys(exposes).find((exposeKey) => + exposes[exposeKey].match(relativePath) + ); + + if (!isUsingMFBundleCommand || !exposeKeyName) { + const { dir, name } = path.parse(relativePath); + return path.format({ dir, name, ext: '' }); + } + + // Remove './' prefix + const exposeName = exposeKeyName.slice(2); + + return `exposed/${exposeName}`; } function getBundleCode( From 6a76bc08b6e6aa93a54ec3bf24c041fc53f2d6a7 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 30 Jun 2025 22:19:40 +0200 Subject: [PATCH 08/11] fix: return void promise from loadRemote --- packages/core/src/runtime/remote-module-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/runtime/remote-module-registry.js b/packages/core/src/runtime/remote-module-registry.js index 9955b25f..2617aa54 100644 --- a/packages/core/src/runtime/remote-module-registry.js +++ b/packages/core/src/runtime/remote-module-registry.js @@ -36,7 +36,7 @@ export async function loadRemoteToRegistry(id) { const remoteModule = await loadRemote(id); cloneModule(remoteModule, registry[id]); })(); - return loading[id]; + await loading[id]; } } From 72c25864c7d72c7c866df3369b1f2dcf3fb29520 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 1 Jul 2025 11:52:14 +0200 Subject: [PATCH 09/11] fix: public path --- packages/core/src/modules/asyncRequire.ts | 26 +++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/core/src/modules/asyncRequire.ts b/packages/core/src/modules/asyncRequire.ts index 7d8a389f..5445e344 100644 --- a/packages/core/src/modules/asyncRequire.ts +++ b/packages/core/src/modules/asyncRequire.ts @@ -6,8 +6,8 @@ function joinComponents(prefix: string, suffix: string) { // get the public path from the url // e.g. http://host:8081/a/b.bundle -> http://host:8081/a -function getPublicPath(url: string) { - return url.split('/').slice(0, -1).join('/'); +function getPublicPath(url?: string) { + return url?.split('/').slice(0, -1).join('/'); } function isUrl(url: string) { @@ -17,11 +17,11 @@ function isUrl(url: string) { // get bundle id from the bundle path // e.g. /a/b.bundle?platform=ios -> a/b // e.g. http://host:8081/a/b.bundle -> a/b -function getBundleId(bundlePath: string) { +function getBundleId(bundlePath: string, publicPath: string) { let path = bundlePath; // remove the public path if it's an url if (isUrl(path)) { - path = path.replace(getPublicPath(path), ''); + path = path.replace(publicPath, ''); } // remove the leading slash if (path.startsWith('/')) { @@ -33,13 +33,12 @@ function getBundleId(bundlePath: string) { return path.replace('.bundle', ''); } -function isSameOrigin(url: string, origin?: string) { +function isSameOrigin(url: string, originPublicPath?: string) { // if it's not a fully qualified url, we assume it's the same origin if (!isUrl(url)) { return true; } - - return origin && url.startsWith(origin); + return !!originPublicPath && url.startsWith(originPublicPath); } // prefix the bundle path with the public path @@ -59,7 +58,7 @@ function getBundlePath(bundlePath: string, bundleOrigin?: string) { if (!bundleOrigin) { return bundlePath; } - return joinComponents(getPublicPath(bundleOrigin), bundlePath); + return joinComponents(bundleOrigin, bundlePath); } function buildLoadBundleAsyncWrapper() { @@ -75,7 +74,11 @@ function buildLoadBundleAsyncWrapper() { return async (originalBundlePath: string) => { const scope = globalThis.__FEDERATION__.__NATIVE__[__METRO_GLOBAL_PREFIX__]; - const bundlePath = getBundlePath(originalBundlePath, scope.origin); + // entry is always in the root directory of assets associated with remote + // based on that, we extract the public path from the origin URL + // e.g. http://example.com/a/b/c/mf-manfiest.json -> http://example.com/a/b/c + const publicPath = getPublicPath(scope.origin); + const bundlePath = getBundlePath(originalBundlePath, publicPath); // ../../node_modules/ -> ..%2F..%2Fnode_modules/ so that it's not automatically sanitized const encodedBundlePath = bundlePath.replaceAll('../', '..%2F'); @@ -84,13 +87,14 @@ function buildLoadBundleAsyncWrapper() { // when the origin is not the same, it means we are loading a remote container // we can return early since dependencies are processed differently for entry bundles - if (!isSameOrigin(bundlePath, scope.origin)) { + if (!isSameOrigin(bundlePath, publicPath)) { return result; } // at this point the code in the bundle has been evaluated // but not yet executed through metroRequire - const bundleId = getBundleId(bundlePath); + // note: at this point, public path is always defined + const bundleId = getBundleId(bundlePath, publicPath!); const shared = scope.deps.shared[bundleId]; const remotes = scope.deps.remotes[bundleId]; From eff105fe0798f0d8cd7f400cf150a84735618b10 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 1 Jul 2025 12:42:31 +0200 Subject: [PATCH 10/11] refactor: be more explicit in serializer --- packages/core/src/plugin/serializer.ts | 32 +++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/core/src/plugin/serializer.ts b/packages/core/src/plugin/serializer.ts index 868cf2a7..7e5db261 100644 --- a/packages/core/src/plugin/serializer.ts +++ b/packages/core/src/plugin/serializer.ts @@ -5,6 +5,7 @@ import baseJSBundle from 'metro/src/DeltaBundler/Serializers/baseJSBundle'; import CountingSet from 'metro/src/lib/CountingSet'; import bundleToString from 'metro/src/lib/bundleToString'; import type { ModuleFederationConfigNormalized, Shared } from '../types'; +import { ConfigError } from '../utils/errors'; type CustomSerializer = SerializerConfigT['customSerializer']; @@ -26,6 +27,7 @@ export function getModuleFederationSerializer( } // skip non-project source like node_modules + // this includes handling of shared modules! if (!isProjectSource(entryPoint, options.projectRoot)) { return getBundleCode(entryPoint, preModules, graph, options); } @@ -169,19 +171,33 @@ function getBundlePath( isUsingMFBundleCommand: boolean ) { const relativePath = path.relative(projectRoot, entryPoint); - const exposeKeyName = Object.keys(exposes).find((exposeKey) => - exposes[exposeKey].match(relativePath) - ); - - if (!isUsingMFBundleCommand || !exposeKeyName) { + if (!isUsingMFBundleCommand) { const { dir, name } = path.parse(relativePath); return path.format({ dir, name, ext: '' }); } - // Remove './' prefix - const exposeName = exposeKeyName.slice(2); + // try to match with an exposed module first + const exposedMatchedKey = Object.keys(exposes).find((exposeKey) => + exposes[exposeKey].match(relativePath) + ); - return `exposed/${exposeName}`; + if (exposedMatchedKey) { + // handle as exposed module + let exposedName = exposedMatchedKey; + // Remove './' prefix + if (exposedName.startsWith('./')) { + exposedName = exposedName.slice(2); + } + return `exposed/${exposedName}`; + } + + throw new ConfigError( + `Unable to handle entry point: ${relativePath}. ` + + 'Expected to match an entrypoint with one of the exposed keys, but failed. ' + + 'This is most likely a configuration error. ' + + 'If you believe this is not a configuration issue, please report it as a bug. ' + + `Debug info: entryPoint="${entryPoint}", projectRoot="${projectRoot}", exposesKeys=[${Object.keys(exposes).join(', ')}]` + ); } function getBundleCode( From 4bae852e4d8ab6e6b53e45b8db23e0a832554804 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 1 Jul 2025 12:48:09 +0200 Subject: [PATCH 11/11] refactor: cleanup --- packages/core/src/plugin/serializer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/plugin/serializer.ts b/packages/core/src/plugin/serializer.ts index 7e5db261..7742bcd8 100644 --- a/packages/core/src/plugin/serializer.ts +++ b/packages/core/src/plugin/serializer.ts @@ -170,15 +170,15 @@ function getBundlePath( exposes: ModuleFederationConfigNormalized['exposes'], isUsingMFBundleCommand: boolean ) { - const relativePath = path.relative(projectRoot, entryPoint); + const relativeEntryPath = path.relative(projectRoot, entryPoint); if (!isUsingMFBundleCommand) { - const { dir, name } = path.parse(relativePath); + const { dir, name } = path.parse(relativeEntryPath); return path.format({ dir, name, ext: '' }); } // try to match with an exposed module first const exposedMatchedKey = Object.keys(exposes).find((exposeKey) => - exposes[exposeKey].match(relativePath) + exposes[exposeKey].match(relativeEntryPath) ); if (exposedMatchedKey) { @@ -192,7 +192,7 @@ function getBundlePath( } throw new ConfigError( - `Unable to handle entry point: ${relativePath}. ` + + `Unable to handle entry point: ${relativeEntryPath}. ` + 'Expected to match an entrypoint with one of the exposed keys, but failed. ' + 'This is most likely a configuration error. ' + 'If you believe this is not a configuration issue, please report it as a bug. ' +