The Future of Module Federation
Over the past 4 years, I have encountered my fair share of limitations in Module Federation. It did the job it was built to do, import code from other bundles. But over time, as use cases, scale, and userbase got larger — there were design oversights that would have been really helpful to consider.
I never expected federation to become as popular as it has, in hindsight there are adjustments that could be made to make it more powerful and helpful. This is something the Infra team saw as well, and they ended up forking the plugin, implementing browser devtools, and addressing many gotchas.
Over the next quarter, we are going to try and separate our internal couplings present in the fork, and instead — implement some aspects directly into Module Federation.
To do so, federation needs to change, in a non breaking manner.
Module Federation Redesign · module-federation/universe · Discussion #1170
Hooks
We deliberately did not add hooks into the plugin as end users would not need or should not tinker around with internal mechanics. I agree with that, but i dont think we considered that we should offer some “framework api”
If federation needs to be modified at all we must either change webpack or fork the whole plugin, ouch. And fork we did in order to integrate it with better management systems and tools.
In version 1.5, we want to introduce hooks so that enhancing federation doesnt require forking the pluign entirely.
Framework APIs
Similar to hooks, in general we need more mid level apis either at build time or runtime. The standard {get,init} api can feel a little too limiting when you're dealing with complex applications. More plugability at runtime and compile time would give framework authors more to work with.
TypeScript Support and Remote Types
There is an increasing emphasis on incorporating robust type systems to enhance code safety and maintainability. Recognizing this need, the next iteration of module federation is set to introduce **built-in **TypeScript support for remote types.
This feature aims to seamlessly integrate remote type definitions, allowing developers to leverage the strong typing capabilities of TypeScript across federated modules.
By doing so, it enhances interoperability, reduces the risk of type-related errors, and fosters a more coherent development experience.
This sets the stage for further innovation in creating more reliable and efficient distributed systems.
Initialization Phases
A major one is the concept of middleware and startup code.
Hosts should be able to apply express-like middleware on the remotes they consume. This would help alot with AB tests, dynamic environment switching, authorization and permissions, error handling and so on.
But, middleware on the consumer end is not always enough or appropriate. Sometimes I may need or want to ship some “startup” code as the author of a remote. Some piece of runtime code that can be executed during the initialization phase of a remote. Like getting environment variables, or ensuring data needed is ready or exists, or inject providers into the application.
Remotes need a way to be able to prepare their consuming environment, if they need to.
Lifecycle
Similar and adjacent to middleware, we we could benefit from a basic lifecycle. Something like a plugin system thats at runtime, that can provide us with more callbacks to react to whats going on.
The middleware implementation provides outlines various hooks that provide opportunities to intervene in the federated module process:
beforeInit & init: Preparing the initial environment and managing configurations.
beforeLoadRemote & loadRemoteMatch: Control over the loading of remote federated modules.
loadRemote: Dynamic control over the loading process of individual federated modules.
errorLoadRemote: Managing errors that occur during the remote loading process.
beforeLoadShare & loadShare: Control over shared modules and their behavior.
beforePreloadRemote: Optimization and control over preload operations.
Middleware:
How should one interact with the middleware flow in an integrated manner?
new ModuleFederationPlugin({
middleware: './src/federation-middleware.js'
})
//federation-middleware.js
module.exports = {
beforeInit,
beforeLoadingRemote,
loadRemoteMatch,
loadRemote,
errorLoadRemote,
beforeLoadShare,
beforePreloadRemote
}
At Runtime:
hooks = new PluginSystem({
beforeInit: new SyncWaterfallHook<{
userOptions: UserOptions;
options: Options;
origin: VmokHost;
}>('beforeInit'),
init: new SyncHook<
[
{
options: Options;
origin: VmokHost;
},
],
void
>(),
beforeLoadRemote: new AsyncWaterfallHook<{
id: string;
options: Options;
origin: VmokHost;
}>('beforeLoadRemote'),
loadRemoteMatch: new AsyncWaterfallHook<{
id: string;
pkgNameOrAlias: string;
expose: string;
remote: RemoteInfo;
options: Options;
origin: VmokHost;
}>('loadRemoteMatch'),
loadRemote: new AsyncHook<
[
{
id: string;
expose: string;
pkgNameOrAlias: string;
remote: RemoteInfo;
options: ModuleOptions;
origin: VmokHost;
exposeModule: any;
moduleInstance: Module;
},
],
void
>('loadRemote'),
errorLoadRemote: new AsyncHook<
[
{
id: string;
error: unknown;
},
],
void
>('errorLoadRemote'),
beforeLoadShare: new AsyncWaterfallHook<{
pkgName: string;
shareInfo?: Shared;
shared: Options['shared'];
origin: VmokHost;
}>('beforeLoadShare'),
loadShare: new AsyncHook<[VmokHost, string, ShareInfos]>(),
beforePreloadRemote: new AsyncHook<{
preloadOps: Array<PreloadRemoteArgs>;
options: Options;
origin: VmokHost;
}>(),
});
Startup
The startup code in Module Federation represents an architectural evolution where remotes have their own lifecycle. This allows nuanced control over the bootstrapping of federated modules, working in tandem with middleware to enable an intricately negotiated 4-way handshake.
new ModuleFederationPlugin({
startup: './src/federation-startup.js'
})
Examples and Applications
The startup phase incorporates various elements that can be tailored to specific needs:
Environment Configuration: Set up runtime environments for different federated modules.
State Management: Manage global states and inject middleware in state stores.
Real-Time Communication: Manage real-time channels like WebSockets.
Critical Resource Preloading: Preload resources for a smoother user experience.
Error Handling: Handle errors with auto-recovery mechanisms.
Data Integrity: Implement integrity checks on loaded modules.
Access Control: Implement runtime access control.
Theming and Localization: Apply themes or localization settings dynamically.
Cache Management: Manage cache across federated modules.
Integration with Legacy Systems: Integrate with older systems by adapting interfaces.
Custom UI Components Loading: Load or unload UI components dynamically.
Health Monitoring and Reporting: Embed health checks for critical dependencies.
Compliance and Security Enforcement: Ensure compliance with legal or business rules.
Experimentation and A/B Testing: Support dynamic A/B testing without altering the core codebase.
Plug-and-Play Extensibility: Enable third-party developers to extend functionalities.
4-Way Handshake
During the 4-way handshake process, middleware and startup code work collaboratively:
Remote Initialization (Startup): Execution of prescribed startup tasks.
Middleware Execution: Enables the host to perform actions and adapt behavior.
Dependency Linking: Completes the connection and linking in the dependency graph.
Application Execution: Begins executing the application.
Startup Hooks/Lifecycle
Startup hooks focus on the initialization and preparation actions taken by the providers/remotes. They may be the mirror image of the middleware hooks, focusing on provision rather than consumption.
startupHooks = new PluginSystem({
beforeStartup: new SyncWaterfallHook<{
moduleOptions: ModuleOptions;
environment: Environment;
provider: VmokProvider;
}>('beforeStartup'),
onStartup: new SyncHook<
[
{
provider: VmokProvider;
environment: Environment;
},
],
void
>('onStartup'),
beforeInitializeRemote: new AsyncWaterfallHook<{
id: string;
initializationOptions: InitializationOptions;
provider: VmokProvider;
}>('beforeInitializeRemote'),
initializeRemote: new AsyncHook<
[
{
id: string;
initializationOptions: InitializationOptions;
provider: VmokProvider;
remoteInstance: Remote;
},
],
void
>('initializeRemote'),
errorInitializeRemote: new AsyncHook<
[
{
id: string;
error: unknown;
},
],
void
>('errorInitializeRemote'),
beforePrepareEnvironment: new AsyncWaterfallHook<{
environment: Environment;
provider: VmokProvider;
}>('beforePrepareEnvironment'),
prepareEnvironment: new AsyncHook<
[
{
environment: Environment;
provider: VmokProvider;
},
],
void
>('prepareEnvironment'),
beforeFinalizeStartup: new AsyncWaterfallHook<{
finalizationOptions: FinalizationOptions;
provider: VmokProvider;
}>('beforeFinalizeStartup'),
finalizeStartup: new AsyncHook<
[
{
finalizationOptions: FinalizationOptions;
provider: VmokProvider;
},
],
void
>('finalizeStartup'),
});
Explanation of Startup Hooks
beforeStartup: Allows initial configurations.
onStartup: Core initialization logic.
beforeInitializeRemote: Preprocessing before initializing a remote module.
initializeRemote: Initialization of a remote module.
errorInitializeRemote: Handling errors during initialization.
beforePrepareEnvironment: Actions before environment configuration.
prepareEnvironment: Actual environment setup.
beforeFinalizeStartup: Preprocessing before finalizing the startup.
finalizeStartup: Final actions, including clean-up or validation.
Middleware & Startup as a whole
The integration of middleware and startup code in Module Federation’s architecture marks a significant advancement in handling federated modules. They offer a symmetrical design where middleware focuses on the consumption and host perspective, while startup code targets the provisioning and provider’s view.
Middleware
With various hooks for controlling and customizing different stages of federated module loading and execution, middleware enables a tailored experience for consumers. It provides a granular level of control over aspects such as remote loading, error management, shared module behavior, and preload operations. Middleware allows an intricate alignment with the complex demands of modern distributed systems, offering flexibility, robustness, and efficient handling of federated modules.
Startup
Startup code, on the other hand, introduces a nuanced lifecycle for remotes, focusing on initialization and preparation actions. It emphasizes the provider’s perspective, allowing actions like environment configuration, error handling, integration with legacy systems, cache management, and more. The startup hooks provide a rich lifecycle model that complements the consumption-oriented middleware hooks.
Combined Effect
Together, middleware and startup form a cohesive and comprehensive protocol that caters to both sides of federated module operations. The well-designed hooks and lifecycle phases provide a powerful toolkit for developers to have fine-grained control over the process, from initialization to execution. The 4-way handshake model illustrates how these two components work in unison to ensure a smooth and effective operation.
In essence, the combined power of middleware and startup in Module Federation fosters a more adaptive, resilient, and scalable architecture. It reflects a thoughtful design that anticipates the multifaceted needs of modern development environments, providing the tools and methodologies to create intricate, distributed systems with ease and precision. Whether it’s the flexible customization offered by middleware or the robust initialization provided by startup, this duo forms the backbone of an intelligent system that is poised to revolutionize the way federated modules are handled and executed.
Runtime API / SDK
Middleware, lifecycles, its all related, so is the need for an SDK and better runtime API.
Runtime API should expose the middleware primitives in some way, hookable lifecycles that can be used in react for example.
The SDK on the other hand, aims to take what we are building into the Webpack runtime and turn it inside out.
Federation v1.5 would be more painful to try and support in other build tools, federation already is tricky or “same idea not compatible”. To support other tools, i dont know if their APIs would be worth the effort — its just difficult, federation uses many of Webpacks apis to pull it off so well.
So, instead of maintaining a replica of what Webpack can already do… what if we shipped an empty “webpack runtime”, wrapped its webpack_require exports in a library, and ship it to NPM?
import {createContainer} from '@module-federation/sdk';
const federationConfig = {
name: 'app2',
exposes: {
"./Button": () => {
return import('./App').then(f => () => f)
}
},
shared: {
react: {
version: '18.2.0',
import: () => import('react').then(f => () => f),
weakRef: require.resolveWeak('react'),
},
'react-dom': {
version: '18.2.0',
import: () => import('react-dom').then(f => () => f),
weakRef: require.resolveWeak('react-dom'),
}
}
}
export default createContainer(federationConfig) // => {get,init}
Plugin Hooks
Compile Time Hooks for Module Federation:
This aspect is not fully developed, but the following should offer a window into the direction
Initialization Hooks
- beforeCompileInit: Called before initializing the compilation process.
beforeCompileInit: new SyncHook<[InitializationOptions]>('beforeCompileInit'),
“Filesystem” Hooks
customizeFilesystem: Customizing the filesystem for loading chunks or altering the definition of a filesystem during compilation.
customizeFilesystem: new AsyncHook<[FilesystemOptions]>('customizeFilesystem'),
Template Generation Hooks
beforeTemplateGeneration: Preprocessing before generating templates for federated modules.
beforeTemplateGeneration: new AsyncWaterfallHook<[TemplateOptions]>('beforeTemplateGeneration'),
generateTemplate: Generating templates for federated modules during compile time.
generateTemplate: new AsyncHook<[GenerateTemplateOptions]>('generateTemplate'),
API Manipulation Hooks
beforeAPIAlteration: Called before altering or extending the API interfaces.
beforeAPIAlteration: new SyncHook<[APIOptions]>('beforeAPIAlteration'),
alterAPI: Altering or adding more exports onto the main interface.
alterAPI: new AsyncHook<[AlterAPIOptions]>('alterAPI'),
Using (Web|Rs)pack Runtime as a Library? A Possibility
Much of the federation’s value lies in Webpack’s runtime. The idea is to use a “Webpack runtime” to inject dynamic imports into a container-producing factory.
This concept resembles single-spa, where an entry point could be exported from any build as a remote entry or host. The runtime would boot the application, with no need to use Webpack to build it.
Instead of replicating what Webpack does, the idea is to use its pre-built runtime, modify it slightly, and employ Webpack’s actual runtime as a framework. The parent build tool would handle script loading, so the SDK’s responsibilities would be minimal.
Creating a clean SDK with only a Webpack plugin might not be possible, and though it’s not an easy task, it’s preferable to recreating federation with a smaller build API.
As someone who’s worked extensively with Webpack, I’m grateful for having Rspack. It allows flexibility with plugins and has proven more valuable in the long run despite a learning curve.
Difficult architectural or business problems became more manageable or even trivial. With rspack, compatibility is maintained with Webpack, but there’s room for enhancement, and its speed opens new possibilities.
It’s a balance between speed and capability. If rspack can build an 18,000 file codebase in 4 seconds, not 60, it’s fast enough that new powerful capabilities could be introduced without noticeable delays. Doing some of these tasks in Javascript might not be feasible.
Wrapping it up
The evolution of module federation is marked by the development of more refined and specialized APIs. These improvements enable developers to address framework-level challenges, facilitating more nuanced control over the handling of and formation of a distributed system and its module tree.
The introduction of middleware, startup hooks, and compile-time plugin hooks represents a significant step forward. These capabilities allow for a greater range of customization, enriching the architectures scope and utility.
The full potential of these advancements can only be realized within a unified meta-framework that encapsulates the diverse needs of modern development. This is the role that ModernJS aims to fulfill in future iterations. As a platform, it integrates these new features into a coherent whole, providing a practical and efficient approach to building and managing distributed systems.
By giving module federation a “home” within ModernJS, these new capabilities can be leveraged to solve real-world challenges, without losing sight of the underlying technical complexities. This alignment between innovation and practicality forms the foundation for further growth and refinement in the field of micro-frontends and code distribution.
Modern.js
*A Progressive React Framework for modern web development.*modernjs.dev
Top comments (0)