I am trying to create a Proof of Concept of a federated app, that is a microfrontend based app, where:
- the
hostis a Remix app, which federation is configured using@module-federation/vite - the
remoteis the mfe to be consumed that I generate with just Vite and@module-federation/vitemodule.
The core idea is to have a Remix app that loads a mfe when navigating to a particular route, in my test it is /mfe.
While on the mfe side everything is working fine and I can create the mfe file correctly, I cannot make the host Remix app build when I add the federation plugin to its vite.config.ts.
Here is the error, please note that I want to keep ssr: true in host app (which is Remix default value):
vite v5.4.10 building SSR bundle for production...
✓ 0 modules transformed.
x Build failed in 12ms
[add-entry] ENOENT: no such file or directory, open 'C:\DEV\poc_remix\virtual:remix\server-build'
at Object.readFileSync (node:fs:441:20)
at Object.buildStart (C:\DEV\poc_remix\node_modules\@module-federation\vite\lib\index.cjs:109:41)
at file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:20789:40
at async Promise.all (index 8)
at async PluginDriver.hookParallel (file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:20717:9)
at async file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:21664:13
at async catchUnfinishedHookActions (file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:21135:16)
at async rollupInternal (file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:21661:5)
at async Module.build (file:///C:/DEV/poc_remix/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:65443:14)
at async viteBuild (C:\DEV\poc_remix\node_modules\@remix-run\dev\dist\vite\build.js:212:5) {
errno: -4058,
code: 'PLUGIN_ERROR',
syscall: 'open',
path: 'C:\\DEV\\poc_remix\\virtual:remix\\server-build',
pluginCode: 'ENOENT',
plugin: 'add-entry',
hook: 'buildStart'
}
Inspired by official module federation examples, here are my configuration files.
The host vite.config.ts config file:
export default defineConfig({
plugins: [
federation({
name: "remix_host",
remotes: {
mfe: {
type: "module",
name: "mfe",
entry: "http://localhost:3000/mfe.js",
},
},
exposes: {},
filename: "mfe.js",
shared: {
react: { singleton: true },
},
}),
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
routes: (defineRoutes) =>
defineRoutes((route) => {
route("/", "routes/home.tsx", { index: true });
route("/mfe", "routes/test-mfe.tsx");
}),
}),
tsconfigPaths(),
],
build: {
target: "chrome89",
},
});
The remote mfe vite.config.ts config file:
export default defineConfig({
plugins: [
federation({
name: "mfe",
filename: "mfe.js",
exposes: {
"./mfeButton": "./src/mfeButton",
},
remotes: {},
shared: {
react: {
singleton: true,
},
},
}),
react(),
],
build: {
target: "chrome89",
},
server: {
port: 3000,
},
});
The host app package.json deps sections:
"dependencies": {
"@module-federation/vite": "^1.1.5",
"@remix-run/node": "^2.13.1",
"@remix-run/react": "^2.13.1",
"@remix-run/serve": "^2.13.1",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@remix-run/dev": "^2.13.1",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"autoprefixer": "^10.4.19",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.38",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1"
},
The remote mfe package.json deps section:
"dependencies": {
"@module-federation/vite": "^1.1.5",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
}
Here is the test-mfe.tsx:
/* eslint-disable import/no-unresolved */
import { lazy } from "react";
const RemoteMfeButton = lazy(
async () =>
// @ts-expect-error loading remote
await import("mfe/mfeButton")
);
export default function TestMFE() {
return (
<>
<p>I AM A MFE</p>
<RemoteMfeButton />
</>
);
}
In the host vite.config.ts file I tried:
- drop all Remix future flag
- add/remove
entryGlobalName: 'remote'andshareScope: 'default'infederationconfiguration - add/remove
manifest: truetofederationconfiguration - reordering plugin list within the config
But error did not change. If I remove federation config, then Remix app builds normally.
My questions are:
- Is there any Remix limitation to work with
@module-federation/vitethat I am not aware of? - Perhaps the limitation is on
@module-federation/viteside, and not in Remix? - The error happens on the
server-buildso perhaps I should find a way of isolating federation build step only for the client side build? If so, any idea on how to do that?
I see these possible workarounds, but I would like to avoid them:
- having Remix loading an external module built in plain Vite, so that I can leverage
@module-federation/vitewithout mixing it up with Remix. Such module will be thehostand will actually just bridge theremotemfe loading - manage the mfe loading directly, basically writing a very custom module federation plugin for
host. Something that I would like to avoid.