From 47dccec53f0ad249ae7d0f63a504b1e45bd4fa93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=88=AA?= Date: Wed, 29 Apr 2026 10:56:25 +0800 Subject: [PATCH] fix(webpack-bundler-runtime): preserve share scopes on repeated init Reuse an existing non-default share scope when repeated container init receives an incomplete host shareScopeMap instead of overwriting it with an empty scope or the wrong fallback. Add a regression test covering repeated init with empty and missing custom share-scope entries, and keep the related initContainerEntry array/non-array test suites green. --- .../initContainerEntry.array.spec.ts | 96 ++++++++++++++++++- .../src/initContainerEntry.ts | 84 ++++++++++------ 2 files changed, 149 insertions(+), 31 deletions(-) diff --git a/packages/webpack-bundler-runtime/__tests__/initContainerEntry.array.spec.ts b/packages/webpack-bundler-runtime/__tests__/initContainerEntry.array.spec.ts index 48ce5ea4385..5f5db1ffc8f 100644 --- a/packages/webpack-bundler-runtime/__tests__/initContainerEntry.array.spec.ts +++ b/packages/webpack-bundler-runtime/__tests__/initContainerEntry.array.spec.ts @@ -119,7 +119,7 @@ describe('initContainerEntry with array-based share scopes', () => { }); // Execute - const result = initContainerEntry(mockOptions); + initContainerEntry(mockOptions); // Verify expect( @@ -432,7 +432,7 @@ describe('initContainerEntry with array-based share scopes', () => { test('should behave differently for proxyInitializeSharing=false vs true with array shareScopeKey', () => { // Mock setup for shared=false (proxyInitializeSharing=false) - const mockIFunctionFalse = jest.fn().mockImplementation((key) => { + const mockIFunctionFalse = jest.fn().mockImplementation((_key) => { return Promise.resolve(true); }); @@ -492,7 +492,7 @@ describe('initContainerEntry with array-based share scopes', () => { test('should handle proxyInitializeSharing=false with array shareScopeKey', async () => { // Setup with shared: false (making proxyInitializeSharing false) - const mockIFunction = jest.fn().mockImplementation((key) => { + const mockIFunction = jest.fn().mockImplementation((_key) => { return Promise.resolve(true); }); @@ -1114,4 +1114,94 @@ describe('initContainerEntry with array-based share scopes', () => { expect(mockIFunction).toHaveBeenCalledWith('key3', ['test-scope']); expect(mockIFunction).toHaveBeenCalledTimes(3); }); + + test('should preserve non-default share scopes across repeated init with incomplete host shareScopeMap', () => { + const defaultScope = { + react: { + '18.2.0': { + scope: ['default'], + }, + }, + }; + const customScope = { + '@tanstack/react-query': { + '5.0.0': { + scope: ['custom'], + }, + }, + }; + const shareScopeMap: Record> = {}; + const federationInstance = createMockFederationInstance({ + shareScopeMap, + initShareScopeMap: jest.fn( + (scopeName: string, scope: Record) => { + shareScopeMap[scopeName] = scope; + }, + ), + }); + const webpackRequire = createMockWebpackRequire({ + I: jest.fn().mockReturnValue(Promise.resolve(true)), + federation: createMockFederation({ + instance: federationInstance, + initOptions: { + name: 'test-app', + shared: false, + }, + }), + }); + + initContainerEntry( + createMockOptions({ + webpackRequire, + shareScopeKey: ['default', 'custom'], + shareScope: defaultScope, + remoteEntryInitOptions: createMockRemoteEntryInitOptions({ + shareScopeKeys: ['default', 'custom'], + shareScopeMap: { + default: defaultScope, + custom: customScope, + }, + }), + }), + ); + + expect(federationInstance.shareScopeMap.custom).toBe(customScope); + + const emptyCustomScopeInitOptions = createMockRemoteEntryInitOptions({ + shareScopeKeys: ['default', 'custom'], + shareScopeMap: { + default: defaultScope, + custom: {}, + }, + }); + + initContainerEntry( + createMockOptions({ + webpackRequire, + shareScopeKey: ['default', 'custom'], + shareScope: defaultScope, + remoteEntryInitOptions: emptyCustomScopeInitOptions, + }), + ); + + expect(federationInstance.shareScopeMap.custom).toBe(customScope); + + const missingCustomScopeInitOptions = createMockRemoteEntryInitOptions({ + shareScopeKeys: ['default', 'custom'], + shareScopeMap: { + default: defaultScope, + }, + }); + + initContainerEntry( + createMockOptions({ + webpackRequire, + shareScopeKey: ['default', 'custom'], + shareScope: defaultScope, + remoteEntryInitOptions: missingCustomScopeInitOptions, + }), + ); + + expect(federationInstance.shareScopeMap.custom).toBe(customScope); + }); }); diff --git a/packages/webpack-bundler-runtime/src/initContainerEntry.ts b/packages/webpack-bundler-runtime/src/initContainerEntry.ts index f92d38df3d3..d012a866332 100644 --- a/packages/webpack-bundler-runtime/src/initContainerEntry.ts +++ b/packages/webpack-bundler-runtime/src/initContainerEntry.ts @@ -27,7 +27,43 @@ export function initContainerEntry( }); const hostShareScopeKeys = remoteEntryInitOptions?.shareScopeKeys; - const hostShareScopeMap = remoteEntryInitOptions?.shareScopeMap; + const hostShareScopeMap = remoteEntryInitOptions?.shareScopeMap || {}; + const existingShareScopeMap = federationInstance.shareScopeMap || {}; + + const hasOwnScope = (scopeMap: Record, key: string) => + Object.prototype.hasOwnProperty.call(scopeMap, key); + const isEmptyShareScope = (scope: Record | undefined) => + !scope || !Object.keys(scope).length; + const resolveShareScope = ( + key: string, + fallbackShareScope: Record, + options: { fallbackWhenEmpty?: boolean } = {}, + ) => { + const currentShareScope = hostShareScopeMap[key]; + + if ( + hasOwnScope(hostShareScopeMap, key) && + !isEmptyShareScope(currentShareScope) + ) { + return currentShareScope; + } + + if ( + hasOwnScope(existingShareScopeMap, key) && + (!hasOwnScope(hostShareScopeMap, key) || + isEmptyShareScope(currentShareScope)) + ) { + return existingShareScopeMap[key]; + } + + if (hasOwnScope(hostShareScopeMap, key)) { + return options.fallbackWhenEmpty && isEmptyShareScope(currentShareScope) + ? fallbackShareScope + : currentShareScope; + } + + return fallbackShareScope; + }; // host: 'default' remote: 'default' remote['default'] = hostShareScopeMap['default'] // host: ['default', 'scope1'] remote: 'default' remote['default'] = hostShareScopeMap['default']; remote['scope1'] = hostShareScopeMap['scop1'] @@ -36,43 +72,35 @@ export function initContainerEntry( if (!shareScopeKey || typeof shareScopeKey === 'string') { const key = shareScopeKey || 'default'; if (Array.isArray(hostShareScopeKeys)) { - // const sc = hostShareScopeMap![key]; - // if (!sc) { - // throw new Error('shareScopeKey is not exist in hostShareScopeMap'); - // } - // federationInstance.initShareScopeMap(key, sc, { - // hostShareScopeMap: remoteEntryInitOptions?.shareScopeMap || {}, - // }); - hostShareScopeKeys.forEach((hostKey) => { - if (!hostShareScopeMap![hostKey]) { - hostShareScopeMap![hostKey] = {}; + const sc = resolveShareScope(hostKey, {}); + if ( + !hasOwnScope(hostShareScopeMap, hostKey) || + (hasOwnScope(existingShareScopeMap, hostKey) && + isEmptyShareScope(hostShareScopeMap[hostKey])) + ) { + hostShareScopeMap[hostKey] = sc; } - const sc = hostShareScopeMap![hostKey]; federationInstance.initShareScopeMap(hostKey, sc, { - hostShareScopeMap: remoteEntryInitOptions?.shareScopeMap || {}, + hostShareScopeMap, }); }); } else { - federationInstance.initShareScopeMap(key, shareScope, { - hostShareScopeMap: remoteEntryInitOptions?.shareScopeMap || {}, + const sc = resolveShareScope(key, shareScope, { + fallbackWhenEmpty: true, + }); + federationInstance.initShareScopeMap(key, sc, { + hostShareScopeMap, }); } } else { shareScopeKey.forEach((key) => { - if (!hostShareScopeKeys || !hostShareScopeMap) { - federationInstance.initShareScopeMap(key, shareScope, { - hostShareScopeMap: remoteEntryInitOptions?.shareScopeMap || {}, - }); - return; - } - - if (!hostShareScopeMap[key]) { - hostShareScopeMap[key] = {}; - } - const sc = hostShareScopeMap[key]; + const sc = + !hostShareScopeKeys || !remoteEntryInitOptions?.shareScopeMap + ? resolveShareScope(key, shareScope, { fallbackWhenEmpty: true }) + : resolveShareScope(key, {}); federationInstance.initShareScopeMap(key, sc, { - hostShareScopeMap: remoteEntryInitOptions?.shareScopeMap || {}, + hostShareScopeMap, }); }); } @@ -89,7 +117,7 @@ export function initContainerEntry( return webpackRequire.I(shareScopeKey || 'default', initScope); } - var proxyInitializeSharing = Boolean( + const proxyInitializeSharing = Boolean( webpackRequire.federation.initOptions.shared, );