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.

  • Type
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.

  • Type
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.

  • Type
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.

  • Type
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.

  • Type
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.

  • Type
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.

  • Type
async function errorLoadRemote(
  args: ErrorLoadRemoteOptions,
): Promise<void | unknown>;

type ErrorLoadRemoteOptions = {
  id: string;
  error: unknown;
  options?: any;
  from: 'build' | 'runtime';
  lifecycle: 'onLoad' | 'beforeRequest';
  origin: FederationHost;
};
  • Example
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.

  • Type
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.

  • Type
function resolveShare(args: ResolveShareOptions): ResolveShareOptions;

type ResolveShareOptions = {
  shareScopeMap: ShareScopeMap;
  scope: string;
  pkgName: string;
  version: string;
  GlobalFederation: Federation;
  resolver: () => Shared | undefined;
};
  • Example
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.

  • Type
async function beforePreloadRemote(
  args: BeforePreloadRemoteOptions,
): BeforePreloadRemoteOptions;

type BeforePreloadRemoteOptions = {
  preloadOps: Array<PreloadRemoteArgs>;
  options: Options;
  origin: FederationHost;
};

generatePreloadAssets

AsyncHook

Generates preload assets based on configuration.

  • Type
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

  • Type
function createScript(args: CreateScriptOptions): HTMLScriptElement | {script?: HTMLScriptElement, timeout?: number } | void;

type CreateScriptOptions = {
  url: string;
  attrs?: Record<string, any>;
};
  • Example
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 }
      }
    },
  };
};

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

  • Type
function createScript(args: LoadEntryOptions): HTMLScriptElement | {script?: HTMLScriptElement, timeout?: number } | 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}
  • Exmaple Delegate Modules
// 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"