Skip to content

Commit

Permalink
feat(nextjs-mf): add support for Next.js App Router
Browse files Browse the repository at this point in the history
  • Loading branch information
prasannamestha committed Jan 21, 2024
1 parent 73eb07e commit fd98cb2
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ class FederationRuntimePlugin {
// TODO: maybe set this variable as constant is better https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L176
entryItem.import = ['./src'];
}
if (!entryItem.import.includes(entryFilePath)) {
if (
!entryItem.import.includes(entryFilePath) &&
entryItem.layer !== 'rsc' // TODO: remove this when adding support for RSC
) {
entryItem.import.unshift(entryFilePath);
}
});
Expand Down
27 changes: 21 additions & 6 deletions packages/nextjs-mf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ This plugin enables Module Federation on Next.js

## Supports

- next ^13 || ^12(?)
- next ^14 || ^13 || ^12(?)
- SSR included!
- App router included (experimental)

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

## Usage in Next.js App Router

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`).

```js
new NextFederationPlugin({
name: '',
filename: '',
remotes: {},
exposes: {},
shared: ['next/navigation'], // This is important!
});
```

## Demo

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

import Sample from 'next2/sampleComponent';
```
## RuntimePlugins

## RuntimePlugins

To provide extensibility and "middleware" for federation, you can refer to `@module-federation/runtime`

```js
// next.config.js
new NextFederationPlugin({
runtimePlugins: [
require.resolve('./path/to/myRuntimePlugin.js')
]
})
runtimePlugins: [require.resolve('./path/to/myRuntimePlugin.js')],
});
```

## Utilities
Expand Down
3 changes: 2 additions & 1 deletion packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ export class NextFederationPlugin {
compiler: Compiler,
isServer: boolean,
): ModuleFederationPluginOptions {
const defaultShared = retrieveDefaultShared(isServer);
const defaultShared = retrieveDefaultShared(isServer, this._options.shared);

const noop = this.getNoopPath();
return {
...this._options,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { retrieveDefaultShared } from './next-fragments';

describe('retrieveDefaultShared', () => {
it('should return empty object if "next/navigation" is shared', () => {
const defaultShared = retrieveDefaultShared(false, ['next/navigation']);
expect(defaultShared).toMatchObject({});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { container, Compiler } from 'webpack';
import type {
ModuleFederationPluginOptions,
Shared,
SharedObject,
} from '@module-federation/utilities';
import {
Expand All @@ -15,12 +16,23 @@ import { hasLoader, injectRuleLoader } from '../../loaders/helpers';
* @param {boolean} isServer - Boolean indicating if the code is running on the server.
* @returns {SharedObject} The default share scope based on the environment.
*/
export const retrieveDefaultShared = (isServer: boolean): SharedObject => {
export const retrieveDefaultShared = (
isServer: boolean,
shared?: Shared,
): SharedObject => {
// If the code is running on the server, treat some Next.js internals as import false to make them external
// This is because they will be provided by the server environment and not by the remote container
if (isServer) {
return DEFAULT_SHARE_SCOPE;
}

if (Array.isArray(shared)) {
if (shared.includes('next/navigation')) {
// Disable default shared scope for Next.js app router.
// This is needed to prevent errors related to react-dom
return {};
}
}
// If the code is running on the client/browser, always bundle Next.js internals
return DEFAULT_SHARE_SCOPE_BROWSER;
};
Expand Down Expand Up @@ -58,7 +70,7 @@ export const applyPathFixes = (compiler: Compiler, options: any) => {
if (rule?.oneOf) {
//@ts-ignore
rule.oneOf.forEach((oneOfRule) => {
if (hasLoader(oneOfRule, 'react-refresh-utils')) {
if (hasLoader(oneOfRule, 'react-refresh-utils') && oneOfRule.exclude) {
oneOfRule.exclude = [oneOfRule.exclude, /universe\/packages/];
}
});
Expand Down

0 comments on commit fd98cb2

Please sign in to comment.