This plugin enables Module Federation on Next.js
This is a workaround to hard limitations caused by Next.js being synchronous.
I am working on an update to Webpack Core which will circumvent projects with older architecture (like Next.js).
This is a stable and viable workaround to leverage Module Federation until this issue is resolved.
- next ^10.2.x
- Client side only
Once I PR webpack, this workaround will no longer be required.
You can see it in action here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs (needs to be updated)
The current version supports NextJS 10.2. If you start with a new NextJS project (11.0 or newer) you need to downgrade to 10.2 by specifying the version in your package.json for both "next1" and "next2".
"dependencies": {
...
"next": "10.2.3",
...
},
Then you need to remove your node_modules
folder and reinstall dependencies:
$ cd next1
$ rm -rf node_modules/
$ yarn
Finally install the plugin as a dependency:
$ yarn add @module-federation/nextjs-mf
- Use
withFederatedSidecar
in yournext.config.js
of the app that you wish to expose modules from. We'll call this "next2".
// next.config.js
const { withFederatedSidecar } = require("@module-federation/nextjs-mf");
module.exports = withFederatedSidecar({
name: "next2",
filename: "static/chunks/remoteEntry.js",
exposes: {
"./sampleComponent": "./components/sampleComponent.js",
},
shared: {
react: {
// Notice shared are NOT eager here.
requiredVersion: false,
singleton: true,
}
},
})({
future: {
webpack5: true
},
// your original next.config.js export
});
- For the consuming application, we'll call it "next1", add an instance of the ModuleFederationPlugin to your webpack config:
module.exports = {
webpack(config) {
config.plugins.push(
new options.webpack.container.ModuleFederationPlugin({
remoteType: "var",
remotes: {
next2: "next2",
},
shared: {
react: {
// Notice shared ARE eager here.
eager: true,
singleton: true,
requiredVersion: false,
}
},
})
);
return config;
},
};
- Make sure you have an
_app.js
file, then add the loader
// we attach next internals to share scope at runtime
config.module.rules.push({
test: /_app.js/,
loader: "@module-federation/nextjs-mf/lib/federation-loader.js",
});
Note that your next.config.js
for "next1" should look similar to below:
const {
withFederatedSidecar,
federationLoader,
} = require("@module-federation/nextjs-mf");
const deps = require("./package.json").dependencies;
module.exports = withFederatedSidecar({
name: "next1",
filename: "static/chunks/remoteEntry.js",
exposes: {},
shared: {
react: {
// Notice shared are NOT eager here.
requiredVersion: false,
singleton: true,
},
},
})({
// your original next.config.js export
future: {
webpack5: true,
},
webpack(config, options) {
const { webpack } = options;
config.experiments = { topLevelAwait: true };
config.module.rules.push({
test: /_app.js/,
loader: "@module-federation/nextjs-mf/lib/federation-loader.js",
});
config.plugins.push(
new webpack.container.ModuleFederationPlugin({
remoteType: "var",
remotes: {
next2: "next2",
},
shared: {
react: {
// Notice shared ARE eager here.
eager: true,
singleton: true,
requiredVersion: false,
},
},
})
);
return config;
},
});
- Add the remote entry for "next2" to the
pages/_document.js
for "next1"
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<script src="http://next2-domain-here.com/_next/static/chunks/remoteEntry.js" />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
- Use next/dynamic to import from your remotes
import dynamic from "next/dynamic";
...
const SampleComponent = dynamic(() => import("next2/sampleComponent"), {
ssr: false,
});