From 15529896441fe9b8affb676916b990e6cf58678c Mon Sep 17 00:00:00 2001 From: Andrew Knapp Date: Fri, 10 Apr 2026 16:18:47 -0400 Subject: [PATCH] fix(runtime-core): guard targetShared.get before calling in loadShare When the host's share scope contains a dependency that a remote does not declare, the share scope entry has no get() factory. The loadShare method calls targetShared.get!() without checking, crashing with 'targetShared.get is not a function'. Add a typeof check before both call sites in SharedHandler.loadShare. If get is not a function, return an empty factory instead of crashing. Fixes #2497 --- .changeset/fix-shared-get-guard.md | 18 ++++++++ .../src/runtime/remote-module-registry.js | 7 ++- packages/runtime-core/src/shared/index.ts | 10 +++- packages/runtime/__tests__/shares.spec.ts | 46 +++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-shared-get-guard.md diff --git a/.changeset/fix-shared-get-guard.md b/.changeset/fix-shared-get-guard.md new file mode 100644 index 00000000000..1f76eb60993 --- /dev/null +++ b/.changeset/fix-shared-get-guard.md @@ -0,0 +1,18 @@ +--- +'@module-federation/runtime-core': patch +'@module-federation/metro-core': patch +--- + +fix: guard `targetShared.get` before calling in `loadShare` + +When the host's share scope contains a dependency that a remote does not +declare, the share scope entry has no `get()` factory. `loadShare` called +`targetShared.get!()` without checking, crashing with +`targetShared.get is not a function`. Added a typeof guard before both +call sites in `SharedHandler.loadShare`, returning `false` (the documented +miss sentinel) so callers can fall back correctly. + +Also guard the `factory()` call in `metro-core`'s `remote-module-registry` +to handle the `false` return from `loadShare` without crashing. + +Fixes #2497 diff --git a/packages/metro-core/src/runtime/remote-module-registry.js b/packages/metro-core/src/runtime/remote-module-registry.js index 2617aa54dfb..c51af912a2c 100644 --- a/packages/metro-core/src/runtime/remote-module-registry.js +++ b/packages/metro-core/src/runtime/remote-module-registry.js @@ -55,6 +55,9 @@ export async function loadSharedToRegistryAsync(id) { registry[id] = {}; loading[id] = (async () => { const factory = await loadShare(id); + if (!factory) { + return; + } const sharedModule = factory(); cloneModule(sharedModule, registry[id]); })(); @@ -67,7 +70,9 @@ export function loadSharedToRegistrySync(id) { return; } loading[id] = loadShareSync(id); - registry[id] = loading[id](); + if (loading[id]) { + registry[id] = loading[id](); + } } export function getModuleFromRegistry(id) { diff --git a/packages/runtime-core/src/shared/index.ts b/packages/runtime-core/src/shared/index.ts index 50b6b30d12a..ea962fd12e8 100644 --- a/packages/runtime-core/src/shared/index.ts +++ b/packages/runtime-core/src/shared/index.ts @@ -189,7 +189,10 @@ export class SharedHandler { return factory; } else { const asyncLoadProcess = async () => { - const factory = await targetShared.get!(); + if (typeof targetShared.get !== 'function') { + return false as unknown as () => T; + } + const factory = await targetShared.get(); addUseIn(targetShared, host.options.name); targetShared.loaded = true; targetShared.lib = factory; @@ -217,7 +220,10 @@ export class SharedHandler { const targetShared = directShare(shareOptionsRes, _useTreeShaking); const asyncLoadProcess = async () => { - const factory = await targetShared.get!(); + if (typeof targetShared.get !== 'function') { + return false as unknown as () => T; + } + const factory = await targetShared.get(); targetShared.lib = factory; targetShared.loaded = true; addUseIn(targetShared, host.options.name); diff --git a/packages/runtime/__tests__/shares.spec.ts b/packages/runtime/__tests__/shares.spec.ts index cca49ba2f5a..cc88114e2d5 100644 --- a/packages/runtime/__tests__/shares.spec.ts +++ b/packages/runtime/__tests__/shares.spec.ts @@ -948,4 +948,50 @@ describe('load share while shared has multiple versions', () => { assert(sharedRes, "sharedRes can't be null"); expect(sharedRes.version).toEqual('16.0.0'); }); + + // Regression test for https://github.com/module-federation/core/issues/2497 + // When the host's share scope contains a dep the remote doesn't declare, + // the scope entry has no get() factory. loadShare must not crash. + it('loadShare does not crash when share scope entry has no get factory', async () => { + const host = init({ + name: '@federation/host-extra-dep', + remotes: [], + shared: { + react: { + version: '18.0.0', + scope: ['default'], + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '18.0.0', + from: '@federation/host-extra-dep', + })), + shareConfig: { + singleton: true, + requiredVersion: '^18.0.0', + }, + }, + }, + }); + + // Inject a share scope entry without a get() factory, simulating what + // happens when the host registers a shared dep the remote doesn't declare. + const scope = host.shareScopeMap['default'] || {}; + host.shareScopeMap['default'] = scope; + scope['moment'] = { + '2.30.0': { + version: '2.30.0', + from: 'remote-without-moment', + scope: ['default'], + shareConfig: { + singleton: false, + requiredVersion: '^2.30.0', + }, + } as any, + }; + + // Should return false (miss sentinel) instead of crashing + const result = await host.loadShare('moment'); + expect(result).toBe(false); + }); });