@module-federation/nextjs-mf 2.3.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.
- package/.prettierignore +2 -0
- package/.prettierrc +7 -1
- package/README.md +213 -77
- package/lib/NextFederationPlugin.js +477 -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 +34 -0
- package/lib/loaders/nextPageMapLoader.js +129 -0
- package/lib/utils.js +97 -0
- package/package.json +16 -6
- package/docs/MFCover.png +0 -0
- package/index.d.ts +0 -2
- package/index.js +0 -5
- package/lib/federation-loader.js +0 -27
- package/lib/noop.js +0 -0
- package/lib/with-federated-sidecar.d.ts +0 -11
- package/lib/with-federated-sidecar.js +0 -120
|
@@ -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,22 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"public": true,
|
|
3
3
|
"name": "@module-federation/nextjs-mf",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.1.2",
|
|
5
5
|
"description": "Module Federation helper for NextJS",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"types": "index.d.ts",
|
|
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"
|
|
16
19
|
},
|
|
17
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",
|
|
18
24
|
"next": "11.0.1",
|
|
19
25
|
"prettier": "2.3.2",
|
|
20
|
-
"
|
|
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"
|
|
21
31
|
}
|
|
22
32
|
}
|
package/docs/MFCover.png
DELETED
|
Binary file
|
package/index.d.ts
DELETED
package/index.js
DELETED
package/lib/federation-loader.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const shareNextInternals = `if (process.browser) {
|
|
2
|
-
Object.assign(__webpack_share_scopes__.default, {
|
|
3
|
-
"next/link": {
|
|
4
|
-
[next.version]: {
|
|
5
|
-
loaded: true,
|
|
6
|
-
get: () => Promise.resolve(() => require("next/link")),
|
|
7
|
-
},
|
|
8
|
-
},
|
|
9
|
-
"next/head": {
|
|
10
|
-
[next.version]: {
|
|
11
|
-
loaded: true,
|
|
12
|
-
get: () => Promise.resolve(() => require("next/head")),
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
"next/dynamic": {
|
|
16
|
-
[next.version]: {
|
|
17
|
-
loaded: true,
|
|
18
|
-
get: () => Promise.resolve(() => require("next/dynamic")),
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
module.exports = function (source) {
|
|
26
|
-
return shareNextInternals + source
|
|
27
|
-
};
|
package/lib/noop.js
DELETED
|
File without changes
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { ModuleFederationPluginOptions } from "webpack/lib/container/ModuleFederationPlugin";
|
|
2
|
-
|
|
3
|
-
export type WithFederatedSidecarOptions = {
|
|
4
|
-
removePlugins?: string[];
|
|
5
|
-
stats?: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export function withFederatedSidecar(
|
|
9
|
-
federationPluginOptions: ModuleFederationPluginOptions,
|
|
10
|
-
withModuleFederationOptions?: WithFederatedSidecarOptions
|
|
11
|
-
): (nextConfig?: any) => any;
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
const path = require("path");
|
|
2
|
-
|
|
3
|
-
const FederatedStatsPlugin = require("webpack-federated-stats-plugin");
|
|
4
|
-
|
|
5
|
-
const withModuleFederation =
|
|
6
|
-
(
|
|
7
|
-
federationPluginOptions,
|
|
8
|
-
{
|
|
9
|
-
removePlugins = [
|
|
10
|
-
"BuildManifestPlugin",
|
|
11
|
-
"ReactLoadablePlugin",
|
|
12
|
-
"DropClientPage",
|
|
13
|
-
"WellKnownErrorsPlugin",
|
|
14
|
-
],
|
|
15
|
-
stats,
|
|
16
|
-
} = {}
|
|
17
|
-
) =>
|
|
18
|
-
(nextConfig = {}) =>
|
|
19
|
-
Object.assign({}, nextConfig, {
|
|
20
|
-
/**
|
|
21
|
-
* @param {import("webpack").Configuration} config
|
|
22
|
-
* @param {*} options
|
|
23
|
-
* @returns {import("webpack").Configuration}
|
|
24
|
-
*/
|
|
25
|
-
webpack(config, options) {
|
|
26
|
-
const { webpack } = options;
|
|
27
|
-
|
|
28
|
-
if (!options.isServer) {
|
|
29
|
-
/**
|
|
30
|
-
* @type {{ webpack: import("webpack") }}
|
|
31
|
-
*/
|
|
32
|
-
config.plugins.push({
|
|
33
|
-
__NextFederation__: true,
|
|
34
|
-
/**
|
|
35
|
-
* @param {import("webpack").Compiler} compiler
|
|
36
|
-
*/
|
|
37
|
-
apply(compiler) {
|
|
38
|
-
compiler.hooks.afterCompile.tapAsync(
|
|
39
|
-
"NextFederation",
|
|
40
|
-
(compilation, done) => {
|
|
41
|
-
const toDrop = new Set(removePlugins || []);
|
|
42
|
-
|
|
43
|
-
const filteredPlugins = compilation.options.plugins.filter(
|
|
44
|
-
(plugin) => {
|
|
45
|
-
if (
|
|
46
|
-
(plugin.constructor &&
|
|
47
|
-
toDrop.has(plugin.constructor.name)) ||
|
|
48
|
-
plugin.__NextFederation__
|
|
49
|
-
) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
);
|
|
56
|
-
// attach defaults that always need to be shared
|
|
57
|
-
Object.assign(federationPluginOptions.shared, {
|
|
58
|
-
"next/dynamic": {
|
|
59
|
-
requiredVersion: false,
|
|
60
|
-
singleton: true,
|
|
61
|
-
},
|
|
62
|
-
"next/link": {
|
|
63
|
-
requiredVersion: false,
|
|
64
|
-
singleton: true,
|
|
65
|
-
},
|
|
66
|
-
"next/head": {
|
|
67
|
-
requiredVersion: false,
|
|
68
|
-
singleton: true,
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
/** @type {import("webpack").WebpackOptionsNormalized} */
|
|
72
|
-
const webpackOptions = {
|
|
73
|
-
cache: false,
|
|
74
|
-
...compilation.options,
|
|
75
|
-
output: {
|
|
76
|
-
...compilation.options.output,
|
|
77
|
-
chunkLoadingGlobal: undefined,
|
|
78
|
-
devtoolNamespace: undefined,
|
|
79
|
-
uniqueName: federationPluginOptions.name,
|
|
80
|
-
},
|
|
81
|
-
entry: {
|
|
82
|
-
noop: { import: [path.resolve(__dirname, "noop.js")] },
|
|
83
|
-
},
|
|
84
|
-
plugins: [
|
|
85
|
-
...filteredPlugins,
|
|
86
|
-
new webpack.container.ModuleFederationPlugin(
|
|
87
|
-
federationPluginOptions
|
|
88
|
-
),
|
|
89
|
-
],
|
|
90
|
-
optimization: {
|
|
91
|
-
...compilation.options.optimization,
|
|
92
|
-
runtimeChunk: false,
|
|
93
|
-
splitChunks: undefined,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
if (stats) {
|
|
98
|
-
webpackOptions.plugins.push(
|
|
99
|
-
new FederatedStatsPlugin({ filename: stats })
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
webpack(webpackOptions, (err) => {
|
|
104
|
-
done(err);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (typeof nextConfig.webpack === "function") {
|
|
113
|
-
return nextConfig.webpack(config, options);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return config;
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
module.exports = withModuleFederation;
|