module-federation / vite

Vite Plugin for Module Federation
MIT License
342 stars 27 forks source link

HMR Does not work #183

Open harrisKAsher opened 1 week ago

harrisKAsher commented 1 week ago

Hi, I am trying to migrate from @originjs/vite-plugin-federation to this as it seems better supported and has HMR support. But I cannot get the HMR to work from the remotes (though the app loads). Currently I am just trying to get this set up between 3 different apps.

This is my host/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';
import dotenv from 'dotenv';
const env = dotenv.config({ path: `../../.env` });

export default defineConfig({
    plugins: [
        federation({
            name: 'host',
            remotes: {
                core: {
                    type: 'module',
                    name: 'core',
                    entry: `${env.parsed?.VITE_CORE_URL}/assets/remoteEntry.js`,
                    entryGlobalName: 'core',
                    shareScope: 'default'
                },
                globalStore: {
                    type: 'module',
                    name: 'globalStore',
                    entry: `${env.parsed?.VITE_GLOBAL_STORE_URL}/assets/remoteEntry.js`,
                    entryGlobalName: 'globalStore',
                    shareScope: 'default'
                }
            },
            filename: 'assets/remoteEntry.js',
            shared: ['react', 'react-dom', 'react-router-dom']
        }),
        react()
    ],
    build: {
        modulePreload: false,
        target: 'esnext',
        minify: false,
        cssCodeSplit: false
    }
});

This is my core/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';
import dotenv from 'dotenv';
const env = dotenv.config({ path: `../../.env` });

export default defineConfig({
    plugins: [
        federation({
            name: 'core',
            filename: 'assets/remoteEntry.js',
            remotes: {
                globalStore: {
                    type: 'module',
                    name: 'globalStore',
                    entry: `${env.parsed?.VITE_GLOBAL_STORE_URL}/assets/remoteEntry.js`,
                    entryGlobalName: 'globalStore',
                    shareScope: 'default'
                }
            },
            exposes: {
                './App': './src/App.tsx'
            },
            shared: ['react', 'react-dom', 'react-router-dom']
        }),
        react()
    ],
    build: {
        modulePreload: false,
        target: 'esnext',
        minify: false,
        cssCodeSplit: false
    }
});

My globalStore/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';

export default defineConfig({
    plugins: [
        federation({
            name: 'globalStore',
            filename: 'assets/remoteEntry.js',
            exposes: {
                './Store': './src/store.tsx'
            },
            shared: ['react', 'react-dom', 'react-router-dom']
        }),
        react()
    ],
    build: {
        modulePreload: false,
        target: 'esnext',
        minify: false,
        cssCodeSplit: false
    }
});

The globalStore shouldn't matter as right now I am just trying to get the HMR working between Host and Core and have basically made my App.tsx to look like this in core. I have to keep globalStore in my config though as otherwise it will break as other components need it even though they aren't imported anywhere.

import React, { useEffect } from 'react';

export default function App() {
    return (
        <div>Core App Test</div>
    );
}

And in Host its this

import React from 'react';
const Core = React.lazy(() => import('core/App'));

export default function App() {
    return (
        <>
            <Core />
        </>
    );
}

So as you can see, just about as barebones as it gets, and yet HMR does not work. Though in the console I do see this

[vite] hot updated: /src/App.tsx [client:223:18](http://localhost:3100/@vite/client)

And in the network tab I can see this, which even includes the changes I made to core/App.tsx! Which just baffels me that they are there, yet it will not update them on my host.

import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/App.tsx");import __vite__cjsImport0_react_jsxDevRuntime from "/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=f6c91154"; const jsxDEV = __vite__cjsImport0_react_jsxDevRuntime["jsxDEV"];
import RefreshRuntime from "/@react-refresh";
const inWebWorker = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
let prevRefreshReg;
let prevRefreshSig;
if (import.meta.hot && !inWebWorker) {
    if (!window.__vite_plugin_react_preamble_installed__) {
        throw new Error("@vitejs/plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201");
    }
    prevRefreshReg = window.$RefreshReg$;
    prevRefreshSig = window.$RefreshSig$;
    window.$RefreshReg$ = (type, id) => {
        RefreshRuntime.register(type, "/Users/MyUserName/Projects/MyApp/apps/core/src/App.tsx " + id);
    };
    window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
}
export default function Index() {
    return /* @__PURE__ */ jsxDEV("div", { children: "Core App Test Change" }, void 0, false, {
        fileName: "/Users/MyUserName/Projects/MyApp/apps/core/src/App.tsx",
        lineNumber: 25,
        columnNumber: 5
    }, this);
}
_c = Index;
var _c;
$RefreshReg$(_c, "Index");
if (import.meta.hot && !inWebWorker) {
    window.$RefreshReg$ = prevRefreshReg;
    window.$RefreshSig$ = prevRefreshSig;
}
if (import.meta.hot && !inWebWorker) {
    RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
        RefreshRuntime.registerExportsForReactRefresh("/Users/MyUserName/Projects/MyApp/apps/core/src/App.tsx", currentExports);
        import.meta.hot.accept((nextExports) => {
            if (!nextExports) return;
            const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate("/Users/MyUserName/Projects/MyApp/apps/core/src/App.tsx", currentExports, nextExports);
            if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
        });
    });
}

Either I am missing or something is broken. If you need further information from me please let me know. Thank you for the support.

gioboa commented 1 week ago

Hi, you can use this plugin to update your remotes in the host application when something is changing

harrisKAsher commented 1 week ago

Hi, I am not sure how that helps? I thought this package was suppose to work with HMR by default? I didn't see anything in the Documentation that I needed to use a separate plugin to get HMR.

That being said, I am not sure what your are suggesting what I do with that plugin, would it be putting something like this in my host/vite.cofnig.ts?

ViteRestart({
    restart: [`${env.parsed?.VITE_CORE_URL}/assets/remoteEntry.js`]
})

If so, that doesn't work.

gioboa commented 1 week ago

Yep, you need to add this plugin in the host vite configuration. It takes the file system paths to watch

harrisKAsher commented 1 week ago

Okay... That is a touch disappointing as I was hoping there would be a way to have the reset just happens. The way your describing would require me to have every developer edit the path so that it points to their location on their machine.

I guess to elaborate a bit more, my hope was to have my host app allow connections from other teams and be hosted on an AWS S3 Bucket. Then we could point the Host at the remote localhost and we could develop that way so other teams didn't have to have access to the host app. But I am assuming that is not possible, correct? (Just a note, I did test that concept with @originjs/vite-plugin-federation, it works, just couldn't do HMR.)

ScriptedAlchemy commented 1 week ago

Im not sure if vite ever natively supported hmr, as far as i know fast refresh and hmr only work in webpack and rspack based builds as a built-in solution.

zhangHongEn commented 1 week ago

The built-in HMR in Vite works well. You can see it in action by starting both the host and remote in dev mode.

zhangHongEn commented 1 week ago

Your host is in build mode, and the remote is in dev mode. For this scenario, additional configuration is required.

zhangHongEn commented 1 week ago
1.  React must use the development version.
2.  react-refresh should be a singleton.
3.  Install React Chrome DevTools.
ScriptedAlchemy commented 1 week ago

Does my chrome dev tools for federation work with this package? Id so we have a module proxy that comes with fast refresh and hmr baked in. Allows us to hmr and develop against production with a local override

zhangHongEn commented 1 week ago

Does my chrome dev tools for federation work with this package?

yes


Can it bypass the rules of React Refresh?

ScriptedAlchemy commented 1 week ago

When you enable hmr switch a runtime plugin is added that uses resolve share hook and injects react runtime and dev versions from unpkg

zhangHongEn commented 1 week ago

When you enable hmr switch a runtime plugin is added that uses resolve share hook and injects react runtime and dev versions from unpkg

Then it should theoretically be supported already. I’ll check the shared configuration soon and see how to adapt it to work with @vitejs/plugin-react.

@harrisKAsher Have you tried the Module Federation Chrome extension?

zhangHongEn commented 1 week ago
image image

@ScriptedAlchemy Didn’t see unpkg.

ScriptedAlchemy commented 1 week ago

Does a prod and dev mix work tho? Give it a try. What i know is the devtool implements a dev react to bypass mixing issues. Check if the chrome tools runtime plugin is added in the federation globals. It might be something we just import and add in our plugin. Not sure if extention injects it or if we auto register it on our side

harrisKAsher commented 1 week ago

@zhangHongEn I just installed the Module Federation Chrome extension to try it out and no matter what I do I get told "No ModuleInfo Detected".

ScriptedAlchemy commented 1 week ago

@zhangHongEn I just installed the Module Federation Chrome extension to try it out and no matter what I do I get told "No ModuleInfo Detected".

Did you use the json remote protocol. Not the js one

harrisKAsher commented 1 week ago

No, I am using the js one. I tried just renaming the entry to be remoteEntry.json but that just results in a JSON parse error. I have also added manifest: true to each of my configs as the error message suggested that could be the problem. But it seems to be my files are still being generated as a js file not a json file. (I will include the error below in case it helps).

Error: [ Federation Runtime ]: [ Federation Runtime ]: [ Federation Runtime ]: Failed to get manifest.
args: {"manifestUrl":"http://localhost:3110/assets/remoteEntry.json","moduleName":"globalStore"}
https://module-federation.io/guide/troubleshooting/runtime/RUNTIME-003
Original Error Message:
 SyntaxError: JSON.parse: unexpected character at line 2 column 3 of the JSON data
ScriptedAlchemy commented 1 week ago

The docs state you have to use the json protocol. A js file should emit alongside something like mf-manifest.json

zhangHongEn commented 1 week ago

@harrisKAsher mf-manifest.json https://module-federation.io/configure/manifest.html

harrisKAsher commented 1 week ago

Okay, I think I am making progress.

Here is what my host one looks like now, the core and globalStore one looks really similar.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';
import dotenv from 'dotenv';
const env = dotenv.config({ path: `../../.env` });

export default defineConfig({
    plugins: [
        federation({
            name: 'host',
            manifest: {
                fileName: 'mf-manifest.json'
            },
            remotes: {
                core: `core@${env.parsed?.VITE_CORE_URL}/mf-manifest.json`,
                globalStore: `globalStore@${env.parsed?.VITE_GLOBAL_STORE_URL}/mf-manifest.json`
            },
            shared: {
                react: {
                    singleton: true
                },
                'react-dom': {
                    singleton: true
                },
                'react-router-dom': {
                    singleton: true
                }
            }
        }),
        react()
    ],
    build: {
        modulePreload: false,
        target: 'esnext',
        minify: false,
        cssCodeSplit: false
    }
});

But when I try to run this it gets stuck in an infinite loop stating [ Federation Runtime ]: The remote "core" is already registered. If you want to merge the remote, you can set "force: true". Says the same thing about globalStore as well.

And in case it helps, this is what mf-manifest.json looks like on host.

{
  "id": "host",
  "name": "host",
  "metaData": {
    "name": "host",
    "type": "app",
    "buildInfo": {
      "buildVersion": "1.0.0",
      "buildName": "host"
    },
    "remoteEntry": {
      "name": "remoteEntry-[hash]",
      "path": "",
      "type": "module"
    },
    "ssrRemoteEntry": {
      "name": "remoteEntry-[hash]",
      "path": "",
      "type": "module"
    },
    "types": {
      "path": "",
      "name": ""
    },
    "globalName": "host",
    "pluginVersion": "0.2.5",
    "publicPath": "/"
  },
  "shared": [
    {
      "id": "host:react",
      "name": "react",
      "version": "18.3.1",
      "requiredVersion": "^18.3.1",
      "assets": {
        "js": {
          "async": [],
          "sync": []
        },
        "css": {
          "async": [],
          "sync": []
        }
      }
    },
    {
      "id": "host:react-dom",
      "name": "react-dom",
      "version": "18.3.1",
      "requiredVersion": "^18.3.1",
      "assets": {
        "js": {
          "async": [],
          "sync": []
        },
        "css": {
          "async": [],
          "sync": []
        }
      }
    }
  ],
  "remotes": [
    {
      "federationContainerName": "http://localhost:3110/mf-manifest.json",
      "moduleName": "Store",
      "alias": "globalStore",
      "entry": "*"
    },
    {
      "federationContainerName": "http://localhost:3100/mf-manifest.json",
      "moduleName": "App",
      "alias": "core",
      "entry": "*"
    }
  ],
  "exposes": []
}
ScriptedAlchemy commented 1 week ago

As far as i know, vite doesnt support circular imports, like remote importing host exposed modules, its one-way - currently rspack/webpack implementations have omnidirectional support

Is a remote trying to import something from host?

harrisKAsher commented 1 week ago

No, it shouldn't be.

Host loads GlobalStore and Core Core load GlobalStore GlobalStore loads nothing else

When I was using @originjs/vite-plugin-federation I was able to have core load itself to get the GlobalStore, but I separated it out for this as it was clearly wasn't working, but when I just use remoteEntry.js without the mf-manifest.json it loads just fine.

zhangHongEn commented 1 week ago

@harrisKAsher Could you provide a reproducible example? I plan to resolve the circular references and other related issues that are causing the loop.​

zhangHongEn commented 1 week ago

For example, I found that @module-federation/bridge-vue3 also causes Vite to call init multiple times. I need to make it more robust.

ScriptedAlchemy commented 1 week ago

Multiple initializations aren't a problem. It's a warning, but there's no real danger. Next.js does it too.

It's probably circular remote imports, which I'm not sure if Vite just doesn't work with cyclic host remotes or if it doesn't work with cyclic remotes at all.

zhangHongEn commented 1 week ago

https://github.com/module-federation/vite/blob/166052450d6b556431da32adf81452d33b756c32/src/virtualModules/virtualRemoteEntry.ts#L133

Here’s the faulty code; it leads to an infinite loop.

ScriptedAlchemy commented 1 week ago

Okay so in our bundlers we track if it was already initialized, so circular remotes do not infinitely initialize

zhangHongEn commented 1 week ago

I haven’t yet thought carefully about how to log it without polluting the global scope.

harrisKAsher commented 1 week ago

@zhangHongEn Do you still need a reproducible example?

ScriptedAlchemy commented 1 week ago

https://github.com/module-federation/core/blob/main/packages/webpack-bundler-runtime/src/initializeSharing.ts

I also export a createContainer function from my packages which just uses webpacks actual mechanics directly for initialization. https://github.com/module-federation/core/blob/main/packages/webpack-bundler-runtime/src/container.ts

we do not need to do it on global scope

  // handling circular init calls
  var initToken = initTokens[shareScopeName];
  if (!initToken)
    initToken = initTokens[shareScopeName] = { from: mfInstance.name };
  if (initScope.indexOf(initToken) >= 0) return;
  initScope.push(initToken);

Note that create container works in other bundlers. This was how remote entry generation in esbuild and I think rollup work

zhangHongEn commented 1 week ago

@zhangHongEn Do you still need a reproducible example?

I haven’t reproduced the infinite loop myself, but it indeed needs optimization. No example is needed.

harrisKAsher commented 1 week ago

Okay, well I was pretty close to done, so I just finished it up. Here it is in case it helps.

https://github.com/harrisKAsher/Federation-Vite-Demo

ScriptedAlchemy commented 1 week ago

The init token is the main aspect that most miss in the runtime implementation. The init scope is passed through all remotes as another argument so everyone knows what everyone else has so far.

harrisKAsher commented 1 week ago

Sorry, not sure I follow, what is the init token? I can't find anything in the docs about such a thing.

ScriptedAlchemy commented 1 week ago

Sorry, not sure I follow, what is the init token? I can't find anything in the docs about such a thing.

Speaking to maintainers in thread. Sorry!

harrisKAsher commented 1 week ago

Oh, okay. Thought it was a direct response to me. 😅

Also, another thing I am encountering (not sure its related or not) is in a remote app when I import a component from a package (ComponentA) that internally imports another component (ComponentB) from a dependent package, it fails to load the entire federated remote app. If I already import ComponentB in the remote app then everything works. This issue doesn't occur when running I go to the remote apps endpoint and view it outside the federated instance. It also doesn't happen if I do a build and preview it in the Federated Instance. I have tried a bunch of different adjustments to make ComponentA and/or ComponentB shared with the vite config but none that changes anything.

ScriptedAlchemy commented 1 week ago

I'm not sure about that one.

Good litmus test is to try it with rspack or rsbuild (our vite-like experience).

If the same setup works with rspack/rsbuild, then it's a problem with vite implementation.

If it doesn't, then it's a bug in our runtime.

gioboa commented 1 week ago

Okay, well I was pretty close to done, so I just finished it up. Here it is in case it helps.

https://github.com/harrisKAsher/Federation-Vite-Demo

Thanks for sharing, I'll copy your example in the repo to show how to do it

harrisKAsher commented 4 days ago

I'm not sure about that one.

Good litmus test is to try it with rspack or rsbuild (our vite-like experience).

If the same setup works with rspack/rsbuild, then it's a problem with vite implementation.

If it doesn't, then it's a bug in our runtime.

So I was trying to reproduce this and I cannot do it without one thing: getting from a private npm repository. My organization has an internal package of components to make things uniform across the various web applications we make. Its kind of similar to HeadlessUI. Anyway, that is what is breaking the app, and some of them are pretty simple. Literally some of them are just div's with styles on them. But it seems that if an internal package relies on another internal package that is when it breaks.

My only theory is it is breaking because of how the federation in dev mode can't get the internal package for some reason? Not really sure how to verify that though as I am not that familiar with internal packages. I tried with other packages that are similar such as HeadlessUI but I could not get it to break in the same way.

talkohavy commented 4 days ago

I was reading intently thus far 👀 waiting to see a solution on this.

Everything @harrisKAsher described is happening to me as well. Line by line. Word for word.

Here's is my example, which is very much similar to his: vite-micro-frontends-react. Like him, I kept things very basic & simple (although my case has 1 parent host & 2 child remotes, no grand-children, 1 child is on build (appears on host's "remotes"), and the other child is dynamically imported).

zhangHongEn commented 4 days ago

I will prioritize solving this problem as the top priority when I have time

---- Replied Message ---- | From | @.> | | Date | 11/19/2024 07:20 | | To | module-federation/vite @.> | | Cc | zhn @.>, Mention @.> | | Subject | Re: [module-federation/vite] HMR Does not work (Issue #183) |

I was reading intently thus far 👀 waiting to see a solution on this.

@.*** described is happening to me as well. Line by line. Word for word.

Here's is my example, which is very much similar to his: vite-micro-frontends-react. Like him, I kept things very basic & simple (although my case has 1 parent host & 2 child remotes, no grand-children, 1 child is on build (appears on host's "remotes"), and the other child is dynamically imported).

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>

ScriptedAlchemy commented 4 days ago

So for me to support. I'll require a rspack or webpack based attempt. Then i can help. Otherwise we will need to rely on our community partners who maintain vite to proxy the needs back to me from a runtime perspective.

Core team only allocated resources to

We can wait for @zhangHongEn to take a look and contact Core team, but just letting yall know where my support kicks in. 🥰🥰

ScriptedAlchemy commented 4 days ago

Is it like missing css or is there something else like missing JS modules. Missing css would be tailwind tree shake classes it can't see. That can be solved with allow list. If it's missing modules on the other hand. That's something we need to investigate etc

zhangHongEn commented 4 days ago

This issue mentions two problems:

  1. Infinite loop
  2. React HMR (Hot Module Replacement)
ScriptedAlchemy commented 4 days ago

Just a heads up. You can contact me on Twitter / bluesky

I have a LLC dedicated to NDA signing operations for such cases. Would allow you to send me a zip of your node modules and all that I can just extract and run without any liabilities on either end.

Pain in the ass but we can pull that never IF it's needed. On my end it's not a problem. Sign a few hundred a year haha 😄

zhangHongEn commented 4 days ago

Thanks to @ScriptedAlchemy for providing a solution approach, which allows me to implement it without checking the pack code; the only thing missing is time.

1: Solution is here 2: I haven’t seen the Chrome plugin successfully injecting React

ScriptedAlchemy commented 4 days ago

Thanks to @ScriptedAlchemy for providing a solution approach, which allows me to implement it without checking the pack code; the only thing missing is time.

1: Solution is here 2: I haven’t seen the Chrome plugin successfully injecting React

Ahhh time. The great evil of all mankind 🫠

gioboa commented 3 days ago

Thanks @ScriptedAlchemy for your core help. this means a lot for this project. Can I suggest to use the official MF public channel Using this channel we avoid lack of alignment.

talkohavy commented 3 days ago

Alright guys, here's the news. I'll start from the end:

HMR does work for rsbuild ✅ HMR doesn't work for @module-federation/vite... ❌

I've created a small project that illustrates the 2 bugs: one is the infinite loop, and the other is the HMR not working.

The project: module-federation-with-vite-vs-rsbuild

Steps to reproduce:

HMR works with Rsbuild

  1. checkout from master to a branch called rsbuild
  2. Navigate to apps/booksMF, and run:
pnpm install
pnpm rsdev
  1. Navigate to apps/host, and run:
pnpm install
pnpm rsdev

the app will open on http://localhost:3000

  1. In your IDE, open file booksMF/src/components/App/App.tsx, and change the text of the h1 from "Vite + React" to whatever, and see that the HMR works properly.

HMR doesn't works with Vite

  1. Stop the rsbuild of both host and books servers.
  2. Navigate to apps/booksMF, and run:
pnpm dev
  1. Navigate to apps/host, and run:
pnpm dev

the app will open on http://localhost:3000

The result: an infinite loop! the app will never appear, and you'll see a white screen.

Different Approach (where vite's wepabb works but HMR doesn't):

  1. Open up the vite.config.ts of booksMF, and replace manifest: true, (line 15) with filename: 'remoteEntry.js', (line 16). Uncomment one and comment out the other. Now, in vite.config.ts of host, change entry: 'http://localhost:3001/mf-manifest.json', to entry: 'http://localhost:3001/remoteEntry.js',.

Result: try running both projects now with pnpm dev again - you'll succeed! But if you'll open up the file booksMF/src/components/App/App.tsx, and change something in it, you'll find that the HMR doesn't work.

harrisKAsher commented 3 days ago

Is it like missing css or is there something else like missing JS modules. Missing css would be tailwind tree shake classes it can't see. That can be solved with allow list. If it's missing modules on the other hand. That's something we need to investigate etc

I am assuming this in response to my importing issue from a private repository for packages? I have realized thats probably a separate issue so I have made a new issue here. But just for the record, there is missing css, but that's not the problem as the components my organization makes uses styled components. So I believe it is missing the module.