Describe the bug
Share-scope provider selection in @module-federation/runtime-core does not distinguish a real shared provider from a consume-only import: false stub.
Containers that declare shared: { pkg: { import: false } } still register a scope entry whose get() throws Shared module '…' must be provided by host. During version / singleton selection that stub can win (same version, first registrant, or higher declared version than a real provider), so consumers resolve to the throwing stub instead of a real provider that is already in the scope.
This is the root cause that bundler-side workarounds (Vite generated helpers, and e.g. a consumer afterRegisterShare plugin that removes the stub from shareScopeMap) keep having to paper over.
Verified facts (2026-09-11)
- Docs say
import: false means the package is not packaged into the remote and only the consumer/host instance should be used — it is not a competing provider.
@module-federation/runtime-core@2.9.0 (dist/shared/index.js, getRegisteredShare, registerShared) has no reference to shareConfig.import === false. A stub registered for an import: false share is a fully selectable candidate under version-first if it has the highest version.
- Source on
main: packages/runtime-core/src/utils/share.ts selection compares versions / loaded state only. SharedConfig in runtime-core currently does not even type the import field, though callers attach it.
@module-federation/vite filters stubs only inside its own generated helpers (__mfSelectSharedProvider, __mfIsOwnStub, PRs #953, #1178, #1184, #1230). Anything that goes through runtime-core loadShare directly — e.g. nested remote containers initialising into the parent scope — is unprotected.
- Downstream mitigations (not a substitute for a core fix) remove the stub from
shareScopeMap in an afterRegisterShare hook while keeping options.shared metadata, restoring a displaced real provider.
Minimal repro (suggested)
new ModuleFederation({
shared: {
react: [
// consume-only stub — get() throws "must be provided by host"
stub({ import: false, version: '19.2.8' }),
// real provider with a lower version
provider({ version: '19.2.7' }),
],
},
});
await loadShare('react');
// actual: selects the throwing stub (highest version)
// expected: selects the real 19.2.7 provider
Reuse / mirror the cases from consumer-share-scope specs that assert stub vs real provider selection (e.g. frontend-utils vite-consumer-share-scope.spec.ts and Vite virtualRemoteEntry.test.ts import:false bridging cases).
Historical reports
- vite#952 — throwing provider wins version selection
- vite#1229 — bridging another container's consume-only stub
Expected
When choosing a provider for a share:
- Prefer a real provider (
import !== false, with a usable get/lib) over any import: false stub at the same or competing version.
- Keep the stub's metadata available for
requiredVersion / strictVersion checks if needed.
- Only fall back to an
import: false stub when no real provider is available (then the existing "must be provided by host" error remains correct).
Suggested fix location
packages/runtime-core SharedHandler / packages/runtime-core/src/utils/share.ts (getRegisteredShare / version finders) and matching unit coverage under packages/runtime-core/__tests__/. Optionally extend SharedConfig to include import?: false so the field is first-class.
Alternative (if maintainers prefer not to change selection): have the Vite plugin not register stubs into the share scope at all (writeLoadShareModule / remoteEntry init). That would still leave non-Vite runtimes unprotected; selection skip in runtime-core is the portable fix.
Version
@module-federation/runtime-core 2.9.0 / current main
Describe the bug
Share-scope provider selection in
@module-federation/runtime-coredoes not distinguish a real shared provider from a consume-onlyimport: falsestub.Containers that declare
shared: { pkg: { import: false } }still register a scope entry whoseget()throwsShared module '…' must be provided by host. During version / singleton selection that stub can win (same version, first registrant, or higher declared version than a real provider), so consumers resolve to the throwing stub instead of a real provider that is already in the scope.This is the root cause that bundler-side workarounds (Vite generated helpers, and e.g. a consumer
afterRegisterShareplugin that removes the stub fromshareScopeMap) keep having to paper over.Verified facts (2026-09-11)
import: falsemeans the package is not packaged into the remote and only the consumer/host instance should be used — it is not a competing provider.@module-federation/runtime-core@2.9.0(dist/shared/index.js,getRegisteredShare,registerShared) has no reference toshareConfig.import === false. A stub registered for animport: falseshare is a fully selectable candidate underversion-firstif it has the highest version.main:packages/runtime-core/src/utils/share.tsselection compares versions / loaded state only.SharedConfigin runtime-core currently does not even type theimportfield, though callers attach it.@module-federation/vitefilters stubs only inside its own generated helpers (__mfSelectSharedProvider,__mfIsOwnStub, PRs #953, #1178, #1184, #1230). Anything that goes through runtime-coreloadSharedirectly — e.g. nested remote containers initialising into the parent scope — is unprotected.shareScopeMapin anafterRegisterSharehook while keepingoptions.sharedmetadata, restoring a displaced real provider.Minimal repro (suggested)
Reuse / mirror the cases from consumer-share-scope specs that assert stub vs real provider selection (e.g. frontend-utils
vite-consumer-share-scope.spec.tsand VitevirtualRemoteEntry.test.tsimport:false bridging cases).Historical reports
Expected
When choosing a provider for a share:
import !== false, with a usableget/lib) over anyimport: falsestub at the same or competing version.requiredVersion/strictVersionchecks if needed.import: falsestub when no real provider is available (then the existing "must be provided by host" error remains correct).Suggested fix location
packages/runtime-coreSharedHandler/packages/runtime-core/src/utils/share.ts(getRegisteredShare/ version finders) and matching unit coverage underpackages/runtime-core/__tests__/. Optionally extendSharedConfigto includeimport?: falseso the field is first-class.Alternative (if maintainers prefer not to change selection): have the Vite plugin not register stubs into the share scope at all (
writeLoadShareModule/ remoteEntryinit). That would still leave non-Vite runtimes unprotected; selection skip in runtime-core is the portable fix.Version
@module-federation/runtime-core2.9.0/ currentmain