@module-federation/nextjs-mf 3.0.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +2 -0
- package/.prettierrc +7 -0
- package/README.md +174 -119
- package/lib/NextFederationPlugin.js +479 -0
- package/lib/include-defaults.js +16 -0
- package/lib/index.js +3 -0
- package/lib/loaders/fixImageLoader.js +25 -0
- package/lib/loaders/fixUrlLoader.js +25 -0
- package/lib/loaders/helpers.js +35 -0
- package/lib/loaders/nextPageMapLoader.js +129 -0
- package/lib/utils.js +97 -0
- package/package.json +20 -5
- package/index.js +0 -5
- package/lib/federation-loader.js +0 -1
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/README.md
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
This plugin enables Module Federation on Next.js
|
|
4
4
|
|
|
5
|
-
This is a stable and viable solution to leverage Module Federation [until this issue is resolved](https://github.com/webpack/webpack/issues/11811).
|
|
6
|
-
|
|
7
5
|
### Supports
|
|
8
6
|
|
|
9
|
-
- next ^
|
|
10
|
-
- Client side only
|
|
7
|
+
- next ^11.x.x || ^12.x.x
|
|
8
|
+
- Client side only, SSR has a PR open. Help needed
|
|
9
|
+
|
|
10
|
+
I highly recommend referencing this application which takes advantage of the best capabilities:
|
|
11
|
+
https://github.com/module-federation/module-federation-examples
|
|
12
|
+
|
|
13
|
+
## Looking for SSR support?
|
|
14
|
+
|
|
15
|
+
SSR support for federated applications is much harder, as such - it utilizes a different licensing model.
|
|
16
|
+
If you need SSR support, consider this package instead - it does everything that nextjs-mf does, and them some.
|
|
17
|
+
https://app.privjs.com/buy/packageDetail?pkg=@module-federation/nextjs-ssr
|
|
18
|
+
|
|
19
|
+
There is a pull request moving SSR into this repo and package - but it is not ready yet.
|
|
11
20
|
|
|
12
21
|
## Whats shared by default?
|
|
13
22
|
|
|
@@ -15,190 +24,236 @@ Under the hood we share some next internals automatically
|
|
|
15
24
|
You do not need to share these packages, sharing next internals yourself will cause errors.
|
|
16
25
|
|
|
17
26
|
```js
|
|
18
|
-
const
|
|
19
|
-
|
|
27
|
+
const DEFAULT_SHARE_SCOPE = {
|
|
28
|
+
react: {
|
|
29
|
+
singleton: true,
|
|
20
30
|
requiredVersion: false,
|
|
31
|
+
},
|
|
32
|
+
'react/': {
|
|
21
33
|
singleton: true,
|
|
34
|
+
requiredVersion: false,
|
|
35
|
+
},
|
|
36
|
+
'react-dom': {
|
|
37
|
+
singleton: true,
|
|
38
|
+
requiredVersion: false,
|
|
22
39
|
},
|
|
23
|
-
|
|
40
|
+
'next/dynamic': {
|
|
24
41
|
requiredVersion: false,
|
|
25
42
|
singleton: true,
|
|
26
43
|
},
|
|
27
|
-
|
|
44
|
+
'styled-jsx': {
|
|
28
45
|
requiredVersion: false,
|
|
29
46
|
singleton: true,
|
|
30
47
|
},
|
|
31
|
-
|
|
48
|
+
'next/link': {
|
|
32
49
|
requiredVersion: false,
|
|
33
50
|
singleton: true,
|
|
34
51
|
},
|
|
35
|
-
|
|
52
|
+
'next/router': {
|
|
36
53
|
requiredVersion: false,
|
|
37
54
|
singleton: true,
|
|
38
55
|
},
|
|
39
|
-
|
|
56
|
+
'next/script': {
|
|
57
|
+
requiredVersion: false,
|
|
58
|
+
singleton: true,
|
|
59
|
+
},
|
|
60
|
+
'next/head': {
|
|
40
61
|
requiredVersion: false,
|
|
41
62
|
singleton: true,
|
|
42
63
|
},
|
|
43
64
|
};
|
|
44
65
|
```
|
|
45
66
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
There's a bug in next.js which causes it to attempt and fail to resolve federated imports on files imported into the `pages/index.js`
|
|
49
|
-
|
|
50
|
-
Its recommended using the low-level api to be safe.
|
|
67
|
+
## Usage
|
|
51
68
|
|
|
52
69
|
```js
|
|
53
|
-
const SampleComponent = dynamic(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
ssr: false,
|
|
57
|
-
}
|
|
58
|
-
);
|
|
70
|
+
const SampleComponent = dynamic(() => import('next2/sampleComponent'), {
|
|
71
|
+
ssr: false,
|
|
72
|
+
});
|
|
59
73
|
```
|
|
60
74
|
|
|
61
|
-
|
|
75
|
+
If you want support for sync imports. It is possible in next@12 as long as there is an async boundary.
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
#### See the implementation here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs/home/pages
|
|
78
|
+
|
|
79
|
+
With async boundary installed at the page level. You can then do the following
|
|
64
80
|
|
|
65
81
|
```js
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"./sampleComponent": "./components/sampleComponent.js",
|
|
72
|
-
},
|
|
73
|
-
shared: {
|
|
74
|
-
react: {
|
|
75
|
-
// Notice shared are NOT eager here.
|
|
76
|
-
requiredVersion: false,
|
|
77
|
-
singleton: true,
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
removePlugins: [
|
|
83
|
-
// optional
|
|
84
|
-
// these are the defaults
|
|
85
|
-
"BuildManifestPlugin",
|
|
86
|
-
"ReactLoadablePlugin",
|
|
87
|
-
"DropClientPage",
|
|
88
|
-
"WellKnownErrorsPlugin",
|
|
89
|
-
"ModuleFederationPlugin",
|
|
90
|
-
],
|
|
91
|
-
publicPath: "auto", // defaults to 'auto', is optional
|
|
92
|
-
}
|
|
93
|
-
);
|
|
82
|
+
if (process.browser) {
|
|
83
|
+
const SomeHook = require('next2/someHook');
|
|
84
|
+
}
|
|
85
|
+
// if client only file
|
|
86
|
+
import SomeComponent from 'next2/someComponent';
|
|
94
87
|
```
|
|
95
88
|
|
|
89
|
+
Make sure you are using `mini-css-extract-plugin@2` - version 2 supports resolving assets through `publicPath:'auto'`
|
|
90
|
+
|
|
96
91
|
## Demo
|
|
97
92
|
|
|
98
93
|
You can see it in action here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs
|
|
99
94
|
|
|
100
|
-
##
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
const SampleComponent = dynamic(() => import('next2/sampleComponent'), {
|
|
99
|
+
ssr: false,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If you want support for sync imports. It is possible in next@12 as long as there is an async boundary.
|
|
104
|
+
|
|
105
|
+
#### See the implementation here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs/home/pages
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
With async boundary installed at the page level. You can then do the following
|
|
103
108
|
|
|
104
109
|
```js
|
|
105
|
-
|
|
106
|
-
const
|
|
110
|
+
if (process.browser) {
|
|
111
|
+
const SomeHook = require('next2/someHook');
|
|
112
|
+
}
|
|
113
|
+
// if client only file
|
|
114
|
+
import SomeComponent from 'next2/someComponent';
|
|
115
|
+
```
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
Make sure you are using `mini-css-extract-plugin@2` - version 2 supports resolving assets through `publicPath:'auto'`
|
|
118
|
+
|
|
119
|
+
## Options
|
|
120
|
+
|
|
121
|
+
This plugin works exactly like ModuleFederationPlugin, use it as you'd normally.
|
|
122
|
+
Note that we already share react and next stuff for you automatically.
|
|
123
|
+
|
|
124
|
+
Also NextFederationPlugin has own optional argument `extraOptions` where you can unlock additional features of this plugin:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
new NextFederationPlugin({
|
|
128
|
+
name: ...,
|
|
129
|
+
filename: ...,
|
|
130
|
+
remotes: ...,
|
|
131
|
+
exposes: ...,
|
|
132
|
+
shared: ...,
|
|
133
|
+
extraOptions: {
|
|
134
|
+
exposePages: true, // `false` by default
|
|
135
|
+
enableImageLoaderFix: true, // `false` by default
|
|
136
|
+
enableUrlLoaderFix: true, // `false` by default
|
|
137
|
+
skipSharingNextInternals: true // `false` by default
|
|
120
138
|
},
|
|
121
|
-
})({
|
|
122
|
-
// your original next.config.js export
|
|
123
139
|
});
|
|
124
140
|
```
|
|
125
141
|
|
|
126
|
-
|
|
142
|
+
- `exposePages` – exposes automatically all nextjs pages for you and theirs `./pages-map`.
|
|
143
|
+
- `enableImageLoaderFix` – adds public hostname to all assets bundled by `nextjs-image-loader`. So if you serve remoteEntry from `http://example.com` then all bundled assets will get this hostname in runtime. It's something like Base URL in HTML but for federated modules.
|
|
144
|
+
- `enableUrlLoaderFix` – adds public hostname to all assets bundled by `url-loader`.
|
|
145
|
+
- `skipSharingNextInternals` - skips sharing of common nextjs modules. Helpful when you would like explicit control over shared modules, such as a non-nextjs host with a federated nextjs child application.
|
|
146
|
+
|
|
147
|
+
## Demo
|
|
148
|
+
|
|
149
|
+
You can see it in action here: https://github.com/module-federation/module-federation-examples/pull/2147
|
|
150
|
+
|
|
151
|
+
## Implementing the Plugin
|
|
152
|
+
|
|
153
|
+
1. Use `NextFederationPlugin` in your `next.config.js` of the app that you wish to expose modules from. We'll call this "next2".
|
|
127
154
|
|
|
128
155
|
```js
|
|
156
|
+
// next.config.js
|
|
157
|
+
const NextFederationPlugin = require('@module-federation/nextjs-mf/NextFederationPlugin');
|
|
158
|
+
|
|
129
159
|
module.exports = {
|
|
130
160
|
webpack(config, options) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
shared: {
|
|
138
|
-
react: {
|
|
139
|
-
// Notice shared ARE eager here.
|
|
140
|
-
eager: true,
|
|
141
|
-
singleton: true,
|
|
142
|
-
requiredVersion: false,
|
|
161
|
+
if (!options.isServer) {
|
|
162
|
+
config.plugins.push(
|
|
163
|
+
new NextFederationPlugin({
|
|
164
|
+
name: 'next2',
|
|
165
|
+
remotes: {
|
|
166
|
+
next1: `next1@http://localhost:3001/_next/static/chunks/remoteEntry.js`,
|
|
143
167
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
filename: 'static/chunks/remoteEntry.js',
|
|
169
|
+
exposes: {
|
|
170
|
+
'./title': './components/exposedTitle.js',
|
|
171
|
+
'./checkout': './pages/checkout',
|
|
172
|
+
'./pages-map': './pages-map.js',
|
|
147
173
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
test: /pages\/_app.[jt]sx?/,
|
|
155
|
-
loader: "@module-federation/nextjs-mf/lib/federation-loader.js",
|
|
156
|
-
});
|
|
174
|
+
shared: {
|
|
175
|
+
// whatever else
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
}
|
|
157
180
|
|
|
158
181
|
return config;
|
|
159
182
|
},
|
|
160
183
|
};
|
|
184
|
+
|
|
185
|
+
// _app.js or some other file in as high up in the app (like next's new layouts)
|
|
186
|
+
// this ensures various parts of next.js are imported and "used" somewhere so that they wont be tree shaken out
|
|
187
|
+
import '@module-federation/nextjs-mf/lib/include-defaults';
|
|
161
188
|
```
|
|
162
189
|
|
|
163
|
-
|
|
190
|
+
2. For the consuming application, we'll call it "next1", add an instance of the ModuleFederationPlugin to your webpack config, and ensure you have a [custom Next.js App](https://nextjs.org/docs/advanced-features/custom-app) `pages/_app.js` (or `.tsx`):
|
|
191
|
+
Inside that \_app.js or layout.js file, ensure you import `include-defaults` file
|
|
164
192
|
|
|
165
193
|
```js
|
|
166
|
-
|
|
194
|
+
// next.config.js
|
|
167
195
|
|
|
168
|
-
|
|
169
|
-
static async getInitialProps(ctx) {
|
|
170
|
-
const initialProps = await Document.getInitialProps(ctx);
|
|
171
|
-
return { ...initialProps };
|
|
172
|
-
}
|
|
196
|
+
const NextFederationPlugin = require('@module-federation/nextjs-mf/NextFederationPlugin');
|
|
173
197
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
198
|
+
module.exports = {
|
|
199
|
+
webpack(config, options) {
|
|
200
|
+
if (!options.isServer) {
|
|
201
|
+
config.plugins.push(
|
|
202
|
+
new NextFederationPlugin({
|
|
203
|
+
name: 'next1',
|
|
204
|
+
remotes: {
|
|
205
|
+
next2: `next2@http://localhost:3000/_next/static/chunks/remoteEntry.js`,
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
187
210
|
|
|
188
|
-
|
|
211
|
+
return config;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// _app.js or some other file in as high up in the app (like next's new layouts)
|
|
216
|
+
// this ensures various parts of next.js are imported and "used" somewhere so that they wont be tree shaken out
|
|
217
|
+
import '@module-federation/nextjs-mf/lib/include-defaults';
|
|
189
218
|
```
|
|
190
219
|
|
|
191
|
-
|
|
220
|
+
4. Use next/dynamic or low level api to import remotes.
|
|
192
221
|
|
|
193
222
|
```js
|
|
194
|
-
import dynamic from
|
|
223
|
+
import dynamic from 'next/dynamic';
|
|
195
224
|
|
|
196
225
|
const SampleComponent = dynamic(
|
|
197
|
-
() => window.next2.get(
|
|
226
|
+
() => window.next2.get('./sampleComponent').then((factory) => factory()),
|
|
198
227
|
{
|
|
199
228
|
ssr: false,
|
|
200
229
|
}
|
|
201
230
|
);
|
|
231
|
+
|
|
232
|
+
// or
|
|
233
|
+
|
|
234
|
+
const SampleComponent = dynamic(() => import('next2/sampleComponent'), {
|
|
235
|
+
ssr: false,
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Utilities
|
|
240
|
+
|
|
241
|
+
Ive added a util for dynamic chunk loading, in the event you need to load remote containers dynamically.
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
import { injectScript } from '@module-federation/nextjs-mf/lib/utils';
|
|
245
|
+
// if i have remotes in my federation plugin, i can pass the name of the remote
|
|
246
|
+
injectScript('home').then((remoteContainer) => {
|
|
247
|
+
remoteContainer.get('./exposedModule');
|
|
248
|
+
});
|
|
249
|
+
// if i want to load a custom remote not known at build time.
|
|
250
|
+
|
|
251
|
+
injectScript({
|
|
252
|
+
global: 'home',
|
|
253
|
+
url: 'http://somthing.com/remoteEntry.js',
|
|
254
|
+
}).then((remoteContainer) => {
|
|
255
|
+
remoteContainer.get('./exposedModule');
|
|
256
|
+
});
|
|
202
257
|
```
|
|
203
258
|
|
|
204
259
|
## Contact
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var helpers = require('./loaders/helpers');
|
|
5
|
+
var nextPageMapLoader = require('./loaders/nextPageMapLoader');
|
|
6
|
+
|
|
7
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
8
|
+
|
|
9
|
+
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
10
|
+
|
|
11
|
+
const CHILD_PLUGIN_NAME = 'ChildFederationPlugin';
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../../declarations/plugins/container/ModuleFederationPlugin").ExternalsType} ExternalsType */
|
|
14
|
+
/** @typedef {import("../../declarations/plugins/container/ModuleFederationPlugin").ModuleFederationPluginOptions} ModuleFederationPluginOptions */
|
|
15
|
+
|
|
16
|
+
/** @typedef {import("webpack").Shared} Shared */
|
|
17
|
+
/** @typedef {import("webpack").Compiler} Compiler */
|
|
18
|
+
|
|
19
|
+
class ModuleFederationPlugin {
|
|
20
|
+
/**
|
|
21
|
+
* @param {ModuleFederationPluginOptions} options options
|
|
22
|
+
*/
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this._options = options;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Apply the plugin
|
|
29
|
+
* @param {Compiler} compiler the compiler instance
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
32
|
+
apply(compiler) {
|
|
33
|
+
const { _options: options } = this;
|
|
34
|
+
const webpack = compiler.webpack;
|
|
35
|
+
const { ContainerPlugin, ContainerReferencePlugin } = webpack.container;
|
|
36
|
+
const { SharePlugin } = webpack.sharing;
|
|
37
|
+
const library = options.library || { type: 'var', name: options.name };
|
|
38
|
+
const remoteType =
|
|
39
|
+
options.remoteType ||
|
|
40
|
+
(options.library && /** @type {ExternalsType} */ options.library.type) ||
|
|
41
|
+
'script';
|
|
42
|
+
if (
|
|
43
|
+
library &&
|
|
44
|
+
!compiler.options.output.enabledLibraryTypes.includes(library.type)
|
|
45
|
+
) {
|
|
46
|
+
compiler.options.output.enabledLibraryTypes.push(library.type);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
options.exposes &&
|
|
51
|
+
(Array.isArray(options.exposes)
|
|
52
|
+
? options.exposes.length > 0
|
|
53
|
+
: Object.keys(options.exposes).length > 0)
|
|
54
|
+
) {
|
|
55
|
+
new ContainerPlugin({
|
|
56
|
+
name: options.name,
|
|
57
|
+
library,
|
|
58
|
+
filename: options.filename,
|
|
59
|
+
runtime: options.runtime,
|
|
60
|
+
exposes: options.exposes,
|
|
61
|
+
}).apply(compiler);
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
options.remotes &&
|
|
65
|
+
(Array.isArray(options.remotes)
|
|
66
|
+
? options.remotes.length > 0
|
|
67
|
+
: Object.keys(options.remotes).length > 0)
|
|
68
|
+
) {
|
|
69
|
+
new ContainerReferencePlugin({
|
|
70
|
+
remoteType,
|
|
71
|
+
remotes: options.remotes,
|
|
72
|
+
}).apply(compiler);
|
|
73
|
+
}
|
|
74
|
+
if (options.shared) {
|
|
75
|
+
new SharePlugin({
|
|
76
|
+
shared: options.shared,
|
|
77
|
+
shareScope: options.shareScope,
|
|
78
|
+
}).apply(compiler);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class RemoveRRRuntimePlugin {
|
|
84
|
+
/**
|
|
85
|
+
* Apply the plugin
|
|
86
|
+
* @param {Compiler} compiler the compiler instance
|
|
87
|
+
* @returns {void}
|
|
88
|
+
*/
|
|
89
|
+
apply(compiler) {
|
|
90
|
+
const webpack = compiler.webpack;
|
|
91
|
+
|
|
92
|
+
compiler.hooks.thisCompilation.tap(
|
|
93
|
+
'RemoveRRRuntimePlugin',
|
|
94
|
+
(compilation) => {
|
|
95
|
+
compilation.hooks.processAssets.tap(
|
|
96
|
+
{
|
|
97
|
+
name: 'RemoveRRRuntimePlugin',
|
|
98
|
+
state: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
|
|
99
|
+
},
|
|
100
|
+
(assets) => {
|
|
101
|
+
Object.keys(assets).forEach((filename) => {
|
|
102
|
+
if (filename.endsWith('.js') || filename.endsWith('.mjs')) {
|
|
103
|
+
const asset = compilation.getAsset(filename);
|
|
104
|
+
const newSource = asset.source
|
|
105
|
+
.source()
|
|
106
|
+
.replace(/RefreshHelpers/g, 'NoExist');
|
|
107
|
+
const updatedAsset = new webpack.sources.RawSource(newSource);
|
|
108
|
+
|
|
109
|
+
if (asset) {
|
|
110
|
+
compilation.updateAsset(filename, updatedAsset);
|
|
111
|
+
} else {
|
|
112
|
+
compilation.emitAsset(filename, updatedAsset);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const DEFAULT_SHARE_SCOPE = {
|
|
124
|
+
react: {
|
|
125
|
+
singleton: true,
|
|
126
|
+
requiredVersion: false,
|
|
127
|
+
},
|
|
128
|
+
'react/jsx-runtime': {
|
|
129
|
+
singleton: true,
|
|
130
|
+
requiredVersion: false,
|
|
131
|
+
},
|
|
132
|
+
'react-dom': {
|
|
133
|
+
singleton: true,
|
|
134
|
+
requiredVersion: false,
|
|
135
|
+
},
|
|
136
|
+
'next/dynamic': {
|
|
137
|
+
requiredVersion: false,
|
|
138
|
+
singleton: true,
|
|
139
|
+
},
|
|
140
|
+
'styled-jsx': {
|
|
141
|
+
requiredVersion: false,
|
|
142
|
+
singleton: true,
|
|
143
|
+
},
|
|
144
|
+
'next/link': {
|
|
145
|
+
requiredVersion: false,
|
|
146
|
+
singleton: true,
|
|
147
|
+
},
|
|
148
|
+
'next/router': {
|
|
149
|
+
requiredVersion: false,
|
|
150
|
+
singleton: true,
|
|
151
|
+
},
|
|
152
|
+
'next/script': {
|
|
153
|
+
requiredVersion: false,
|
|
154
|
+
singleton: true,
|
|
155
|
+
},
|
|
156
|
+
'next/head': {
|
|
157
|
+
requiredVersion: false,
|
|
158
|
+
singleton: true,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
class ChildFederation {
|
|
163
|
+
constructor(options, extraOptions = {}) {
|
|
164
|
+
this._options = options;
|
|
165
|
+
this._extraOptions = extraOptions;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
apply(compiler) {
|
|
169
|
+
const webpack = compiler.webpack;
|
|
170
|
+
webpack.EntryPlugin;
|
|
171
|
+
const LibraryPlugin = webpack.library.EnableLibraryPlugin;
|
|
172
|
+
webpack.container.ModuleFederationPlugin;
|
|
173
|
+
webpack.container.ContainerPlugin;
|
|
174
|
+
const LoaderTargetPlugin = webpack.LoaderTargetPlugin;
|
|
175
|
+
const library = compiler.options.output.library;
|
|
176
|
+
|
|
177
|
+
compiler.hooks.thisCompilation.tap(CHILD_PLUGIN_NAME, (compilation) => {
|
|
178
|
+
const buildName = this._options.name;
|
|
179
|
+
const childOutput = {
|
|
180
|
+
...compiler.options.output,
|
|
181
|
+
publicPath: 'auto',
|
|
182
|
+
chunkLoadingGlobal: buildName + 'chunkLoader',
|
|
183
|
+
uniqueName: buildName,
|
|
184
|
+
library: {
|
|
185
|
+
name: buildName,
|
|
186
|
+
type: library.type,
|
|
187
|
+
},
|
|
188
|
+
chunkFilename: compiler.options.output.chunkFilename.replace(
|
|
189
|
+
'.js',
|
|
190
|
+
'-fed.js'
|
|
191
|
+
),
|
|
192
|
+
filename: compiler.options.output.chunkFilename.replace(
|
|
193
|
+
'.js',
|
|
194
|
+
'-fed.js'
|
|
195
|
+
),
|
|
196
|
+
};
|
|
197
|
+
const externalizedShares = Object.entries(DEFAULT_SHARE_SCOPE).reduce(
|
|
198
|
+
(acc, item) => {
|
|
199
|
+
const [key, value] = item;
|
|
200
|
+
acc[key] = { ...value, import: false };
|
|
201
|
+
if (key === 'react/jsx-runtime') {
|
|
202
|
+
delete acc[key].import;
|
|
203
|
+
}
|
|
204
|
+
return acc;
|
|
205
|
+
},
|
|
206
|
+
{}
|
|
207
|
+
);
|
|
208
|
+
const childCompiler = compilation.createChildCompiler(
|
|
209
|
+
CHILD_PLUGIN_NAME,
|
|
210
|
+
childOutput,
|
|
211
|
+
[
|
|
212
|
+
new ModuleFederationPlugin({
|
|
213
|
+
// library: {type: 'var', name: buildName},
|
|
214
|
+
...this._options,
|
|
215
|
+
exposes: {
|
|
216
|
+
...this._options.exposes,
|
|
217
|
+
...(this._extraOptions.exposePages
|
|
218
|
+
? nextPageMapLoader.exposeNextjsPages(compiler.options.context)
|
|
219
|
+
: {}),
|
|
220
|
+
},
|
|
221
|
+
runtime: false,
|
|
222
|
+
shared: {
|
|
223
|
+
...(this._extraOptions.skipSharingNextInternals
|
|
224
|
+
? {}
|
|
225
|
+
: externalizedShares),
|
|
226
|
+
...this._options.shared,
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
new webpack.web.JsonpTemplatePlugin(childOutput),
|
|
230
|
+
new LoaderTargetPlugin('web'),
|
|
231
|
+
new LibraryPlugin('var'),
|
|
232
|
+
new webpack.DefinePlugin({
|
|
233
|
+
'process.env.REMOTES': JSON.stringify(this._options.remotes),
|
|
234
|
+
'process.env.CURRENT_HOST': JSON.stringify(this._options.name),
|
|
235
|
+
}),
|
|
236
|
+
new AddRuntimeRequirementToPromiseExternal(),
|
|
237
|
+
]
|
|
238
|
+
);
|
|
239
|
+
new RemoveRRRuntimePlugin().apply(childCompiler);
|
|
240
|
+
|
|
241
|
+
childCompiler.options.module.rules.forEach((rule) => {
|
|
242
|
+
// next-image-loader fix which adds remote's hostname to the assets url
|
|
243
|
+
if (
|
|
244
|
+
this._extraOptions.enableImageLoaderFix &&
|
|
245
|
+
helpers.hasLoader(rule, 'next-image-loader')
|
|
246
|
+
) {
|
|
247
|
+
helpers.injectRuleLoader(rule, {
|
|
248
|
+
loader: path__default["default"].resolve(__dirname, './loaders/fixImageLoader.js'),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// url-loader fix for which adds remote's hostname to the assets url
|
|
253
|
+
if (
|
|
254
|
+
this._extraOptions.enableUrlLoaderFix &&
|
|
255
|
+
helpers.hasLoader(rule, 'url-loader')
|
|
256
|
+
) {
|
|
257
|
+
helpers.injectRuleLoader({
|
|
258
|
+
loader: path__default["default"].resolve(__dirname, './loaders/fixUrlLoader.js'),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const MiniCss = childCompiler.options.plugins.find((p) => {
|
|
264
|
+
return p.constructor.name === 'NextMiniCssExtractPlugin';
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const removePlugins = [
|
|
268
|
+
'NextJsRequireCacheHotReloader',
|
|
269
|
+
'BuildManifestPlugin',
|
|
270
|
+
'WellKnownErrorsPlugin',
|
|
271
|
+
'WebpackBuildEventsPlugin',
|
|
272
|
+
'HotModuleReplacementPlugin',
|
|
273
|
+
'NextMiniCssExtractPlugin',
|
|
274
|
+
'NextFederationPlugin',
|
|
275
|
+
'CopyFilePlugin',
|
|
276
|
+
'ProfilingPlugin',
|
|
277
|
+
'DropClientPage',
|
|
278
|
+
'ReactFreshWebpackPlugin',
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
childCompiler.options.plugins = childCompiler.options.plugins.filter(
|
|
282
|
+
(plugin) => !removePlugins.includes(plugin.constructor.name)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (MiniCss) {
|
|
286
|
+
new MiniCss.constructor({
|
|
287
|
+
...MiniCss.options,
|
|
288
|
+
filename: MiniCss.options.filename.replace('.css', '-fed.css'),
|
|
289
|
+
chunkFilename: MiniCss.options.chunkFilename.replace(
|
|
290
|
+
'.css',
|
|
291
|
+
'-fed.css'
|
|
292
|
+
),
|
|
293
|
+
}).apply(childCompiler);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
childCompiler.options.experiments.lazyCompilation = false;
|
|
297
|
+
childCompiler.options.optimization.runtimeChunk = false;
|
|
298
|
+
delete childCompiler.options.optimization.splitChunks;
|
|
299
|
+
childCompiler.outputFileSystem = compiler.outputFileSystem;
|
|
300
|
+
if (compiler.options.mode === 'development') {
|
|
301
|
+
childCompiler.run((err, stats) => {
|
|
302
|
+
if (err) {
|
|
303
|
+
console.error(err);
|
|
304
|
+
throw new Error(err);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
childCompiler.runAsChild((err, stats) => {
|
|
309
|
+
if (err) {
|
|
310
|
+
console.error(err);
|
|
311
|
+
throw new Error(err);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
class AddRuntimeRequirementToPromiseExternal {
|
|
320
|
+
apply(compiler) {
|
|
321
|
+
compiler.hooks.compilation.tap(
|
|
322
|
+
'AddRuntimeRequirementToPromiseExternal',
|
|
323
|
+
(compilation) => {
|
|
324
|
+
const RuntimeGlobals = compiler.webpack.RuntimeGlobals;
|
|
325
|
+
// if (compilation.outputOptions.trustedTypes) {
|
|
326
|
+
compilation.hooks.additionalModuleRuntimeRequirements.tap(
|
|
327
|
+
'AddRuntimeRequirementToPromiseExternal',
|
|
328
|
+
(module, set, context) => {
|
|
329
|
+
if (module.externalType === 'promise') {
|
|
330
|
+
set.add(RuntimeGlobals.loadScript);
|
|
331
|
+
set.add(RuntimeGlobals.require);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
// }
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function extractUrlAndGlobal(urlAndGlobal) {
|
|
342
|
+
const index = urlAndGlobal.indexOf('@');
|
|
343
|
+
if (index <= 0 || index === urlAndGlobal.length - 1) {
|
|
344
|
+
throw new Error(`Invalid request "${urlAndGlobal}"`);
|
|
345
|
+
}
|
|
346
|
+
return [urlAndGlobal.substring(index + 1), urlAndGlobal.substring(0, index)];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function generateRemoteTemplate(url, global) {
|
|
350
|
+
return `promise new Promise(function (resolve, reject) {
|
|
351
|
+
var __webpack_error__ = new Error();
|
|
352
|
+
if (typeof ${global} !== 'undefined') return resolve();
|
|
353
|
+
__webpack_require__.l(
|
|
354
|
+
${JSON.stringify(url)},
|
|
355
|
+
function (event) {
|
|
356
|
+
if (typeof ${global} !== 'undefined') return resolve();
|
|
357
|
+
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
|
|
358
|
+
var realSrc = event && event.target && event.target.src;
|
|
359
|
+
__webpack_error__.message =
|
|
360
|
+
'Loading script failed.\\n(' + errorType + ': ' + realSrc + ')';
|
|
361
|
+
__webpack_error__.name = 'ScriptExternalLoadError';
|
|
362
|
+
__webpack_error__.type = errorType;
|
|
363
|
+
__webpack_error__.request = realSrc;
|
|
364
|
+
reject(__webpack_error__);
|
|
365
|
+
},
|
|
366
|
+
${JSON.stringify(global)},
|
|
367
|
+
);
|
|
368
|
+
}).then(function () {
|
|
369
|
+
const proxy = {
|
|
370
|
+
get: ${global}.get,
|
|
371
|
+
init: (args) => {
|
|
372
|
+
const handler = {
|
|
373
|
+
get(target, prop) {
|
|
374
|
+
if (target[prop]) {
|
|
375
|
+
Object.values(target[prop]).forEach(function(o) {
|
|
376
|
+
if(o.from === '_N_E') {
|
|
377
|
+
o.loaded = true
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
return target[prop]
|
|
382
|
+
},
|
|
383
|
+
set(target, property, value, receiver) {
|
|
384
|
+
if (target[property]) {
|
|
385
|
+
return target[property]
|
|
386
|
+
}
|
|
387
|
+
target[property] = value
|
|
388
|
+
return true
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
${global}.init(new Proxy(__webpack_require__.S.default, handler))
|
|
393
|
+
} catch (e) {
|
|
394
|
+
|
|
395
|
+
}
|
|
396
|
+
${global}.__initialized = true
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!${global}.__initialized) {
|
|
400
|
+
proxy.init()
|
|
401
|
+
}
|
|
402
|
+
return proxy
|
|
403
|
+
})`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function createRuntimeVariables(remotes) {
|
|
407
|
+
return Object.entries(remotes).reduce((acc, remote) => {
|
|
408
|
+
acc[remote[0]] = remote[1].replace('promise ', '');
|
|
409
|
+
return acc;
|
|
410
|
+
}, {});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
class NextFederationPlugin {
|
|
414
|
+
constructor(options) {
|
|
415
|
+
const { extraOptions, ...mainOpts } = options;
|
|
416
|
+
this._options = mainOpts;
|
|
417
|
+
this._extraOptions = extraOptions;
|
|
418
|
+
if (options.remotes) {
|
|
419
|
+
const parsedRemotes = Object.entries(options.remotes).reduce(
|
|
420
|
+
(acc, remote) => {
|
|
421
|
+
if (remote[1].includes('@')) {
|
|
422
|
+
const [url, global] = extractUrlAndGlobal(remote[1]);
|
|
423
|
+
acc[remote[0]] = generateRemoteTemplate(url, global);
|
|
424
|
+
return acc;
|
|
425
|
+
}
|
|
426
|
+
acc[remote[0]] = remote[1];
|
|
427
|
+
return acc;
|
|
428
|
+
},
|
|
429
|
+
{}
|
|
430
|
+
);
|
|
431
|
+
this._options.remotes = parsedRemotes;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
apply(compiler) {
|
|
436
|
+
const webpack = compiler.webpack;
|
|
437
|
+
const sharedForHost = Object.entries({
|
|
438
|
+
...(this._options.shared || {}),
|
|
439
|
+
...DEFAULT_SHARE_SCOPE,
|
|
440
|
+
}).reduce((acc, item) => {
|
|
441
|
+
const [itemKey, shareOptions] = item;
|
|
442
|
+
|
|
443
|
+
const shareKey = 'host' + (item.shareKey || itemKey);
|
|
444
|
+
acc[shareKey] = shareOptions;
|
|
445
|
+
if (!shareOptions.import) {
|
|
446
|
+
acc[shareKey].import = itemKey;
|
|
447
|
+
}
|
|
448
|
+
if (!shareOptions.shareKey) {
|
|
449
|
+
acc[shareKey].shareKey = itemKey;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (DEFAULT_SHARE_SCOPE[itemKey]) {
|
|
453
|
+
acc[shareKey].packageName = itemKey;
|
|
454
|
+
}
|
|
455
|
+
return acc;
|
|
456
|
+
}, {});
|
|
457
|
+
|
|
458
|
+
new webpack.container.ModuleFederationPlugin({
|
|
459
|
+
...this._options,
|
|
460
|
+
exposes: {},
|
|
461
|
+
shared: {
|
|
462
|
+
noop: {
|
|
463
|
+
import: 'data:text/javascript,module.exports = {};',
|
|
464
|
+
requiredVersion: false,
|
|
465
|
+
version: '0',
|
|
466
|
+
},
|
|
467
|
+
...sharedForHost,
|
|
468
|
+
},
|
|
469
|
+
}).apply(compiler);
|
|
470
|
+
new webpack.DefinePlugin({
|
|
471
|
+
'process.env.REMOTES': createRuntimeVariables(this._options.remotes),
|
|
472
|
+
'process.env.CURRENT_HOST': JSON.stringify(this._options.name),
|
|
473
|
+
}).apply(compiler);
|
|
474
|
+
new ChildFederation(this._options, this._extraOptions).apply(compiler);
|
|
475
|
+
new AddRuntimeRequirementToPromiseExternal().apply(compiler);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
module.exports = NextFederationPlugin;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// if(process.browser && (typeof __webpack_share_scopes__ === "undefined" || !__webpack_share_scopes__.default)) {
|
|
2
|
+
// __webpack_init_sharing__('default');
|
|
3
|
+
// }
|
|
4
|
+
require('react');
|
|
5
|
+
require('react-dom');
|
|
6
|
+
require('next/link');
|
|
7
|
+
require('next/router');
|
|
8
|
+
require('next/head');
|
|
9
|
+
require('next/script');
|
|
10
|
+
require('next/dynamic');
|
|
11
|
+
require('styled-jsx');
|
|
12
|
+
if (process.env.NODE_ENV === 'development') {
|
|
13
|
+
require('react/jsx-dev-runtime');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This loader was specially created for tunning next-image-loader result
|
|
3
|
+
* see https://github.com/vercel/next.js/blob/canary/packages/next/build/webpack/loaders/next-image-loader.js
|
|
4
|
+
* It takes regular string
|
|
5
|
+
* `export default {"src":"/_next/static/media/ssl.e3019f0e.svg","height":20,"width":20};`
|
|
6
|
+
* And injects PUBLIC_PATH to it from webpack
|
|
7
|
+
* `export default {"src":__webpack_require__.p+"/static/media/ssl.e3019f0e.svg","height":20,"width":20};`
|
|
8
|
+
*
|
|
9
|
+
*
|
|
10
|
+
* __webpack_require__.p - is a global variable in webpack container which contains publicPath
|
|
11
|
+
* For example: http://localhost:3000/_next
|
|
12
|
+
*
|
|
13
|
+
* @type {(this: import("webpack").LoaderContext<{}>, content: string) => string>}
|
|
14
|
+
*/
|
|
15
|
+
function fixImageLoader(content) {
|
|
16
|
+
// replace(/(.+\:\/\/[^\/]+){0,1}\/.*/i, '$1')
|
|
17
|
+
// this regexp will extract the hostname from publicPath
|
|
18
|
+
// http://localhost:3000/_next/... -> http://localhost:3000
|
|
19
|
+
const currentHostnameCode =
|
|
20
|
+
"__webpack_require__.p.replace(/(.+\\:\\/\\/[^\\/]+){0,1}\\/.*/i, '$1')";
|
|
21
|
+
|
|
22
|
+
return content.replace('"src":', `"src":${currentHostnameCode}+`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = fixImageLoader;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This loader was specially created for tunning url-loader result
|
|
3
|
+
*
|
|
4
|
+
* And injects PUBLIC_PATH to it from webpack runtime
|
|
5
|
+
* `export default __webpack_require__.p + "/static/media/ssl.e3019f0e.svg"`
|
|
6
|
+
*
|
|
7
|
+
* __webpack_require__.p - is a global variable in webpack container which contains publicPath
|
|
8
|
+
* For example: http://localhost:3000/_next
|
|
9
|
+
*
|
|
10
|
+
* @type {(this: import("webpack").LoaderContext<{}>, content: string) => string>}
|
|
11
|
+
*/
|
|
12
|
+
function fixUrlLoader(content) {
|
|
13
|
+
// replace(/(.+\:\/\/[^\/]+){0,1}\/.*/i, '$1')
|
|
14
|
+
// this regexp will extract the hostname from publicPath
|
|
15
|
+
// http://localhost:3000/_next/... -> http://localhost:3000
|
|
16
|
+
const currentHostnameCode =
|
|
17
|
+
"__webpack_require__.p.replace(/(.+\\:\\/\\/[^\\/]+){0,1}\\/.*/i, '$1')";
|
|
18
|
+
|
|
19
|
+
return content.replace(
|
|
20
|
+
'export default "/',
|
|
21
|
+
`export default ${currentHostnameCode}+"/`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = fixUrlLoader;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject a loader into the current module rule.
|
|
3
|
+
* This function mutates `rule` argument!
|
|
4
|
+
*/
|
|
5
|
+
module.exports.injectRuleLoader = function injectRuleLoader(rule, loader) {
|
|
6
|
+
if (rule.loader) {
|
|
7
|
+
rule.use = [loader, { loader: rule.loader, options: rule.options }];
|
|
8
|
+
delete rule.loader;
|
|
9
|
+
delete rule.options;
|
|
10
|
+
} else if (rule.use) {
|
|
11
|
+
rule.use = [loader, ...rule.use];
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check that current module rule has a loader with the provided name.
|
|
17
|
+
*/
|
|
18
|
+
module.exports.hasLoader = function hasLoader(rule, loaderName) {
|
|
19
|
+
if (rule.loader === loaderName) {
|
|
20
|
+
return true;
|
|
21
|
+
} else if (rule.use) {
|
|
22
|
+
for (let i = 0; i < rule.use.length; i++) {
|
|
23
|
+
const loader = rule.use[i];
|
|
24
|
+
// check exact name, eg "url-loader" or its path "node_modules/url-loader/dist/cjs.js"
|
|
25
|
+
if (
|
|
26
|
+
loader.loader && (
|
|
27
|
+
loader.loader === loaderName ||
|
|
28
|
+
loader.loader.includes(`/${loaderName}/`)
|
|
29
|
+
)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const fg = require('fast-glob');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Webpack loader which prepares MF map for NextJS pages
|
|
6
|
+
*
|
|
7
|
+
* @type {(this: import("webpack").LoaderContext<{}>, content: string) => string>}
|
|
8
|
+
*/
|
|
9
|
+
function nextPageMapLoader() {
|
|
10
|
+
const pages = getNextPages(this.rootContext);
|
|
11
|
+
const pageMap = preparePageMap(pages);
|
|
12
|
+
|
|
13
|
+
// const [pagesRoot] = getNextPagesRoot(this.rootContext);
|
|
14
|
+
// this.addContextDependency(pagesRoot);
|
|
15
|
+
|
|
16
|
+
const result = `module.exports = {
|
|
17
|
+
default: ${JSON.stringify(pageMap)},
|
|
18
|
+
};`;
|
|
19
|
+
|
|
20
|
+
this.callback(null, result);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Webpack config generator for `exposes` option.
|
|
25
|
+
* - automatically create `./pages-map` module
|
|
26
|
+
* - automatically add all page modules
|
|
27
|
+
*/
|
|
28
|
+
function exposeNextjsPages(cwd) {
|
|
29
|
+
const pages = getNextPages(cwd);
|
|
30
|
+
|
|
31
|
+
const pageModulesMap = {};
|
|
32
|
+
pages.forEach((page) => {
|
|
33
|
+
// Creating a map of pages to modules
|
|
34
|
+
// './pages/storage/index': './pages/storage/index.tsx',
|
|
35
|
+
// './pages/storage/[...slug]': './pages/storage/[...slug].tsx',
|
|
36
|
+
pageModulesMap['./' + sanitizePagePath(page)] = `./${page}`;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const exposesWithPageMap = {
|
|
40
|
+
'./pages-map': `${__filename}!${__filename}`,
|
|
41
|
+
...pageModulesMap,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return exposesWithPageMap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getNextPagesRoot(appRoot) {
|
|
48
|
+
let pagesDir = 'src/pages/';
|
|
49
|
+
let absPageDir = `${appRoot}/${pagesDir}`;
|
|
50
|
+
if (!fs.existsSync(absPageDir)) {
|
|
51
|
+
pagesDir = 'pages/';
|
|
52
|
+
absPageDir = `${appRoot}/${pagesDir}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [absPageDir, pagesDir];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* From provided ROOT_DIR `scan` pages directory
|
|
60
|
+
* and return list of user defined pages
|
|
61
|
+
* (except special ones, like _app, _document, _error)
|
|
62
|
+
*
|
|
63
|
+
* @type {(rootDir: string) => string[]}
|
|
64
|
+
*/
|
|
65
|
+
function getNextPages(rootDir) {
|
|
66
|
+
const [cwd, pagesDir] = getNextPagesRoot(rootDir);
|
|
67
|
+
|
|
68
|
+
// scan all files in pages folder except pages/api
|
|
69
|
+
let pageList = fg.sync('**/*.{ts,tsx,js,jsx}', {
|
|
70
|
+
cwd,
|
|
71
|
+
onlyFiles: true,
|
|
72
|
+
ignore: ['api/**'],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// remove specific nextjs pages
|
|
76
|
+
const exclude = [
|
|
77
|
+
/^_app\..*/, // _app.tsx
|
|
78
|
+
/^_document\..*/, // _document.tsx
|
|
79
|
+
/^_error\..*/, // _error.tsx
|
|
80
|
+
/^404\..*/, // 404.tsx
|
|
81
|
+
/^500\..*/, // 500.tsx
|
|
82
|
+
/^\[\.\.\..*\]\..*/, // /[...federationPage].tsx
|
|
83
|
+
];
|
|
84
|
+
pageList = pageList.filter((page) => {
|
|
85
|
+
return !exclude.some((r) => r.test(page));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
pageList = pageList.map((page) => `${pagesDir}${page}`);
|
|
89
|
+
|
|
90
|
+
return pageList;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sanitizePagePath(item) {
|
|
94
|
+
return item
|
|
95
|
+
.replace(/^src\/pages\//i, 'pages/')
|
|
96
|
+
.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create MF map from list of NextJS pages
|
|
101
|
+
*
|
|
102
|
+
* From
|
|
103
|
+
* ['pages/index.tsx', 'pages/storage/[...slug].tsx', 'pages/storage/index.tsx']
|
|
104
|
+
* Getting the following map
|
|
105
|
+
* {
|
|
106
|
+
* '/': './pages/index',
|
|
107
|
+
* '/storage/*': './pages/storage/[...slug]',
|
|
108
|
+
* '/storage': './pages/storage/index'
|
|
109
|
+
* }
|
|
110
|
+
*
|
|
111
|
+
* @type {(pages: string[]) => {[key: string]: string}}
|
|
112
|
+
*/
|
|
113
|
+
function preparePageMap(pages) {
|
|
114
|
+
const result = {};
|
|
115
|
+
|
|
116
|
+
pages.forEach((pagePath) => {
|
|
117
|
+
const page = sanitizePagePath(pagePath);
|
|
118
|
+
let key =
|
|
119
|
+
'/' +
|
|
120
|
+
page.replace(/\[\.\.\.[^\]]+\]/gi, '*').replace(/\[([^\]]+)\]/gi, ':$1');
|
|
121
|
+
key = key.replace(/^\/pages\//, '/').replace(/\/index$/, '') || '/';
|
|
122
|
+
result[key] = `./${page}`;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = nextPageMapLoader;
|
|
129
|
+
module.exports.exposeNextjsPages = exposeNextjsPages;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const remoteVars = process.env.REMOTES || {};
|
|
2
|
+
|
|
3
|
+
const runtimeRemotes = Object.entries(remoteVars).reduce(function (acc, item) {
|
|
4
|
+
const [key, value] = item;
|
|
5
|
+
if (typeof value === 'object' && typeof value.then === 'function') {
|
|
6
|
+
acc[key] = { asyncContainer: value };
|
|
7
|
+
} else if (typeof value === 'string') {
|
|
8
|
+
const [global, url] = value.split('@');
|
|
9
|
+
acc[key] = { global, url };
|
|
10
|
+
} else {
|
|
11
|
+
throw new Error(`[mf] Invalid value received for runtime_remote "${key}"`);
|
|
12
|
+
}
|
|
13
|
+
return acc;
|
|
14
|
+
}, {});
|
|
15
|
+
|
|
16
|
+
module.exports.remotes = runtimeRemotes;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return initialized remote container by remote's key or its runtime remote item data.
|
|
20
|
+
*
|
|
21
|
+
* `runtimeRemoteItem` might be
|
|
22
|
+
* { global, url } - values obtained from webpack remotes option `global@url`
|
|
23
|
+
* or
|
|
24
|
+
* { asyncContainer } - async container is a promise that resolves to the remote container
|
|
25
|
+
*/
|
|
26
|
+
function injectScript(keyOrRuntimeRemoteItem) {
|
|
27
|
+
let reference = keyOrRuntimeRemoteItem;
|
|
28
|
+
if (typeof keyOrRuntimeRemoteItem === 'string') {
|
|
29
|
+
reference = runtimeRemotes[keyOrRuntimeRemoteItem];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 1) Load remote container if needed
|
|
33
|
+
let asyncContainer;
|
|
34
|
+
if (reference.asyncContainer) {
|
|
35
|
+
asyncContainer = reference.asyncContainer;
|
|
36
|
+
} else {
|
|
37
|
+
const remoteGlobal = reference.global;
|
|
38
|
+
const __webpack_error__ = new Error();
|
|
39
|
+
asyncContainer = new Promise(function (resolve, reject) {
|
|
40
|
+
if (typeof window[remoteGlobal] !== 'undefined') {
|
|
41
|
+
return resolve(window[remoteGlobal]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
__webpack_require__.l(
|
|
45
|
+
reference.url,
|
|
46
|
+
function (event) {
|
|
47
|
+
if (typeof window[remoteGlobal] !== 'undefined') {
|
|
48
|
+
return resolve(window[remoteGlobal]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var errorType =
|
|
52
|
+
event && (event.type === 'load' ? 'missing' : event.type);
|
|
53
|
+
var realSrc = event && event.target && event.target.src;
|
|
54
|
+
__webpack_error__.message =
|
|
55
|
+
'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
|
|
56
|
+
__webpack_error__.name = 'ScriptExternalLoadError';
|
|
57
|
+
__webpack_error__.type = errorType;
|
|
58
|
+
__webpack_error__.request = realSrc;
|
|
59
|
+
reject(__webpack_error__);
|
|
60
|
+
},
|
|
61
|
+
remoteGlobal
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2) Initialize remote container
|
|
67
|
+
return asyncContainer
|
|
68
|
+
.then(function (container) {
|
|
69
|
+
if (!__webpack_share_scopes__.default) {
|
|
70
|
+
// not always a promise, so we wrap it in a resolve
|
|
71
|
+
return Promise.resolve(__webpack_init_sharing__('default')).then(
|
|
72
|
+
function () {
|
|
73
|
+
return container;
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
return container;
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.then(function (container) {
|
|
81
|
+
try {
|
|
82
|
+
// WARNING: here might be a potential BUG.
|
|
83
|
+
// `container.init` does not return a Promise, and here we do not call `then` on it.
|
|
84
|
+
// But according to [docs](https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers)
|
|
85
|
+
// it must be async.
|
|
86
|
+
// The problem may be in Proxy in NextFederationPlugin.js.
|
|
87
|
+
// or maybe a bug in the webpack itself - instead of returning rejected promise it just throws an error.
|
|
88
|
+
// But now everything works properly and we keep this code as is.
|
|
89
|
+
container.init(__webpack_share_scopes__.default);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// maybe container already initialized so nothing to throw
|
|
92
|
+
}
|
|
93
|
+
return container;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports.injectScript = injectScript;
|
package/package.json
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"public": true,
|
|
3
3
|
"name": "@module-federation/nextjs-mf",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.2.0",
|
|
5
5
|
"description": "Module Federation helper for NextJS",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
8
|
"repository": "https://github.com/module-federation/nextjs-mf",
|
|
9
9
|
"author": "Zack Jackson <zackary.l.jackson@gmail.com>",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"
|
|
12
|
+
"demo": "yarn build && cd demo && yarn install && yarn dev",
|
|
13
|
+
"prettier": "prettier --write \"**/*.{js,json,md,ts,tsx}\"",
|
|
14
|
+
"build": "rm -rf lib && cp -r ./src/ lib/ && rollup -c"
|
|
13
15
|
},
|
|
14
16
|
"dependencies": {
|
|
15
|
-
"
|
|
17
|
+
"chalk": "^4.0.0",
|
|
18
|
+
"fast-glob": "^3.2.11"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@rollup/plugin-commonjs": "^22.0.2",
|
|
22
|
+
"@rollup/plugin-multi-entry": "^4.1.0",
|
|
23
|
+
"@rollup/plugin-node-resolve": "^13.3.0",
|
|
24
|
+
"next": "11.0.1",
|
|
25
|
+
"prettier": "2.3.2",
|
|
26
|
+
"rollup": "^2.78.1",
|
|
27
|
+
"rollup-obfuscator": "^3.0.1",
|
|
28
|
+
"rollup-plugin-node-builtins": "^2.1.2",
|
|
29
|
+
"rollup-plugin-node-globals": "^1.4.0",
|
|
30
|
+
"webpack": "5.45.1"
|
|
16
31
|
}
|
|
17
32
|
}
|
package/index.js
DELETED
package/lib/federation-loader.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require("@module-federation/nextjs-mf/lib/federation-loader")
|