core icon indicating copy to clipboard operation
core copied to clipboard

@module-federation/enhanced/runtime loadRemote() does not resolve remote files properly when loading manifest

Open vincesp opened this issue 11 months ago • 23 comments

Describe the bug

Steps to reproduce:

  • Create producer that exposes an mf-manifest.json
  • Create a consumer that loads consumer dynamically at runtime:
    • registerRemotes() to register mf-manifest.json with a remote URL
    • loadRemote() to load consumer

Result:

  • mf-manifest.json is loaded from remote URL
  • However, files referenced in mf-manifest.json are not resolved properly. Instead loadRemote() tries to load them from the consumer's path instead of from the producer's

In this example, the consumer runs on port 5173 while the producer runs on port 8766.

 GET http://localhost:5173/mf-remote.js net::ERR_ABORTED 404 (Not Found)
 GET http://localhost:5173/js_remote__mf_v__runtimeInit__mf_v__-igscT7wa.js net::ERR_ABORTED 404 (Not Found)
 GET http://localhost:5173/remoteEntry-BuwvPft7.js?import net::ERR_ABORTED 404 (Not Found)

Note that another producer that is accessed using js-remote.js with type: 'module' is running on port 8765 in the linked example. That producer can be loaded successfully.

Reproduction

https://github.com/vincesp/stackblitz-starters-h6gf2a5j

Used Package Manager

npm

System Info

System:
    OS: Windows 10 10.0.19045
    CPU: (16) x64 Intel(R) Xeon(R) W-11955M CPU @ 2.60GHz
    Memory: 8.38 GB / 31.73 GB
  Binaries:
    Node: 22.9.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
    npm: 10.3.0 - C:\Program Files\nodejs\npm.CMD
    pnpm: 9.15.4 - ~\AppData\Roaming\npm\pnpm.CMD
  Browsers:
    Edge: Chromium (130.0.2849.68)

Validations

vincesp avatar Feb 26 '25 14:02 vincesp

try it with rspack/rsbuild otherwise first file issues with module-federation/vite repo.

ScriptedAlchemy avatar Feb 27 '25 20:02 ScriptedAlchemy

@ScriptedAlchemy so you mean, the manifest as generated by @module-federation/vite is not compatible with the manifest as expected by @module-federation/enhanced? Is there a documentation or JSON schema of the manifest file, so we could check quickly if it is correct?

vincesp avatar Feb 28 '25 13:02 vincesp

@ScriptedAlchemy I added another provider with rsbuild to the same repo linked above, and I am getting the same error message.

vincesp avatar Feb 28 '25 13:02 vincesp

I'm having the same issue. I'm using a producer with rsbuild.

rsbuild.config.js (Producer)
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { pluginModuleFederation } from "@module-federation/rsbuild-plugin";

export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: "dynamicWidgets",
      manifest: true,
      getPublicPath: 'function(){ return "http://localhost:2000/"; }',
      exposes: {
        "./Widget": "./src/Widget.jsx",
      },
      shared: ["react", "react-dom"],
    }),
  ],
});
Output mf-manifest.json (Producer)
{
  "id": "dynamicWidgets",
  "name": "dynamicWidgets",
  "metaData": {
    "name": "dynamicWidgets",
    "type": "app",
    "buildInfo": {
      "buildVersion": "0.1.0",
      "buildName": "dynamic-widgets-bundler"
    },
    "remoteEntry": {
      "name": "static/js/dynamicWidgets.2d5c8cf9.js",
      "path": "",
      "type": "global"
    },
    "types": {
      "path": "",
      "name": "",
      "zip": "@mf-types.zip",
      "api": "@mf-types.d.ts"
    },
    "globalName": "dynamicWidgets",
    "pluginVersion": "0.9.1",
    "prefetchInterface": false,
    "getPublicPath": "function(){ return \"http://localhost:2000/\"; }"
  },
  "shared": [
    {
      "id": "dynamicWidgets:react",
      "name": "react",
      "version": "19.0.0",
      "singleton": true,
      "requiredVersion": "^18.0.0",
      "assets": {
        "js": {
          "async": [],
          "sync": [
            "static/js/async/512.edd7a784.js"
          ]
        },
        "css": {
          "async": [],
          "sync": []
        }
      }
    }
  ],
  "remotes": [],
  "exposes": [
    {
      "id": "dynamicWidgets:Widget",
      "name": "Widget",
      "assets": {
        "js": {
          "sync": [
            "static/js/async/__federation_expose_Widget.7e6e928a.js"
          ],
          "async": [
            "static/js/async/512.edd7a784.js"
          ]
        },
        "css": {
          "sync": [],
          "async": []
        }
      },
      "path": "./Widget"
    }
  ]
}
Host
import { loadRemote } from "@module-federation/enhanced/runtime";
import React, { useState, useEffect } from "react";
import { init } from "@module-federation/enhanced/runtime";

const FederationContainer = init({
  name: "chat-web",
  remotes: [],
});

FederationContainer.registerRemotes([
  {
    name: "dynamicWidgets",
    entry: "http://localhost:2000/mf-manifest.json",
  },
]);

const App = () => {
  const [Widget, setWidget] = useState<React.ComponentType<any> | null>(null);

  useEffect(() => {
    loadRemote("dynamicWidgets/Widget").then((module) => {
      console.log(module);
      setWidget(module.default);
    });
  }, []);

  return (
    <div className="content">
      <h1>Rsbuild with React</h1>
      <p>Start building amazing things with Rsbuild.</p>
      {/* <div>{Widget != null && <Widget />}</div> */}
    </div>
  );
};

export default App;

And the error I get is

GET http://localhost:5173/static/js/async/652.8c22491b.js net::ERR_ABORTED 404 (Not Found)

Uncaught (in promise) ChunkLoadError: Loading chunk 652 failed.
(error: http://localhost:5173/static/js/async/652.8c22491b.js)
    at __webpack_require__.f.j (entry.js:8:5255)
    at entry.js:8:2214
    at Array.reduce (<anonymous>)
    at __webpack_require__.e (entry.js:8:2162)
    at ./Widget (entry.js:8:4646)
    at Object.<anonymous> (entry.js:7:7829)

nicoverali avatar Mar 01 '25 01:03 nicoverali

Instead of using rsbuild, now I'm using rspack with this config. Now it's working. I didn't make any change on the host side, so it has to be related to the rsbuild and vite plugin for producers

rspack.config.js (Producer)
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";

export default {
  entry: "./src/index.tsx",
  output: {
    // You need to set a unique value that is not equal to other applications
    uniqueName: "dynamicWidgets",
    // publicPath must be configured if using manifest
    publicPath: "http://localhost:2000/",
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "dynamicWidgets",
      exposes: {
        "./Widget": "./src/index.tsx",
      },
      shared: ["react", "react-dom"],
    }),
  ],
  module: {
    rules: [
      {
        test: /\.jsx$/,
        use: {
          loader: "builtin:swc-loader",
          options: {
            jsc: {
              parser: {
                syntax: "ecmascript",
                jsx: true,
              },
            },
          },
        },
        type: "javascript/auto",
      },
      {
        test: /\.tsx$/,
        use: {
          loader: "builtin:swc-loader",
          options: {
            jsc: {
              parser: {
                syntax: "typescript",
                tsx: true,
              },
            },
          },
        },
        type: "javascript/auto",
      },
    ],
  },
};

nicoverali avatar Mar 01 '25 02:03 nicoverali

@vincesp I found how to fix this: fix-PR

@ScriptedAlchemy correct me if I'm wrong, but when we bundle the producer code we need to know what the url will be so that it's prefixed to all asset requests. With rspack I had to add this in order for the bundle to work.

{
  output: {
    publicPath: "http://localhost:2000/",
  }
}

Also this option will depend on the bundler and the plugin you are using, for example for Vite is:

{
  base:  "http://localhost:2000/"
}

Now what is confusing for me it's the option getPublicPath that we can define in the ModuleFederationOptions, because I thought that that should take care of everything, and in fact when using it I notice that the manifest.json gets this field included:

{ 
  "getPublicPath": "function() { return \"http://localhost:2000/\" }"
}

But this only makes the host query the first resource correctly, once it fetches that resource and it runs, if it has not been bundled correctly it will contain relative path references to other chunks or assets.

I proved this by setting up the following configuration in rspack

{
  entry: "./src/index.tsx",
  output: {
    uniqueName: "dynamicWidgets",
    publicPath: "http://localhost:9999/",
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "dynamicWidgets",
      getPublicPath: 'function() { return "http://localhost:2000/" }',
      exposes: {
        "./Widget": "./src/Widget.tsx",
      },
      shared: ["react", "react-dom"],
    }),
  ],
}

See how I'm basically defining port 9999 for the ouput, which will make the bundle use that as prefix, but the manifest will keep containing the getPublicPath function pointing to 2000, thus, when the host tries to import a module, it fetches the first asset correctly because of the getPublicPath but then that asset fails because it tries to fetch at port 9999

nicoverali avatar Mar 01 '25 02:03 nicoverali

The deployment URL neither of the producer nor of the consumer is known at build time. The information from where to load the producer is provided at runtime dynamically.

While it is hard-coded in this little example, in a realistic scenario, this will be provided through a REST call at runtime. loadRemote() uses this information to build the absolute path of the manifest, and it also must use it for all other paths referenced by the manifest, the same way it does it when using a remoteEntry.js.

vincesp avatar Mar 01 '25 07:03 vincesp

publicPath: "auto" is how we resolve this in rspack and webpack. then it will use the remotes base url for chunk loading and not a relative path like "/" - you dont need to hardcode public path.

"auto" is used in all the examples of module-federation-examples repo, under the hood it uses document.currentScript.src to resolve its own absolute path automatically.

ScriptedAlchemy avatar Mar 11 '25 17:03 ScriptedAlchemy

I added the publicPath: 'auto' option to the rsbuild example in my repo, it still is not working. https://github.com/vincesp/stackblitz-starters-h6gf2a5j/blob/996032b6db558e5657aab50fffb3ba44a73462f1/mf-remote-rsbuild/rsbuild.config.mjs#L6

vincesp avatar Mar 14 '25 10:03 vincesp

My attempts so far:

Result Build tool Format
:heavy_check_mark: Vite + Rollup module
:x: Vite + Rollup manifest
:x: rslib mf = var
:x: rslib manifest

Apparently, rslib cannot create a module build, only a var build.

:heavy_check_mark: = dependencies are resolved relative to the entry :x: = dependencies are resolved relative to the shell application

vincesp avatar Mar 14 '25 13:03 vincesp

There's bugs in esm outputof rspack with federation. I'm working on resolving those for react router rsbuild plugin. My workaround has been to skip using the rsbuild mf plugin and use federation directly added to tools.rspack.plugins

ScriptedAlchemy avatar Mar 15 '25 09:03 ScriptedAlchemy

My attempts so far:

Result Build tool Format
:heavy_check_mark: Vite + Rollup module
:x: Vite + Rollup manifest
:x: rslib mf = var
:x: rslib manifest

Apparently, rslib cannot create a module build, only a var build.

:heavy_check_mark: = dependencies are resolved relative to the entry :x: = dependencies are resolved relative to the shell application

Check docs. Rsbuild defines path different. That's webpscks style. Search for public path in docs and you'll see. Right now that doesn't do anything afik. It's supposed to be assetPrefix or something if I recall

ScriptedAlchemy avatar Mar 15 '25 09:03 ScriptedAlchemy

https://rsbuild.dev/config/dev/asset-prefix

ScriptedAlchemy avatar Mar 18 '25 20:03 ScriptedAlchemy

I am building for prod, so I used https://rsbuild.dev/config/output/asset-prefix instead.

I pushed the changes. With that, I now get:

Result Build tool Format
:heavy_check_mark: Vite + Rollup module
:x: Vite + Rollup manifest
:x: rslib mf = var
:heavy_check_mark:* rslib manifest

*Important: rslib with manifest only works if it is loaded before rslib as var. Otherwise, it won't load and will not even give any error. For that reason, there are buttons in the app now to load each remote separately. Also note additional error messages in the console.

vincesp avatar Mar 20 '25 13:03 vincesp

var remotes need to use correct name. hyphens are not valid.

when i console.log it seems to work

import './style.css'
import markdownit from 'markdown-it'
import {
  registerRemotes,
  loadRemote,
} from '@module-federation/enhanced/runtime'

const remotes = [
  {
    name: 'js-remote',
    entry: 'http://localhost:8765/remoteEntry.js',
    type: 'module',
  },
  {
    name: 'mf-remote',
    entry: 'http://localhost:8766/mf-manifest.json',
  },
  {
    name: 'js_remote_rsbuild',
    entry: 'http://localhost:8767/remoteEntry.js',
    type: 'script',
  },
  {
    name: 'mf-remote-rsbuild',
    entry: 'http://localhost:8768/mf-manifest.json',
  },
]

const exports = {
  'js-remote': 'jsMessage',
  'mf-remote': 'mfMessage',
  'js-remote-rsbuild': 'jsRsMessage',
  'mf-remote-rsbuild': 'mfRsMessage',
}

async function startRemote(remote) {
  try {
    registerRemotes([remote])
    console.log(await loadRemote(remote.name))
    const { [exports[remote.name]]: message } = await loadRemote(remote.name)
    document.querySelector('#result').innerHTML += message
  } catch (e) {
    console.error(e)
    document.querySelector('#result').innerHTML += `
<h2 class="error">${remote.name}</h2>
<pre>
${e}
</pre>
`
  } finally {
    console.log('------------------', remote.name)
  }
}

const md = markdownit()
const result = md.render('# Hello Host!')
document.querySelector('#app').innerHTML = result

for (const remote of remotes) {
  const button = document.createElement('button')
  button.innerText = `Load ${remote.name}`
  button.onclick = () => startRemote(remote)
  document.querySelector('#app').appendChild(button)
}

console.log('------------------')

ScriptedAlchemy avatar Mar 20 '25 20:03 ScriptedAlchemy

Image

ScriptedAlchemy avatar Mar 20 '25 20:03 ScriptedAlchemy

Hello @vincesp , I recently had a similar issue to yours and I was able to solve it by setting in the rsbuild.config the assetPrefix in the output configuration as "auto"

output: {
   assetPrefix: 'auto'
}

linjie997 avatar Mar 21 '25 14:03 linjie997

I fixed the names.

With that, I now get:

Result Build tool Format
:heavy_check_mark: Vite + Rollup module
:x: Vite + Rollup manifest
:heavy_minus_sign: rslib module
:heavy_check_mark: rslib "mf" = var
:heavy_check_mark: rslib manifest

Conclusion:

  • Error messages could be improved overall
  • The broken manifest build seems to be a problem of @module-federation/vite only
  • The module build is a missing feature with @module-federation/rsbuild-plugin

vincesp avatar Mar 22 '25 11:03 vincesp

Esm in rsbuild is not supported currently via the rsbuild plugin. If you use module federation plugin directly and add it to tools.rspack.plugins then you can configure it to work. I had to fix bugs in rust code which prevented proper functioning in rspack but that's now merged but might not be released yet. You can see how it did it in the react router rsbuild example. There's a rspack-contrib org with a react router plugin that I have in its own react router repo. There's a PR open that shows you how I configure rspack directly in rsbuild.

Regarding error messages or docs improvement. A pull request would be appreciated since you know what the error should say that wouldbhave helped you more than me. Authors writing docs is hard but users know what they need to know. So I can edit a PR with extra info if you send me one so it helps others in the future.

ScriptedAlchemy avatar Mar 23 '25 01:03 ScriptedAlchemy

I am not completely sure yet how I would approach the docs, maybe you could clarify first:

  • If we do ng add @angular-architects/module-federation it recommends using native federation
  • The @softarc/native-federation README refers to @gioboa/vite-module-federation for the Vite plugin
  • That module is deprectated, and it further refers to @module-federation/vite
  • If I understand it correctly though, @module-federation/vite cannot build native federation remotes, but only the "old" module federation variant
  • @softarc/native-federation has no reference to an rspack plugin

Should we use module federation or native federation for new projects going forward? What is the list of projects supporting native federation for Vite, rspack, with Vue, Angular, …?

vincesp avatar Mar 24 '25 09:03 vincesp

Id suggest ng-rspack as its from NX and uses rspack. For angular related things.

ScriptedAlchemy avatar Mar 24 '25 22:03 ScriptedAlchemy

And for non-Angular?

vincesp avatar Mar 25 '25 07:03 vincesp

Module-federation/vite can build v2. It uses my runtime, but i think that some scenarios cause issues for users - since the bundler itself has no support and the maintainers have to perform a lot of workarounds to try and get it to behave.

Since you need module factories and chunk generation. Which rollup doesn't really have apis for.

Native federation is a separate thing assume its completely incompatible - it can offer a limited subset of the functionality v1 had. No semver sharing, tho. And import maps shim is very slow + violates many companies CSP. Since it's basically running swc in the browser to recompile code in user machines, then inject it as blob.

I'm not sure what you mean by non angular. Generally, anything that's rspack or rsbuild based is the recommended approach since we own both federation and the rspack ecosystem. Our tools can generate remotes. Anyone can consume our remotes via the runtime. Producing a remote is more challenging, and two-way sharing can also be limited unless you use loadShare to load shared module the vanilla way instead of import from.

Our official recommendation is to use rspack derived ecosystem. Since it's backed by one of the biggest tech companies and has resources.

I'll be focusing on the rust side to address various constraints with esm outputs and federation that I've encountered as well as address rsbuild federation ecosystem as well to support the other build targets since that's not a real hard blocker. It's mainly that it needs refactoring after I fixed other issues last week in the rust end for remix support which relied on esm.

Rolldown will have builtin support for federation. Many of the team is ex-rspack so I imagine it'll work well for vite. But I'm not sure when rolldown will be ready for prime time. It will also depend on coordination with vite / rolldown devs to sync new capabilities that I add to the ecosystem.

I can only speak for webpack and rspack since I'm a maintainer of both these compilers. Others are not maintained by core team, and we have never used these implementations, nor do we know anything about them other than they implement our runtime specification.

Core teams' own usage of federation is either they standalone runtime or via webpack or rspack.

Community who maintains third party integrations send us PRs or contact us if they need assistance with something or alterations to our core. We are happy to accommodate as long as that changes do not impact ByteDance business ops or products. Our primary focus is to support the business requirements of byte, the runtime is bundler agnostic because we need to support rspack and webpack, and the MFE team is not familiar with rust so keeping as much of federation as a library allows us to ship updates to both without having to release new rust binaries each time.

ScriptedAlchemy avatar Mar 25 '25 10:03 ScriptedAlchemy

Stale issue message

github-actions[bot] avatar May 24 '25 15:05 github-actions[bot]

Not stale

vincesp avatar May 24 '25 17:05 vincesp

Stale issue message

github-actions[bot] avatar Jul 24 '25 15:07 github-actions[bot]