Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nextjs-mf): add support for Next.js App Router #2002

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not modify /enhanced in order to fix next.js

If you need to do this, we should add a hook or some option that we can pass down from next.js instead.

/enhanced needs to remain as pure as it can and not patch framework specific issues but rather bugs in webpack etc (like single runtime chunk) X

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, maybe we can add "exclude" option to allow users control whether add our federation runtime entry file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ScriptedAlchemy @ahabhgk How about adding exclude options to allow users control whether add our federation runtime entry file in rspack/webpack ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah im good with that

if (
!entryItem.import.includes(entryFilePath) &&
entryItem.layer !== 'rsc' // TODO: remove this when adding support for RSC
) {
entryItem.import.unshift(entryFilePath);
}
});
Expand Down
13 changes: 7 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 @@ -257,16 +258,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
15 changes: 14 additions & 1 deletion packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import type { Compiler } from 'webpack';
import { getWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import CopyFederationPlugin from '../CopyFederationPlugin';
import { exposeNextjsPages } from '../../loaders/nextPageMapLoader';
import { retrieveDefaultShared, applyPathFixes } from './next-fragments';
import {
retrieveDefaultShared,
applyPathFixes,
hasAppDir,
} from './next-fragments';
import { setOptions } from './set-options';
import {
validateCompilerOptions,
Expand Down Expand Up @@ -127,6 +131,15 @@ export class NextFederationPlugin {
isServer: boolean,
): ModuleFederationPluginOptions {
const defaultShared = retrieveDefaultShared(isServer);

if (hasAppDir(compiler)) {
// These shared deps cause issues with the appDir. Any ideas around this?
delete defaultShared['react'];
delete defaultShared['react/'];
delete defaultShared['react-dom'];
delete defaultShared['react-dom/'];
}

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

describe('hasAppDir', () => {
it('should return true if app directory exists', () => {
const compiler = {
options: {
resolve: { alias: { 'private-next-app-dir': 'test' } },
},
};
// @ts-ignore - not a full `compiler` object
expect(hasAppDir(compiler)).toEqual(true);
});
});
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 @@ -21,6 +22,7 @@ export const retrieveDefaultShared = (isServer: boolean): SharedObject => {
if (isServer) {
return DEFAULT_SHARE_SCOPE;
}

// If the code is running on the client/browser, always bundle Next.js internals
return DEFAULT_SHARE_SCOPE_BROWSER;
};
Expand Down Expand Up @@ -58,10 +60,16 @@ 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/];
}
});
}
});
};

export const hasAppDir = (compiler: Compiler) => {
return Object.keys(compiler.options.resolve.alias || {}).includes(
'private-next-app-dir',
);
};