Skip to content

Commit 7e7a2af

Browse files
thetmanfredsteyer
andcommitted
feat(Build): Dynamic Module Federation. Add config files and helpers.
With module federation you can share dependencies over multiple bundles without loading them multiple times. The dynamic part here allows the host to not know anything about it's remotes. This concept can be used for add-ons to a base bundle. Thanks @manfredsteyer for the help getting this to work! Use the webpack/webpack.mf.js to extend your own webpack configuration with dynamic module federation support. In your main bundle (the "host") use webpack/dynamic_mf.js to configure the entry point. For more information see: docs/developer/module-federation.md. For the general concept see: https://webpack.js.org/concepts/module-federation/ Co-authored-by: Johannes Raggam <thetetet@gmail.com> Co-authored-by: Manfred Steyer <manfred.steyer@gmx.net>
1 parent a5a4ff4 commit 7e7a2af

File tree

5 files changed

+165
-1
lines changed

5 files changed

+165
-1
lines changed

docs/developer/module-federation.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Patternslib Module Federation
2+
3+
## Introduction
4+
5+
For a general introduction see: https://webpack.js.org/concepts/module-federation/
6+
7+
Module Federation allows to share dependencies between bundles.
8+
Each bundle includes the whole set of dependencies.
9+
However, if multiple bundles have the same dependencies they are only loaded once.
10+
11+
For example, if bundle A and B both depend on jQuery and bundle A has already loaded it, bundle B can just reuse the already loaded jQuery file.
12+
But if only bundle B is loaded, it uses its own bundled version of the jQuery library.
13+
14+
There is a host bundle - in the fictional example above our bundle "A".
15+
Other bundles are "remotes" which are initialized for module federation by the host bundle "A"
16+
17+
18+
## How to use it
19+
20+
- Create a new entry point ``index.js`` which only imports the normal entry point.
21+
If your bundle is the host bundle, also import the ``module_federation`` module - importing is enough.
22+
23+
```
24+
import "@patternslib/patternslib/webpack/module_federation";
25+
import("./patterns");
26+
```
27+
28+
- Add the module federation plugin in webpack. There is a configuration factory which you can use for that like so:
29+
30+
```
31+
const package_json = require("./package.json");
32+
const path = require("path");
33+
const patternslib_config = require("@patternslib/patternslib/webpack/webpack.config");
34+
const mf_config = require("@patternslib/patternslib/webpack/webpack.mf");
35+
36+
module.exports = (env, argv) => {
37+
let config = {
38+
entry: {
39+
"bundle.min": path.resolve(__dirname, "src/index.js"),
40+
},
41+
};
42+
43+
config = patternslib_config(env, argv, config, ["mockup"]);
44+
45+
// ...
46+
47+
config.plugins.push(
48+
mf_config({
49+
package_json: package_json,
50+
remote_entry: config.entry["bundle.min"],
51+
})
52+
);
53+
54+
return config;
55+
};
56+
```
57+
58+
That's basically it.
59+
60+
For more information look at the source code at ``@patternslib/webpack/module_federation`` and ``@patternslib/webpack/webpack.mf.js``.
61+

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Webpack entry point for module federation.
2+
import("./patterns");

webpack/module_federation.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Initialize dynamic module federation.
3+
*/
4+
import get_container from "./module_federation--dynamic-federation";
5+
6+
// Patternslib Module Federation bundle prefix.
7+
// This is used to filter for module federation enabled bundles.
8+
// NOTE: This is also defined in ``webpack.mf.js``.
9+
export const MF_NAME_PREFIX = "__patternslib_mf__";
10+
11+
export async function initialize_remote({ remote_name, exposed_module = "./main" }) {
12+
const container = await get_container(remote_name);
13+
const factory = await container.get(exposed_module);
14+
const module = factory();
15+
return module;
16+
}
17+
18+
function document_ready(fn) {
19+
// see if DOM is already available
20+
if (document.readyState === "complete" || document.readyState === "interactive") {
21+
// call on next available tick
22+
setTimeout(fn, 1);
23+
} else {
24+
document.addEventListener("DOMContentLoaded", fn);
25+
}
26+
}
27+
28+
document_ready(function () {
29+
// Automatically initialize all Module Federation enabled Patternslib based
30+
// bundles by filtering for the prefix ``__patternslib_mf__``.
31+
// Do this on document ready, as this is the time where all MF bundles have
32+
// been registered in the global namespace.
33+
const bundles = Object.keys(window).filter((it) => it.indexOf(MF_NAME_PREFIX) === 0);
34+
for (const bundle_name of bundles) {
35+
// Now load + initialize each bundle.
36+
initialize_remote({ remote_name: bundle_name });
37+
console.debug(
38+
`Patternslib Module Federation: Loaded and initialized bundle "${bundle_name}".`
39+
);
40+
}
41+
});

webpack/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module.exports = (env, argv, config, babel_include = []) => {
2525

2626
const base_config = {
2727
entry: {
28-
"bundle.min": path.resolve(__dirname, "../src/patterns.js"),
28+
"bundle.min": path.resolve(__dirname, "../src/index.js"),
2929
"bundle-polyfills.min": path.resolve(__dirname, "../src/polyfills.js"),
3030
},
3131
externals: [

webpack/webpack.mf.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const { ModuleFederationPlugin } = require("webpack").container;
2+
const local_package_json = require("../package.json");
3+
4+
// Patternslib Module Federation bundle prefix.
5+
// This is used to filter for module federation enabled bundles.
6+
// NOTE: This is also defined in ``module_federation.js``.
7+
const MF_NAME_PREFIX = "__patternslib_mf__";
8+
9+
function shared_from_dependencies(...dependencies) {
10+
const shared = {};
11+
for (const deps of dependencies) {
12+
if (!deps) {
13+
continue;
14+
}
15+
for (const [name, version] of Object.entries(deps)) {
16+
shared[name] = {
17+
singleton: true,
18+
requiredVersion: version,
19+
};
20+
if (name === "underscore") {
21+
// Underscore, for some reason, needs to have the version set explicitly
22+
shared[name].requiredVersion = "1.13.2";
23+
}
24+
}
25+
}
26+
return shared;
27+
}
28+
29+
function config({ name, filename = "remote.min.js", package_json, remote_entry }) {
30+
// If no name is given, use the package name.
31+
name = name || package_json?.name || local_package_json.name;
32+
33+
// Create a JS-variable compatible name and add a prefix.
34+
const normalized_bundle_name =
35+
MF_NAME_PREFIX + name.match(/([_$A-Za-z0-9])/g).join("");
36+
37+
return new ModuleFederationPlugin({
38+
name: normalized_bundle_name,
39+
...(remote_entry && {
40+
filename: filename,
41+
exposes: {
42+
"./main": remote_entry,
43+
},
44+
}),
45+
shared: {
46+
...shared_from_dependencies(
47+
local_package_json.dependencies,
48+
package_json?.dependencies
49+
),
50+
},
51+
});
52+
}
53+
54+
// Default export
55+
const module_exports = (module.exports = config);
56+
57+
// Named exports
58+
module_exports.config = config;
59+
module_exports.MF_NAME_PREFIX = MF_NAME_PREFIX;
60+
module_exports.shared_from_dependencies = shared_from_dependencies;

0 commit comments

Comments
 (0)