From 20dfdaaad42d35ac71a8752fbfd6e40fd005d332 Mon Sep 17 00:00:00 2001 From: "dm.choi" Date: Tue, 21 Jul 2026 00:09:22 +0900 Subject: [PATCH] fix(runtime): propagate share scopes to nested remotes --- .changeset/fix-nested-remote-share-scopes.md | 5 ++ .../runtime-core/__tests__/instance.spec.ts | 84 +++++++++++++++++++ packages/runtime-core/src/shared/index.ts | 1 + 3 files changed, 90 insertions(+) create mode 100644 .changeset/fix-nested-remote-share-scopes.md diff --git a/.changeset/fix-nested-remote-share-scopes.md b/.changeset/fix-nested-remote-share-scopes.md new file mode 100644 index 00000000000..0fa1a3d0de9 --- /dev/null +++ b/.changeset/fix-nested-remote-share-scopes.md @@ -0,0 +1,5 @@ +--- +"@module-federation/runtime-core": patch +--- + +Preserve inherited share scopes when initializing nested remotes so loaded singleton dependencies can be reused. diff --git a/packages/runtime-core/__tests__/instance.spec.ts b/packages/runtime-core/__tests__/instance.spec.ts index 9ccaf33bf56..cbec85fe09f 100644 --- a/packages/runtime-core/__tests__/instance.spec.ts +++ b/packages/runtime-core/__tests__/instance.spec.ts @@ -1,6 +1,7 @@ import { describe, it, expect, rs } from '@rstest/core'; import { ModuleFederation, Module } from '../src/index'; import type { ModuleFederationRuntimePlugin } from '../src/type/plugin'; +import type { ShareScopeMap } from '../src/type'; describe('ModuleFederation', () => { it('should initialize with provided arguments', () => { @@ -66,4 +67,87 @@ describe('ModuleFederation', () => { expect(module.initing).toBe(false); expect((module as any).initPromise).toBeUndefined(); }); + + it('preserves inherited share scopes for nested remotes', async () => { + const react19Scope = { + react: { + '19.0.0': { + version: '19.0.0', + from: 'host-a', + }, + }, + } as ShareScopeMap[string]; + const react18Scope = { + react: { + '18.0.0': { + version: '18.0.0', + from: 'host-a', + }, + }, + } as ShareScopeMap[string]; + const hostA = new ModuleFederation({ + name: '@federation/host-a', + remotes: [], + }); + const containerB = new ModuleFederation({ + name: '@federation/container-b', + remotes: [], + }); + + hostA.initShareScopeMap('react18-share-scope', react18Scope); + hostA.initShareScopeMap('react19-share-scope', react19Scope); + + const remoteB = new Module({ + remoteInfo: { + name: '@federation/remote-b', + entry: 'http://localhost:3002/remoteEntry.js', + type: 'global', + entryGlobalName: '__remote_b__', + shareScope: 'react18-share-scope', + }, + host: hostA, + }); + remoteB.remoteEntryExports = { + init: rs.fn((shareScope, _initScope, remoteEntryInitOptions) => { + containerB.initShareScopeMap('react18-share-scope', shareScope, { + hostShareScopeMap: remoteEntryInitOptions.shareScopeMap, + }); + }), + get: rs.fn(), + } as any; + + await remoteB.init(); + + const initRemoteC = rs.fn( + (shareScope, _initScope, remoteEntryInitOptions) => { + expect(shareScope).toBe(react19Scope); + expect( + remoteEntryInitOptions.shareScopeMap['react18-share-scope'], + ).toBe(react18Scope); + expect( + remoteEntryInitOptions.shareScopeMap['react19-share-scope'], + ).toBe(react19Scope); + }, + ); + const remoteC = new Module({ + remoteInfo: { + name: '@federation/remote-c', + entry: 'http://localhost:3003/remoteEntry.js', + type: 'global', + entryGlobalName: '__remote_c__', + shareScope: 'react19-share-scope', + }, + host: containerB, + }); + remoteC.remoteEntryExports = { + init: initRemoteC, + get: rs.fn(), + } as any; + + await remoteC.init(); + + expect(initRemoteC).toHaveBeenCalledTimes(1); + expect(containerB.shareScopeMap['react18-share-scope']).toBe(react18Scope); + expect(containerB.shareScopeMap['react19-share-scope']).toBe(react19Scope); + }); }); diff --git a/packages/runtime-core/src/shared/index.ts b/packages/runtime-core/src/shared/index.ts index 81a01847e41..c23d22feed5 100644 --- a/packages/runtime-core/src/shared/index.ts +++ b/packages/runtime-core/src/shared/index.ts @@ -650,6 +650,7 @@ export class SharedHandler { extraOptions: { hostShareScopeMap?: ShareScopeMap } = {}, ): void { const { host } = this; + Object.assign(this.shareScopeMap, extraOptions.hostShareScopeMap); this.shareScopeMap[scopeName] = shareScope; this.hooks.lifecycle.initContainerShareScopeMap.emit({ shareScope,