What is module federation?
Module federation allows module authors to publish updates that automatically get included in deployed applications. They do this without requiring a rebuild and redeploy cycle for each application using the module, assuming there are no breaking changes in the update.
We’re interested in using Webpack 5’s module federation as part of a microfrontend architecture. That will give our teams more independence as we collaborate on a suite of applications and shared components. Microfrontends aren’t the only thing you can use module federation for, but it’s certainly where a majority of the interest seems to be right now.
One note of caution: Module federation in its current state is unsuitable for loading modules from internet sources. Runtime composition requires more trust in the module publisher compared to compile time composition. That’s because a federated module could be changed at any time and immediately affect application users. Module federation is a good fit for internal modules and microfrontends, assuming high-trust relationships between application and module teams.
Why are TypeScript types a problem?
It’s challenging to build and acquire good TypeScript types for a federated module. That’s primarily because Webpack module federation only loads resources from the federated module at runtime, but TypeScript needs the type information at compile time. There’s no built-in way to publish and acquire the compile-time types.
Even if we could easily publish and load the type declaration files produced by the TypeScript compilation process easily, module federation changes module names and paths.
- When publishing a container (a collection of modules) for use via module federation, you must specify a name for the container and each module it exposes. These names don’t have to match the names/paths of the TypeScript code they reference. So, types from the compilation process aren’t guaranteed to match what gets exposed via the federated module.
- When using a module via federation, the consuming application can choose a different name for the container. So even if naming is consistent for the internal TypeScript code and published modules, the user of the module could name the container differently.
What are our options?
Empty Module Declaration in Host Application
Most examples of module federation using Typescript add an empty module declaration to make the TypeScript compiler happy. However, they don’t add actual, helpful types to the consuming application. This can get the complication process working but leaves a gap that makes developers who like TypeScript types (like me) unhappy.
It might be totally fine to use this approach if the interface exposed by the module is simple and small and infrequently changed. Otherwise, I’d consider beefing up the type declarations to provide more of the support we expect from TypeScript.
Hand-written Module Declaration in Host Application
If the empty module declaration isn’t enough, we could copy or hand-write the type declarations we care about for the interface of the federated module. The declarations need to be present in each application that uses the module and you may need to customize them. That means you must do maintenance and there’s a risk of things getting out of date, but it could work.
Reference Types Across a Monorepo
If the module and applications that use it live together in a monorepo, you have more options. Those options can reduce manual maintenance using relative filesystem paths to load generated type declaration files. The pixability/federated-types module could help wrangle the generated
.d.ts files. The documentation there shows how one might configure the monorepo to share the types.
We decided not to go this route because we have applications that will use our module from outside our monorepo.
NPM Package with Types
Publishing an NPM types package sounds like an easy solution. However, the malleability of container and module names poses a restriction. The publisher and consumer of the federated module must use a naming scheme matching the internal TypeScript code. See this example on GitHub issues for more details on one convention that seems to work.
Because of that shortcoming, we opted not to pursue this route either.
Key tools that enable this approach include:
- dts-loader – a Webpack plugin that collects .d.ts files and emits type declarations that match the federated module’s configuration
- webpack-remote-types-plugin – a Webpack plugin that manages the downloading of a remote tarball containing types
The steps to set up both tools are well documented on their respective GitHub pages, so I won’t repeat the detail here. But, in brief form:
dts-loaderwith the same information used to configure the
ModuleFederationPluginin Webpack, and it will emit type declaration files for each exposed module.
- Add a step to the module’s publish workflow that bundles the resulting
- Configure the
webpack-remote-types-pluginto load the types from the resulting URL
That should close the loop and allow a consuming application to use types exported by a federated module. It’s a good idea to include the
outputDir from the
webpack-remote-types-plugin config in the
.gitignore for the project.
Where can I read more?
If you’re interested in digging deeper, here are a few of the resources I found helpful during my research