Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Host preloading script loads assets from incorrect remote when base not explicit for cross domain remotes #242

Open
lmiller1990 opened this issue Jan 16, 2025 · 5 comments

Comments

@lmiller1990
Copy link

I am trying to migrate from webpack to vite. It's mostly working, except one issue, when the host and remote are deployed on separate domains.

Minimal reproduction: https://github.com/lmiller1990/vite-module-federation-bug

There is a bug where, for production builds (not vite dev) the host will preload assets from its own domain. The remote IS able to load exposed assets from the host, though. Weird!

You can reproduce this by running ./reproduction.sh:

chmod +x reproduction.sh
./reproduction.sh

This will do a non minified, production build, and serve both apps using the serve static server.

Visiting the remote, http://localhost:4000, is fine. It loads a remote module exposed from the host, http://localhost:3000, which logs a greeting (see the console).

Visitng the host, http://localhost:3000, however, shows the issue. The preload-helper tries to load the remote asset, such as place.js, from http://localhost:3000/assets/place-DpMnJR6N.js - it should be from http://localhost:4000/assets/place-DpMnJR6N.js.

The problem is more clear when you look at the preload code:

const scriptRel = 'modulepreload';
const assetsURL = function(dep) { return "/" + dep };

All assets are preloaded from /.

This does NOT happen with vite dev. If the domains are the same, it works as expected:

@lmiller1990
Copy link
Author

I am trying to debug it. vitejs/vite#5214 might be a similar issue.

assetsURL actually comes from Vite: https://github.com/vitejs/vite/blob/f2aed62d0bf1b66e870ee6b4aab80cd1702793ab/packages/vite/src/node/plugins/importAnalysisBuild.ts#L101

Weirdly, this one has both a dep and and importerUrl. The bundled one drops the second arg, presumably it is unused, unsure if that is correct / related or a red herring.

@lmiller1990
Copy link
Author

OK, I got it to work by explicitly adding base in my config. Here's the diff: https://github.com/lmiller1990/vite-module-federation-bug/compare/workaround?expand=1

I guess this makes sense - I don't think we had to specify a base in webpack, I guess Vite works a bit differently regarding that. Does webpack somehow infer the base correctly? Can/should this plugin do the same?

In the provided examples:

I am using native ESM, although I don't think it makes a difference; I tried using commonjs and had the same issue.

Either way, not sure if this is a bug, perhaps either a chance for an update to the documentation? Will leave this one open in case it helps someone else!

@lmiller1990 lmiller1990 changed the title Host preloading script loads assets from incorrect remote Host preloading script loads assets from incorrect remote when base not explicit for cross domain remotes Jan 16, 2025
@gioboa
Copy link
Collaborator

gioboa commented Jan 16, 2025

Hi @lmiller1990 thanks for looking into that, I'm glad you solved the issue.
We can create a section in the readme of this project to highlight this case during the migration from webpack..would you like to open a PR for that? Thanks in advance

@dvelluto
Copy link

dvelluto commented Feb 6, 2025

hi @gioboa I encountered the same issue with vite + vue and module federation with manifest configuration.

the issue is that the loadEsmEntry function

async function loadEsmEntry({ entry, remoteEntryExports }) {
    return new Promise((resolve, reject)=>{
        try {
            if (!remoteEntryExports) {
                if (typeof FEDERATION_ALLOW_NEW_FUNCTION !== 'undefined') {
                    new Function('callbacks', `import("${entry}").then(callbacks[0]).catch(callbacks[1])`)([
                        resolve,
                        reject
                    ]);
                } else {
                    import(/* webpackIgnore: true */ /* @vite-ignore */ entry).then(resolve).catch(reject);
                }
            } else {
                resolve(remoteEntryExports);
            }
        } catch (e) {
            reject(e);
        }
    });
}

is using "entry" to load the federated module, but in mf-manifest.json generated you can see that entry is only assets/remoteEntry

"metaData": {
		"name": "federated",
		"type": "app",
		"buildInfo": {
			"buildVersion": "1.0.0",
			"buildName": "federated"
		},
		"remoteEntry": {
			"name": "assets/remoteEntry.js",
			"path": "",
			"type": "module"
		},
		"ssrRemoteEntry": {
			"name": "assets/remoteEntry.js",
			"path": "",
			"type": "module"
		},
		"types": {
			"path": "",
			"name": ""
		},
		"globalName": "federated",
		"pluginVersion": "0.2.5",
		"publicPath": "/"

and in vite.config I have the federation configured as

federation({
        name: 'federated',
        manifest: true,
        exposes: {
          './component': './apps/federated-ui/src/FederatedComponent.vue',
        },
        shared: [
          'vue',
          'pinia',
]
...

Although giving a getPublicPath function and/or providing base can fix partially the issue, in my case it was increasing the problems as I had different places where the federated module could be hosted with different paths.
e.g.
localhost:5000/assets/remoteEntry.js and on cdn http://cdn.server.com/federated/version-102/assets/remoteEntry.js
when loading form remote and I have the host application on the same CDN, but on different path, it would try to load assets/remoteEntry from http://cdn.server.com/assets/remoteEntry.js

I've solved the issue implementing a plugin which attaches to loadEntry

async loadEntry (args) {
      const { remoteInfo, remoteEntryExports } = args

      return new Promise((resolve, reject)=>{
        if (remoteInfo.version == null) {
          reject(new Error('remoteInfo missing version'))
        } else {
          try { 
            const entry = new URL(remoteInfo.version).origin + remoteInfo.entry
              if (!remoteEntryExports) {
                  import(/* @vite-ignore */ entry).then(resolve).catch(reject);
              } else {
                  resolve(remoteEntryExports);
              }
          } catch (e) {
              reject(e);
          }
        }
    });

but I'm not sure about few things like why in remoteInfo.version there's the mf-manifest url ...

@gioboa
Copy link
Collaborator

gioboa commented Feb 6, 2025

Hi @dvelluto thanks for sharing your steps.
Would you like to drop a PR to solve this issue?
You have the reproducible issue on you local machine so you can debug and solve the problem easily. 🤞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants