Next.js
This plugin enables Module Federation on Next.js
Supports
- next ^14 || ^13 || ^12
- SSR included!
I highly recommend referencing this application which takes advantage of the best capabilities:
https://github.com/module-federation/module-federation-examples
This project supports federated SSR
Whats shared by default?
Under the hood we share some next internals automatically
You do not need to share these packages, sharing next internals yourself will cause errors.
See DEFAULT_SHARE_SCOPE:
export const DEFAULT_SHARE_SCOPE: SharedObject = {
'next/dynamic': {
eager: false,
requiredVersion: false,
singleton: true,
import: undefined,
},
'next/head': {
eager: false,
requiredVersion: false,
singleton: true,
import: undefined,
},
'next/link': {
eager: true,
requiredVersion: false,
singleton: true,
import: undefined,
},
'next/router': {
requiredVersion: false,
singleton: true,
import: false,
eager: false,
},
'next/script': {
requiredVersion: false,
singleton: true,
import: undefined,
eager: false,
},
react: {
singleton: true,
requiredVersion: false,
eager: false,
import: false,
},
'react-dom': {
singleton: true,
requiredVersion: false,
eager: false,
import: false,
},
'react/jsx-dev-runtime': {
singleton: true,
requiredVersion: false,
import: undefined,
eager: false,
},
'react/jsx-runtime': {
singleton: true,
requiredVersion: false,
eager: false,
import: false,
},
'styled-jsx': {
requiredVersion: false,
singleton: true,
import: undefined,
eager: false,
},
'styled-jsx/style': {
requiredVersion: false,
singleton: true,
import: undefined,
eager: false,
},
};
Requirement
I set process.env.NEXT_PRIVATE_LOCAL_WEBPACK = 'true' inside this plugin, but its best if its set in env or command line export.
"Local Webpack" means you must have webpack installed as a dependency, and next will not use its bundled copy of webpack which cannot be used as i need access to all of webpack internals
NEXT_PRIVATE_LOCAL_WEBPACK=true next dev or next build
npm install webpack
Usage
import React, { lazy } from 'react';
const SampleComponent = lazy(() => import('next2/sampleComponent'));
To avoid hydration errors, use React.lazy instead of next/dynamic for lazy loading federated components.
With async boundary installed at the page level. You can then do the following
const SomeHook = require('next2/someHook');
import SomeComponent from 'next2/someComponent';
Demo
You can see it in action here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs-ssr
Options
This plugin works exactly like ModuleFederationPlugin, use it as you'd normally.
Note that we already share react and next stuff for you automatically.
Also NextFederationPlugin has own optional argument extraOptions where you can unlock additional features of this plugin:
new NextFederationPlugin({
name: '',
filename: '',
remotes: {},
exposes: {},
shared: {},
extraOptions: {
debug: boolean, // `false` by default
exposePages: boolean, // `false` by default
enableImageLoaderFix: boolean, // `false` by default
enableUrlLoaderFix: boolean, // `false` by default
skipSharingNextInternals: boolean, // `false` by default
},
});
debug – enables debug mode. It will print additional information about what is going on under the hood.
exposePages – exposes automatically all nextjs pages for you and theirs ./pages-map.
enableImageLoaderFix – adds public hostname to all assets bundled by nextjs-image-loader. So if you serve remoteEntry from http://example.com then all bundled assets will get this hostname in runtime. It's something like Base URL in HTML but for federated modules.
enableUrlLoaderFix – adds public hostname to all assets bundled by url-loader.
skipSharingNextInternals – disables sharing of next internals. You can use it if you want to share next internals yourself or want to use this plugin on non next applications
Demo
You can see it in action here: https://github.com/module-federation/module-federation-examples/pull/2147
Implementing the Plugin
- Use
NextFederationPlugin in your next.config.js of the app that you wish to expose modules from. We'll call this "next2".
// next.config.js
// either from default
const NextFederationPlugin = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
const { isServer } = options;
config.plugins.push(
new NextFederationPlugin({
name: 'next2',
remotes: {
next1: `next1@http://localhost:3001/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
},
filename: 'static/chunks/remoteEntry.js',
exposes: {
'./title': './components/exposedTitle.js',
'./checkout': './pages/checkout',
},
shared: {
// whatever else
},
}),
);
return config;
},
};
// next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
const { isServer } = options;
config.plugins.push(
new NextFederationPlugin({
name: 'next1',
remotes: {
next2: `next2@http://localhost:3000/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
},
}),
);
return config;
},
};
- Use react.lazy, low level api, or require/import from to import remotes.
import React, { lazy } from 'react';
const SampleComponent = lazy(() =>
window.next2.get('./sampleComponent').then((factory) => {
return { default: factory() };
}),
);
// or
const SampleComponent = lazy(() => import('next2/sampleComponent'));
//or
import Sample from 'next2/sampleComponent';
RuntimePlugins
To provide extensibility and "middleware" for federation, you can refer to @module-federation/enhanced/runtime
// next.config.js
new NextFederationPlugin({
runtimePlugins: [require.resolve('./path/to/myRuntimePlugin.js')],
});
Utilities
Ive added a util for dynamic chunk loading, in the event you need to load remote containers dynamically.
InjectScript
import { injectScript } from '@module-federation/nextjs-mf/utils';
// if i have remotes in my federation plugin, i can pass the name of the remote
injectScript('home').then((remoteContainer) => {
remoteContainer.get('./exposedModule');
});
// if i want to load a custom remote not known at build time.
injectScript({
global: 'home',
url: 'http://somthing.com/remoteEntry.js',
}).then((remoteContainer) => {
remoteContainer.get('./exposedModule');
});
revalidate
Enables hot reloading of node server (not client) in production.
This is recommended, without it - servers will not be able to pull remote updates without a full restart.
More info here: https://github.com/module-federation/nextjs-mf/tree/main/packages/node#utilities
// __document.js
import { revalidate } from '@module-federation/nextjs-mf/utils';
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
// can be any lifecycle or implementation you want
ctx?.res?.on('finish', () => {
revalidate().then((shouldUpdate) => {
console.log('finished sending response', shouldUpdate);
});
});
return initialProps;
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
For Express.js
Hot reloading Express.js required additional steps: https://github.com/module-federation/universe/blob/main/packages/node/README.md
Contact
If you have any questions or need to report a bug
Reach me on Twitter @ScriptedAlchemy
Or join this discussion thread: https://github.com/module-federation/module-federation-examples/discussions/978