@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.
@@ -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,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.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 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 sharedDefaults = {
19
- "next/dynamic": {
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
- "styled-jsx": {
40
+ 'next/dynamic': {
24
41
  requiredVersion: false,
25
42
  singleton: true,
26
43
  },
27
- "next/link": {
44
+ 'styled-jsx': {
28
45
  requiredVersion: false,
29
46
  singleton: true,
30
47
  },
31
- "next/router": {
48
+ 'next/link': {
32
49
  requiredVersion: false,
33
50
  singleton: true,
34
51
  },
35
- "next/script": {
52
+ 'next/router': {
36
53
  requiredVersion: false,
37
54
  singleton: true,
38
55
  },
39
- "next/head": {
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
- ## 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.
67
+ ## Usage
51
68
 
52
69
  ```js
53
- const SampleComponent = dynamic(
54
- () => window.next2.get("./sampleComponent").then((factory) => factory()),
55
- {
56
- ssr: false,
57
- }
58
- );
70
+ const SampleComponent = dynamic(() => import('next2/sampleComponent'), {
71
+ ssr: false,
72
+ });
59
73
  ```
60
74
 
61
- Make sure you are using `mini-css-extract-plugin@2` - version 2 supports resolving assets through `publicPath:'auto'`
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
- ## Options
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
- 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
- );
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
- ## How to add a sidecar for exposes to your nextjs app
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
- 1. Use `withFederatedSidecar` in your `next.config.js` of the app that you wish to expose modules from. We'll call this "next2".
107
+ With async boundary installed at the page level. You can then do the following
103
108
 
104
109
  ```js
105
- // next.config.js
106
- const { withFederatedSidecar } = require("@module-federation/nextjs-mf");
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
- 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
- },
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
- 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`):
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
- 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,
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
- // we have to share something to ensure share scope is initialized
145
- "@module-federation/nextjs-mf/lib/noop": {
146
- eager: false,
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
- // 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
- });
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
- 4. Add the remote entry for "next2" to the \_document for "next1"
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
- import Document, { Html, Head, Main, NextScript } from "next/document";
194
+ // next.config.js
167
195
 
168
- class MyDocument extends Document {
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
- 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
- }
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
- export default MyDocument;
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
- 5. Use next/dynamic to import from your remotes
220
+ 4. Use next/dynamic or low level api to import remotes.
192
221
 
193
222
  ```js
194
- import dynamic from "next/dynamic";
223
+ import dynamic from 'next/dynamic';
195
224
 
196
225
  const SampleComponent = dynamic(
197
- () => window.next2.get("./sampleComponent").then((factory) => factory()),
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,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,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": "3.0.1",
4
+ "version": "5.2.0",
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")