Plugin System
Module Federation provides a lightweight runtime plugin system for implementing most of its features and allowing users to extend functionalities.
Plugins developed by developers can modify the default behavior of Module Federation
and add various additional features, including but not limited to:
- Obtaining context information
- Registering lifecycle hooks
- Modifying Module Federation configurations
- ...
Developing Plugins
Plugins are provided in the form of a function similar to () => FederationRuntimePlugin
.
Plugin Example
custom-runtime-plugin.ts
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const runtimePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'my-runtime-plugin',
beforeInit(args) {
console.log('beforeInit: ', args);
return args;
},
beforeRequest(args) {
console.log('beforeRequest: ', args);
return args;
},
afterResolve(args) {
console.log('afterResolve', args);
return args;
},
onLoad(args) {
console.log('onLoad: ', args);
return args;
},
async loadShare(args) {
console.log('loadShare:', args);
},
async beforeLoadShare(args) {
console.log('beforeloadShare:', args);
return args;
},
};
};
export default runtimePlugin;
Registering plugins (either method is acceptable):
- Build-time registration of plugins
rspack.config.ts
const path = require('path');
module.exports = {
plugins: [
new ModuleFederation({
// ...
runtimePlugins: [path.resolve(__dirname, './custom-runtime-plugin.ts')],
}),
],
};
- Runtime registration of plugins
import { registerPlugins } from '@module-federation/enhanced/runtime'
import runtimePlugin from 'custom-runtime-plugin.ts';
registerPlugins([runtimePlugin()]);
Plugin Structure
Function-based plugins can accept an options object and return a plugin instance, managing internal state through closure mechanisms.
The roles of each part are as follows:
- The
name
property is used to label the plugin name.
fn
Various hooks.
Naming Conventions
The naming conventions for plugins are as follows:
- The plugin function is named
xxx-plugin
and is exported with a name.
- The
name
of the plugin follows the xxx-plugin
format.
Here is an example:
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const pluginFooBar = (): FederationRuntimePlugin => ({
name: 'xxx-plugin',
//...
});
export default pluginFooBar;
Hooks
beforeInit
SyncWaterfallHook
Updates the federated host configuration before the initialization process of the remote container.
function beforeInit(args: BeforeInitOptions): BeforeInitOptions;
type BeforeInitOptions = {
userOptions: UserOptions;
options: FederationRuntimeOptions;
origin: FederationHost;
shareInfo: ShareInfos;
};
interface FederationRuntimeOptions {
id?: string;
name: string;
version?: string;
remotes: Array<Remote>;
shared: ShareInfos;
plugins: Array<FederationRuntimePlugin>;
inBrowser: boolean;
}
init
SyncHook
Called during the initialization of the remote container.
function init(args: InitOptions): void;
type InitOptions = {
options: FederationRuntimeOptions;
origin: FederationHost;
};
beforeRequest
AsyncWaterfallHook
Called before resolving the remote container, used to inject containers or update certain content before lookup.
async function beforeRequest(
args: BeforeRequestOptions,
): Promise<BeforeRequestOptions>;
type BeforeRequestOptions = {
id: string;
options: FederationRuntimeOptions;
origin: FederationHost;
};
afterResolve
AsyncWaterfallHook
Called after the container is resolved, allows for redirection or modification of the resolved information.
async function afterResolve(
args: AfterResolveOptions,
): Promise<AfterResolveOptions>;
type AfterResolveOptions = {
id: string;
pkgNameOrAlias: string;
expose: string;
remote: Remote;
options: FederationRuntimeOptions;
origin: FederationHost;
remoteInfo: RemoteInfo;
remoteSnapshot?: ModuleInfo;
};
onLoad
AsyncHook
Triggered when the federated module is fully loaded, allowing access to and modification of the exported content of the loaded file.
async function onLoad(args: OnLoadOptions): Promise<void>;
type OnLoadOptions = {
id: string;
expose: string;
pkgNameOrAlias: string;
remote: Remote;
options: ModuleOptions;
origin: FederationHost;
exposeModule: any;
exposeModuleFactory: any;
moduleInstance: Module;
};
type ModuleOptions = {
remoteInfo: RemoteInfo;
host: FederationHost;
};
interface RemoteInfo {
name: string;
version?: string;
buildVersion?: string;
entry: string;
type: RemoteEntryType;
entryGlobalName: string;
shareScope: string;
}
handlePreloadModule
SyncHook
Handles the logic for preloading federated modules.
function handlePreloadModule(args: HandlePreloadModuleOptions): void;
type HandlePreloadModuleOptions = {
id: string;
name: string;
remoteSnapshot: ModuleInfo;
preloadConfig: PreloadRemoteArgs;
};
errorLoadRemote
AsyncHook
This hook is invoked when the loading of a federated module fails, enabling customized error handling strategies.
It is designed to be triggered during various lifecycle stages of module loading in the event that any of the stages fail.
Utilize args.lifecycle
to identify the specific lifecycle stage that has initiated the call to errorLoadRemote
, allowing for appropriate error handling or fallback mechanisms.
async function errorLoadRemote(
args: ErrorLoadRemoteOptions,
): Promise<void | unknown>;
type ErrorLoadRemoteOptions = {
id: string;
error: unknown;
options?: any;
from: 'build' | 'runtime';
lifecycle: 'onLoad' | 'beforeRequest';
origin: FederationHost;
};
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const fallbackPlugin: () => FederationRuntimePlugin = function () {
return {
name: 'fallback-plugin',
errorLoadRemote(args) {
if(args.lifecycle === 'onLoad') {
const fallback = 'fallback';
return fallback;
} else if (args.lifecycle === 'beforeRequest') {
return args
}
}
};
};
init({
name: '@demo/app-main',
remotes: [
{
name: '@demo/app2',
entry: 'http://localhost:3006/remoteEntry.js',
alias: 'app2'
}
],
plugins: [fallbackPlugin()]
});
loadRemote('app2/un-existed-module').then((mod) => {
expect(mod).toEqual('fallback');
});
beforeLoadShare
AsyncWaterfallHook
Called before attempting to load or negotiate shared modules between federated applications.
async function beforeLoadShare(
args: BeforeLoadShareOptions,
): Promise<BeforeLoadShareOptions>;
type BeforeLoadShareOptions = {
pkgName: string;
shareInfo?: Shared;
shared: Options['shared'];
origin: FederationHost;
};
resolveShare
SyncWaterfallHook
Allows for manual resolution of shared module requests.
function resolveShare(args: ResolveShareOptions): ResolveShareOptions;
type ResolveShareOptions = {
shareScopeMap: ShareScopeMap;
scope: string;
pkgName: string;
version: string;
GlobalFederation: Federation;
resolver: () => Shared | undefined;
};
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const customSharedPlugin: () => FederationRuntimePlugin = function () {
return {
name: 'custom-shared-plugin',
resolveShare(args) {
const { shareScopeMap, scope, pkgName, version, GlobalFederation } = args;
if (pkgName !== 'react') {
return args;
}
// set lib
args.resolver = function () {
shareScopeMap[scope][pkgName][version] = {
lib: ()=>window.React,
loaded:true,
loading: Promise.resolve(()=>window.React)
}; // Manually replace the local share scope with the desired module
return shareScopeMap[scope][pkgName][version];
};
// set get
args.resolver = function () {
shareScopeMap[scope][pkgName][version] = {
get: async ()=>()=>window.React,
}; // Manually replace the local share scope with the desired module
return shareScopeMap[scope][pkgName][version];
};
return args;
},
};
};
init({
name: '@demo/app-main',
shared: {
react: {
version: '17.0.0',
scope: 'default',
lib: () => React,
shareConfig: {
singleton: true,
requiredVersion: '^17.0.0',
},
},
},
plugins: [customSharedPlugin()],
});
window.React = () => 'Desired Shared';
loadShare('react').then((reactFactory) => {
expect(reactFactory()).toEqual(window.React());
});
beforePreloadRemote
AsyncHook
Called before the preload handler executes any preload logic.
async function beforePreloadRemote(
args: BeforePreloadRemoteOptions,
): BeforePreloadRemoteOptions;
type BeforePreloadRemoteOptions = {
preloadOps: Array<PreloadRemoteArgs>;
options: Options;
origin: FederationHost;
};
generatePreloadAssets
AsyncHook
Generates preload assets based on configuration.
async function generatePreloadAssets(
args: GeneratePreloadAssetsOptions,
): Promise<PreloadAssets>;
type GeneratePreloadAssetsOptions = {
origin: FederationHost;
preloadOptions: PreloadOptions[number];
remote: Remote;
remoteInfo: RemoteInfo;
remoteSnapshot: ModuleInfo;
globalSnapshot: GlobalModuleInfo;
};
interface PreloadAssets {
cssAssets: Array<string>;
jsAssetsWithoutEntry: Array<string>;
entryAssets: Array<EntryAssets>;
}
createScript
SyncHook
function createScript(args: CreateScriptOptions): HTMLScriptElement | {script?: HTMLScriptElement, timeout?: number } | void;
type CreateScriptOptions = {
url: string;
attrs?: Record<string, any>;
};
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'change-script-attribute',
createScript({ url, attrs }) {
if (url === testRemoteEntry) {
let script = document.createElement('script');
script.src = testRemoteEntry;
// can modify the attrs object
attrs['loader-hooks'] = 'isTrue';
// or add them to the script
script.setAttribute('crossorigin', 'anonymous');
return script;
}
},
};
};
- Example with script timeout
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'change-script-attribute',
createScript({ url }) {
if (url === testRemoteEntry) {
let script = document.createElement('script');
script.src = testRemoteEntry;
script.setAttribute('loader-hooks', 'isTrue');
script.setAttribute('crossorigin', 'anonymous');
return { script, timeout: 1000 }
}
},
};
};
fetch
The fetch
function allows customizing the request that fetches the manifest JSON. A successful Response
must yield a valid JSON.
AsyncHook
function fetch(manifestUrl: string, requestInit: RequestInit): Promise<Response> | void | false;
- Example for including the credentials when fetching the manifest JSON:
// fetch-manifest-with-credentials-plugin.ts
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
export default function (): FederationRuntimePlugin {
return {
name: 'fetch-manifest-with-credentials-plugin',
fetch(manifestUrl, requestInit) {
return fetch(manifestUrl, {
...requestInit,
credentials: 'include'
});
},
}
};
loadEntry
The loadEntry
function allows for full customization of remotes, enabling you to extend and create new remote types. The following two simple examples demonstrate loading JSON data and module delegation.
asyncHook
function loadEntry(args: LoadEntryOptions): RemoteEntryExports | void;
type LoadEntryOptions = {
createScriptHook: SyncHook,
remoteEntryExports?: RemoteEntryExports,
remoteInfo: RemoteInfo
};
interface RemoteInfo {
name: string;
version?: string;
buildVersion?: string;
entry: string;
type: RemoteEntryType;
entryGlobalName: string;
shareScope: string;
}
export type RemoteEntryExports = {
get: (id: string) => () => Promise<Module>;
init: (
shareScope: ShareScopeMap[string],
initScope?: InitScope,
remoteEntryInitOPtions?: RemoteEntryInitOptions,
) => void | Promise<void>;
};
- Example Loading JSON Data
// load-json-data-plugin.ts
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'load-json-data-plugin',
loadEntry({ remoteInfo }) {
if (remoteInfo.jsonA === "jsonA") {
return {
init(shareScope, initScope, remoteEntryInitOPtions) {},
async get(path) {
const json = await fetch(remoteInfo.entry + ".json").then(res => res.json())
return () => ({
path,
json
})
}
}
}
},
};
};
// module-federation-config
{
remotes: {
jsonA: "jsonA@https://cdn.jsdelivr.net/npm/@module-federation/runtime/package"
}
}
// src/bootstrap.js
import jsonA from "jsonA"
jsonA // {...json data}
// delegate-modules-plugin.ts
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'delegate-modules-plugin',
loadEntry({ remoteInfo }) {
if (remoteInfo.name === "delegateModulesA") {
return {
init(shareScope, initScope, remoteEntryInitOPtions) {},
async get(path) {
path = path.replace("./", "")
const {[path]: factory} = await import("./delegateModulesA.js")
const result = await factory()
return () => result
}
}
}
},
};
};
// ./src/delegateModulesA.js
export async function test1() {
return new Promise(resolve => {
setTimeout(() => {
resolve("test1 value")
}, 3000)
})
}
export async function test2() {
return new Promise(resolve => {
setTimeout(() => {
resolve("test2 value")
}, 3000)
})
}
// module-federation-config
{
remotes: {
delegateModulesA: "delegateModulesA@https://delegateModulesA.js"
}
}
// src/bootstrap.js
import test1 from "delegateModulesA/test1"
import test2 from "delegateModulesA/test2"
test1 // "test1 value"
test2 // "test2 value"