rollup-plugin-federation: Module Federation for Rollup
Configure the Rollup plugin that builds FynApps: what to expose, what to share, and how version ranges decide which copy of a library each app gets.
Module federation for Rollup. Build a bundle that shares modules with, and exposes modules to, other independently built and deployed bundles.
FynMesh: Website · Demo · Shell demo
The plugin generates a container entry for your build: it collects the shared modules and
their importers, emits the container runtime code, and writes federation metadata describing
what the bundle exposes and shares. At runtime those containers are wired together by
federation-js.
Install
npm install --save-dev rollup-plugin-federation
Usage
import federation from "rollup-plugin-federation";
export default {
input: ["src/index.js", "federation-entry.js"],
output: { dir: "dist", format: "esm" },
plugins: [
federation({
name: "plugin_1",
// must also appear in the input array above
filename: "federation-entry.js",
exposes: {
"./bootstrap": "./src/bootstrap.js",
},
shared: {
react: { singleton: true, semver: "^19.0.0" },
},
}),
],
};
filename names the container entry the plugin generates — it has to be listed in Rollup's
input as well, or the entry is never emitted.
Options
| Option | Type | Description |
|---|---|---|
name |
string |
Required. Name of this container. |
filename |
string |
Required. Output filename for the container entry. Also add it to input. |
exposes |
Record<string, string> |
Modules this container exposes to others. |
shared |
Record<string, SharedConfig> |
Modules shared with other containers. |
shareScope |
string |
Namespace for sharing. Defaults to the built-in scope. |
version |
string |
Version of this container. Defaults to the host package's version. |
emitFederationJson |
boolean |
Emit federation info as JSON. Defaults to true. |
debugging |
boolean |
Enable debug logging and output. |
renderDynamicImport |
object |
Dynamic import rendering per import type. |
entry.header / entry.footer |
string |
Code inserted at the start/end of the entry file. |
emitMeta, emitFederationMeta, enrichManifest |
function |
Hooks to emit or enrich metadata. |
Shared modules
shared: {
react: {
singleton: true, // only one instance across all containers
semver: "^19.0.0", // range this container accepts
eager: false, // load at startup rather than lazily
import: true, // bundle a copy, or treat as external
},
}
A dependency consumed only transitively still has to appear in shared with its semver range
— listing it as an external alone is not enough, and a mismatch shows up at runtime as a
duplicate-instance error rather than a build failure.
Metadata the build emits
Alongside the container entry and its chunks, the plugin writes metadata describing the build. Two files, plus one copy embedded in the entry itself.
federation.json — a record of what this build produced: name, filename,
shareScope, each expose mapped to { path, chunks }, each shared module's config, and a
timestamp. Emitted unless emitFederationJson: false, in which case the same info object is
still handed to emitFederationMeta.
Nothing requires it. It is build and serving metadata for your own tooling — the runtime
never needs it, since the entry file carries the container, its exposes and its shares.
federation-combine keeps its bundles record current when the file is there, but works
without it.
An embedded manifest, if you supply enrichManifest. Because dynamic imports are not
discovered until every module has been processed, the manifest cannot exist when the
container code is generated — so the entry gets a { __placeholder: true } stub, and
generateBundle splices the enriched manifest into the emitted code afterwards, matching
both the development and minified forms. The result is exported as __FYNAPP_MANIFEST__ and
assigned onto the container, which lets a runtime read a bundle's metadata without a second
request.
That substitution is a text patch over generated code, and a build where it fails to match still succeeds — it warns and ships the placeholder. If a consumer of yours reads the embedded manifest without a fallback, check for it in CI.
Whatever emitFederationMeta emits. The hook receives the federation info, the runtime,
the plugin context and the bundle, so it can emit metadata in whatever shape its consumers
want. In the fynmesh colorepo, this pair of hooks is what
produces a FynApp's fynapp.manifest.json; see
notes/BUILD-ARTIFACTS.md
there for a worked example of both hooks and how a runtime consumes their output.
Combining small chunks — federation-combine
One chunk is emitted per expose, so a container with many small exposes ships many tiny files
whose cost is almost entirely request overhead. federation-combine folds them into one
physical file per group, while each module keeps its own identity and stays importable by its
own filename.
It is a post-build step over a finished dist directory, not part of the plugin's pipeline — so it can be re-run with a different policy, or applied at deploy time to artifacts someone else built.
federation-combine demo/fynapp-1/dist --dry-run
federation-combine demo/*/dist
--max-module <bytes> only chunks at or below this size are candidates (2048)
--max-bundle <bytes> stop adding to a group at this size (32768)
--min-group <count> a group needs at least this many chunks (2)
--exclude <a,b> fileNames or stems to leave in their own file
--group <name=a,b> an explicit group, bypassing the size rules; repeatable
--drop-individual delete the members' own files (removes the fallback)
--dry-run report what would change, write nothing
What combining leaves behind
The runtime is told which file carries which module by one statement appended to the container entry, so an ordinary import works with no further help — a member's url resolves to its carrier and the carrier is fetched once, however many of its members are asked for.
That statement runs when the entry runs, which is too late for anything that has to decide
what to fetch — a preload hint naming a member url names a file the runtime will never
request. So the map is also written out as federation.bundles.json, the map on its own:
{ "combo-R7IaSAnm.js": ["hello-B9dQ6FmL.js", "getInfo-CH7m3ozA.js"] }
A host can read that without executing anything, and hand it to the runtime up front with
Federation.declareBundles(map, distBase) — which is what makes carriers known before the
first entry loads. Present only when a run actually combined something, and removed again
when one does not, so its presence is itself the signal that a dist has bundles to offer.
Nothing here needs federation.json: the container entry is the one file that calls
_mfContainer, and that call names the container and its version. An app that builds with
emitFederationJson: false combines exactly the same.
Or from a build script:
import { combineDist } from "rollup-plugin-federation";
combineDist("dist", { groups: { startup: ["index", "react-esm-19.production"] } });
Explicit groups are taken exactly as given, which is how a known startup set is kept together and a cold chunk is kept out of a hot one. Everything else is packed smallest-first.
The members' individual files are kept by default: if the map and the files ever disagree, the
runtime says so and loads the module's own file, so a stale map costs one request rather than a
broken page. --drop-individual removes that fallback, and makes the step non-repeatable.
Requires chunks built by this plugin version or newer — each chunk records the physical file it lives in, which is what lets a combined module tell its own url apart from the file it arrived in. Chunks without it are reported and left alone. Combined files carry no sourcemap; the individual files keep theirs, so debug builds should simply not be combined.
Also exported
DependencyAnalyzer and DependencyUtils for inspecting a build's dependency graph,
combineDist (above), plus the plugin's TypeScript types.
License
Apache-2.0