webpack: Module Federation: unable to share externalized dependency between host and remote

Bug report

TLDR: ModuleFederationPlugin won’t expose a host’s dependency to a remote if the dependency is an external.
To be honest, I’m not completely sure if this is a bug, a missing feature, or just a technical limitation of ModuleFederationPlugin… but i’d like to be able to do that 😄

NOTE: i’m using React as an example, but this behavior occurs regardless of what library i’m trying to externalize+share.


Context

Our module federation setup is pretty standard (except for the externals, apparently). We’ve got two apps:

(Host)           (Remote)
SomePage  ---->  SomeWidget

Both of these are React apps, with React stuff shared via a typical ModuleFederationPlugin config:

shared: {
  'react': { singleton: true },
  'react-dom': { singleton: true }
}

This all works fine.

Now, we’d like SomePage to get React from a CDN link, so we tried externalizing it in SomePage via a typical

externals: {
  'react': 'SomeGlobalVar',
  'react-dom': 'AnotherGlobalVar',
}

the idea being that React would get wired up everywhere like this:

Script tag  <script src="/our-cdn/react-stuff.js>
      |
      |
      |   webpack `external` (global var)
      |
      v
SomePage (Host app)
      |
      |
      |    ModuleFederationPlugin's `shared` exposes React
      |
      v
SomeWidget (Remote component)

What is the current behavior?

Unfortunately, despite SomePage successfully loading React from the CDN, this doesn’t work. The externalized React doesn’t end up getting get shared between SomePage and SomeWidget, so we get two copies of React in one tree, which leads to a crash for the usual reasons. From my testing, it appears that despite the ModuleFederationPlugin config, SomePage never puts React into the shared WMF scope, so SomeWidget understandably loads its fallback. The versions are the same between both, and removing them doesn’t change anything. I’ve also tried this with lodash, and that also results in two copies, one from the CDN, and one loaded via SomeWidget’s fallback.

What is the expected behavior?

Dependencies should be shared between host and remote even if they’re externals, as illustrated in the diagram a few paragraphs up.

Steps to reproduce

I’ve added some externals to basic-host-remote from module-federation-examples to reproduce this setup:

https://github.com/lubieowoce/module-federation-examples/tree/externals-test/basic-host-remote

the change is very small. You can run it via cd basic-host-remote && yarn && yarn start, then navigating to http://localhost:3001/. It’ll flash and then turn blank, because when you when mix two copies of React in the same tree, it’ll crash with “Error: Invalid hook call”. This demonstrates that the library isn’t actually being shared between the host and remote.

Other relevant information:

webpack version: 5.70.0 Node.js version: 14.17.6 Operating System: macOS 11.6, ARM

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 11
  • Comments: 19 (12 by maintainers)

Most upvoted comments

I think it should be reopened, this can cause strange regressions where there doesn’t need to be. I believe what would need to change is the SharingRuntimeModule.js

Regarding sharing externals in userspace; something like this might work.

{
  shared: {
    fakeReactKey: {
      import: 'var React',
      import: 'external React',
      import: 'data:text/javascript,module.exports = window.React;',
      shareKey: 'react'
    }
  }
}

But we would need a way to ensure it isnt tree shaken out as being “unused” in the graph.

In the webpack runtime, what we need is something like

register("react", "18.0.0", function() { return React});

I had to make sure all remotes treated react as external too because the host is missing keys.

Yeah, this is kind of what we did, except we made the remote in question have two builds - one w/ externalized react (for interop with stuff that externalizes react), and one “vanilla” (for WMF-native things where sharing Just Works). annoying but it works i guess

anyway, i think we agree that it’s a useful feature, so do you think this issue should be reopened? i mean, for me, i no longer have a pressing need for this, so i’m fine either way. but maybe there’s SOMEONE out there who needs it 😄

(also if you know a way to make sharing externals happen “in userspace”, i’d love to see how! mostly out of curiosity but also hey, might come in handy sometime)

So the problem here. I think, is the remote container initialization phase. If a host marks it as external the runtime should still register react, and just point to the global variable. So the share scope still contains the keys to pass them into remote containers. Otherwise if my remote has import:false set - it crashes because share scope is missing keys, but loadSingleton is still part of the logic