Skip to content

Commit d5d909c

Browse files
feat: delegate modules for next.js (#2756)
1 parent 227b0c5 commit d5d909c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1395
-168
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ This repository is to showcase examples of how Webpack 5's new Module Federation
1515

1616

1717
https://module-federation.github.io/
18-
18+
1919
https://www.youtube.com/playlist?list=PLWSiF9YHHK-DqsFHGYbeAMwbd9xcZbEWJ
20-
20+
2121
https://scriptedalchemy.medium.com/
2222

2323
# Examples
@@ -56,8 +56,9 @@ https://scriptedalchemy.medium.com/
5656
- [x] [NextJS Sidecar Build](./nextjs-sidecar/README.md) — Sidecar build to enable module-federation alongside Next codebases.
5757
- [x] [NextJS v12](./nextjs-v12/README.md) — Operation, with [nextjs-mf](https://www.npmjs.com/package/@module-federation/nextjs-mf).
5858
- [x] [NextJS v13](./nextjs-v13/README.md) — Operation, with [nextjs-mf](https://www.npmjs.com/package/@module-federation/nextjs-mf).
59-
- [x] [NextJS](./nextjs/README.md) — Operation, with [nextjs-mf](https://app.privjs.com/buy/packageDetail?pkg=@module-federation/nextjs-mf).
60-
- [x] 💰[NextJS SSR](./nextjs-ssr/README.md) — Powered by software streams, with [nextjs-ssr](https://app.privjs.com/buy/packageDetail?pkg=@module-federation/nextjs-ssr)
59+
- [x] [NextJS](./nextjs/README.md) — Operation, with [nextjs-mf](https://github.com/module-federation/universe).
60+
- [x] [NextJS SSR](./nextjs-ssr/README.md) — Powered by software streams, with [nextjs-ssr](https://github.com/module-federation/universe)
61+
- [x] [NextJS SSR via Delegates](./nextjs-ssr-delegate-modules/README.md) — Custom glue code for containers and hosts [nextjs-ssr](https://github.com/module-federation/universe)
6162
- [x] [Building A Plugin-based Workflow Designer With Angular and Module Federation](https://github.com/manfredsteyer/module-federation-with-angular-dynamic-workflow-designer) — External Example
6263
- [x] [Vue.js](./vue3-demo/README.md) — Simple host/remote (render function / sfc) example using Vue 3.0.
6364
- [x] [Vue 2 in Vue 3](./vue2-in-vue3/README.md) — Vue 3 application loading remote Vue 2 component.
@@ -68,7 +69,7 @@ https://scriptedalchemy.medium.com/
6869
- [x] [vue3-demo-federation-with-vite](./vue3-demo-federation-with-vite/README.md) — wepack and vite federation integrated projects, webpack/vite both play the role of host and remote
6970
- [x] [quasar-cli-vue3-webpack-javascript](./quasar-cli-vue3-webpack-javascript/README.md) — Module federation integration with Quasar apps running vue3 using quasar-cli (javascript)
7071
- [x] [UMD Federation](./umd-federation) — Support importing umd remote module
71-
72+
7273
**Module Federation Examples** covered by e2e tests with **Cypress** framework, more info about structure and configuration 👉 [here](./cypress/README.md) 👈
7374

7475
# Check out our book

delegate-modules/app1/remote-delegate.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ module.exports = new Promise((resolve, reject) => {
1111
__webpack_error__.name = 'ScriptExternalLoadError';
1212
__webpack_error__.stack = event.stack;
1313
reject(__webpack_error__);
14-
}, global
14+
},
15+
global,
1516
);
1617
})
1718

nextjs-ssr-delegate-modules/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Getting Started
2+
To start, run yarn start and navigate to http://localhost:3001 or another available port.
3+
4+
# Consulting Services Available
5+
For consultations, contact Zackary Jackson at zackary.l.jackson@gmail.com or via Twitter at @ScriptedAlchemy.
6+
7+
# How it Works
8+
This implementation uses our proprietary Software Streams technology to stream commonjs modules at runtime to consuming apps. This technology has not been made publicly available, and we have kept it a guarded secret for the past 2 years despite using it for multiple backend systems.
9+
10+
For the client side, we have enhanced federation interfaces to ensure that the top-level API works as expected, allowing for import(), require, and import from to function. While the serverside has been tested, only import() has been tested on the client side.
11+
12+
An alpha version of Software Streams was leaked a year and a half ago, but it contains security flaws. The federation group has since spent a significant amount of time enhancing the technology. In the future, after the plugin exits beta, we plan to implement stream encryption to prevent code manipulation. This will be achieved through a cypher key known to both the consumer and remote at build time.
13+
14+
We are also exploring the possibility of executing streamed software in a WASM isolate that has limited access to host resources, making it possible to execute untrusted code.
15+
16+
For now, we strongly advise federating only trusted software between servers.
17+
18+
## Security
19+
To ensure proper functionality, the commonjs modules are exposed via _next/static/ssr*. For security reasons, it is recommended to have a CDN or middleware in place that only allows access to this path from an internal network or VPN. Exposing server code publicly through this path can pose a security risk as process.browser is not applied to tree shake server secrets.
20+
21+
## Context
22+
23+
We have three Next.js applications:
24+
25+
- `checkout` on port 3000
26+
- `home` on port 3001
27+
- `shop` on port 3002
28+
29+
These applications use omnidirectional routing, enabling pages and components to be federated between them like in a SPA.
30+
31+
The use of hooks ensures that multiple copies of React are not loaded into the server or client. The omnidirectional routing also hooks into webpack federation loading functions, allowing dynamic loading of remotes using the same functions as those used for static imports (e.g. home/title).
32+
33+
### Sharing
34+
35+
Next.js has pre-shared internal modules through `@module-federation/nextjs-mf`. However, you need to share React through the plugin to ensure that the shared scope runtime requirements are included. Modules that are shared extra must be processed by the plugin, which reconfigures sharing to work within the limitations of Next.js.
36+
37+
The sharing limit is due to Next.js having no async boundary, making it impossible to "pause" the application while webpack manages the shared scope. The author is exploring new methods that could potentially solve the module sharing issue in Next.js, but this is a complex challenge requiring extensive knowledge of webpack and federation within the module graph.
38+
39+
# Delegate Modules
40+
41+
Delegate modules in module federation offer a flexible solution for creating custom connection code between federated modules.
42+
Compared to using the promise `new Promise syntax`, delegate modules are bundled into webpack and can utilize `require` and `import` statements.
43+
This makes them ideal for handling complex requirements such as loading remote modules or customizing the federation API.
44+
However, it's important to note that delegate modules are an advanced feature and may not be necessary for the majority of users.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React, { useEffect } from 'react';
2+
const ExportredTitle = () => {
3+
console.log('---------loading remote component from checkout---------');
4+
useEffect(() => {
5+
console.log('HOOKS WORKS');
6+
}, []);
7+
return (
8+
<div className="hero">
9+
<h1 className="title">
10+
{' '}
11+
This came fom <code>checkout</code> !!!
12+
</h1>
13+
<p className="description">And it works like a charm v2</p>
14+
</div>
15+
);
16+
};
17+
18+
export default ExportredTitle;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const NextFederationPlugin = require('@module-federation/nextjs-mf');
2+
const {createDelegatedModule} = require('@module-federation/utilities');
3+
4+
const remotes = isServer => {
5+
const location = isServer ? 'ssr' : 'chunks';
6+
return {
7+
home: createDelegatedModule(require.resolve('./remote-delegate.js'), {
8+
remote: `home@http://localhost:3001/_next/static/${location}/remoteEntry.js`
9+
}),
10+
shop: createDelegatedModule(require.resolve('./remote-delegate.js'), {
11+
remote: `shop@http://localhost:3002/_next/static/${location}/remoteEntry.js`
12+
}),
13+
checkout: createDelegatedModule(require.resolve('./remote-delegate.js'), {
14+
remote: `checkout@http://localhost:3000/_next/static/${location}/remoteEntry.js`
15+
}),
16+
// home: `home@http://localhost:3001/_next/static/${location}/remoteEntry.js`,
17+
// shop: `shop@http://localhost:3002/_next/static/${location}/remoteEntry.js`,
18+
// checkout: `checkout@http://localhost:3000/_next/static/${location}/remoteEntry.js`,
19+
};
20+
};
21+
22+
module.exports = {
23+
webpack(config, options) {
24+
25+
config.plugins.push(
26+
new NextFederationPlugin({
27+
name: 'checkout',
28+
filename: 'static/chunks/remoteEntry.js',
29+
exposes: {
30+
'./title': './components/exposedTitle.js',
31+
'./checkout': './pages/checkout.js',
32+
'./pages-map': './pages-map.js',
33+
},
34+
remotes: remotes(options.isServer),
35+
extraOptions:{
36+
automaticAsyncBoundary: true
37+
}
38+
}),
39+
);
40+
41+
return config;
42+
},
43+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "delegate-checkout",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "rm -rf .next && next dev",
7+
"build": "next build",
8+
"start": "NODE_ENV=production next start"
9+
},
10+
"dependencies": {
11+
"@module-federation/nextjs-mf": "6.1.1",
12+
"@module-federation/utilities": "1.3.0",
13+
"lodash": "4.17.21",
14+
"next": "13.1.6",
15+
"react": "^18.2.0",
16+
"react-dom": "^18.2.0",
17+
"webpack": "5.72.1"
18+
}
19+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
'/checkout': './checkout',
3+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {Suspense,lazy} from "react";
2+
import App from 'next/app';
3+
import dynamic from 'next/dynamic';
4+
const Nav = dynamic(() => {
5+
return import('home/nav');
6+
},{suspense:true});
7+
8+
function MyApp({ Component, pageProps }) {
9+
return (
10+
<>
11+
<Suspense fallback={'loading'}>
12+
<Nav />
13+
</Suspense>
14+
<Component {...pageProps} />
15+
</>
16+
);
17+
}
18+
19+
MyApp.getInitialProps = async ctx => {
20+
const appProps = await App.getInitialProps(ctx);
21+
return appProps;
22+
};
23+
export default MyApp;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Document, { Html, Head, Main, NextScript } from "next/document";
2+
import React from "react";
3+
import { revalidate, FlushedChunks, flushChunks } from "@module-federation/nextjs-mf/utils";
4+
5+
6+
class MyDocument extends Document {
7+
static async getInitialProps(ctx) {
8+
if(process.env.NODE_ENV === "development" && !ctx.req.url.includes("_next")) {
9+
await revalidate().then((shouldReload) =>{
10+
if (shouldReload) {
11+
ctx.res.writeHead(302, { Location: ctx.req.url });
12+
ctx.res.end();
13+
}
14+
});
15+
} else {
16+
ctx?.res?.on("finish", () => {
17+
revalidate()
18+
});
19+
}
20+
21+
const chunks = await flushChunks()
22+
23+
const initialProps = await Document.getInitialProps(ctx);
24+
return {
25+
...initialProps,
26+
chunks
27+
};
28+
}
29+
30+
render() {
31+
32+
return (
33+
<Html>
34+
<Head>
35+
<meta name="robots" content="noindex" />
36+
<FlushedChunks chunks={this.props.chunks} />
37+
</Head>
38+
39+
<body className="bg-background-grey">
40+
<Main />
41+
<NextScript />
42+
</body>
43+
</Html>
44+
);
45+
}
46+
}
47+
48+
export default MyDocument;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react';
2+
import Head from 'next/head';
3+
4+
const Checkout = props => (
5+
<div>
6+
<Head>
7+
<title>checkout</title>
8+
<link rel="icon" href="/nextjs-ssr/checkout/public/favicon.ico" />
9+
</Head>
10+
11+
<div className="hero">
12+
<h1>checkout page</h1>
13+
<h3 className="title">This is a federated page owned by localhost:3000</h3>
14+
<span>
15+
{' '}
16+
Data from federated <pre>getInitalProps</pre>
17+
</span>
18+
<br />
19+
<pre>{JSON.stringify(props, null, 2)}</pre>
20+
</div>
21+
<style jsx>{`
22+
.hero {
23+
width: 100%;
24+
color: #333;
25+
}
26+
.title {
27+
margin: 0;
28+
width: 100%;
29+
padding-top: 80px;
30+
line-height: 1.15;
31+
font-size: 20px;
32+
}
33+
.title,
34+
.description {
35+
text-align: center;
36+
}
37+
`}</style>
38+
</div>
39+
);
40+
Checkout.getInitialProps = async () => {
41+
const swapi = await fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json());
42+
console.log(swapi);
43+
console.log('swapi');
44+
return swapi;
45+
};
46+
export default Checkout;

0 commit comments

Comments
 (0)