Hi all,
I’m excited to share that I’m going to be working with Axim to build a reference implementation of runtime module loading and shared dependencies as described in the recently-merged OEP-65! If you haven’t been following along, I encourage you to go read it for more detailed background than I’ll give here.
Next Steps
OEP-65 was merged as “Provisional”. That means we believe it’s a good idea and agreed on a way forward, but full acceptance is pending an actual implementation of the architectural decisions it describes. We’re expecting to build the implementation using webpack module federation.
At a very high level, this will look like:
- Build Configuration: Adding webpack configuration to frontend-build that allows MFEs to be built as loadable modules rather than complete applications.
- MFEs: Picking a few MFEs to start converting in a backwards compatible way. Tentatively, we think we might start with frontend-app-authn and frontend-app-learner-dashboard.
- Shell Application: Create a new “shell” application which will act as the entry point for all MFEs (eventually).
- Frontend Plugin Framework: Extend FPF with a new “module plugin” type for loading our MFE modules at runtime without an iframe.
I expect this work to take most of the rest of 2024, and as mentioned above, it should be able to proceed in a mostly backwards compatible way.
There are a lot of details yet to figure out, as well as some concerns that are important but tangentially related. There are a few big ones that stand out in my mind. Forgive me if this is a bit speculative or has any glaring holes; these are complicated decisions, which is why I wanted to shed some light on them!
Build tooling
Simultaneous to OEP-65, we’ve been discussing whether we should attempt to move from webpack to another build tool, like Vite or Rspack. While any given MFE is significantly faster to build and iterate on than the old monolith, when the entire platform is built together, it’s feeling slow. This problem overlaps with OEP-65 to the extent that we believe module federation is the right tool to improve Open edX’s frontend composability, and webpack is the most robust implementation of that around. The only other known options are Vite and Rspack.
I feel fairly strongly that we don’t want to ‘roll our own’ version of module federation, and the alternative module federation implementations are either not robust enough (Vite), or not widely adopted enough (Rspack) to feel like a safe move. We’re picking a tool for the whole platform and community; it should be as much of an ‘industry standard’ as we can manage. If Vite had better module federation tooling I think it’d be particularly compelling, but in my testing it just doesn’t seem like it’s there, and it lacks some features.
So with that in mind, I think we stick with webpack for now. That said, there are some things we can do to squeeze more performance out of webpack. I’ve found that using swc-loader instead of babel-loader shaved a few seconds off production builds, and our SCSS compilation is surprisingly slow. Further, we should perhaps look at using pnpm or yarn, since npm itself seems to be a bottleneck for us. npm install
seems to take more time than the actual build, from what I’ve seen. Also, the switch to module federation will ultimately speed things up for most MFEs by virtue of needing to bundle less actual code, as some large dependencies move to the shell.
This is all to say, as part of this effort, I’m not going to be trying to replace webpack. I think this is an area, though, that we should keep our collective eye on. And if anyone wants to do a deep dive on Tutor’s MFE building with me, I’m all ears and ready to learn.
Merging shared frontend repositories
We’ve also been discussing whether some of our foundational frontend repositories should just be merged. Specifically, this is about frontend-build, frontend-platform, frontend-plugin-framework, frontend-component-header, frontend-component-footer, and the new repository for the “shell” application.
Originally we kept some of these things (frontend-platform, frontend-build) for several reasons. One, they have different jobs, and were conceptually separate from each other. Two, we weren’t sure whether they would be ubiquitously used by all our MFEs, at the time! Turns out… the ‘platform’ for all our MFEs is universal, which is actually pretty great. All MFEs should be using all of the repositories shown above, and in around 6 years we haven’t had any real push back on that recommendation, just widespread adoption. As we talk about adding this new ‘shell’ application as an entry point for all MFEs (eventually), it begs the question… do we really need 7 repositories for all this if every MFE needs all of it?
Further, it’s all becoming more inter-connected, and these libraries all depend on each other, and they’re not as conceptually isolated from each other as they used to be. frontend-build and frontend-platforms share responsibility for our environment config, which is central to fronted-plugin-framework, which we want to use to configure our headers/footers as plugin slots, all of which will be spun up and owned by the new shell application.
Should there be one repository, and dare I say it’s just frontend-platform
?
A strawperson proposal:
frontend-platform
continues to be published as a library and is a runtime dependency of MFEs.frontend-plugin-framework
is folded intofrontend-platform
and exposed through the runtime library.- The
shell application
is built infrontend-platform
and owns the application initialization process that MFEs do themselves today. It consumes the ‘services’ like logging and analytics, initializes i18n, etc. frontend-build
is folded intofrontend-platform
and is joined with the shell application to provide a single development server for MFE development, exposed as a “buildtime library” to compliment the existing ‘runtime library’.frontend-component-header
andfrontend-component-footer
are folded into the shell application and wrapped in plugin slots. By default they’re bundled as ‘direct plugins’, but can easily be tree-shaken out if not needed by converting them to module plugins and loading them from somewhere else.frontend-platform
exposes an API for MFEs to request a given header when being loaded. We double down on extensibility and configuration of the header/footer to reduce the need to build custom ones.
CSS and Paragon
There’s been an ongoing effort to land some PRs around design tokens, CSS variables, and external stylesheets. I think this is ultimately related. The shell application gives us the opportunity to share a single stylesheet between all MFEs by default, and SCSS/CSS processing is actually a significant chunk of our MFE build time. With the ongoing Paragon work and creation of the shell, I think we can significantly streamline our styling over time, getting to a place where we share a single Paragon stylesheet and remove SCSS processing from our MFEs all together (in a world with utility classes and css variables, it’s not adding much).
Plugin slots, configuration, and customization
Finally, we should talk about how to extend the frontend platform. This is a vision I’ve had rattling around in my brain for a number of years now, and just started to really solidify into something that feels powerful as we were working through the details of OEP-65, and as frontend-plugin-framework has started to take shape.
To say something that may sound a bit audacious - I believe that the default installation of the platform should be a single repository, frontend-platform, with all the default MFEs linked in as direct plugins through env.config.js. The MFEs are pre-built, pre-tested artifacts, released as packages on npm/Github. By default, you run one build command and have the entire platform ready to go.
Anyone who has need of more granular, decoupled deployment of MFEs can instead turn their direct plugin MFEs into module plugins loaded via module federation. The MFE is then independently deployed and is no longer built as part of the shell. This is just a configuration change (plus taking on the extra operational overhead we already have today). Additional MFEs and plugins are added via the same configuration.
Further, dependencies like the logging service, analytics service, Google analytics, tag manager, hot jar, Zendesk, and anything else loaded as a script tag in the header, can all be loaded via plugins and plugin slots; just non-visual ones.
To support all this, there are a few things we need:
- The ability to either use env.config.js to configure MFEs, or to use a runtime module to configure MFEs (i.e., the module contains the env.config.js and is loaded first thing at runtime). The MFE plugin API will no longer be sufficient since it can only express JSON, not JavaScript.
- A few new types of plugin slots. Plugin routes, plugin scripts, plugin stylesheets. All of these are very doable.
- Thoughtful application of plugin slots for the above with sensible default plugins.
- Ultimately, guard rails on this configuration to make it easy to use. This could look like Typescript types, helper functions, a CLI, linting, etc., to ensure that the configuration you’re writing is actually correct.
Conclusion
I’m really excited about all of this, and am thrilled for the opportunity to get the frontend platform to a place that works and works well for the community. Let me know what I’ve missed, what resonates, and everything in between.