You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have multiple Angular MFEs that expose web components, plus several of my own libraries. Consider two MFEs (MFE1, MFE2) and two libs (libBig, libSmall).
libBigdefines an InjectionTokenSOME_TOKEN (it only declares it, it does not provide it).
libSmall has libBig as a peer dependency and internally does inject(SOME_TOKEN).
Both MFE1 and MFE2 use libBig and provideSOME_TOKEN themselves.
I use shareAll, so I want to share as much as possible whenever versions match exactly.
MFE1 and MFE2 have the same version of libSmall, but different versions of libBig.
At runtime this breaks. MFE1 works: it provides SOME_TOKEN (from its libBig), and the shared libSmall injects it fine. But MFE2 provides SOME_TOKEN from its ownlibBig, while the shared libSmall still tries to inject the SOME_TOKEN from the otherlibBig. Those are two different token objects, so we get a NullInjectorError — the token libSmall is asking for is not the one MFE2 provided.
Note: building a full demo repo for this is not trivial (it needs two MFEs, two lib versions and the peer-dependency wiring). I'm happy to put one together if the description alone isn't enough to reproduce/understand it.
Why it breaks
flowchart TB
subgraph SharedScope["Shared runtime (single import map)"]
LS["libSmall@1.0.0<br/>(shared singleton — ONE instance)<br/>import { SOME_TOKEN } from 'libBig'"]
end
subgraph MFE1["MFE1"]
B1["libBig@1<br/>defines SOME_TOKEN → TOKEN_OBJ_A"]
P1["provide(SOME_TOKEN = TOKEN_OBJ_A)"]
end
subgraph MFE2["MFE2"]
B2["libBig@2<br/>defines SOME_TOKEN → TOKEN_OBJ_B"]
P2["provide(SOME_TOKEN = TOKEN_OBJ_B)"]
end
LS -. "import 'libBig' resolves ONCE<br/>to libBig@1 (winner)" .-> B1
P1 -->|"provides TOKEN_OBJ_A"| B1
P2 -->|"provides TOKEN_OBJ_B"| B2
LS ==>|"inject(SOME_TOKEN)=TOKEN_OBJ_A ✅ found in MFE1"| P1
LS ==>|"inject(SOME_TOKEN)=TOKEN_OBJ_A ❌ MFE2 has only TOKEN_OBJ_B → NullInjectorError"| P2
style B1 fill:#d5f5d5
style B2 fill:#f5d5d5
style LS fill:#fff3cd
Loading
There is only one shared libSmall instance. Its import { SOME_TOKEN } from 'libBig' is resolved once, to a single libBig (the winner — MFE1's), because import-map scopes are keyed by the importing module's own URL, not by the caller. So libSmall.inject(SOME_TOKEN) always asks for MFE1's token object. Inside MFE2, the injector only has MFE2's token object → NullInjectorError.
I have a solution proposal — and why pooling (#49) doesn't seem to fully cover it
Pooling as described in #49 forces a group of externals to come from one coherent source. That helps when the members can actually be served from one anchor. But in my case the two libBig versions genuinely differ, so libBig can never collapse to one instance — there is nothing to pool. Pooling addresses "members of a group resolved from different remotes", whereas my problem is "a shared dependent (libSmall) is silently pinned to one specific version of a split dependency (libBig)". So it needs to propagate up the dependency graph, automatically.
The real issue is the sharing decision itself. Today it seems to decide shareability of libSmall from its own version number only: when MFE2 is loaded, NF sees libSmall at the same version as already loaded, and reuses it without re-wiring the imports. That assumption is wrong, because the already-loaded libSmall is wired against MFE1's libBig.
The fix should be: the identity used to decide whether libSmall is shareable must include the resolved versions of its (peer) dependencies, not just its own version. Concretely:
When deciding if libSmall can be reused, also check its dependency closure (e.g. libBig) and compare the resolved instances across the consumers.
If they all match → share as today.
If even one dependency resolves differently (as libBig does here) → treat libSmall as not shareable for that consumer: load a second copy and re-wire its imports against MFE2's dependencies (i.e. scope it so its import 'libBig' resolves to MFE2's libBig).
This should propagate transitively: a split in libBig "poisons" libSmall, and anything depending on libSmall in turn.
This does mean a singleton dependent effectively can't stay a singleton when one of its deps is version-split — which is unavoidable, but at least a warning there would already help.
Possibly related issues in the pre-v4 repo
While searching angular-architects/module-federation-plugin I found these, which look like the same underlying "duplicate token identity because a dependency exists as multiple instances" family:
Is there anything I can already do in the pre-v4 versions to work around this (config, scoping, forcing libSmall to not be shared, etc.), while a proper fix is considered?
Problem
I have multiple Angular MFEs that expose web components, plus several of my own libraries. Consider two MFEs (
MFE1,MFE2) and two libs (libBig,libSmall).libBigdefines anInjectionTokenSOME_TOKEN(it only declares it, it does not provide it).libSmallhaslibBigas a peer dependency and internally doesinject(SOME_TOKEN).MFE1andMFE2uselibBigand provideSOME_TOKENthemselves.shareAll, so I want to share as much as possible whenever versions match exactly.MFE1andMFE2have the same version oflibSmall, but different versions oflibBig.At runtime this breaks.
MFE1works: it providesSOME_TOKEN(from itslibBig), and the sharedlibSmallinjects it fine. ButMFE2providesSOME_TOKENfrom its ownlibBig, while the sharedlibSmallstill tries to inject theSOME_TOKENfrom the otherlibBig. Those are two different token objects, so we get aNullInjectorError— the tokenlibSmallis asking for is not the oneMFE2provided.Why it breaks
flowchart TB subgraph SharedScope["Shared runtime (single import map)"] LS["libSmall@1.0.0<br/>(shared singleton — ONE instance)<br/>import { SOME_TOKEN } from 'libBig'"] end subgraph MFE1["MFE1"] B1["libBig@1<br/>defines SOME_TOKEN → TOKEN_OBJ_A"] P1["provide(SOME_TOKEN = TOKEN_OBJ_A)"] end subgraph MFE2["MFE2"] B2["libBig@2<br/>defines SOME_TOKEN → TOKEN_OBJ_B"] P2["provide(SOME_TOKEN = TOKEN_OBJ_B)"] end LS -. "import 'libBig' resolves ONCE<br/>to libBig@1 (winner)" .-> B1 P1 -->|"provides TOKEN_OBJ_A"| B1 P2 -->|"provides TOKEN_OBJ_B"| B2 LS ==>|"inject(SOME_TOKEN)=TOKEN_OBJ_A ✅ found in MFE1"| P1 LS ==>|"inject(SOME_TOKEN)=TOKEN_OBJ_A ❌ MFE2 has only TOKEN_OBJ_B → NullInjectorError"| P2 style B1 fill:#d5f5d5 style B2 fill:#f5d5d5 style LS fill:#fff3cdThere is only one shared
libSmallinstance. Itsimport { SOME_TOKEN } from 'libBig'is resolved once, to a singlelibBig(the winner — MFE1's), because import-mapscopesare keyed by the importing module's own URL, not by the caller. SolibSmall.inject(SOME_TOKEN)always asks for MFE1's token object. Inside MFE2, the injector only has MFE2's token object →NullInjectorError.I have a solution proposal — and why pooling (#49) doesn't seem to fully cover it
Pooling as described in #49 forces a group of externals to come from one coherent source. That helps when the members can actually be served from one anchor. But in my case the two
libBigversions genuinely differ, solibBigcan never collapse to one instance — there is nothing to pool. Pooling addresses "members of a group resolved from different remotes", whereas my problem is "a shared dependent (libSmall) is silently pinned to one specific version of a split dependency (libBig)". So it needs to propagate up the dependency graph, automatically.The real issue is the sharing decision itself. Today it seems to decide shareability of
libSmallfrom its own version number only: whenMFE2is loaded, NF seeslibSmallat the same version as already loaded, and reuses it without re-wiring the imports. That assumption is wrong, because the already-loadedlibSmallis wired againstMFE1'slibBig.The fix should be: the identity used to decide whether
libSmallis shareable must include the resolved versions of its (peer) dependencies, not just its own version. Concretely:libSmallcan be reused, also check its dependency closure (e.g.libBig) and compare the resolved instances across the consumers.libBigdoes here) → treatlibSmallas not shareable for that consumer: load a second copy and re-wire its imports againstMFE2's dependencies (i.e. scope it so itsimport 'libBig'resolves to MFE2'slibBig).libBig"poisons"libSmall, and anything depending onlibSmallin turn.This does mean a
singletondependent effectively can't stay a singleton when one of its deps is version-split — which is unavoidable, but at least a warning there would already help.Possibly related issues in the pre-v4 repo
While searching
angular-architects/module-federation-pluginI found these, which look like the same underlying "duplicate token identity because a dependency exists as multiple instances" family:No provider for InjectionToken TRANSLOCO_TRANSPILER, with the note that the token is "unique per ES module".Question
Is there anything I can already do in the pre-v4 versions to work around this (config, scoping, forcing
libSmallto not be shared, etc.), while a proper fix is considered?