Skip to content

Commit 99b2cff

Browse files
feat(nextjs-mf): add support for Next.js App Router
1 parent 73eb07e commit 99b2cff

File tree

7 files changed

+76
-80
lines changed

7 files changed

+76
-80
lines changed

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,11 @@
164164
"eslint-plugin-react-hooks": "4.6.0",
165165
"eslint-plugin-simple-import-sort": "10.0.0",
166166
"form-data": "4.0.0",
167+
"graceful-fs": "^4.2.9",
167168
"highlight.js": "10.7.3",
168169
"html-webpack-plugin": "5.6.0",
169170
"husky": "8.0.3",
170-
"inquirer": "9.2.12",
171+
"inquirer": "8.2.6",
171172
"jest": "29.7.0",
172173
"jest-environment-jsdom": "29.7.0",
173174
"jest-environment-node": "29.7.0",
@@ -193,8 +194,10 @@
193194
"qwik-nx": "1.1.1",
194195
"qwik-speak": "0.19.0",
195196
"react-refresh": "0.14.0",
197+
"rimraf": "^3.0.2",
196198
"rollup-plugin-copy": "3.5.0",
197199
"stopword": "2.0.8",
200+
"strip-ansi": "^6.0.0",
198201
"swc-loader": "0.2.3",
199202
"tailwindcss": "3.4.0",
200203
"ts-jest": "29.1.1",
@@ -210,13 +213,10 @@
210213
"vitest": "0.32.4",
211214
"vitest-fetch-mock": "^0.2.2",
212215
"vue-tsc": "1.8.27",
216+
"wast-loader": "^1.11.5",
213217
"webpack": "^5.88.2",
214218
"webpack-virtual-modules": "0.6.1",
215-
"whatwg-fetch": "^3.6.2",
216-
"graceful-fs": "^4.2.9",
217-
"rimraf": "^3.0.2",
218-
"strip-ansi": "^6.0.0",
219-
"wast-loader": "^1.11.5"
219+
"whatwg-fetch": "^3.6.2"
220220
},
221221
"config": {
222222
"commitizen": {

packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ class FederationRuntimePlugin {
151151
// TODO: maybe set this variable as constant is better https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L176
152152
entryItem.import = ['./src'];
153153
}
154-
if (!entryItem.import.includes(entryFilePath)) {
154+
if (
155+
!entryItem.import.includes(entryFilePath) &&
156+
entryItem.layer !== 'rsc' // TODO: remove this when adding support for RSC
157+
) {
155158
entryItem.import.unshift(entryFilePath);
156159
}
157160
});

packages/nextjs-mf/README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ This plugin enables Module Federation on Next.js
1212

1313
## Supports
1414

15-
- next ^13 || ^12(?)
15+
- next ^14 || ^13 || ^12(?)
1616
- SSR included!
17+
- App router included (experimental)
1718

1819
I highly recommend referencing this application which takes advantage of the best capabilities:
1920
https://github.com/module-federation/module-federation-examples
@@ -144,6 +145,20 @@ const SomeHook = require('next2/someHook');
144145
import SomeComponent from 'next2/someComponent';
145146
```
146147

148+
## Usage in Next.js App Router
149+
150+
In order to use module federation in projects with app router, you must pass `next/navigation` as a shared dependency. This will disable sharing default dependencies (`DEFAULT_SHARE_SCOPE`).
151+
152+
```js
153+
new NextFederationPlugin({
154+
name: '',
155+
filename: '',
156+
remotes: {},
157+
exposes: {},
158+
shared: ['next/navigation'], // This is important!
159+
});
160+
```
161+
147162
## Demo
148163

149164
You can see it in action here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs-ssr
@@ -257,16 +272,16 @@ const SampleComponent = lazy(() => import('next2/sampleComponent'));
257272

258273
import Sample from 'next2/sampleComponent';
259274
```
260-
## RuntimePlugins
275+
276+
## RuntimePlugins
277+
261278
To provide extensibility and "middleware" for federation, you can refer to `@module-federation/runtime`
262279

263280
```js
264281
// next.config.js
265282
new NextFederationPlugin({
266-
runtimePlugins: [
267-
require.resolve('./path/to/myRuntimePlugin.js')
268-
]
269-
})
283+
runtimePlugins: [require.resolve('./path/to/myRuntimePlugin.js')],
284+
});
270285
```
271286

272287
## Utilities

packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ export class NextFederationPlugin {
126126
compiler: Compiler,
127127
isServer: boolean,
128128
): ModuleFederationPluginOptions {
129-
const defaultShared = retrieveDefaultShared(isServer);
129+
const defaultShared = retrieveDefaultShared(isServer, this._options.shared);
130+
130131
const noop = this.getNoopPath();
131132
return {
132133
...this._options,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { retrieveDefaultShared } from './next-fragments';
2+
3+
describe('retrieveDefaultShared', () => {
4+
it('should return empty object if "next/navigation" is shared', () => {
5+
const defaultShared = retrieveDefaultShared(false, ['next/navigation']);
6+
expect(defaultShared).toMatchObject({});
7+
});
8+
});

packages/nextjs-mf/src/plugins/NextFederationPlugin/next-fragments.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { container, Compiler } from 'webpack';
22
import type {
33
ModuleFederationPluginOptions,
4+
Shared,
45
SharedObject,
56
} from '@module-federation/utilities';
67
import {
@@ -15,12 +16,23 @@ import { hasLoader, injectRuleLoader } from '../../loaders/helpers';
1516
* @param {boolean} isServer - Boolean indicating if the code is running on the server.
1617
* @returns {SharedObject} The default share scope based on the environment.
1718
*/
18-
export const retrieveDefaultShared = (isServer: boolean): SharedObject => {
19+
export const retrieveDefaultShared = (
20+
isServer: boolean,
21+
shared?: Shared,
22+
): SharedObject => {
1923
// If the code is running on the server, treat some Next.js internals as import false to make them external
2024
// This is because they will be provided by the server environment and not by the remote container
2125
if (isServer) {
2226
return DEFAULT_SHARE_SCOPE;
2327
}
28+
29+
if (Array.isArray(shared)) {
30+
if (shared.includes('next/navigation')) {
31+
// Disable default shared scope for Next.js app router.
32+
// This is needed to prevent errors related to react-dom
33+
return {};
34+
}
35+
}
2436
// If the code is running on the client/browser, always bundle Next.js internals
2537
return DEFAULT_SHARE_SCOPE_BROWSER;
2638
};
@@ -58,7 +70,7 @@ export const applyPathFixes = (compiler: Compiler, options: any) => {
5870
if (rule?.oneOf) {
5971
//@ts-ignore
6072
rule.oneOf.forEach((oneOfRule) => {
61-
if (hasLoader(oneOfRule, 'react-refresh-utils')) {
73+
if (hasLoader(oneOfRule, 'react-refresh-utils') && oneOfRule.exclude) {
6274
oneOfRule.exclude = [oneOfRule.exclude, /universe\/packages/];
6375
}
6476
});

0 commit comments

Comments
 (0)