Summary
When a webpack ModuleFederationPlugin host consumes a vite-built remote, the remote's shared modules are never seeded before its remoteEntry.init() evaluates, so its prebundled dep chunks throw at top-level evaluation:
[Module Federation] Failed to bridge external shared module "@emotion/react"
TypeError: react_exports.createContext is not a function
[Module Federation] Failed to bridge external shared module "@mui/system" (same)
[Module Federation] Failed to bridge external shared module "@mui/material" (same)
This happens with default configuration on both sides. The error surfaces as RUNTIME-015 at the host.
Versions:
| package |
version |
@module-federation/vite (remote) |
1.20.7 |
@module-federation/enhanced (host) |
2.8.2 |
@module-federation/runtime |
2.8.2 |
| webpack |
5.98.0 |
| react / @mui/material / @emotion |
18.3.1 / 6.5.0 / 11.11.x |
Reproduction
Minimal repro (2 apps, ~10 files): https://github.com/longngo0617/mf-webpack-host-vite-remote-repro
pnpm --dir remote install && pnpm --dir host install
pnpm --dir remote dev # vite remote -> :5001
pnpm --dir host serve # webpack host -> :5002
Open http://localhost:5002. Expected SUCCESS appRemote/App; actual is the error above.
Root cause as far as I traced it
1. Webpack's register() never sets lib/loaded. From the host bundle:
var register = (name, version, factory, eager) => {
var versions = scope[name] = scope[name] || {};
var activeVersion = versions[version];
if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from)))
versions[version] = { get: factory, from: uniqueName, eager: !!eager };
};
So a webpack-provided share is always { get, from, eager }. Observed share scope at failure time:
react: 18.3.1 from=shellHost lib=false loaded=false
__mf_module_cache__.share: {} // empty
2. The vite plugin's seeding bails on exactly that shape. In the generated remoteEntry.js:
const pendingExternalProvider = typeof __mfGetPendingExternalSharedProvider === 'function'
? __mfGetPendingExternalSharedProvider(pkg, share) : undefined;
if (pendingExternalProvider && !pendingExternalProvider.lib && !pendingExternalProvider.loaded) {
return; // <-- react is index 0 of __mfSeedOrder
}
and __mfGetPendingExternalSharedProvider only returns a provider when isWebpackProvider(provider) is true:
function isWebpackProvider(provider) {
if (typeof provider?.get !== 'function') return false;
const source = Function.prototype.toString.call(provider.get);
return source.includes('__webpack_require__');
}
Because react is first in __mfSeedOrder, __mfFirstRuntimeSeedBarrierIndex === 0, so __mfImmediateSeedKeys is empty and nothing is seeded before init.
This looks like the core defect: isWebpackProvider() being true is precisely the signal that lib/loaded will never be populated, because webpack's register() does not populate them. Yet the guard treats it the same as "provider not ready yet, wait" — the one case where waiting cannot help. Calling provider.get() and seeding when isWebpackProvider(provider) is true would seem more correct.
3. version-first guarantees the worst possible timing. In runtime-core/dist/shared/index.js (initializeSharing):
if (host.options.shareStrategy === "version-first" || strategy === "version-first")
host.options.remotes.forEach((remote) => {
if (remote.shareScope === shareScopeName) promises.push(initRemoteModule(remote.name));
});
version-first must init every remote to compare versions, so the host's own first loadShare('react') triggers the remote's init() — before the host has materialised anything. I confirmed the ordering by proxying __mf_module_cache__.share: every bridge failure is logged before default:react is ever written.
4. Consequence: the workaround must be applied on both sides. The remote's strategy is baked into its artifact as a string literal, gating its own bridge:
// remote built with default version-first:
if (singleton && 'version-first' !== 'loaded-first') return; // -> bridge compiled out
// remote built with loaded-first:
if (singleton && 'loaded-first' !== 'loaded-first') return; // -> bridge runs
Since every share here is singleton: true, under version-first __mfBridgeMaterializedProvider is dead code in the remote — so no host-side setting can reach it.
Matrix (cold cache each run, from the repro)
host shareStrategy |
remote shareStrategy |
result |
default (version-first) |
default (version-first) |
❌ react_exports.createContext is not a function |
loaded-first |
default |
❌ styled_default is not a function + RUNTIME-015 |
| default |
loaded-first |
❌ react_exports.createContext is not a function |
loaded-first |
loaded-first |
✅ works |
Setting shareStrategy: 'loaded-first' on both sides is the only working combination. eager: true on the host's shared deps does not help — it makes the provider's getter synchronous but still leaves lib/loaded unset, which is what the guard tests.
Notes for reproducing
Three conditions matter, and getting any of them wrong changes the failure mode:
- The host must consume a shared dep itself (
import * as React from 'react') — its first loadShare() is what calls initializeSharing().
- The remote must import package indexes (
@mui/material), not deep subpaths — their prebundled dep chunks call createContext() / styled() at top level.
- The host's
uniqueName must sort above the remote's name, so the host keeps the react provider slot (see the uniqueName > activeVersion.from tiebreak in register()). With the remote sorting higher it wins the slot and the symptom becomes a deadlock — the host's own import React never resolves — rather than this throw.
Point 3 may be worth a look independently: which container ends up owning a shared module for equal versions depends on the lexicographic ordering of container names, which is a surprising thing for behaviour to hinge on.
Possibly related
#4718 looks like the same family (shared react unresolved inside a vite remote's __loadShare__ virtual module) but a different setup — vite host, React 19, useContext on null. Different cause as far as I can tell, flagging in case it helps.
Used Package Manager
pnpm
System Info
System:
OS: macOS 26.6.1
CPU: (10) arm64 Apple M1 Pro
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.16.0
npm: 11.13.0
pnpm: 11.3.0
Browsers:
Chrome: 151.0.7922.174
Safari: 26.6
Validations
Summary
When a webpack
ModuleFederationPluginhost consumes a vite-built remote, the remote's shared modules are never seeded before itsremoteEntry.init()evaluates, so its prebundled dep chunks throw at top-level evaluation:This happens with default configuration on both sides. The error surfaces as
RUNTIME-015at the host.Versions:
@module-federation/vite(remote)@module-federation/enhanced(host)@module-federation/runtimeReproduction
Minimal repro (2 apps, ~10 files): https://github.com/longngo0617/mf-webpack-host-vite-remote-repro
Open
http://localhost:5002. ExpectedSUCCESS appRemote/App; actual is the error above.Root cause as far as I traced it
1. Webpack's
register()never setslib/loaded. From the host bundle:So a webpack-provided share is always
{ get, from, eager }. Observed share scope at failure time:2. The vite plugin's seeding bails on exactly that shape. In the generated
remoteEntry.js:and
__mfGetPendingExternalSharedProvideronly returns a provider whenisWebpackProvider(provider)is true:Because
reactis first in__mfSeedOrder,__mfFirstRuntimeSeedBarrierIndex === 0, so__mfImmediateSeedKeysis empty and nothing is seeded before init.This looks like the core defect:
isWebpackProvider()being true is precisely the signal thatlib/loadedwill never be populated, because webpack'sregister()does not populate them. Yet the guard treats it the same as "provider not ready yet, wait" — the one case where waiting cannot help. Callingprovider.get()and seeding whenisWebpackProvider(provider)is true would seem more correct.3.
version-firstguarantees the worst possible timing. Inruntime-core/dist/shared/index.js(initializeSharing):version-firstmust init every remote to compare versions, so the host's own firstloadShare('react')triggers the remote'sinit()— before the host has materialised anything. I confirmed the ordering by proxying__mf_module_cache__.share: every bridge failure is logged beforedefault:reactis ever written.4. Consequence: the workaround must be applied on both sides. The remote's strategy is baked into its artifact as a string literal, gating its own bridge:
Since every share here is
singleton: true, underversion-first__mfBridgeMaterializedProvideris dead code in the remote — so no host-side setting can reach it.Matrix (cold cache each run, from the repro)
shareStrategyshareStrategyversion-first)version-first)react_exports.createContext is not a functionloaded-firststyled_default is not a function+RUNTIME-015loaded-firstreact_exports.createContext is not a functionloaded-firstloaded-firstSetting
shareStrategy: 'loaded-first'on both sides is the only working combination.eager: trueon the host's shared deps does not help — it makes the provider's getter synchronous but still leaveslib/loadedunset, which is what the guard tests.Notes for reproducing
Three conditions matter, and getting any of them wrong changes the failure mode:
import * as React from 'react') — its firstloadShare()is what callsinitializeSharing().@mui/material), not deep subpaths — their prebundled dep chunks callcreateContext()/styled()at top level.uniqueNamemust sort above the remote's name, so the host keeps the react provider slot (see theuniqueName > activeVersion.fromtiebreak inregister()). With the remote sorting higher it wins the slot and the symptom becomes a deadlock — the host's ownimport Reactnever resolves — rather than this throw.Point 3 may be worth a look independently: which container ends up owning a shared module for equal versions depends on the lexicographic ordering of container names, which is a surprising thing for behaviour to hinge on.
Possibly related
#4718 looks like the same family (shared
reactunresolved inside a vite remote's__loadShare__virtual module) but a different setup — vite host, React 19,useContexton null. Different cause as far as I can tell, flagging in case it helps.Used Package Manager
pnpm
System Info
System: OS: macOS 26.6.1 CPU: (10) arm64 Apple M1 Pro Shell: 5.9 - /bin/zsh Binaries: Node: 24.16.0 npm: 11.13.0 pnpm: 11.3.0 Browsers: Chrome: 151.0.7922.174 Safari: 26.6Validations