@module-federation/nextjs-mf 3.0.1 → 5.1.2

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.
@@ -0,0 +1,2 @@
1
+ .next
2
+ lib
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "singleQuote": true,
3
+ "arrowParens": "always",
4
+ "tabWidth": 2,
5
+ "useTabs": false,
6
+ "trailingComma": "es5"
7
+ }
package/README.md CHANGED
@@ -2,12 +2,19 @@
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.2.x || ^11.x.x || ^12.x.x
10
- - Client side only
7
+ - next ^11.x.x || ^12.x.x
8
+ - Client side only, SSR is another package currently in beta
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
11
18
 
12
19
  ## Whats shared by default?
13
20
 
@@ -15,190 +22,234 @@ Under the hood we share some next internals automatically
15
22
  You do not need to share these packages, sharing next internals yourself will cause errors.
16
23
 
17
24
  ```js
18
- const sharedDefaults = {
19
- "next/dynamic": {
25
+ const DEFAULT_SHARE_SCOPE = {
26
+ react: {
27
+ singleton: true,
20
28
  requiredVersion: false,
29
+ },
30
+ 'react/': {
21
31
  singleton: true,
32
+ requiredVersion: false,
22
33
  },
23
- "styled-jsx": {
34
+ 'react-dom': {
35
+ singleton: true,
36
+ requiredVersion: false,
37
+ },
38
+ 'next/dynamic': {
24
39
  requiredVersion: false,
25
40
  singleton: true,
26
41
  },
27
- "next/link": {
42
+ 'styled-jsx': {
28
43
  requiredVersion: false,
29
44
  singleton: true,
30
45
  },
31
- "next/router": {
46
+ 'next/link': {
32
47
  requiredVersion: false,
33
48
  singleton: true,
34
49
  },
35
- "next/script": {
50
+ 'next/router': {
36
51
  requiredVersion: false,
37
52
  singleton: true,
38
53
  },
39
- "next/head": {
54
+ 'next/script': {
55
+ requiredVersion: false,
56
+ singleton: true,
57
+ },
58
+ 'next/head': {
40
59
  requiredVersion: false,
41
60
  singleton: true,
42
61
  },
43
62
  };
44
63
  ```
45
64
 
46
- ## Things to watch out for
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.
65
+ ## Usage
51
66
 
52
67
  ```js
53
- const SampleComponent = dynamic(
54
- () => window.next2.get("./sampleComponent").then((factory) => factory()),
55
- {
56
- ssr: false,
57
- }
58
- );
68
+ const SampleComponent = dynamic(() => import('next2/sampleComponent'), {
69
+ ssr: false,
70
+ });
59
71
  ```
60
72
 
61
- Make sure you are using `mini-css-extract-plugin@2` - version 2 supports resolving assets through `publicPath:'auto'`
73
+ If you want support for sync imports. It is possible in next@12 as long as there is an async boundary.
62
74
 
63
- ## Options
75
+ #### See the implementation here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs/home/pages
76
+
77
+ With async boundary installed at the page level. You can then do the following
64
78
 
65
79
  ```js
66
- withFederatedSidecar(
67
- {
68
- name: "next2",
69
- filename: "static/chunks/remoteEntry.js",
70
- exposes: {
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
- );
80
+ if (process.browser) {
81
+ const SomeHook = require('next2/someHook');
82
+ }
83
+ // if client only file
84
+ import SomeComponent from 'next2/someComponent';
94
85
  ```
95
86
 
87
+ Make sure you are using `mini-css-extract-plugin@2` - version 2 supports resolving assets through `publicPath:'auto'`
88
+
96
89
  ## Demo
97
90
 
98
91
  You can see it in action here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs
99
92
 
100
- ## How to add a sidecar for exposes to your nextjs app
93
+ ## Usage
101
94
 
102
- 1. Use `withFederatedSidecar` in your `next.config.js` of the app that you wish to expose modules from. We'll call this "next2".
95
+ ```js
96
+ const SampleComponent = dynamic(() => import('next2/sampleComponent'), {
97
+ ssr: false,
98
+ });
99
+ ```
100
+
101
+ If you want support for sync imports. It is possible in next@12 as long as there is an async boundary.
102
+
103
+ #### See the implementation here: https://github.com/module-federation/module-federation-examples/tree/master/nextjs/home/pages
104
+
105
+ With async boundary installed at the page level. You can then do the following
103
106
 
104
107
  ```js
105
- // next.config.js
106
- const { withFederatedSidecar } = require("@module-federation/nextjs-mf");
108
+ if (process.browser) {
109
+ const SomeHook = require('next2/someHook');
110
+ }
111
+ // if client only file
112
+ import SomeComponent from 'next2/someComponent';
113
+ ```
107
114
 
108
- module.exports = withFederatedSidecar({
109
- name: "next2",
110
- filename: "static/chunks/remoteEntry.js",
111
- exposes: {
112
- "./sampleComponent": "./components/sampleComponent.js",
113
- },
114
- shared: {
115
- react: {
116
- // Notice shared are NOT eager here.
117
- requiredVersion: false,
118
- singleton: true,
119
- },
115
+ Make sure you are using `mini-css-extract-plugin@2` - version 2 supports resolving assets through `publicPath:'auto'`
116
+
117
+ ## Options
118
+
119
+ This plugin works exactly like ModuleFederationPlugin, use it as you'd normally.
120
+ Note that we already share react and next stuff for you automatically.
121
+
122
+ Also NextFederationPlugin has own optional argument `extraOptions` where you can unlock additional features of this plugin:
123
+
124
+ ```js
125
+ new NextFederationPlugin({
126
+ name: ...,
127
+ filename: ...,
128
+ remotes: ...,
129
+ exposes: ...,
130
+ shared: ...,
131
+ extraOptions: {
132
+ exposePages: true, // `false` by default
133
+ enableImageLoaderFix: true, // `false` by default
134
+ enableUrlLoaderFix: true, // `false` by default
120
135
  },
121
- })({
122
- // your original next.config.js export
123
136
  });
124
137
  ```
125
138
 
126
- 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`):
139
+ - `exposePages` exposes automatically all nextjs pages for you and theirs `./pages-map`.
140
+ - `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.
141
+ - `enableUrlLoaderFix` – adds public hostname to all assets bundled by `url-loader`.
142
+
143
+ ## Demo
144
+
145
+ You can see it in action here: https://github.com/module-federation/module-federation-examples/pull/2147
146
+
147
+ ## Implementing the Plugin
148
+
149
+ 1. Use `NextFederationPlugin` in your `next.config.js` of the app that you wish to expose modules from. We'll call this "next2".
127
150
 
128
151
  ```js
152
+ // next.config.js
153
+ const NextFederationPlugin = require('@module-federation/nextjs-mf/NextFederationPlugin');
154
+
129
155
  module.exports = {
130
156
  webpack(config, options) {
131
- config.plugins.push(
132
- new options.webpack.container.ModuleFederationPlugin({
133
- remoteType: "var",
134
- remotes: {
135
- next2: "next2",
136
- },
137
- shared: {
138
- react: {
139
- // Notice shared ARE eager here.
140
- eager: true,
141
- singleton: true,
142
- requiredVersion: false,
157
+ if (!options.isServer) {
158
+ config.plugins.push(
159
+ new NextFederationPlugin({
160
+ name: 'next2',
161
+ remotes: {
162
+ next1: `next1@http://localhost:3001/_next/static/chunks/remoteEntry.js`,
143
163
  },
144
- // we have to share something to ensure share scope is initialized
145
- "@module-federation/nextjs-mf/lib/noop": {
146
- eager: false,
164
+ filename: 'static/chunks/remoteEntry.js',
165
+ exposes: {
166
+ './title': './components/exposedTitle.js',
167
+ './checkout': './pages/checkout',
168
+ './pages-map': './pages-map.js',
147
169
  },
148
- },
149
- })
150
- );
151
-
152
- // we attach next internals to share scope at runtime
153
- config.module.rules.push({
154
- test: /pages\/_app.[jt]sx?/,
155
- loader: "@module-federation/nextjs-mf/lib/federation-loader.js",
156
- });
170
+ shared: {
171
+ // whatever else
172
+ },
173
+ })
174
+ );
175
+ }
157
176
 
158
177
  return config;
159
178
  },
160
179
  };
180
+
181
+ // _app.js or some other file in as high up in the app (like next's new layouts)
182
+ // this ensures various parts of next.js are imported and "used" somewhere so that they wont be tree shaken out
183
+ import '@module-federation/nextjs-mf/lib/include-defaults';
161
184
  ```
162
185
 
163
- 4. Add the remote entry for "next2" to the \_document for "next1"
186
+ 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`):
187
+ Inside that \_app.js or layout.js file, ensure you import `include-defaults` file
164
188
 
165
189
  ```js
166
- import Document, { Html, Head, Main, NextScript } from "next/document";
190
+ // next.config.js
167
191
 
168
- class MyDocument extends Document {
169
- static async getInitialProps(ctx) {
170
- const initialProps = await Document.getInitialProps(ctx);
171
- return { ...initialProps };
172
- }
192
+ const NextFederationPlugin = require('@module-federation/nextjs-mf/NextFederationPlugin');
173
193
 
174
- render() {
175
- return (
176
- <Html>
177
- <Head />
178
- <body>
179
- <Main />
180
- <script src="http://next2-domain-here.com/_next/static/chunks/remoteEntry.js" />
181
- <NextScript />
182
- </body>
183
- </Html>
184
- );
185
- }
186
- }
194
+ module.exports = {
195
+ webpack(config, options) {
196
+ if (!options.isServer) {
197
+ config.plugins.push(
198
+ new NextFederationPlugin({
199
+ name: 'next1',
200
+ remotes: {
201
+ next2: `next2@http://localhost:3000/_next/static/chunks/remoteEntry.js`,
202
+ },
203
+ })
204
+ );
205
+ }
187
206
 
188
- export default MyDocument;
207
+ return config;
208
+ },
209
+ };
210
+
211
+ // _app.js or some other file in as high up in the app (like next's new layouts)
212
+ // this ensures various parts of next.js are imported and "used" somewhere so that they wont be tree shaken out
213
+ import '@module-federation/nextjs-mf/lib/include-defaults';
189
214
  ```
190
215
 
191
- 5. Use next/dynamic to import from your remotes
216
+ 4. Use next/dynamic or low level api to import remotes.
192
217
 
193
218
  ```js
194
- import dynamic from "next/dynamic";
219
+ import dynamic from 'next/dynamic';
195
220
 
196
221
  const SampleComponent = dynamic(
197
- () => window.next2.get("./sampleComponent").then((factory) => factory()),
222
+ () => window.next2.get('./sampleComponent').then((factory) => factory()),
198
223
  {
199
224
  ssr: false,
200
225
  }
201
226
  );
227
+
228
+ // or
229
+
230
+ const SampleComponent = dynamic(() => import('next2/sampleComponent'), {
231
+ ssr: false,
232
+ });
233
+ ```
234
+
235
+ ## Utilities
236
+
237
+ Ive added a util for dynamic chunk loading, in the event you need to load remote containers dynamically.
238
+
239
+ ```js
240
+ import { injectScript } from '@module-federation/nextjs-mf/lib/utils';
241
+ // if i have remotes in my federation plugin, i can pass the name of the remote
242
+ injectScript('home').then((remoteContainer) => {
243
+ remoteContainer.get('./exposedModule');
244
+ });
245
+ // if i want to load a custom remote not known at build time.
246
+
247
+ injectScript({
248
+ global: 'home',
249
+ url: 'http://somthing.com/remoteEntry.js',
250
+ }).then((remoteContainer) => {
251
+ remoteContainer.get('./exposedModule');
252
+ });
202
253
  ```
203
254
 
204
255
  ## Contact
@@ -0,0 +1,477 @@
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
+ ...externalizedShares,
224
+ ...this._options.shared,
225
+ },
226
+ }),
227
+ new webpack.web.JsonpTemplatePlugin(childOutput),
228
+ new LoaderTargetPlugin('web'),
229
+ new LibraryPlugin('var'),
230
+ new webpack.DefinePlugin({
231
+ 'process.env.REMOTES': JSON.stringify(this._options.remotes),
232
+ 'process.env.CURRENT_HOST': JSON.stringify(this._options.name),
233
+ }),
234
+ new AddRuntimeRequirementToPromiseExternal(),
235
+ ]
236
+ );
237
+ new RemoveRRRuntimePlugin().apply(childCompiler);
238
+
239
+ childCompiler.options.module.rules.forEach((rule) => {
240
+ // next-image-loader fix which adds remote's hostname to the assets url
241
+ if (
242
+ this._extraOptions.enableImageLoaderFix &&
243
+ helpers.hasLoader(rule, 'next-image-loader')
244
+ ) {
245
+ helpers.injectRuleLoader(rule, {
246
+ loader: path__default["default"].resolve(__dirname, './loaders/fixImageLoader.js'),
247
+ });
248
+ }
249
+
250
+ // url-loader fix for which adds remote's hostname to the assets url
251
+ if (
252
+ this._extraOptions.enableUrlLoaderFix &&
253
+ helpers.hasLoader(rule, 'url-loader')
254
+ ) {
255
+ helpers.injectRuleLoader({
256
+ loader: path__default["default"].resolve(__dirname, './loaders/fixUrlLoader.js'),
257
+ });
258
+ }
259
+ });
260
+
261
+ const MiniCss = childCompiler.options.plugins.find((p) => {
262
+ return p.constructor.name === 'NextMiniCssExtractPlugin';
263
+ });
264
+
265
+ const removePlugins = [
266
+ 'NextJsRequireCacheHotReloader',
267
+ 'BuildManifestPlugin',
268
+ 'WellKnownErrorsPlugin',
269
+ 'WebpackBuildEventsPlugin',
270
+ 'HotModuleReplacementPlugin',
271
+ 'NextMiniCssExtractPlugin',
272
+ 'NextFederationPlugin',
273
+ 'CopyFilePlugin',
274
+ 'ProfilingPlugin',
275
+ 'DropClientPage',
276
+ 'ReactFreshWebpackPlugin',
277
+ ];
278
+
279
+ childCompiler.options.plugins = childCompiler.options.plugins.filter(
280
+ (plugin) => !removePlugins.includes(plugin.constructor.name)
281
+ );
282
+
283
+ if (MiniCss) {
284
+ new MiniCss.constructor({
285
+ ...MiniCss.options,
286
+ filename: MiniCss.options.filename.replace('.css', '-fed.css'),
287
+ chunkFilename: MiniCss.options.chunkFilename.replace(
288
+ '.css',
289
+ '-fed.css'
290
+ ),
291
+ }).apply(childCompiler);
292
+ }
293
+
294
+ childCompiler.options.experiments.lazyCompilation = false;
295
+ childCompiler.options.optimization.runtimeChunk = false;
296
+ delete childCompiler.options.optimization.splitChunks;
297
+ childCompiler.outputFileSystem = compiler.outputFileSystem;
298
+ if (compiler.options.mode === 'development') {
299
+ childCompiler.run((err, stats) => {
300
+ if (err) {
301
+ console.error(err);
302
+ throw new Error(err);
303
+ }
304
+ });
305
+ } else {
306
+ childCompiler.runAsChild((err, stats) => {
307
+ if (err) {
308
+ console.error(err);
309
+ throw new Error(err);
310
+ }
311
+ });
312
+ }
313
+ });
314
+ }
315
+ }
316
+
317
+ class AddRuntimeRequirementToPromiseExternal {
318
+ apply(compiler) {
319
+ compiler.hooks.compilation.tap(
320
+ 'AddRuntimeRequirementToPromiseExternal',
321
+ (compilation) => {
322
+ const RuntimeGlobals = compiler.webpack.RuntimeGlobals;
323
+ // if (compilation.outputOptions.trustedTypes) {
324
+ compilation.hooks.additionalModuleRuntimeRequirements.tap(
325
+ 'AddRuntimeRequirementToPromiseExternal',
326
+ (module, set, context) => {
327
+ if (module.externalType === 'promise') {
328
+ set.add(RuntimeGlobals.loadScript);
329
+ set.add(RuntimeGlobals.require);
330
+ }
331
+ }
332
+ );
333
+ // }
334
+ }
335
+ );
336
+ }
337
+ }
338
+
339
+ function extractUrlAndGlobal(urlAndGlobal) {
340
+ const index = urlAndGlobal.indexOf('@');
341
+ if (index <= 0 || index === urlAndGlobal.length - 1) {
342
+ throw new Error(`Invalid request "${urlAndGlobal}"`);
343
+ }
344
+ return [urlAndGlobal.substring(index + 1), urlAndGlobal.substring(0, index)];
345
+ }
346
+
347
+ function generateRemoteTemplate(url, global) {
348
+ return `promise new Promise(function (resolve, reject) {
349
+ var __webpack_error__ = new Error();
350
+ if (typeof ${global} !== 'undefined') return resolve();
351
+ __webpack_require__.l(
352
+ ${JSON.stringify(url)},
353
+ function (event) {
354
+ if (typeof ${global} !== 'undefined') return resolve();
355
+ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
356
+ var realSrc = event && event.target && event.target.src;
357
+ __webpack_error__.message =
358
+ 'Loading script failed.\\n(' + errorType + ': ' + realSrc + ')';
359
+ __webpack_error__.name = 'ScriptExternalLoadError';
360
+ __webpack_error__.type = errorType;
361
+ __webpack_error__.request = realSrc;
362
+ reject(__webpack_error__);
363
+ },
364
+ ${JSON.stringify(global)},
365
+ );
366
+ }).then(function () {
367
+ const proxy = {
368
+ get: ${global}.get,
369
+ init: (args) => {
370
+ const handler = {
371
+ get(target, prop) {
372
+ if (target[prop]) {
373
+ Object.values(target[prop]).forEach(function(o) {
374
+ if(o.from === '_N_E') {
375
+ o.loaded = true
376
+ }
377
+ })
378
+ }
379
+ return target[prop]
380
+ },
381
+ set(target, property, value, receiver) {
382
+ if (target[property]) {
383
+ return target[property]
384
+ }
385
+ target[property] = value
386
+ return true
387
+ }
388
+ }
389
+ try {
390
+ ${global}.init(new Proxy(__webpack_require__.S.default, handler))
391
+ } catch (e) {
392
+
393
+ }
394
+ ${global}.__initialized = true
395
+ }
396
+ }
397
+ if (!${global}.__initialized) {
398
+ proxy.init()
399
+ }
400
+ return proxy
401
+ })`;
402
+ }
403
+
404
+ function createRuntimeVariables(remotes) {
405
+ return Object.entries(remotes).reduce((acc, remote) => {
406
+ acc[remote[0]] = remote[1].replace('promise ', '');
407
+ return acc;
408
+ }, {});
409
+ }
410
+
411
+ class NextFederationPlugin {
412
+ constructor(options) {
413
+ const { extraOptions, ...mainOpts } = options;
414
+ this._options = mainOpts;
415
+ this._extraOptions = extraOptions;
416
+ if (options.remotes) {
417
+ const parsedRemotes = Object.entries(options.remotes).reduce(
418
+ (acc, remote) => {
419
+ if (remote[1].includes('@')) {
420
+ const [url, global] = extractUrlAndGlobal(remote[1]);
421
+ acc[remote[0]] = generateRemoteTemplate(url, global);
422
+ return acc;
423
+ }
424
+ acc[remote[0]] = remote[1];
425
+ return acc;
426
+ },
427
+ {}
428
+ );
429
+ this._options.remotes = parsedRemotes;
430
+ }
431
+ }
432
+
433
+ apply(compiler) {
434
+ const webpack = compiler.webpack;
435
+ const sharedForHost = Object.entries({
436
+ ...(this._options.shared || {}),
437
+ ...DEFAULT_SHARE_SCOPE,
438
+ }).reduce((acc, item) => {
439
+ const [itemKey, shareOptions] = item;
440
+
441
+ const shareKey = 'host' + (item.shareKey || itemKey);
442
+ acc[shareKey] = shareOptions;
443
+ if (!shareOptions.import) {
444
+ acc[shareKey].import = itemKey;
445
+ }
446
+ if (!shareOptions.shareKey) {
447
+ acc[shareKey].shareKey = itemKey;
448
+ }
449
+
450
+ if (DEFAULT_SHARE_SCOPE[itemKey]) {
451
+ acc[shareKey].packageName = itemKey;
452
+ }
453
+ return acc;
454
+ }, {});
455
+
456
+ new webpack.container.ModuleFederationPlugin({
457
+ ...this._options,
458
+ exposes: {},
459
+ shared: {
460
+ noop: {
461
+ import: 'data:text/javascript,module.exports = {};',
462
+ requiredVersion: false,
463
+ version: '0',
464
+ },
465
+ ...sharedForHost,
466
+ },
467
+ }).apply(compiler);
468
+ new webpack.DefinePlugin({
469
+ 'process.env.REMOTES': createRuntimeVariables(this._options.remotes),
470
+ 'process.env.CURRENT_HOST': JSON.stringify(this._options.name),
471
+ }).apply(compiler);
472
+ new ChildFederation(this._options, this._extraOptions).apply(compiler);
473
+ new AddRuntimeRequirementToPromiseExternal().apply(compiler);
474
+ }
475
+ }
476
+
477
+ 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,3 @@
1
+ const NextFederationPlugin = require('./NextFederationPlugin');
2
+
3
+ module.exports = NextFederationPlugin;
@@ -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,34 @@
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 === loaderName ||
27
+ loader.loader.includes(`/${loaderName}/`)
28
+ ) {
29
+ return true;
30
+ }
31
+ }
32
+ }
33
+ return false;
34
+ };
@@ -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": "3.0.1",
4
+ "version": "5.1.2",
5
5
  "description": "Module Federation helper for NextJS",
6
- "main": "index.js",
7
- "readme": "README.md",
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
- "postinstall": "npm install @module-federation/nextjs-mf --registry https://r.privjs.com"
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
- "@module-federation/nextjs-mf": "^3.5.0"
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
@@ -1,5 +0,0 @@
1
- const {withFederatedSidecar} = require("@module-federation/nextjs-mf");
2
-
3
- module.exports = {
4
- withFederatedSidecar,
5
- };
@@ -1 +0,0 @@
1
- module.exports = require("@module-federation/nextjs-mf/lib/federation-loader")