Skip to content

runtime-core: provider selection should skip import:false consume-only stubs when a real provider exists #5058

Description

@tonoizer

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)

  1. 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.
  2. @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.
  3. 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.
  4. @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.
  5. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions