Skip to content

Commit

Permalink
update plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
ScriptedAlchemy committed Jan 26, 2021
1 parent b9e9995 commit 6ab8e3f
Show file tree
Hide file tree
Showing 16 changed files with 351 additions and 144 deletions.
150 changes: 127 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

| <a href="https://module-federation.myshopify.com/products/practical-module-federation" target="_blank"><img src="./docs/MFCover.png" alt='Practical Module Federation Book' width="95%"/></a> | <a href="https://module-federation.myshopify.com/products/practical-module-federation" target="_blank">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.</a> |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

#### 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 (
<Html>
{patchSharing()}
<script src="http://localhost:3000/_next/static/chunks/webpack.js" />
<script src="http://localhost:3000/_next/static/runtime/remoteEntry.js" />
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
```

### 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");
```

| <a href="https://module-federation.myshopify.com/products/practical-module-federation" target="_blank"><img src="./docs/MFCover.png" alt='Practical Module Federation Book' width="95%"/></a> | <a href="https://module-federation.myshopify.com/products/practical-module-federation" target="_blank">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.</a> |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
## 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
- <script src="http://localhost:3000/_next/static/chunks/webpack.js" />
- <script src="http://localhost:3000/_next/static/runtime/remoteEntry.js" />
+ <script src="http://localhost:3000/_next/static/remoteEntryMerged.js" />
```
102 changes: 102 additions & 0 deletions bin/nextjs-mf.js
Original file line number Diff line number Diff line change
@@ -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 (
<Html>
{patchSharing()}
{/* <script src="http://localhost:3000/_next/static/remoteEntryMerged.js" /> */}
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
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>", "port")
.action(upgrade);

program.parse(process.argv);
12 changes: 0 additions & 12 deletions example/README.md

This file was deleted.

25 changes: 0 additions & 25 deletions example/package.json

This file was deleted.

5 changes: 0 additions & 5 deletions example/public/index.html

This file was deleted.

12 changes: 0 additions & 12 deletions example/src/App.js

This file was deleted.

5 changes: 0 additions & 5 deletions example/src/Button.js

This file was deleted.

5 changes: 0 additions & 5 deletions example/src/bootstrap.js

This file was deleted.

1 change: 0 additions & 1 deletion example/src/index.js

This file was deleted.

49 changes: 0 additions & 49 deletions example/webpack.config.js

This file was deleted.

8 changes: 7 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
const withModuleFederation = require("./withModuleFederation");
const patchSharing = require("./patchSharing");
const MergeRuntime = require("./merge-runtime");
module.exports = MergeRuntime
module.exports = {
withModuleFederation,
patchSharing,
MergeRuntime,
};
Loading

0 comments on commit 6ab8e3f

Please sign in to comment.