diff --git a/ExtendedModuleFederationPlugin.js b/ModuleFederationEnhancedPlugin.js similarity index 79% rename from ExtendedModuleFederationPlugin.js rename to ModuleFederationEnhancedPlugin.js index cb84a26..6536547 100644 --- a/ExtendedModuleFederationPlugin.js +++ b/ModuleFederationEnhancedPlugin.js @@ -6,9 +6,9 @@ const GenerateModuleMap = require("./src/GenerateModuleMap"); const GenerateRemoteMap = require("./src/GenerateRemoteMap"); const GenerateRemoteUrlMap = require("./src/GenerateRemoteUrlMap"); const AddRuntimeRequirementsToExternal = require("./src/AddRuntimeRequirementsToExternal"); +const HandleRemoteObject = require("./src/HandleRemoteObject"); - -class ExtendedModuleFederationPlugin extends ModuleFederationPlugin { +class ModuleFederationEnhancedPlugin extends ModuleFederationPlugin { constructor(options) { if (!options.exposes) { options.exposes = {}; @@ -21,6 +21,8 @@ class ExtendedModuleFederationPlugin extends ModuleFederationPlugin { ...GenerateRemoteUrlMap(options), }; + options.remotes = HandleRemoteObject(options.remotes); + super(options); this.options = options; } @@ -30,4 +32,4 @@ class ExtendedModuleFederationPlugin extends ModuleFederationPlugin { new AddRuntimeRequirementsToExternal().apply(compiler) } } -module.exports = ExtendedModuleFederationPlugin; +module.exports = ModuleFederationEnhancedPlugin; diff --git a/README.md b/README.md index bd1d74f..8bea206 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ ## Idea -Webpack Federation can do quite a lot, but orchestrating it can sometimes be a little messy or complex with its given api. The idea is to offer some additonal lifecycles directly on webpack interfaces to allow for pluggability and a more standardized ecosystem - - +Webpack Federation can do quite a lot, but orchestrating it can sometimes be a little messy or complex with its given api. The idea is to offer some additonal lifecycles directly on webpack interfaces to allow for pluggability and a more standardized ecosystem # Remotes @@ -18,7 +16,7 @@ new ModuleFederationPlugin({ }, (importValue) => { if(window.remoteOverrides['app1']) { - // if an override exists, change the script that will be injected. + // if an override exists, change the script that will be injected. return window.remoteOverrides['app1'] // --> otherRemote@http://otherUrl } else { return importValue // do nothing @@ -46,45 +44,82 @@ new ModuleFederationPlugin({ ``` ## Middleware + `promise new Promise` is handy, but it would be great if one could compose a series of middlewares into the promise chain in a standard manner. This would be useful for + - controling versions/overrides at runtime - injecting env variables before webpack connects the containers together - doing some series of actions during the initial import() for a remote that only happens once - prior to a remote being injected -- additional security or auth gates without having to write wrapper code around your exposed modules. +- additional security or auth gates without having to write wrapper code around your exposed modules. ## onBeforeGet -The lifecycle of federation is as follows. -1) inject remote (happens once) -2) init() remote (happens once) -3) get() exposed module (happens for every import) +The lifecycle of federation is as follows. + +1. inject remote (happens once) +2. init() remote (happens once) +3. get() exposed module (happens for every import) -After middleware has been run, and the remote container injected into the application, and has initialized. +After middleware has been run, and the remote container injected into the application, and has initialized. -Sometimes i might want to do something before calling the underlaying get() of a container. perhaps re-route the module request, or set up distributed logging automatically before import() resolves and starts executing. +Sometimes i might want to do something before calling the underlaying get() of a container. perhaps re-route the module request, or set up distributed logging automatically before import() resolves and starts executing. onBeforeGet is basically middleware but for the module import itself instead of container injection +### Remote as an Object + +Initially intended to handle the async default usage, but proves a better way of hadling remote URL based on envs or whatever. + +| Prop | Description | +| ------- | ------------------------------------------------------------------------------------------------- | +| async | if it must be wrapped aroung with `promise new Promise` | +| name | the name of the remote, compiled out as the name before the `@` | +| url | the url of the remoteEntry for of the remote, compiled out as the url after the `@ ` | +| onError | optional function to be called on the async if the remote is offline or had any exception loading | + + +The `async` prop, embed the Promise Based approach as in the [Webpack docs](https://webpack.js.org/concepts/module-federation/), turning the values passed as an objet to the `promise new Promise(resolve => {` that provides an outofbox handling of offline remotes. +#### Usage + +```js + remotes: { + app1: "app1@myApp1.com/remoteEntry.js", + app2: { + name: "app2", + url: isProd ? urlProd : urlDev + }, + app3: { + async: true, + name: "app3", + url: "http://coolAppRunningOnCloud.com.br/remoteEntry.js", + }, + app2: { + name: "app2", + url: process.env.FINAL_REMOTE_ENTRY + }, + } +``` ### Custom Maps #### Extended to remoteEntry: -| Prop | Description | -| --------- | --------------------------------------------------- | -| moduleMap | list of all available modules from a single remote. | -| remoteMap | list of all remotes available for consumption | -| remoteUrlMap | list of all remotes URL to initilize | + +| Prop | Description | +| ------------ | --------------------------------------------------- | +| moduleMap | list of all available modules from a single remote. | +| remoteMap | list of all remotes available for consumption | +| remoteUrlMap | list of all remotes URL to initilize | #### Usage ```js -const ModuleFederationEnhacedPlugin = require("@module-federation/ModuleFederationEnhacedPlugin"); +const ModuleFederationEnhancedPlugin = require("@module-federation/ModuleFederationEnhancedPlugin"); module.export = { //... rest of your config plugins: [ - new ModuleFederationEnhacedPlugin({ + new ModuleFederationEnhancedPlugin({ name: "myApp", library: { type: "var", name: "app2" }, filename: "remoteEntry.js", @@ -114,10 +149,10 @@ import remoteMap from "myApp/remoteMap"; import remoteUrlMap from "myApp/remoteUrlMap"; ``` - #### Chunk Map -| Prop | Description | -| --------- | --------------------------------------------------- | +| Prop | Description | +| ------------- | ------------------------------------------------------------- | | chunkMap.json | list of all chunkNames and create a json file on dist folder. | + # Got ideas? Open a issue diff --git a/package.json b/package.json index a327de0..26fecd9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@module-federation/ModuleFederationEnhacedPlugin", + "name": "@module-federation/ModuleFederationEnhancedPlugin", "version": "1.0.0", - "main": "ModuleFederationEnhacedPlugin.js", + "main": "ModuleFederationEnhancedPlugin.js", "license": "MIT", "scripts": { "prettier": "prettier --write \"**/*.{js,json,md,ts,tsx}\"" @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": ".../ModuleFederationEnhacedPlugin.git" + "url": ".../ModuleFederationEnhancedPlugin.git" }, "dependencies": { "find-package-json": "^1.0.0" diff --git a/src/GenerateChunkMap.js b/src/GenerateChunkMap.js index 1bc02c2..46be629 100644 --- a/src/GenerateChunkMap.js +++ b/src/GenerateChunkMap.js @@ -21,4 +21,4 @@ const GenerateChunkMap = (compiler) => { ); }; -module.exports = { GenerateChunkMap }; +module.exports = GenerateChunkMap; diff --git a/src/GenerateRemoteUrlMap.js b/src/GenerateRemoteUrlMap.js index 6a5835e..a771946 100644 --- a/src/GenerateRemoteUrlMap.js +++ b/src/GenerateRemoteUrlMap.js @@ -1,9 +1,16 @@ +const validateRemoteType = (remoteName, options) => { + const remote = options.remotes[remoteName]; + return typeof remote === "string" + ? { [remoteName]: remote.split("@")[1] } + : { [remote.name]: remote.url }; +}; + const GenerateRemoteUrlMap = (options) => { if (options.remotes) { return { "./remoteUrlMap": `data:application/json,${JSON.stringify( Object.keys(options.remotes).map((remoteName) => { - return { [remoteName]: options.remotes[remoteName].split("@")[1] }; + return validateRemoteType(remoteName, options); }) )}`, }; diff --git a/src/HandleRemoteObject.js b/src/HandleRemoteObject.js new file mode 100644 index 0000000..294b37a --- /dev/null +++ b/src/HandleRemoteObject.js @@ -0,0 +1,58 @@ +const defaultOnError = () => { + const module = { + get: () => () => {}, + init: () => () => {}, + }; + resolve(module); +}; + +const dynamicRemote = (remote) => { + return `(resolve) => { + const script = document.createElement("script"); + script.src = "${remote.url}"; + script.onload = () => { + const module = { + get: (request) => window["${remote.name}"].get(request), + init: (arg) => { + try { + return window["${remote.name}"].init(arg); + } catch (e) { + console.log("Problem loading remote ${remote.name}", e); + } + }, + }; + resolve(module); + }; + script.onerror = ${ + remote.onError ? remote.onError.toString() : defaultOnError.toString() + } + document.head.appendChild(script); + }`; +}; + +const handleAsyncRemote = (remote) => { + return `promise new Promise(${dynamicRemote(remote).toString()})`; +}; + +const mountFinalRemoteValue = (remote) => { + if (remote.async) { + return handleAsyncRemote(remote); + } else { + //Any other implementation of remote as an object goes here + } + + return remote.name + "@" + remote.url; +}; + +const HandleRemoteObject = (remotes) => { + const _newRemotes = {}; + Object.keys(remotes)?.forEach((remoteName) => { + const remote = remotes[remoteName]; + _newRemotes[remoteName] = + typeof remote === "string" ? remote : mountFinalRemoteValue(remote); + }); + + return _newRemotes; +}; + +module.exports = HandleRemoteObject;