diff --git a/.changeset/ten-hounds-kick.md b/.changeset/ten-hounds-kick.md new file mode 100644 index 00000000000..1a724edca27 --- /dev/null +++ b/.changeset/ten-hounds-kick.md @@ -0,0 +1,5 @@ +--- +'@module-federation/runtime': patch +--- + +Improve runtime instance resolution by selecting instances from global federation state (including remote-aware matching) and keeping `getInstance` stable after module resets. diff --git a/packages/runtime/__tests__/api.spec.ts b/packages/runtime/__tests__/api.spec.ts index 8214cd6c945..dd493dc90a0 100644 --- a/packages/runtime/__tests__/api.spec.ts +++ b/packages/runtime/__tests__/api.spec.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from 'vitest'; -import { init } from '../src'; +import { describe, it, expect, vi } from 'vitest'; +import { init, getInstance, createInstance } from '../src'; // eslint-disable-next-line max-lines-per-function describe('api', () => { @@ -83,6 +83,25 @@ describe('api', () => { }); expect(FM3).not.toBe(FM4); }); + it('getInstance resolves the global instance after reset', async () => { + const FM = init({ + name: '@federation/get-instance-global', + remotes: [], + }); + + vi.resetModules(); + const { getInstance: getInstanceAfterReset } = await import('../src'); + expect(getInstanceAfterReset('@federation/get-instance-global')).toBe(FM); + }); + + it('createInstance updates getInstance', () => { + const FM = createInstance({ + name: '@federation/create-instance', + remotes: [], + }); + + expect(getInstance('@federation/create-instance')).toBe(FM); + }); it('alias check', () => { // 校验 alias 是否等于 remote.name 和 remote.alias 的前缀,如果是则报错 diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 42db1dfec4d..73dcab30c7a 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,13 +1,19 @@ import { ModuleFederation, type UserOptions, + CurrentGlobal, getGlobalFederationConstructor, + matchRemoteWithNameAndExpose, setGlobalFederationInstance, assert, setGlobalFederationConstructor, } from '@module-federation/runtime-core'; -import { runtimeDescMap, RUNTIME_009 } from '@module-federation/error-codes'; -import { getGlobalFederationInstance } from './utils'; +import { + runtimeDescMap, + getShortErrorMsg, + RUNTIME_009, +} from '@module-federation/error-codes'; +import { getBuilderId, getGlobalFederationInstance } from './utils'; export { loadScript, @@ -28,10 +34,65 @@ export function createInstance(options: UserOptions) { getGlobalFederationConstructor() || ModuleFederation; const instance = new ModuleFederationConstructor(options); setGlobalFederationInstance(instance); + FederationInstance = instance; return instance; } let FederationInstance: ModuleFederation | null = null; +function resolveFederationInstance( + name?: string, + version?: string, + remoteId?: string, +): ModuleFederation | null { + const buildId = getBuilderId(); + if (buildId) { + const buildInstance = getGlobalFederationInstance(name || '', version); + if (buildInstance) { + FederationInstance = buildInstance; + return buildInstance; + } + } + + if (name) { + const namedInstance = getGlobalFederationInstance(name, version); + if (namedInstance) { + FederationInstance = namedInstance; + return namedInstance; + } + } + + if (remoteId) { + const instances = CurrentGlobal.__FEDERATION__?.__INSTANCES__ || []; + const matchingInstance = instances.find((instance) => + matchRemoteWithNameAndExpose(instance.options.remotes, remoteId), + ); + if (matchingInstance) { + FederationInstance = matchingInstance; + return matchingInstance; + } + } + + if ( + FederationInstance && + (!remoteId || + matchRemoteWithNameAndExpose( + FederationInstance.options.remotes, + remoteId, + )) + ) { + return FederationInstance; + } + + if (!name && !version) { + const instances = CurrentGlobal.__FEDERATION__?.__INSTANCES__ || []; + if (instances.length === 1) { + FederationInstance = instances[0]; + return FederationInstance; + } + } + + return null; +} /** * @deprecated Use createInstance or getInstance instead */ @@ -54,67 +115,72 @@ export function init(options: UserOptions): ModuleFederation { export function loadRemote( ...args: Parameters ): Promise { - assert(FederationInstance, RUNTIME_009, runtimeDescMap); - const loadRemote: typeof FederationInstance.loadRemote = - FederationInstance.loadRemote; + const instance = resolveFederationInstance(undefined, undefined, args[0]); + assert(instance, getShortErrorMsg(RUNTIME_009, runtimeDescMap)); + const loadRemote: typeof instance.loadRemote = instance.loadRemote; // eslint-disable-next-line prefer-spread - return loadRemote.apply(FederationInstance, args); + return loadRemote.apply(instance, args); } export function loadShare( ...args: Parameters ): Promise T | undefined)> { - assert(FederationInstance, RUNTIME_009, runtimeDescMap); + const instance = resolveFederationInstance(); + assert(instance, getShortErrorMsg(RUNTIME_009, runtimeDescMap)); // eslint-disable-next-line prefer-spread - const loadShare: typeof FederationInstance.loadShare = - FederationInstance.loadShare; - return loadShare.apply(FederationInstance, args); + const loadShare: typeof instance.loadShare = instance.loadShare; + return loadShare.apply(instance, args); } export function loadShareSync( ...args: Parameters ): () => T | never { - assert(FederationInstance, RUNTIME_009, runtimeDescMap); - const loadShareSync: typeof FederationInstance.loadShareSync = - FederationInstance.loadShareSync; + const instance = resolveFederationInstance(); + assert(instance, getShortErrorMsg(RUNTIME_009, runtimeDescMap)); + const loadShareSync: typeof instance.loadShareSync = + instance.loadShareSync; // eslint-disable-next-line prefer-spread - return loadShareSync.apply(FederationInstance, args); + return loadShareSync.apply(instance, args); } export function preloadRemote( ...args: Parameters ): ReturnType { - assert(FederationInstance, RUNTIME_009, runtimeDescMap); + const instance = resolveFederationInstance(); + assert(instance, getShortErrorMsg(RUNTIME_009, runtimeDescMap)); // eslint-disable-next-line prefer-spread - return FederationInstance.preloadRemote.apply(FederationInstance, args); + return instance.preloadRemote.apply(instance, args); } export function registerRemotes( ...args: Parameters ): ReturnType { - assert(FederationInstance, RUNTIME_009, runtimeDescMap); + const instance = resolveFederationInstance(); + assert(instance, getShortErrorMsg(RUNTIME_009, runtimeDescMap)); // eslint-disable-next-line prefer-spread - return FederationInstance.registerRemotes.apply(FederationInstance, args); + return instance.registerRemotes.apply(instance, args); } export function registerPlugins( ...args: Parameters ): ReturnType { - assert(FederationInstance, RUNTIME_009, runtimeDescMap); + const instance = resolveFederationInstance(); + assert(instance, getShortErrorMsg(RUNTIME_009, runtimeDescMap)); // eslint-disable-next-line prefer-spread - return FederationInstance.registerPlugins.apply(FederationInstance, args); + return instance.registerPlugins.apply(instance, args); } -export function getInstance() { - return FederationInstance; +export function getInstance(name?: string, version?: string) { + return resolveFederationInstance(name, version); } export function registerShared( ...args: Parameters ): ReturnType { - assert(FederationInstance, RUNTIME_009, runtimeDescMap); + const instance = resolveFederationInstance(); + assert(instance, getShortErrorMsg(RUNTIME_009, runtimeDescMap)); // eslint-disable-next-line prefer-spread - return FederationInstance.registerShared.apply(FederationInstance, args); + return instance.registerShared.apply(instance, args); } // Inject for debug