Migrate Angular Module Federation off the Nx wrappers Migrate this Angular workspace off the Nx Module Federation APIs that Nx v24 removes. Follow every step at https://nx.dev/docs/kb/migrate-angular-module-federation.md in order. Default to Path A (keep the current bundler, use the official plugin). Take Path B (Native Federation) only when the user asks for it.
Rules 2 to 7 cover Path A. On Path B, follow that section of the page instead:
1. Before editing, run production builds and keep every `mf-stats.json` (`dist/apps/<app>/mf-stats.json` on webpack, `dist/apps/<app>/browser/mf-stats.json` on Rspack). Reproduce its `shared` array, `exposes`, and container name. If a generated Rspack app fails its initial `maximumError` budget, raise it. If webpack 5.111 or later fails with `Path variable [contenthash:20] not implemented`, pin webpack to 5.110 or earlier.
2. Replace `withModuleFederation` and `NxModuleFederationPlugin` from `@nx/module-federation/angular` with `ModuleFederationPlugin` from `@module-federation/enhanced/webpack` or `/rspack`, and drop `NxModuleFederationDevServerPlugin`. Keep `@nx/angular:webpack-browser` with `customWebpackConfig` on webpack, and `createConfig` from `@nx/angular-rspack` on Rspack.
3. Browser containers are ES modules: set `library: { type: 'module' }`, `experiments.outputModule: true` on webpack or `output.module: true` on Rspack, `filename: 'remoteEntry.mjs'` on webpack and `remoteEntry.js` on Rspack, `dts: false`, `output.publicPath: 'auto'`, `output.uniqueName`, and `optimization.runtimeChunk: false`. An Rspack host keeps its runtime chunk under the dev server. Write `remotes` as plain URLs with no `name@` prefix, one entry per remote even when the host stats list it once per exposed module.
4. On webpack, add `resolve.alias` entries for every workspace library referenced through `tsconfig` paths. Never alias a `<remote>/<exposed>` entry such as `remoteA/Routes`, `remoteNg/Module`, or a `federate-module` path. Rspack's `createConfig` reads the paths itself and needs no alias.
5. A host that already calls `registerRemotes` from `@module-federation/enhanced/runtime` is done, including `mf-manifest.json` entries with no `type`. For hosts on `@nx/angular/mf`, convert `setRemoteDefinitions` to `registerRemotes` with `type: 'module'` and `loadRemoteModule(name, './Routes')` to `loadRemote('<container-name>/Routes')`. Map keys and `loadRemote` prefixes are the normalized container name. Map values must be full `remoteEntry.mjs` URLs (`remoteEntry.js` on Rspack).
6. On webpack, replace the `serve` target with `@nx/angular:dev-server`. On Rspack keep the inferred targets. Keep each remote's `dependsOn: ['<host>:serve']`, set `headers: { 'Access-Control-Allow-Origin': '*' }` on every remote's serve target (on Rspack under `devServer.headers` in `rspack.config.ts`), and add a `serve-static` target per app using `@nx/web:file-server` with `staticFilePath`, `dependsOn: ['build']`, `spa: true`, `watch: false`, never `buildTarget`. Point Playwright `webServer.command`, or Cypress `webServerCommands` and `ciWebServerCommand`, at `nx run-many -t serve-static -p <host> <remotes>`.
7. Remove `@nx/module-federation` only after the new setup builds and runs. Move `@module-federation/enhanced` to `dependencies` (`pnpm add -P` on pnpm). Rebuild uncached, production last. On Rspack a development build needs `NGRS_CONFIG=development`, or it exits 0 with production output. After removing the package, rename any copy left in `node_modules` (pnpm keeps one under `.pnpm/` for optional peers) for one uncached build, then put it back.
8. Do not claim hot-update parity or shared-state parity from a green build. Remote edits reach the host after a reload, before and after this migration. On Rspack the first load after the dev server starts can need one reload. `Cannot find module '<id>'` while loading `./Routes` means a stale cache: run `nx reset`, delete `.angular/cache`, and rebuild. Report what you verified and what you did not.
9. On Path B, install the `@angular-architects/native-federation` release matching the workspace's Angular major and minor. A host calling `loadRemote('remoteA/Routes')` switches to `loadRemoteModule('remoteA', './Routes')`.
10. For apps with a server build (a `server` target or a `webpack.server.config.*` file), follow https://nx.dev/docs/kb/migrate-angular-module-federation-ssr after Path A. Path B does not cover server-side rendering. Stop and report if the user asks for Path B on one.
Reference: https://nx.dev/docs/kb/migrate-angular-module-federation.md
Migrate this Angular workspace off the Nx Module Federation APIs that Nx v24 removes. Follow every step at https://nx.dev/docs/kb/migrate-angular-module-federation.md in order. Default to Path A (keep the current bundler, use the official plugin). Take Path B (Native Federation) only when the user asks for it.
Rules 2 to 7 cover Path A. On Path B, follow that section of the page instead:
1. Before editing, run production builds and keep every `mf-stats.json` (`dist/apps/<app>/mf-stats.json` on webpack, `dist/apps/<app>/browser/mf-stats.json` on Rspack). Reproduce its `shared` array, `exposes`, and container name. If a generated Rspack app fails its initial `maximumError` budget, raise it. If webpack 5.111 or later fails with `Path variable [contenthash:20] not implemented`, pin webpack to 5.110 or earlier.
2. Replace `withModuleFederation` and `NxModuleFederationPlugin` from `@nx/module-federation/angular` with `ModuleFederationPlugin` from `@module-federation/enhanced/webpack` or `/rspack`, and drop `NxModuleFederationDevServerPlugin`. Keep `@nx/angular:webpack-browser` with `customWebpackConfig` on webpack, and `createConfig` from `@nx/angular-rspack` on Rspack.
3. Browser containers are ES modules: set `library: { type: 'module' }`, `experiments.outputModule: true` on webpack or `output.module: true` on Rspack, `filename: 'remoteEntry.mjs'` on webpack and `remoteEntry.js` on Rspack, `dts: false`, `output.publicPath: 'auto'`, `output.uniqueName`, and `optimization.runtimeChunk: false`. An Rspack host keeps its runtime chunk under the dev server. Write `remotes` as plain URLs with no `name@` prefix, one entry per remote even when the host stats list it once per exposed module.
4. On webpack, add `resolve.alias` entries for every workspace library referenced through `tsconfig` paths. Never alias a `<remote>/<exposed>` entry such as `remoteA/Routes`, `remoteNg/Module`, or a `federate-module` path. Rspack's `createConfig` reads the paths itself and needs no alias.
5. A host that already calls `registerRemotes` from `@module-federation/enhanced/runtime` is done, including `mf-manifest.json` entries with no `type`. For hosts on `@nx/angular/mf`, convert `setRemoteDefinitions` to `registerRemotes` with `type: 'module'` and `loadRemoteModule(name, './Routes')` to `loadRemote('<container-name>/Routes')`. Map keys and `loadRemote` prefixes are the normalized container name. Map values must be full `remoteEntry.mjs` URLs (`remoteEntry.js` on Rspack).
6. On webpack, replace the `serve` target with `@nx/angular:dev-server`. On Rspack keep the inferred targets. Keep each remote's `dependsOn: ['<host>:serve']`, set `headers: { 'Access-Control-Allow-Origin': '*' }` on every remote's serve target (on Rspack under `devServer.headers` in `rspack.config.ts`), and add a `serve-static` target per app using `@nx/web:file-server` with `staticFilePath`, `dependsOn: ['build']`, `spa: true`, `watch: false`, never `buildTarget`. Point Playwright `webServer.command`, or Cypress `webServerCommands` and `ciWebServerCommand`, at `nx run-many -t serve-static -p <host> <remotes>`.
7. Remove `@nx/module-federation` only after the new setup builds and runs. Move `@module-federation/enhanced` to `dependencies` (`pnpm add -P` on pnpm). Rebuild uncached, production last. On Rspack a development build needs `NGRS_CONFIG=development`, or it exits 0 with production output. After removing the package, rename any copy left in `node_modules` (pnpm keeps one under `.pnpm/` for optional peers) for one uncached build, then put it back.
8. Do not claim hot-update parity or shared-state parity from a green build. Remote edits reach the host after a reload, before and after this migration. On Rspack the first load after the dev server starts can need one reload. `Cannot find module '<id>'` while loading `./Routes` means a stale cache: run `nx reset`, delete `.angular/cache`, and rebuild. Report what you verified and what you did not.
9. On Path B, install the `@angular-architects/native-federation` release matching the workspace's Angular major and minor. A host calling `loadRemote('remoteA/Routes')` switches to `loadRemoteModule('remoteA', './Routes')`.
10. For apps with a server build (a `server` target or a `webpack.server.config.*` file), follow https://nx.dev/docs/kb/migrate-angular-module-federation-ssr after Path A. Path B does not cover server-side rendering. Stop and report if the user asks for Path B on one.
Reference: https://nx.dev/docs/kb/migrate-angular-module-federation.md
Migrate this Angular workspace off the Nx Module Federation APIs that Nx v24 removes. Follow every step at https://nx.dev/docs/kb/migrate-angular-module-federation.md in order. Default to Path A (keep the current bundler, use the official plugin). Take Path B (Native Federation) only when the user asks for it.
Rules 2 to 7 cover Path A. On Path B, follow that section of the page instead:
1. Before editing, run production builds and keep every `mf-stats.json` (`dist/apps/<app>/mf-stats.json` on webpack, `dist/apps/<app>/browser/mf-stats.json` on Rspack). Reproduce its `shared` array, `exposes`, and container name. If a generated Rspack app fails its initial `maximumError` budget, raise it. If webpack 5.111 or later fails with `Path variable [contenthash:20] not implemented`, pin webpack to 5.110 or earlier.
2. Replace `withModuleFederation` and `NxModuleFederationPlugin` from `@nx/module-federation/angular` with `ModuleFederationPlugin` from `@module-federation/enhanced/webpack` or `/rspack`, and drop `NxModuleFederationDevServerPlugin`. Keep `@nx/angular:webpack-browser` with `customWebpackConfig` on webpack, and `createConfig` from `@nx/angular-rspack` on Rspack.
3. Browser containers are ES modules: set `library: { type: 'module' }`, `experiments.outputModule: true` on webpack or `output.module: true` on Rspack, `filename: 'remoteEntry.mjs'` on webpack and `remoteEntry.js` on Rspack, `dts: false`, `output.publicPath: 'auto'`, `output.uniqueName`, and `optimization.runtimeChunk: false`. An Rspack host keeps its runtime chunk under the dev server. Write `remotes` as plain URLs with no `name@` prefix, one entry per remote even when the host stats list it once per exposed module.
4. On webpack, add `resolve.alias` entries for every workspace library referenced through `tsconfig` paths. Never alias a `<remote>/<exposed>` entry such as `remoteA/Routes`, `remoteNg/Module`, or a `federate-module` path. Rspack's `createConfig` reads the paths itself and needs no alias.
5. A host that already calls `registerRemotes` from `@module-federation/enhanced/runtime` is done, including `mf-manifest.json` entries with no `type`. For hosts on `@nx/angular/mf`, convert `setRemoteDefinitions` to `registerRemotes` with `type: 'module'` and `loadRemoteModule(name, './Routes')` to `loadRemote('<container-name>/Routes')`. Map keys and `loadRemote` prefixes are the normalized container name. Map values must be full `remoteEntry.mjs` URLs (`remoteEntry.js` on Rspack).
6. On webpack, replace the `serve` target with `@nx/angular:dev-server`. On Rspack keep the inferred targets. Keep each remote's `dependsOn: ['<host>:serve']`, set `headers: { 'Access-Control-Allow-Origin': '*' }` on every remote's serve target (on Rspack under `devServer.headers` in `rspack.config.ts`), and add a `serve-static` target per app using `@nx/web:file-server` with `staticFilePath`, `dependsOn: ['build']`, `spa: true`, `watch: false`, never `buildTarget`. Point Playwright `webServer.command`, or Cypress `webServerCommands` and `ciWebServerCommand`, at `nx run-many -t serve-static -p <host> <remotes>`.
7. Remove `@nx/module-federation` only after the new setup builds and runs. Move `@module-federation/enhanced` to `dependencies` (`pnpm add -P` on pnpm). Rebuild uncached, production last. On Rspack a development build needs `NGRS_CONFIG=development`, or it exits 0 with production output. After removing the package, rename any copy left in `node_modules` (pnpm keeps one under `.pnpm/` for optional peers) for one uncached build, then put it back.
8. Do not claim hot-update parity or shared-state parity from a green build. Remote edits reach the host after a reload, before and after this migration. On Rspack the first load after the dev server starts can need one reload. `Cannot find module '<id>'` while loading `./Routes` means a stale cache: run `nx reset`, delete `.angular/cache`, and rebuild. Report what you verified and what you did not.
9. On Path B, install the `@angular-architects/native-federation` release matching the workspace's Angular major and minor. A host calling `loadRemote('remoteA/Routes')` switches to `loadRemoteModule('remoteA', './Routes')`.
10. For apps with a server build (a `server` target or a `webpack.server.config.*` file), follow https://nx.dev/docs/kb/migrate-angular-module-federation-ssr after Path A. Path B does not cover server-side rendering. Stop and report if the user asks for Path B on one.
Reference: https://nx.dev/docs/kb/migrate-angular-module-federation.md
Nx v23 deprecates the Angular Module Federation generators, executors, runtime helpers, and the withModuleFederation and NxModuleFederationPlugin wrappers in @nx/module-federation/angular. These APIs wrap the official Module Federation plugin, which supports both webpack and Rspack. Nx v24 removes the wrappers, so migrate your apps to the official plugin directly.
Migrate before you upgrade to v24. The old packages still build, which gives you a baseline to compare against. Nx will also ship migrations for this later.
You have one decision to make before you start:
- Path A keeps your bundler and swaps the wrapper for the official plugin. Your build target, routes, and deployment model stay the same. It is the conservative option and the smallest diff.
- Path B moves to Native Federation and onto the esbuild-based
@angular/build:applicationbuilder. It is the larger change, and the one more likely to track Angular's own direction.
What replaces what
Section titled “What replaces what”| Going away in v24 | Replacement |
|---|---|
withModuleFederation from @nx/module-federation/angular | ModuleFederationPlugin from @module-federation/enhanced/webpack |
NxModuleFederationPlugin from @nx/module-federation/angular | ModuleFederationPlugin from @module-federation/enhanced/rspack |
setRemoteDefinitions, setRemoteDefinition from @nx/angular/mf | registerRemotes from @module-federation/enhanced/runtime |
loadRemoteModule from @nx/angular/mf | loadRemote from @module-federation/enhanced/runtime |
@nx/angular:module-federation-dev-server | @nx/angular:dev-server, plus the targets you start yourself |
NxModuleFederationDevServerPlugin | Not supported |
@nx/angular:module-federation-static-server | Not supported |
The last two ran processes and built nothing. NxModuleFederationDevServerPlugin was the Rspack form of the dev-server executor, which started the host and every remote from one nx serve. module-federation-static-server served the remotes you weren't working on from one port. Step 6 replaces both with targets you start yourself.
Capture a baseline first
Section titled “Capture a baseline first”A remote can render correctly while it and the host hold separate copies of a shared library. Record what works now:
- Serve one host at a time with its remotes and visit the routes that load each remote.
- Edit a file in the host and in a remote, and note how each change reaches the page.
- Before you rebuild, copy each app's
mf-stats.jsonto a folder such asbaseline/. It's atdist/apps/<app>/mf-stats.jsonon webpack anddist/apps/<app>/browser/mf-stats.jsonon Rspack. Both paths reproduce itssharedmap,exposes, and container name. - Note each app's
serveport for the remote URLs in Step 3.
Step 1: inventory the workspace
Section titled “Step 1: inventory the workspace”grep -rE "@nx/module-federation|@nx/angular/mf|module-federation-dev-server|module-federation-static-server|module-federation-dev-ssr|withModuleFederationForSSR" --include="*.ts" --include="*.js" --include="*.json" --exclude-dir=node_modules --exclude-dir=.nx --exclude-dir=dist .On Windows:
Get-ChildItem -Recurse -File -Include *.ts,*.js,*.json | Where-Object FullName -notmatch 'node_modules|dist|\.nx' | Select-String -Pattern '@nx/module-federation|@nx/angular/mf|module-federation-dev-server|module-federation-static-server|module-federation-dev-ssr|withModuleFederationForSSR'A generated webpack host has withModuleFederation in webpack.config.ts and webpack.prod.config.ts, a module-federation.config.ts, a build target on @nx/angular:webpack-browser with customWebpackConfig, and a serve target on @nx/angular:module-federation-dev-server. A generated Rspack host has inferred targets and an rspack.config.ts that merges a webpack.config.ts holding NxModuleFederationPlugin and NxModuleFederationDevServerPlugin, plus a webpack.prod.config.ts that nothing loads. Remotes expose ./Routes (./Module for NgModule remotes) and any path federate-module added. Their serve target depends on the host's serve.
Sort your hosts into two groups by reading each src/main.ts. Step 5 only applies to the second group.
- Static hosts have no federation call in
main.ts. They reach remotes through animport('remoteA/Routes')in the routes file, resolved by atsconfig.base.jsonpath. - Runtime-loaded hosts fetch a manifest in
main.ts. If that chain ends ininitorregisterRemotesfrom@module-federation/enhanced/runtime, the host needs no source change on Path A. Nx 23.2 generates--dynamichosts this way. If it ends insetRemoteDefinitionsfrom@nx/angular/mf, Step 5 converts it.
Server-side rendered apps (a server target or a webpack.server.config.ts) follow Path A, then migrate server-side rendered apps. Their server build fails in between.
Path A, the official plugin on webpack or Rspack
Section titled “Path A, the official plugin on webpack or Rspack”Step 2: install the official package
Section titled “Step 2: install the official package”Workspaces from the host generator already list @module-federation/enhanced, usually under devDependencies. Application code imports its runtime. Move it to dependencies, or install it if it's missing:
npm add --save-prod @module-federation/enhancedpnpm add -P @module-federation/enhancedyarn add @module-federation/enhancedbun add @module-federation/enhancedStep 3: translate the federation config
Section titled “Step 3: translate the federation config”Nx accepted a list of project names and resolved each one to a URL from the project graph. The official plugin needs the values written out.
Container name and import alias
Section titled “Container name and import alias”The container name is normalized. Every character outside [a-zA-Z0-9_$], and a leading character that cannot start an identifier, becomes _. dyn-shell publishes as dyn_shell, and that is the form name takes.
The import alias stays the project name, and so do the remoteA/Routes entries in tsconfig.base.json that a static host's import() resolves through:
// a project named dyn-shell that consumes a project named remote-amodule.exports = { name: 'dyn_shell', // container, normalized remotes: { 'remote-a': 'http://localhost:4201/remoteEntry.mjs', // key is the project name },};The shared map
Section titled “The shared map”Every app needs its own map, copied from that app's baseline mf-stats.json. Nx shared every npm package the app depends on in the graph, plus each package's secondary entry points, so expect entries such as @angular/common/http and @angular/core/primitives/signals that your source never imports.
Take singleton, strictVersion, requiredVersion, and eager from each entry:
// dist/apps/shell/mf-stats.json, one entry of the shared array{ "name": "@angular/core", "version": "22.1.7", "singleton": true, "requiredVersion": "~22.1.0", "strictVersion": true, "eager": false,}'@angular/core': { singleton: true, strictVersion: true, requiredVersion: '~22.1.0' },eager: false is the plugin default and can be left out, but Rspack stats mark most @angular/* entries eager: true and those need copying.
Drop the version from npm entries. Keep it on source-only workspace libraries, which have no package.json for the plugin to read. The stats show them as version: '0.0.0' with requiredVersion: '^0.0.0' and no strictVersion.
Remote URLs
Section titled “Remote URLs”Each remote gets a full URL on its serve port. The host's baseline mf-stats.json has these under remotes[].federationContainerName, one row per exposed module the host uses. Write one entry per remote. Angular containers are ES modules, and their URLs take no name@ prefix.
import type { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = { name: 'shell', remotes: ['remoteA', 'remoteB'],};
export default config;module.exports = { name: 'shell', // normalized, so dyn-shell becomes dyn_shell filename: 'remoteEntry.mjs', // remoteEntry.js on Rspack library: { type: 'module' }, dts: false, remotes: { remoteA: 'http://localhost:4201/remoteEntry.mjs', // .js on Rspack remoteB: 'http://localhost:4202/remoteEntry.mjs', }, shared: { '@angular/core': { singleton: true, strictVersion: true, requiredVersion: '~22.1.0', }, '@angular/core/primitives/signals': { singleton: true, strictVersion: true, requiredVersion: '~22.1.0', }, // ... every entry the baseline mf-stats.json lists '@myorg/state': { singleton: true, requiredVersion: '^0.0.0', version: '0.0.0', }, },};Every app's config carries name, filename, library, and dts, and hosts add remotes. Remotes keep their existing exposes map, whose workspace-root paths resolve once Step 4 adds the workspace root to resolve.modules.
Production remote URLs live in webpack.prod.config.ts as ['remoteA', 'http://remote-a.example.com/'] tuples. On webpack the production configuration loads that file through customWebpackConfig. Rspack never loads it. Fold those URLs into the one config, adding the /remoteEntry.mjs that Nx appended for you. If the file only has the generator's commented example, you have one URL set for every configuration.
Key the URLs on the Nx task configuration, because the Angular builders don't set NODE_ENV.
const isProd = process.env.NX_TASK_TARGET_CONFIGURATION === 'production';
module.exports = { // ... name, filename, library, dts, shared remotes: { remoteA: isProd ? 'https://remote-a.example.com/remoteEntry.mjs' : 'http://localhost:4201/remoteEntry.mjs', },};Step 4: replace the wrapper in the bundler config
Section titled “Step 4: replace the wrapper in the bundler config”@nx/angular:webpack-browser passes the Angular build's webpack config to the function your customWebpackConfig exports. That function was withModuleFederation. Replace it with one that adds the plugin and the settings the wrapper applied.
Before:
import { withModuleFederation } from '@nx/module-federation/angular';import config from './module-federation.config';
export default withModuleFederation(config, { dts: false });After:
const { ModuleFederationPlugin,} = require('@module-federation/enhanced/webpack');const { join } = require('node:path');const mf = require('./module-federation.config.js');
module.exports = (config) => ({ ...config, output: { ...config.output, uniqueName: mf.name, publicPath: 'auto' }, experiments: { ...(config.experiments ?? {}), outputModule: true }, resolve: { ...config.resolve, modules: [ ...(config.resolve?.modules ?? ['node_modules']), join(__dirname, '../..'), ], alias: { ...(config.resolve?.alias ?? {}), '@myorg/state': join(__dirname, '../../libs/state/src/index.ts'), }, }, optimization: { ...config.optimization, runtimeChunk: false }, plugins: [...(config.plugins ?? []), new ModuleFederationPlugin(mf)],});Point customWebpackConfig.path at this file in project.json, delete the production configuration's override of it, and keep @nx/angular:webpack-browser as the executor.
Add the resolve.alias entries in every federated app, hosts included. The federation plugin resolves exposed modules outside the Angular compilation, which is the only place tsconfig paths apply. Alias workspace libraries only. Aliasing a <remote>/<exposed> path such as remoteA/Routes, remoteNg/Module, or a federate-module path bundles that remote into the host.
The generated rspack.config.ts calls createConfig from @nx/angular-rspack and merges in a webpack.config.ts holding NxModuleFederationPlugin and NxModuleFederationDevServerPlugin. Keep that structure. Replace the merged file's contents and drop the dev-server plugin:
Before:
import { NxModuleFederationPlugin } from '@nx/module-federation/angular';import config from './module-federation.config';
export default { plugins: [new NxModuleFederationPlugin({ config }, { dts: false })],};After:
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';import { join } from 'node:path';import mf from './module-federation.config.js';
export default { output: { uniqueName: mf.name, publicPath: 'auto', module: true }, resolve: { modules: ['node_modules', join(__dirname, '../..')] }, optimization: { // hosts only, remotes set runtimeChunk: false unconditionally ...(process.env['RSPACK_SERVE'] || process.env['WEBPACK_SERVE'] ? {} : { runtimeChunk: false }), // keeps the wrapper's chunk layout, without it one chunk is merged away splitChunks: { cacheGroups: { default: false, common: false } }, }, plugins: [new ModuleFederationPlugin(mf)],};Under the dev server, a host keeps the runtime chunk createConfig gives it, as the wrapper did.
Rspack containers keep filename: 'remoteEntry.js', so host remotes URLs end in /remoteEntry.js rather than .mjs. Skip resolve.alias, since createConfig reads tsconfig paths itself.
Step 5: convert runtime-loaded remotes
Section titled “Step 5: convert runtime-loaded remotes”This step converts the runtime-loaded hosts from Step 1 that call setRemoteDefinitions from @nx/angular/mf. Static hosts and hosts already on registerRemotes skip it.
The host keeps its manifest and keeps ModuleFederationPlugin in its build with remotes: {}, since registerRemotes and loadRemote do nothing without the runtime it creates.
import { setRemoteDefinitions } from '@nx/angular/mf';
fetch('/module-federation.manifest.json') .then((res) => res.json()) .then(setRemoteDefinitions) .then(() => import('./bootstrap'));import { loadRemoteModule } from '@nx/angular/mf';
export const appRoutes: Route[] = [ { path: 'remoteA', loadChildren: () => loadRemoteModule('remoteA', './Routes').then((m) => m.remoteRoutes), },];import { registerRemotes } from '@module-federation/enhanced/runtime';
fetch('/module-federation.manifest.json') .then((res) => res.json()) .then((remotes: Record<string, string>) => registerRemotes( Object.entries(remotes).map(([name, entry]) => ({ name, entry, type: 'module' as const, })) ) ) .then(() => import('./bootstrap'));import { loadRemote } from '@module-federation/enhanced/runtime';
export const appRoutes: Route[] = [ { path: 'remoteA', loadChildren: () => loadRemote<typeof import('remoteA/Routes')>('remoteA/Routes').then( (m) => m!.remoteRoutes ), },];Map values must be full URLs. Append /remoteEntry.mjs (/remoteEntry.js on Rspack) to any bare origin, which setRemoteDefinitions used to complete for you. A remote's mf-manifest.json URL works too. Register remoteEntry.mjs values with type: 'module', while manifest values carry their own type.
Map keys and loadRemote prefixes use the normalized container name. A project named remote-a is remote_a in the manifest and loadRemote('remote_a/Routes') at the call site.
Step 6: replace the serve orchestration
Section titled “Step 6: replace the serve orchestration”@nx/angular:module-federation-dev-server built and served the remotes along with the host, and --devRemotes picked which ones ran live. The official plugin only handles the build, so you start the processes yourself.
Dev servers
Section titled “Dev servers”On webpack, switch the host's serve executor to @nx/angular:dev-server. Keep port, publicHost, and the per-configuration buildTarget options. Remove devRemotes, skipRemotes, static, isInitialHost, and pathToManifestFile, which the dev server rejects. On Rspack, keep the inferred build and serve targets.
Every remote needs headers: { 'Access-Control-Allow-Origin': '*' }, on the serve target on webpack or under devServer.headers in rspack.config.ts on Rspack. The host loads remoteEntry.mjs cross-origin, and the Angular dev server sends no CORS header by default.
Keep each remote's dependsOn: ['shell:serve'] and start everything you want live from one run-many, naming each app:
nx run-many -t serve -p shell remoteA remoteBStatic servers
Section titled “Static servers”Replace every app's generated serve-static target, hosts included, with one that serves the build output directly:
{ "targets": { "serve-static": { "executor": "@nx/web:file-server", "continuous": true, "dependsOn": ["build"], "options": { "staticFilePath": "dist/apps/remoteB", // dist/apps/remoteB/browser on Rspack "port": 4202, "spa": true, "watch": false, }, }, },}Replace the whole target, including configurations and defaultConfiguration, and leave buildTarget unset. With it set, the file server rebuilds the app on top of the dependsOn build, and that nested build fails inside e2e runs. Keep spa: true so a refresh on a deep route doesn't 404.
Each remote's serve-static port must match the port in the host's remotes URL.
End-to-end projects
Section titled “End-to-end projects”Generated Playwright projects start the host with nx run shell:serve, which no longer starts the remotes. Point webServer.command at nx run-many -t serve-static -p shell remoteA remoteB. A remote's own e2e project needs only -p remoteA, with webServer.url and baseURL on its serve-static port.
Cypress projects set these commands under nxE2EPreset in cypress.config.ts:
nxE2EPreset(__filename, { cypressDir: 'src', webServerCommands: { default: 'nx run-many -t serve-static -p shell remoteA remoteB', production: 'nx run-many -t serve-static -p shell remoteA remoteB', }, ciWebServerCommand: 'nx run-many -t serve-static -p shell remoteA remoteB',});Target defaults
Section titled “Target defaults”In nx.json, drop the { "env": "NX_MF_DEV_REMOTES" } input from the @nx/angular:webpack-browser target default and keep the rest of the entry.
Step 7: validate the result
Section titled “Step 7: validate the result”Check the result in a browser against your baseline:
- Each remote route renders in the host.
- A shared mutable value written in the host reads back in each remote after a client-side route change.
- Editing a remote's component shows in the host after a reload, as in the baseline.
- The e2e suite passes on the replacement web server.
Then check the builds:
- Production and development both build with
--skipNxCacheafter you deletedist/apps/<app>and.angular/cache. Build production last because both configurations write the samemf-stats.json. On Rspack, build development asNGRS_CONFIG=development nx build <app> --configuration=development. - Each app's production
mf-stats.jsonlists the same container name, remotes, exposes, andsharedentries as the baseline. Compare only those fields, since asset hashes andusedIntext change.
Step 8: clean up the old setup
Section titled “Step 8: clean up the old setup”Once the new setup builds and runs, delete module-federation.config.ts, webpack.prod.config.ts, and on webpack webpack.config.ts. Rspack keeps webpack.config.ts because rspack.config.ts imports it.
Then remove the package and rebuild uncached:
npm remove @nx/module-federationPath B, rewrite to native federation
Section titled “Path B, rewrite to native federation”Native Federation does the same runtime composition with ES modules and import maps, on the esbuild @angular/build:application builder. Expect to rewrite the federation layer, starting from an init generator that expects an Angular CLI app.
Step 2: install and initialize
Section titled “Step 2: install and initialize”Install the release matching your Angular major and minor. On Angular 22.1:
npm add @angular-architects/native-federation@~22.1.0It belongs in dependencies.
Migrate remotes together with every host that loads them. A Module Federation host can't load a Native Federation remote. If a host can't move yet, keep its remotes on Path A until it can.
The generator refuses an app that already has a bootstrap.ts. Fold bootstrap.ts back into main.ts, delete it, and run the generator per app. On a runtime-loaded host, also drop the fetch(...) chain that ends in setRemoteDefinitions or registerRemotes, keeping only the bootstrap body. The dynamic-host type writes and reads its own public/federation.manifest.json. Delete the old module-federation.manifest.json.
nx g @angular-architects/native-federation:init --project=shell --port=4200 --type=hostnx g @angular-architects/native-federation:init --project=remoteA --port=4201 --type=remoteUse --type=host for a static host and --type=dynamic-host for one that reads a manifest.
The generator creates federation.config.mjs, tsconfig.federation.json, a new bootstrap.ts and main.ts, moves the app onto an esbuild target running @angular/build:application, wraps build and serve in @angular-architects/native-federation:build, and adds es-module-shims to the polyfills. It also adds es-module-shims under dependencies and @softarc/native-federation-orchestrator under devDependencies, and runs an install.
The host's remote map comes from every other application's serve port, or serve-original once that app has moved. Run the generator in any order. Apps with no port land at 4200, and Step 3 prunes them from the map. The wrapped serve gets port: 0 and falls through to the port on serve-original.
Delete dist/apps/<app> before the first build. The new output goes under browser/, next to the old webpack files. The first build also rewrites the files list in tsconfig.federation.json. Commit after the first build that follows Step 3.
Step 3: finish what the generator left
Section titled “Step 3: finish what the generator left”On an Nx Module Federation app, the generator leaves this cleanup:
- Delete
module-federation.config.ts,webpack.config.ts, andwebpack.prod.config.ts, and remove thecustomWebpackConfigit copied onto theesbuildtarget and itsproductionconfiguration. - Replace the placeholder
'./Component'entry in each remote'sexposeswith the route file, as a path from the workspace root:exposes: { './Routes': './apps/remoteA/src/app/remote-entry/entry.routes.ts' }. - Share workspace libraries reached through
tsconfigpaths withsharedMappings: ['@myorg/state']in every app'sfederation.config.mjs.shareAllonly coverspackage.jsondependencies. - Prune the host's remote map, which lists every application with
buildandservetargets under a camel-cased key, other hosts included. With--type=hostthe map is the first argument ofinitFederationinmain.ts, and with--type=dynamic-hostit ispublic/federation.manifest.json. Keep thehostRemoteEntryoption the generator writes next to it. - On every app's
serve-originaltarget, swap the executor to@angular/build:dev-serverand droppublicHost, which that builder does not accept. Keep a remote'sheaders. - Point
extract-i18nat<app>:esbuildon@angular/build:extract-i18n. - Mark the wrapped
servecontinuous: true. It has no configurations, so useserve-staticfor a production-like local run. Give the wrappedbuildcache: trueandoutputs({workspaceRoot}/dist/apps/<app>). Cache hits start from the second build. - Move the
dependsOn: ['shell:serve']the generator renamed ontoserve-originalback to the newserve. When a second host uses the same remotes, name every app in therun-many. - Replace each app's whole
serve-statictarget,defaultConfigurationincluded, with the one from Path A Step 6 usingstaticFilePath: dist/apps/<app>/browser, and point the e2ewebServercommands atnx run-many -t serve-staticas in that step. - Remove the
@nx/angular:webpack-browserentry fromtargetDefaultsinnx.jsononce every app has moved. - Rewrite host routes to use Native Federation's
loadRemoteModule. Static hosts replace eachimport('remoteA/Routes')with the call below, and so do hosts callingloadRemote('remoteA/Routes'). Hosts on@nx/angular/mfswap only the import:
import { loadRemoteModule } from '@angular-architects/native-federation';
export const appRoutes: Route[] = [ { path: 'remoteA', loadChildren: () => loadRemoteModule('remoteA', './Routes').then((m) => m.remoteRoutes), },];- Last, once every app has moved and no target names a webpack executor, drop the
remoteA/Routesentries fromtsconfig.base.jsonpaths andnpm remove @nx/module-federation @module-federation/enhanced @nx/webpack webpack-merge. Removing them earlier breaks the graph commands the other steps need.
Builds land under dist/apps/<app>/browser/ with a remoteEntry.json per app and no mf-stats.json. Validate as in Path A Step 7, running the parity check against remoteEntry.json:
- Every baseline npm
sharedentry appears as a singleton with the samerequiredVersionrange. sharedMappingslibraries show up as~<version>withstrictVersion.- The container
nameis the project name as written, not normalized. - Remote URLs are in
main.tsor the manifest.
shareAll adds more shared packages than the old mf-stats.json listed. Trim them with its skip option if they matter to you.
What the official plugin does not do
Section titled “What the official plugin does not do”--devRemotesselection. Which remotes run live and which serve from a build is now the set of targets you start.- Automatic remote fallback. A host fails the request when a remote is down. Add your own error handling if you need it.
For anything beyond this migration, use the official Module Federation configuration reference and, for Path B, the Native Federation documentation.