From 6ab8e3f5611249f3463d48950919db59f6765cb0 Mon Sep 17 00:00:00 2001
From: Zack Jackson
Date: Mon, 25 Jan 2021 17:50:39 -0800
Subject: [PATCH] update plugin
---
README.md | 150 ++++++++++++++++++++++++++++++++------
bin/nextjs-mf.js | 102 ++++++++++++++++++++++++++
example/README.md | 12 ---
example/package.json | 25 -------
example/public/index.html | 5 --
example/src/App.js | 12 ---
example/src/Button.js | 5 --
example/src/bootstrap.js | 5 --
example/src/index.js | 1 -
example/webpack.config.js | 49 -------------
index.js | 8 +-
package.json | 20 +++--
patchSharing.js | 17 +++++
react-dom.js | 11 +++
react.js | 11 +++
withModuleFederation.js | 62 ++++++++++++++++
16 files changed, 351 insertions(+), 144 deletions(-)
create mode 100755 bin/nextjs-mf.js
delete mode 100644 example/README.md
delete mode 100644 example/package.json
delete mode 100644 example/public/index.html
delete mode 100644 example/src/App.js
delete mode 100644 example/src/Button.js
delete mode 100644 example/src/bootstrap.js
delete mode 100644 example/src/index.js
delete mode 100644 example/webpack.config.js
create mode 100644 patchSharing.js
create mode 100644 react-dom.js
create mode 100644 react.js
create mode 100644 withModuleFederation.js
diff --git a/README.md b/README.md
index 5e5d2f7..836278c 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,141 @@
-# Module Federation Runtime Chunk Merging
+# Module Federation For Next.js
-This plugin makes module federation work as expected when using
-`runtimeChunk:"single"` as an optimization tactic.
+This plugin enables Module Federation on Next.js
-## Usage
+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](https://github.com/webpack/webpack/issues/11811).
+
+### Supports
+
+- next ^9.5.6
+- SSG
+- SSR
+
+**Once I PR webpack, this workaround will no longer be required.**
+
+# Check out our book
+
+| | We will be actively updating this book over the next year as we learn more about best practices and what issues people are running into with Module Federation, as well as with every release of Webpack as it moves towards a release candidate and release. So with your one purchase you are buying a whole year of updates. |
+| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+
+#### Demo
+
+You can see it in action here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs
+
+## How to use on a fresh nextjs app
+
+```sh
+yarn global add @module-federation/nextjs-mf
+```
+
+Run this inside of a fresh nextjs install.
+
+```sh
+nextjs-mf upgrade -p 3001
+```
+
+## How to use on an existing app
+
+1. Use `withModuleFederation` in your `next.config.js`
```js
-plugins: [
- new ModuleFedSingleRuntimePlugin(),
- new ModuleFederationPlugin({
- name: "app2",
- filename: "remoteEntry.js",
+// next.config.js
+const { withModuleFederation } = require("@module-federation/nextjs-mf");
+const path = require("path");
+
+module.exports = {
+ webpack: (config, options) => {
+ const { buildId, dev, isServer, defaultLoaders, webpack } = options;
+ const mfConf = {
+ mergeRuntime: true, //this is experimental, read below
+ name: "next2",
+ library: { type: config.output.libraryTarget, name: "next2" },
+ filename: "static/runtime/remoteEntry.js",
+ remotes: {
+ // For SSR, resolve to disk path (or you can use code streaming if you have access)
+ next1: isServer
+ ? path.resolve(
+ __dirname,
+ "../next1/.next/server/static/runtime/remoteEntry.js"
+ )
+ : "next1", // for client, treat it as a global
+ },
exposes: {
- "./Button": "./src/Button",
+ "./nav": "./components/nav",
},
- shared: { react: { singleton: true }, "react-dom": { singleton: true } },
- }),
- new HtmlWebpackPlugin({
- template: "./public/index.html",
- }),
- ]
+ shared: ["lodash"],
+ };
+ // Configures ModuleFederation and other Webpack properties
+ withModuleFederation(config, options, mfConf);
+
+ return config;
+ },
+};
```
-# Example
+2. Add the `patchSharing` to `_document.js`. This will solve the react sharing issue.
-Can be found in the `/example` directory
+```jsx
+import Document, { Html, Head, Main, NextScript } from "next/document";
+import { patchSharing } from "@module-federation/nextjs-mf";
+class MyDocument extends Document {
+ static async getInitialProps(ctx) {
+ const initialProps = await Document.getInitialProps(ctx);
+ return { ...initialProps };
+ }
+ render() {
+ return (
+
+ {patchSharing()}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+```
-### Supports
-Webpack 5 with Federated Modules
+3. Use top-level-await
-# Check out our book
+```js
+// some-component.js
+const Nav = (await import("../components/nav")).default;
+const _ = await import("lodash");
+```
-| | We will be actively updating this book over the next year as we learn more about best practices and what issues people are running into with Module Federation, as well as with every release of Webpack as it moves towards a release candidate and release. So with your one purchase you are buying a whole year of updates. |
-| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+## Experimental
+
+Use at your own risk.
+
+Next.js uses `runtimeChunk:'single'`
+Which forces us to also add the webpack script itself. Till this is fixed in webpack, heres a plugin that will merge the runtimes back together for MF
+
+This can be enabled via `mergeRuntime` flag. This is not part of Module Federation, its part of this plugin.
+
+`withModuleFederation(config, options, {mergeRuntime:true,...mfConf})`
+
+You can manually add it as follows
+
+```js
+const { MergeRuntime } = require("@module-federation/nextjs-mf");
+// in your next config.
+config.plugins.push(new MergeRuntime({ filename: "remoteEntry" }));
+```
+
+This allows the following to be done
+
+```diff
+-
+-
++
+```
diff --git a/bin/nextjs-mf.js b/bin/nextjs-mf.js
new file mode 100755
index 0000000..5a6be5a
--- /dev/null
+++ b/bin/nextjs-mf.js
@@ -0,0 +1,102 @@
+#!node
+const { Command } = require("commander");
+const program = new Command();
+const path = require("path");
+const fs = require("fs");
+
+program.version(require(path.resolve(__dirname, "../package.json")).version);
+
+const nextConfigJS = ({ name, port }) => `const {
+ withModuleFederation,
+ MergeRuntime,
+} = require("@module-federation/nextjs-mf");
+const path = require("path");
+
+module.exports = {
+ webpack: (config, options) => {
+ const { buildId, dev, isServer, defaultLoaders, webpack } = options;
+ const mfConf = {
+ name: "${name}",
+ library: { type: config.output.libraryTarget, name: "${name}" },
+ filename: "static/runtime/remoteEntry.js",
+ remotes: {
+ // test1: isServer
+ // ? path.resolve(
+ // __dirname,
+ // "../test1/.next/server/static/runtime/remoteEntry.js"
+ // )
+ // : "test1", // for client, treat it as a global
+ },
+ exposes: {},
+ shared: [],
+ };
+
+ // Configures ModuleFederation and other Webpack properties
+ withModuleFederation(config, options, mfConf);
+
+ config.plugins.push(new MergeRuntime());
+
+ if (!isServer) {
+ config.output.publicPath = "http://localhost:${port}/_next/";
+ }
+
+ return config;
+ },
+};`;
+
+const documentJS = () => `import Document, { Html, Head, Main, NextScript } from "next/document";
+import { patchSharing } from "@module-federation/nextjs-mf";
+
+class MyDocument extends Document {
+ static async getInitialProps(ctx) {
+ const initialProps = await Document.getInitialProps(ctx);
+ return { ...initialProps };
+ }
+
+ render() {
+ return (
+
+ {patchSharing()}
+ {/* */}
+
+
+
+
+
+ );
+ }
+}
+
+export default MyDocument;`;
+
+const upgrade = ({ port: p }) => {
+ const port = p || 3000;
+
+ const nextJSMFpkgJSON = JSON.parse(
+ fs.readFileSync(path.resolve(__dirname, "../package.json")).toString()
+ );
+
+ // Upgrade package.json
+ const pkgJSON = JSON.parse(fs.readFileSync("package.json").toString());
+ const name = pkgJSON.name;
+ pkgJSON.resolutions = {
+ webpack: "5.1.3",
+ };
+ pkgJSON.scripts.dev = `next dev -p ${port}`;
+ pkgJSON.dependencies[
+ "@module-federation/nextjs-mf"
+ ] = `^${nextJSMFpkgJSON.version}`;
+ fs.writeFileSync("package.json", JSON.stringify(pkgJSON, null, 2));
+
+ fs.writeFileSync("next.config.js", nextConfigJS({ name, port }));
+ fs.writeFileSync("pages/_document.js", documentJS({ name, port }));
+};
+
+program
+ .command("upgrade")
+ .description("upgrade the NextJS instance in the current directory")
+ .option("-p, --port ", "port")
+ .action(upgrade);
+
+program.parse(process.argv);
diff --git a/example/README.md b/example/README.md
deleted file mode 100644
index d5afaf8..0000000
--- a/example/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Example
-
-run one of these
-
-```json
- "scripts": {
- "start": "webpack-cli serve --open",
- "build": "webpack --mode production",
- "serve": "serve dist -p 5002",
- "clean": "rm -rf dist"
- }
-```
diff --git a/example/package.json b/example/package.json
deleted file mode 100644
index f60bddd..0000000
--- a/example/package.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "@basic-host-remote/app2",
- "version": "0.0.0",
- "private": true,
- "devDependencies": {
- "@babel/core": "7.12.10",
- "@babel/preset-react": "7.12.10",
- "babel-loader": "8.2.2",
- "html-webpack-plugin": "4.5.1",
- "serve": "11.3.2",
- "webpack": "5.14.0",
- "webpack-cli": "4.4.0",
- "webpack-dev-server": "3.11.2"
- },
- "scripts": {
- "start": "webpack-cli serve --open",
- "build": "webpack --mode production",
- "serve": "serve dist -p 5002",
- "clean": "rm -rf dist"
- },
- "dependencies": {
- "react": "^16.13.0",
- "react-dom": "^16.13.0"
- }
-}
diff --git a/example/public/index.html b/example/public/index.html
deleted file mode 100644
index 350e8ec..0000000
--- a/example/public/index.html
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/example/src/App.js b/example/src/App.js
deleted file mode 100644
index b549d3a..0000000
--- a/example/src/App.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import LocalButton from "./Button";
-import React from "react";
-
-const App = () => (
-