From cc3fe4c2e4e1a72538c19513831ea41fa17c2630 Mon Sep 17 00:00:00 2001 From: "dm.choi" Date: Sun, 2 Aug 2026 01:06:14 +0900 Subject: [PATCH 1/2] fix(runtime-core): sync shared options after dynamic registration --- .../fix-runtime-register-shared-options.md | 5 + .../runtime-core/__tests__/instance.spec.ts | 109 ++++++++++++++++++ packages/runtime-core/src/core.ts | 3 +- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-runtime-register-shared-options.md diff --git a/.changeset/fix-runtime-register-shared-options.md b/.changeset/fix-runtime-register-shared-options.md new file mode 100644 index 00000000000..21108a5a36b --- /dev/null +++ b/.changeset/fix-runtime-register-shared-options.md @@ -0,0 +1,5 @@ +--- +"@module-federation/runtime-core": patch +--- + +Keep runtime-registered shared options available to `loadShare()`. diff --git a/packages/runtime-core/__tests__/instance.spec.ts b/packages/runtime-core/__tests__/instance.spec.ts index 9ccaf33bf56..ff64cfff49a 100644 --- a/packages/runtime-core/__tests__/instance.spec.ts +++ b/packages/runtime-core/__tests__/instance.spec.ts @@ -66,4 +66,113 @@ describe('ModuleFederation', () => { expect(module.initing).toBe(false); expect((module as any).initPromise).toBeUndefined(); }); + + it('registers dynamic shared modules for loadShare', async () => { + const GM = new ModuleFederation({ + name: '@federation/dynamic-shared', + remotes: [], + shared: {}, + }); + const sharedFactory = () => ({ name: 'dynamic-shared' }); + + GM.registerShared({ + 'dynamic-shared': { + version: '1.0.0', + get: () => Promise.resolve(sharedFactory), + }, + }); + + expect(GM.options.shared['dynamic-shared']).toHaveLength(1); + expect(GM.shareScopeMap.default['dynamic-shared']['1.0.0']).toBeDefined(); + + const loadedShared = await GM.loadShare<{ name: string }>('dynamic-shared'); + + expect(loadedShared).toBe(sharedFactory); + expect(loadedShared?.()).toEqual({ name: 'dynamic-shared' }); + }); + + it('preserves existing and configured shared options when registering dynamically', () => { + const existingFactory = () => ({ name: 'existing-shared' }); + const GM = new ModuleFederation({ + name: '@federation/dynamic-shared-config', + remotes: [], + shared: { + 'existing-shared': { + version: '1.0.0', + scope: ['default', 'legacy'], + lib: existingFactory, + shareConfig: { + singleton: true, + requiredVersion: '^1.0.0', + eager: true, + strictVersion: true, + }, + }, + }, + }); + + GM.registerShared({ + 'configured-shared': { + version: '2.0.0', + scope: ['default', 'custom'], + get: () => Promise.resolve(() => ({ name: 'configured-shared' })), + shareConfig: { + singleton: true, + requiredVersion: '^2.0.0', + eager: true, + strictVersion: true, + }, + }, + }); + + expect(GM.options.shared['existing-shared'][0]).toMatchObject({ + version: '1.0.0', + scope: ['default', 'legacy'], + lib: existingFactory, + shareConfig: { + singleton: true, + requiredVersion: '^1.0.0', + eager: true, + strictVersion: true, + }, + }); + expect(GM.options.shared['configured-shared'][0]).toMatchObject({ + version: '2.0.0', + scope: ['default', 'custom'], + shareConfig: { + singleton: true, + requiredVersion: '^2.0.0', + eager: true, + strictVersion: true, + }, + }); + expect(GM.shareScopeMap.custom['configured-shared']['2.0.0']).toBeDefined(); + }); + + it('preserves array shared options and re-registration semantics', () => { + const GM = new ModuleFederation({ + name: '@federation/dynamic-array-shared', + remotes: [], + shared: {}, + }); + const shared = { + 'array-shared': [ + { + version: '1.0.0', + get: () => Promise.resolve(() => ({ version: '1.0.0' })), + }, + { + version: '2.0.0', + get: () => Promise.resolve(() => ({ version: '2.0.0' })), + }, + ], + } as const; + + GM.registerShared(shared); + const registeredArrayShared = GM.options.shared['array-shared']; + GM.registerShared(shared); + + expect(registeredArrayShared).toHaveLength(2); + expect(GM.options.shared['array-shared']).toBe(registeredArrayShared); + }); }); diff --git a/packages/runtime-core/src/core.ts b/packages/runtime-core/src/core.ts index 7887eec0832..cc9910a8f8f 100644 --- a/packages/runtime-core/src/core.ts +++ b/packages/runtime-core/src/core.ts @@ -472,9 +472,10 @@ export class ModuleFederation { } registerShared(shared: UserOptions['shared']) { - this.sharedHandler.registerShared(this.options, { + const { allShareInfos } = this.sharedHandler.registerShared(this.options, { ...this.options, shared, }); + this.options.shared = allShareInfos; } } From 42292570cc58b6256244af7528f76c4eedb988f1 Mon Sep 17 00:00:00 2001 From: "dm.choi" Date: Tue, 18 Aug 2026 23:31:48 +0900 Subject: [PATCH 2/2] fix(runtime-core): keep runtime shared registration out of user options --- .../fix-runtime-register-shared-options.md | 2 +- .../runtime-core/__tests__/instance.spec.ts | 42 +++++++++-------- packages/runtime-core/src/core.ts | 3 +- packages/runtime-core/src/shared/index.ts | 18 +++++--- packages/runtime-core/src/utils/share.ts | 46 +++++++++++-------- 5 files changed, 64 insertions(+), 47 deletions(-) diff --git a/.changeset/fix-runtime-register-shared-options.md b/.changeset/fix-runtime-register-shared-options.md index 21108a5a36b..f436e28ec8c 100644 --- a/.changeset/fix-runtime-register-shared-options.md +++ b/.changeset/fix-runtime-register-shared-options.md @@ -2,4 +2,4 @@ "@module-federation/runtime-core": patch --- -Keep runtime-registered shared options available to `loadShare()`. +Keep runtime-registered shared options available to `loadShare()` and `initializeSharing()` without mutating user configuration. diff --git a/packages/runtime-core/__tests__/instance.spec.ts b/packages/runtime-core/__tests__/instance.spec.ts index ff64cfff49a..e373d438c21 100644 --- a/packages/runtime-core/__tests__/instance.spec.ts +++ b/packages/runtime-core/__tests__/instance.spec.ts @@ -67,7 +67,7 @@ describe('ModuleFederation', () => { expect((module as any).initPromise).toBeUndefined(); }); - it('registers dynamic shared modules for loadShare', async () => { + it('registers dynamic shared modules without mutating options', async () => { const GM = new ModuleFederation({ name: '@federation/dynamic-shared', remotes: [], @@ -82,16 +82,27 @@ describe('ModuleFederation', () => { }, }); - expect(GM.options.shared['dynamic-shared']).toHaveLength(1); + expect(GM.options.shared).toEqual({}); expect(GM.shareScopeMap.default['dynamic-shared']['1.0.0']).toBeDefined(); + GM.initOptions({ + name: '@federation/dynamic-shared', + remotes: [], + }); + + expect(GM.options.shared).toEqual({}); + + GM.initShareScopeMap('default', {}); + GM.initializeSharing(); + + expect(GM.shareScopeMap.default['dynamic-shared']['1.0.0']).toBeDefined(); const loadedShared = await GM.loadShare<{ name: string }>('dynamic-shared'); expect(loadedShared).toBe(sharedFactory); expect(loadedShared?.()).toEqual({ name: 'dynamic-shared' }); }); - it('preserves existing and configured shared options when registering dynamically', () => { + it('does not mutate configured shared options when registering dynamically', () => { const existingFactory = () => ({ name: 'existing-shared' }); const GM = new ModuleFederation({ name: '@federation/dynamic-shared-config', @@ -112,10 +123,10 @@ describe('ModuleFederation', () => { }); GM.registerShared({ - 'configured-shared': { + 'existing-shared': { version: '2.0.0', scope: ['default', 'custom'], - get: () => Promise.resolve(() => ({ name: 'configured-shared' })), + get: () => Promise.resolve(() => ({ name: 'existing-shared' })), shareConfig: { singleton: true, requiredVersion: '^2.0.0', @@ -125,6 +136,7 @@ describe('ModuleFederation', () => { }, }); + expect(GM.options.shared['existing-shared']).toHaveLength(1); expect(GM.options.shared['existing-shared'][0]).toMatchObject({ version: '1.0.0', scope: ['default', 'legacy'], @@ -136,17 +148,7 @@ describe('ModuleFederation', () => { strictVersion: true, }, }); - expect(GM.options.shared['configured-shared'][0]).toMatchObject({ - version: '2.0.0', - scope: ['default', 'custom'], - shareConfig: { - singleton: true, - requiredVersion: '^2.0.0', - eager: true, - strictVersion: true, - }, - }); - expect(GM.shareScopeMap.custom['configured-shared']['2.0.0']).toBeDefined(); + expect(GM.shareScopeMap.custom['existing-shared']['2.0.0']).toBeDefined(); }); it('preserves array shared options and re-registration semantics', () => { @@ -169,10 +171,12 @@ describe('ModuleFederation', () => { } as const; GM.registerShared(shared); - const registeredArrayShared = GM.options.shared['array-shared']; + expect(GM.options.shared).toEqual({}); + GM.initializeSharing(); GM.registerShared(shared); - expect(registeredArrayShared).toHaveLength(2); - expect(GM.options.shared['array-shared']).toBe(registeredArrayShared); + expect(GM.shareScopeMap.default['array-shared']['1.0.0']).toBeDefined(); + expect(GM.shareScopeMap.default['array-shared']['2.0.0']).toBeDefined(); + expect(GM.options.shared).toEqual({}); }); }); diff --git a/packages/runtime-core/src/core.ts b/packages/runtime-core/src/core.ts index cc9910a8f8f..7887eec0832 100644 --- a/packages/runtime-core/src/core.ts +++ b/packages/runtime-core/src/core.ts @@ -472,10 +472,9 @@ export class ModuleFederation { } registerShared(shared: UserOptions['shared']) { - const { allShareInfos } = this.sharedHandler.registerShared(this.options, { + this.sharedHandler.registerShared(this.options, { ...this.options, shared, }); - this.options.shared = allShareInfos; } } diff --git a/packages/runtime-core/src/shared/index.ts b/packages/runtime-core/src/shared/index.ts index 81a01847e41..1bccbd52317 100644 --- a/packages/runtime-core/src/shared/index.ts +++ b/packages/runtime-core/src/shared/index.ts @@ -27,6 +27,7 @@ import { } from '../utils/hooks'; import { formatShareConfigs, + mergeShareInfos, getRegisteredShare, getTargetSharedOptions, getGlobalShareScope, @@ -48,6 +49,7 @@ import { createRemoteEntryInitOptions } from '../module'; export class SharedHandler { host: ModuleFederation; shareScopeMap: ShareScopeMap; + private shareInfos: ShareInfos; hooks = new PluginSystem({ beforeRegisterShare: new SyncWaterfallHook<{ pkgName: string; @@ -114,6 +116,7 @@ export class SharedHandler { constructor(host: ModuleFederation) { this.host = host; this.shareScopeMap = {}; + this.shareInfos = {}; this.initTokens = {}; this._setGlobalShareScopeMap(host.options); } @@ -134,7 +137,7 @@ export class SharedHandler { pkgName, shareInfo, selectedShared, - shared: this.host.options.shared, + shared: this.shareInfos, shareScopeMap: this.shareScopeMap, lifecycle, origin: this.host, @@ -161,7 +164,7 @@ export class SharedHandler { this.hooks.lifecycle.errorLoadShare.emit({ pkgName, shareInfo, - shared: this.host.options.shared, + shared: this.shareInfos, shareScopeMap: this.shareScopeMap, lifecycle, origin: this.host, @@ -179,6 +182,7 @@ export class SharedHandler { globalOptions, userOptions, ); + this.shareInfos = mergeShareInfos(this.shareInfos, allShareInfos); const sharedKeys = Object.keys(newShareInfos); sharedKeys.forEach((sharedKey) => { @@ -227,7 +231,7 @@ export class SharedHandler { const shareOptions = getTargetSharedOptions({ pkgName, extraOptions, - shareInfos: host.options.shared, + shareInfos: this.shareInfos, }); let shareOptionsRes: Shared | undefined = shareOptions; @@ -247,7 +251,7 @@ export class SharedHandler { const loadShareRes = await this.hooks.lifecycle.beforeLoadShare.emit({ pkgName, shareInfo: shareOptions, - shared: host.options.shared, + shared: this.shareInfos, origin: host, }); @@ -477,8 +481,8 @@ export class SharedHandler { } } }; - Object.keys(host.options.shared).forEach((shareName) => { - const sharedArr = host.options.shared[shareName]; + Object.keys(this.shareInfos).forEach((shareName) => { + const sharedArr = this.shareInfos[shareName]; sharedArr.forEach((shared) => { if (shared.scope.includes(shareScopeName)) { register(shareName, shared); @@ -516,7 +520,7 @@ export class SharedHandler { const shareOptions = getTargetSharedOptions({ pkgName, extraOptions, - shareInfos: host.options.shared, + shareInfos: this.shareInfos, }); try { diff --git a/packages/runtime-core/src/utils/share.ts b/packages/runtime-core/src/utils/share.ts index 98bf0864e34..9444efe1de4 100644 --- a/packages/runtime-core/src/utils/share.ts +++ b/packages/runtime-core/src/utils/share.ts @@ -77,6 +77,33 @@ function formatShare( }; } +export function mergeShareInfos( + previous: ShareInfos, + next: ShareInfos, +): ShareInfos { + const merged = Object.keys(previous).reduce((res, shareKey) => { + res[shareKey] = [...previous[shareKey]]; + return res; + }, {} as ShareInfos); + + Object.keys(next).forEach((shareKey) => { + if (!merged[shareKey]) { + merged[shareKey] = [...next[shareKey]]; + return; + } + + const versions = new Set(merged[shareKey].map((shared) => shared.version)); + next[shareKey].forEach((shared) => { + if (!versions.has(shared.version)) { + versions.add(shared.version); + merged[shareKey].push(shared); + } + }); + }); + + return merged; +} + export function formatShareConfigs( prevOptions: Options, newOptions: UserOptions, @@ -95,24 +122,7 @@ export function formatShareConfigs( return res; }, {} as ShareInfos); - const allShareInfos = { - ...prevOptions.shared, - }; - - Object.keys(newShareInfos).forEach((shareKey) => { - if (!allShareInfos[shareKey]) { - allShareInfos[shareKey] = newShareInfos[shareKey]; - } else { - newShareInfos[shareKey].forEach((newUserSharedOptions) => { - const isSameVersion = allShareInfos[shareKey].find( - (sharedVal) => sharedVal.version === newUserSharedOptions.version, - ); - if (!isSameVersion) { - allShareInfos[shareKey].push(newUserSharedOptions); - } - }); - } - }); + const allShareInfos = mergeShareInfos(prevOptions.shared, newShareInfos); return { allShareInfos, newShareInfos }; }