Migrate Nx Module Federation to the official plugins Migrate this workspace off the Nx Module Federation APIs that Nx v24 will remove, keeping each app's current bundler. Follow every step at https://nx.dev/docs/kb/migrate-from-nx-module-federation.md in order. Scope is React client-side Rspack and webpack. Stop and report if you find Angular or server-side rendering apps.
Rules that override any shortcut you are tempted to take:
1. Before editing, run production builds and keep every `dist/apps/<app>/mf-stats.json`. Its `shared` array, `exposes`, and container name are what you reproduce. Do not derive sharing from package.json.
2. Replace `withModuleFederation` and `NxModuleFederationPlugin` with `ModuleFederationPlugin` from `@module-federation/enhanced/rspack` or `/webpack`. Keep `NxAppRspackPlugin`, `NxAppWebpackPlugin`, `NxReactRspackPlugin`, `NxReactWebpackPlugin`. Do not switch bundlers or regenerate apps.
3. Set `name` to the project name with every character outside `[a-zA-Z0-9_$]` replaced by `_`, plus `filename: 'remoteEntry.js'`, `dts: false`, and on webpack `remoteType: 'script'`. Drop any `library` entry.
4. Write the `shared` map from the baseline `mf-stats.json`, copying `singleton`, `strictVersion`, `requiredVersion`, and `eager` per entry. Secondary entry points are separate keys. Give a source-only workspace library an explicit `version`.
5. Move remotes to a runtime manifest: `registerRemotes` with `type: 'global'` and full `remoteEntry.js` URLs, `loadRemote('<container-name>/Module')` at the call sites, and `implicitDependencies` on the host. Keep static `remotes` entries only if the user asks to stay static.
6. Restore the bundler settings the wrapper used to set: `output.uniqueName`, `output.publicPath: 'auto'`, `output.clean: true`, `optimization.runtimeChunk: false`, app-plugin `runtimeChunk: false` and `commonChunk: false`, `devServer.hot: true` with an `Access-Control-Allow-Origin` header, `lazyCompilation: false` on Rspack, and `output.scriptType: 'text/javascript'` on webpack.
7. Move `build` and `serve` to `nx:run-commands` running the Rspack or webpack CLI, with `NODE_ENV` per configuration. Add a `serve-static` target per app using `@nx/web:file-server` with `staticFilePath`, `dependsOn: ['build']`, `spa: true`, `watch: false`, never `buildTarget`.
8. Delete the old config files and remove `@nx/module-federation` only after the new setup builds and runs.
9. Do not claim hot-update parity, shared-state parity, or deployment-path correctness from a green build. Report what you verified and what you did not.
Reference: https://nx.dev/docs/kb/migrate-from-nx-module-federation.md
Migrate this workspace off the Nx Module Federation APIs that Nx v24 will remove, keeping each app's current bundler. Follow every step at https://nx.dev/docs/kb/migrate-from-nx-module-federation.md in order. Scope is React client-side Rspack and webpack. Stop and report if you find Angular or server-side rendering apps.
Rules that override any shortcut you are tempted to take:
1. Before editing, run production builds and keep every `dist/apps/<app>/mf-stats.json`. Its `shared` array, `exposes`, and container name are what you reproduce. Do not derive sharing from package.json.
2. Replace `withModuleFederation` and `NxModuleFederationPlugin` with `ModuleFederationPlugin` from `@module-federation/enhanced/rspack` or `/webpack`. Keep `NxAppRspackPlugin`, `NxAppWebpackPlugin`, `NxReactRspackPlugin`, `NxReactWebpackPlugin`. Do not switch bundlers or regenerate apps.
3. Set `name` to the project name with every character outside `[a-zA-Z0-9_$]` replaced by `_`, plus `filename: 'remoteEntry.js'`, `dts: false`, and on webpack `remoteType: 'script'`. Drop any `library` entry.
4. Write the `shared` map from the baseline `mf-stats.json`, copying `singleton`, `strictVersion`, `requiredVersion`, and `eager` per entry. Secondary entry points are separate keys. Give a source-only workspace library an explicit `version`.
5. Move remotes to a runtime manifest: `registerRemotes` with `type: 'global'` and full `remoteEntry.js` URLs, `loadRemote('<container-name>/Module')` at the call sites, and `implicitDependencies` on the host. Keep static `remotes` entries only if the user asks to stay static.
6. Restore the bundler settings the wrapper used to set: `output.uniqueName`, `output.publicPath: 'auto'`, `output.clean: true`, `optimization.runtimeChunk: false`, app-plugin `runtimeChunk: false` and `commonChunk: false`, `devServer.hot: true` with an `Access-Control-Allow-Origin` header, `lazyCompilation: false` on Rspack, and `output.scriptType: 'text/javascript'` on webpack.
7. Move `build` and `serve` to `nx:run-commands` running the Rspack or webpack CLI, with `NODE_ENV` per configuration. Add a `serve-static` target per app using `@nx/web:file-server` with `staticFilePath`, `dependsOn: ['build']`, `spa: true`, `watch: false`, never `buildTarget`.
8. Delete the old config files and remove `@nx/module-federation` only after the new setup builds and runs.
9. Do not claim hot-update parity, shared-state parity, or deployment-path correctness from a green build. Report what you verified and what you did not.
Reference: https://nx.dev/docs/kb/migrate-from-nx-module-federation.md
Migrate this workspace off the Nx Module Federation APIs that Nx v24 will remove, keeping each app's current bundler. Follow every step at https://nx.dev/docs/kb/migrate-from-nx-module-federation.md in order. Scope is React client-side Rspack and webpack. Stop and report if you find Angular or server-side rendering apps.
Rules that override any shortcut you are tempted to take:
1. Before editing, run production builds and keep every `dist/apps/<app>/mf-stats.json`. Its `shared` array, `exposes`, and container name are what you reproduce. Do not derive sharing from package.json.
2. Replace `withModuleFederation` and `NxModuleFederationPlugin` with `ModuleFederationPlugin` from `@module-federation/enhanced/rspack` or `/webpack`. Keep `NxAppRspackPlugin`, `NxAppWebpackPlugin`, `NxReactRspackPlugin`, `NxReactWebpackPlugin`. Do not switch bundlers or regenerate apps.
3. Set `name` to the project name with every character outside `[a-zA-Z0-9_$]` replaced by `_`, plus `filename: 'remoteEntry.js'`, `dts: false`, and on webpack `remoteType: 'script'`. Drop any `library` entry.
4. Write the `shared` map from the baseline `mf-stats.json`, copying `singleton`, `strictVersion`, `requiredVersion`, and `eager` per entry. Secondary entry points are separate keys. Give a source-only workspace library an explicit `version`.
5. Move remotes to a runtime manifest: `registerRemotes` with `type: 'global'` and full `remoteEntry.js` URLs, `loadRemote('<container-name>/Module')` at the call sites, and `implicitDependencies` on the host. Keep static `remotes` entries only if the user asks to stay static.
6. Restore the bundler settings the wrapper used to set: `output.uniqueName`, `output.publicPath: 'auto'`, `output.clean: true`, `optimization.runtimeChunk: false`, app-plugin `runtimeChunk: false` and `commonChunk: false`, `devServer.hot: true` with an `Access-Control-Allow-Origin` header, `lazyCompilation: false` on Rspack, and `output.scriptType: 'text/javascript'` on webpack.
7. Move `build` and `serve` to `nx:run-commands` running the Rspack or webpack CLI, with `NODE_ENV` per configuration. Add a `serve-static` target per app using `@nx/web:file-server` with `staticFilePath`, `dependsOn: ['build']`, `spa: true`, `watch: false`, never `buildTarget`.
8. Delete the old config files and remove `@nx/module-federation` only after the new setup builds and runs.
9. Do not claim hot-update parity, shared-state parity, or deployment-path correctness from a green build. Report what you verified and what you did not.
Reference: https://nx.dev/docs/kb/migrate-from-nx-module-federation.md
Nx v23 deprecates the Module Federation wrappers, runtime helpers, dev-server executors, and generators that @nx/module-federation, @nx/react, @nx/rspack, and @nx/angular ship today. Nx v24 will remove them. Their replacement is the official Module Federation plugins, which withModuleFederation already wraps internally, so your Rspack apps stay on Rspack and your webpack apps stay on webpack.
Migrate before you upgrade to v24, while the old packages still work and you can compare the result against a baseline you captured yourself. Nx will also ship migrations for this later.
What replaces what
Section titled “What replaces what”| Going away in v24 | Replacement |
|---|---|
withModuleFederation from @nx/module-federation/rspack | ModuleFederationPlugin from @module-federation/enhanced/rspack |
withModuleFederation from @nx/module-federation/webpack | ModuleFederationPlugin from @module-federation/enhanced/webpack |
NxModuleFederationPlugin from @nx/module-federation/rspack | ModuleFederationPlugin from @module-federation/enhanced/rspack |
setRemoteDefinitions, setRemoteDefinition from @nx/react/mf | registerRemotes from @module-federation/enhanced/runtime |
loadRemoteModule from @nx/react/mf | loadRemote from @module-federation/enhanced/runtime |
@nx/react:module-federation-dev-server and the Rspack equivalent | nx:run-commands running your bundler dev server |
NxModuleFederationDevServerPlugin | Not supported |
@nx/react:module-federation-static-server and the Rspack equivalent | Not supported |
@nx/react/module-federation and @nx/rspack/module-federation re-exports | Same as the @nx/module-federation entry they re-export |
sharePackages, shareWorkspaceLibraries, mapRemotes from @nx/module-federation | Not supported. Write the resolved values out, Step 4 |
The two unsupported entries coordinated processes rather than built code, and this migration skips them. NxModuleFederationDevServerPlugin is the Rspack form of the dev-server executor, which started each remote alongside the host so one nx serve brought the whole system up. @nx/react:module-federation-static-server served every remote you were not developing from a single port. You start the processes you want yourself now, which is why Step 5 moves remotes to a runtime manifest.
Keep NxAppRspackPlugin, NxAppWebpackPlugin, NxReactRspackPlugin, and NxReactWebpackPlugin. They are general bundler plugins and are not part of this removal.
Capture a baseline first
Section titled “Capture a baseline first”Federation failures are quiet: a remote renders correctly while the host and the remote hold separate copies of a shared library. Write down what works now, so you have something to compare against:
- Serve the host and its remotes, and visit the routes that load each remote.
- Edit a file in the host and in a remote. Both should refresh the app.
- Run a production build and note the artifacts you depend on, such as each remote's
remoteEntry.js. The official plugin can order or name chunks differently, so treat the federation config as the thing to reproduce rather than a byte-identical bundle. - Copy each
dist/apps/<app>/mf-stats.jsonout ofdistbefore you rebuild. It holds the resolvedsharedmap,exposes, and container name that Steps 3 and 4 reproduce. - Note each app's
serveport, which the remote URLs in Step 5 use.
Step 1: inventory the workspace
Section titled “Step 1: inventory the workspace”Search for every consumer before you edit one:
grep -rE "@nx/module-federation|@nx/react/module-federation|@nx/rspack/module-federation|@nx/react/mf|module-federation-dev-server|module-federation-static-server|module-federation-ssr-dev-server|NX_MF_DEV_REMOTES" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" --exclude-dir=node_modules --exclude-dir=.nx --exclude-dir=dist .On Windows:
Get-ChildItem -Recurse -File -Include *.ts,*.tsx,*.js,*.json | Where-Object FullName -notmatch 'node_modules|dist|\.nx' | Select-String -Pattern '@nx/module-federation|@nx/react/module-federation|@nx/rspack/module-federation|@nx/react/mf|module-federation-dev-server|module-federation-static-server|module-federation-ssr-dev-server|NX_MF_DEV_REMOTES'Record, per app, whether it exposes modules, consumes them, or does both, along with its production remote URLs, custom upstream options, and any runtime plugins.
Stop if the search turns up Angular apps or server-side rendering. Angular containers are ES modules and need different settings, covered in migrate Angular Module Federation. The server-side rendering executors have no equivalent here.
Step 2: install the official packages
Section titled “Step 2: install the official packages”A workspace built by the host generator already has @module-federation/enhanced in its root package.json. Move it under dependencies if it sits in devDependencies, since application code imports the federation runtime, then run your package manager's install so the lockfile follows. Install it if it is missing:
npm add --save-prod @module-federation/enhancedStep 7 runs the bundler CLI directly, so check that you have one. Rspack workspaces declare @rspack/cli already. A webpack workspace that only ever built through the Nx executor usually has no webpack-cli, even though node_modules/.bin/webpack exists:
npm add -D webpack-cliUse pnpm add, yarn add, or bun add in place of npm add throughout.
Step 3: translate the federation config
Section titled “Step 3: translate the federation config”Nx accepts a list of project names and resolves each one to a URL from the project graph. The official plugin takes explicit values, so write them 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_$] becomes _, and so does a leading character that cannot start an identifier, so webpack-host publishes as webpack_host. Use that form for name.
The import alias stays the original project name. webpackRemoteA/Module keeps working because the key in remotes is the alias, not the container name.
Options Nx passed for you
Section titled “Options Nx passed for you”Nx set filename to remoteEntry.js, set remoteType to script on webpack, and passed through the dts: false override from your withModuleFederation call. Set all three yourself. Without dts: false, every remote build logs [ Module Federation DTS ] Error ... #TYPE-001.
Drop any library entry. Nx never passed it to the plugin, and the { type: 'var', name: '<project-name>' } the generator wrote publishes an un-normalized global. On Rspack a var or window type was what turned on remoteType: 'script' and output.scriptType: 'text/javascript', so carry those two by hand if your Rspack config had such a library.
Remote URLs
Section titled “Remote URLs”Each app lists only its direct remotes. A remote that consumes other remotes is both a provider and a consumer, so it gets both maps.
Generated apps also carry a webpack.config.prod.* file wired through build.configurations.production.webpackConfig, holding tuples such as ['webpackRemoteA', 'http://localhost:4701/']. Nx appended remoteEntry.js to any tuple URL without a filename, with or without a trailing slash. Fold both sets into one config keyed on NODE_ENV, with exactly one slash before remoteEntry.js.
import type { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = { name: 'webpack-host', remotes: ['webpackRemoteA', 'webpackRemoteB'],};
export default config;const isProd = process.env.NODE_ENV === 'production';
module.exports = { name: 'webpack_host', // normalized filename: 'remoteEntry.js', dts: false, remoteType: 'script', // webpack only, Rspack defaults to script remotes: { webpackRemoteA: isProd ? 'webpackRemoteA@https://remote-a.example.com/remoteEntry.js' : 'webpackRemoteA@http://localhost:4701/remoteEntry.js', webpackRemoteB: isProd ? 'webpackRemoteB@https://remote-b.example.com/remoteEntry.js' : 'webpackRemoteB@http://localhost:4702/remoteEntry.js', }, shared: { // ... see Step 4 },};Remote configs keep their exposes map as it is. Step 5 replaces a top-level host's remotes map with a runtime manifest, so those URLs are needed only until then. A remote that consumes other remotes keeps the map it writes here.
Step 4: write the shared map
Section titled “Step 4: write the shared map”Nx derived shared from the project graph, covering framework packages, their secondary entry points, every workspace library the app imports, and the npm dependencies of those libraries in turn. The official plugin shares only what you list.
Read the values from the baseline mf-stats.json rather than deriving them, and copy singleton, strictVersion, requiredVersion, and eager per entry:
module.exports = { // ... name, filename, dts, remotes shared: { '@myorg/state': { singleton: true, requiredVersion: false, version: '1.0.0', }, react: { singleton: true, strictVersion: true, requiredVersion: '^19.0.0', eager: true, }, 'react-dom': { singleton: true, strictVersion: true, requiredVersion: '^19.0.0', eager: true, }, 'react-dom/client': { singleton: true, strictVersion: true, requiredVersion: '^19.0.0', }, 'react/jsx-runtime': { singleton: true, strictVersion: true, requiredVersion: '^19.0.0', }, },};Four things decide whether the map you write matches the one Nx produced:
- Secondary entry points are separate keys. A key such as
pkgdoes not coverpkg/subpathrequests, so list the subpaths the stats show. additionalSharedentries set their own values. The same package can appear with a different range and nostrictVersion, which is why you copy rather than derive. An entry the stats do not list was never used and can go.- The stats rewrite one field. A callback that set
requiredVersion: falseshows up asrequiredVersion: '^<version>'. For a workspace library, keep thefalseand theversionyour callback wrote. - A source-only workspace library needs an explicit
version. It has no published version for the plugin to compare, andsingleton: truealone did not make host and remotes resolve to one copy in testing. Use the version the library declares or one your team agrees on, and do not give the same version to two different implementations.
A config that called sharePackages, shareWorkspaceLibraries, or mapRemotes keeps the same shape once you replace each call with the values it returned, which the stats give you:
// before: shared: sharePackages(['react', 'react-dom'])// after:const sharedNpm = (names, version) => Object.fromEntries( names.map((name) => [ name, { singleton: true, strictVersion: true, requiredVersion: version }, ]) );
module.exports = { // ... shared: sharedNpm(['react', 'react-dom'], '^19.0.0'),};Step 5: move remotes to a runtime manifest
Section titled “Step 5: move remotes to a runtime manifest”A runtime manifest holds the remote URLs in a file the host fetches at startup, so you change a URL without rebuilding and the host boots whether or not a given remote is running. It is the upstream default, documented in the manifest reference, and it is what replaces the static server this migration removes.
Register the remotes before the async boundary that imports your bootstrap, and translate loadRemoteModule(name, './Module') into loadRemote(name + '/Module').
const RemoteA = React.lazy(() => import('webpackRemoteA/Module'));import { registerRemotes } from '@module-federation/enhanced/runtime';
fetch('/assets/module-federation.manifest.json') .then((res) => res.json()) .then((remotes: Record<string, string>) => registerRemotes( Object.entries(remotes).map(([name, entry]) => ({ name, entry, type: 'global' as const, })) ) ) .then(() => import('./bootstrap'));import { loadRemote } from '@module-federation/enhanced/runtime';
const RemoteA = React.lazy( () => loadRemote<{ default: React.ComponentType }>( 'webpackRemoteA/Module' ) as Promise<{ default: React.ComponentType }>);{ "webpackRemoteA": "http://localhost:4701/remoteEntry.js", "webpackRemoteB": "http://localhost:4702/remoteEntry.js"}Three things to get right:
- Every value is a full entry URL.
loadRemoteModuleappended/remoteEntry.mjsto a bare origin such ashttp://localhost:4701, andregisterRemotespasses the string through unchanged.output.publicPath: 'auto'then resolves each remote's chunks against wherever its entry loaded from. A deployment path prefix belongs in the manifest you ship, not the one you run locally, since@nx/web:file-serverserves each app at its own root. - Each key is the remote's normalized container name.
type: 'global'tells the runtime to read the container fromglobalThis[name], which is how Nx built these remotes, so the key has to match thenamefrom Step 3. Registration replaces the alias: the registered name is also the prefixloadRemotetakes, so a project namedmy-remoteismy_remotein the manifest andloadRemote('my_remote/Module')at the call site. - The host loses its project-graph edge. A static
import('<remote>/Module')gave Nx a host-to-remote edge. Once those imports are gone, addimplicitDependencieson the host so the remotes still build first andnx affectedstill treats a remote change as affecting the host. You can also drop that host's<remote>/Moduleentries fromtsconfig.base.jsonand anyremotes.d.ts, since nothing imports those paths now. - Per-environment URLs move into the manifest. The
NODE_ENVternary from Step 3 has no equivalent here. The manifest is an asset, so ship the one that belongs to each environment, through afileReplacementspair or by writing the file at deploy time.
Only a top-level host converts. A remote that consumes other remotes keeps the static remotes map from Step 3, because a host loads that remote's exposed module rather than its main.ts, so a registration placed there never runs.
A host that already calls init or registerRemotes from the upstream runtime is done, whatever generated it. A host that fetches a manifest and hands it to setRemoteDefinitions still needs the conversion above, since that helper is one of the removed APIs. A host that used setRemoteUrlResolver from @nx/react/mf to compute URLs keeps that logic and passes each resolved URL as the entry in the same registerRemotes call.
Step 6: swap the wrapper in the bundler config
Section titled “Step 6: swap the wrapper in the bundler config”composePlugins(withNx(), withReact(), withModuleFederation(config)) returns an Nx-specific config function that the Rspack and webpack CLIs cannot run. Replace it with a standard config object that adds ModuleFederationPlugin itself, which is why build and serve move to the CLI in Step 7.
The options on your old build target become NxAppRspackPlugin or NxAppWebpackPlugin options. When you move them:
- Paths such as
main,index,tsConfig,assets, andstylesresolve from the project root, soapps/webpack-host/src/main.tsbecomes./src/main.ts. fileReplacementspaths resolve from the workspace root. Drop a pair whose files do not exist.- Values that differed per named configuration become
process.env.NODE_ENV === 'production'ternaries, which Step 7 sets per configuration. The generator's per-configurationoptimization,outputHashing,sourceMap,namedChunks, andvendorChunkvalues match what the plugin derives fromNODE_ENV, so they can go. - Leave no build options behind on the target. The plugin merges whatever the running target still carries in
project.json, and those win.
The wrapper also mutated the bundler config on its way through, so the config below sets those values directly:
| Setting | Value |
|---|---|
output.uniqueName | The project name, not the normalized container name |
output.publicPath | 'auto' |
output.clean | true. The Rspack app plugin defaults it, the webpack one does not, and without it a development build lands on top of production files |
output.scriptType | 'text/javascript' on webpack |
resolve.modules | node_modules plus the workspace root, which is what the wrapper set and what an exposes path written from the workspace root needs |
optimization.runtimeChunk | false, and the same as an app-plugin option, which overwrites the top-level value |
commonChunk | false in the app-plugin options. NxAppWebpackPlugin defaults it to true where withNx() left it unset, which adds a common.<hash>.js per remote |
splitChunks.cacheGroups | default and common set to false, only if the old config used NxModuleFederationPlugin. withModuleFederation did not set them |
lazyCompilation (Rspack) | false. rspack-cli turns it on in dev mode and it breaks federation |
devServer.hot | true on every app, remotes included, so editing a remote updates the host instead of resetting its state |
devServer.headers | Access-Control-Allow-Origin, which is what lets a host on one port fetch remoteEntry.js from another |
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');const { ModuleFederationPlugin,} = require('@module-federation/enhanced/rspack');const { join } = require('node:path');const mf = require('./module-federation.config');
module.exports = { lazyCompilation: false, output: { path: join(__dirname, '../../dist/apps/rspack-host'), publicPath: 'auto', uniqueName: 'rspack-host', clean: true, }, resolve: { modules: ['node_modules', join(__dirname, '../..')] }, optimization: { runtimeChunk: false, // only if the old config used NxModuleFederationPlugin splitChunks: { cacheGroups: { default: false, common: false } }, }, devServer: { port: 4703, hot: true, static: false, headers: { 'Access-Control-Allow-Origin': '*' }, historyApiFallback: { index: '/index.html', disableDotRule: true }, }, plugins: [ new NxAppRspackPlugin({ // ... your existing options, minus outputPath runtimeChunk: false, commonChunk: false, }), new NxReactRspackPlugin(), new ModuleFederationPlugin(mf), ],};const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');const { ModuleFederationPlugin,} = require('@module-federation/enhanced/webpack');const { join } = require('node:path');const mf = require('./module-federation.config');
module.exports = { output: { path: join(__dirname, '../../dist/apps/webpack-host'), publicPath: 'auto', uniqueName: 'webpack-host', scriptType: 'text/javascript', clean: true, }, resolve: { modules: ['node_modules', join(__dirname, '../..')] }, optimization: { runtimeChunk: false }, devServer: { port: 4700, hot: true, static: false, headers: { 'Access-Control-Allow-Origin': '*' }, historyApiFallback: { index: '/index.html', disableDotRule: true }, }, plugins: [ new NxAppWebpackPlugin({ // ... your existing options, minus outputPath runtimeChunk: false, commonChunk: false, }), new NxReactWebpackPlugin(), new ModuleFederationPlugin(mf), ],};The Nx dev server also derived devMiddleware.publicPath from baseHref, mapped publicHost to client.webSocketURL, and passed proxyConfig, allowedHosts, and the ssl options through. A workspace that served under a sub-path, behind a proxy, or over HTTPS carries each of those into devServer by hand.
Step 7: update the targets
Section titled “Step 7: update the targets”The official plugin federates the build. It does not coordinate processes, so the host's serve no longer starts its remotes and you wire up the set you want.
Point build and serve at the bundler CLI, with NODE_ENV standing in for the named configurations the executor used to pass:
{ "targets": { "build": { "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/dist/apps/rspack-host"], "defaultConfiguration": "production", "options": { "command": "rspack build --config rspack.config.js", "cwd": "apps/rspack-host", }, "configurations": { "production": { "env": { "NODE_ENV": "production" } }, "development": { "env": { "NODE_ENV": "development" } }, }, }, "serve": { "executor": "nx:run-commands", "continuous": true, "defaultConfiguration": "development", "options": { "command": "rspack serve --config rspack.config.js", "cwd": "apps/rspack-host", }, "configurations": { "development": { "env": { "NODE_ENV": "development" } }, "production": { "env": { "NODE_ENV": "production" } }, }, }, "serve-static": { "executor": "@nx/web:file-server", "continuous": true, "dependsOn": ["build"], "options": { "staticFilePath": "dist/apps/rspack-host", "port": 4703, "spa": true, "watch": false, }, }, },}On webpack, the commands are webpack build --config webpack.config.js and webpack serve --config webpack.config.js.
With a runtime manifest from Step 5, you serve the host on its own and add remotes as you need them, live or from their last build:
nx serve rspack-hostnx run-many -t serve-static -p rspackRemoteA rspackRemoteBOne behavior change to expect: the old executor set NODE_ENV=production for every configuration, so a development build that now runs with NODE_ENV=development gets development-mode output such as react/jsx-dev-runtime, which your shared map does not list because the baseline stats came from a production build. A development serve therefore shares slightly less than the old executor did. Set NODE_ENV=production on both configurations if you want the old output back.
serve-static serves the output with staticFilePath and leaves buildTarget unset, since the file server rebuilds the app itself whenever that option is present, which duplicates the dependsOn build and can fail with Recursive task invocation detected from inside an e2e run that already built it. Keep spa: true on every app, or a refresh on a deep route returns a 404.
Point an e2e project's web server command at nx run-many -t serve-static -p <host> <remotes>, with its URL on the host's serve-static port.
Finally, remove the targetDefaults entries in nx.json keyed on @nx/rspack:rspack or @nx/webpack:webpack. The NX_MF_DEV_REMOTES input they carried is dead. Move anything else they held, such as cache, inputs, and dependsOn, onto the generic build default. Target defaults also take a filter, so settings that applied to one executor can apply to a set of projects instead:
{ "targetDefaults": { "build": [ { "filter": { "projects": ["apps/*", "!apps/legacy"] }, "cache": true, "inputs": ["production", "^production"], }, ], },}See the task pipeline reference for the full syntax.
Step 8: validate the result
Section titled “Step 8: validate the result”A green build is not the acceptance criterion. The migration is done when everything you recorded in the baseline works again, plus these:
- Production and development configurations both build with
--skipNxCache, production last, since both write the samemf-stats.json. tsc -p apps/<app>/tsconfig.app.json --noEmitpasses for each app. The app plugin type-checks asynchronously, so a build exits 0 on type errors.- Each app's production
mf-stats.jsonlists the same container name, exposes, andsharedentries as the baseline. A host you moved to a runtime manifest has an empty build-timeremotesby design, so check its manifest against the baseline URLs instead. - The e2e suite passes against the replacement web server.
Record baseline failures separately rather than weakening an assertion to get a pass.
Step 9: clean up the old setup
Section titled “Step 9: clean up the old setup”Once the new setup builds and runs, delete the files it replaced: module-federation.config.ts, rspack.config.ts or webpack.config.ts, and any *.config.prod.*. Leaving them in place keeps the graph reading the old values.
Then remove the package, once no config or source file references the removed APIs:
npm remove @nx/module-federation@nx/rspack on v23 depends on @nx/module-federation, so on an Rspack workspace it stays in node_modules and its absence from package.json proves nothing. @nx/react lists it as an optional peer, so on a webpack-only workspace npm remove takes it out. Keep @module-federation/enhanced as a direct dependency and commit the lockfile.
What the official plugins do not do
Section titled “What the official plugins do not do”--devRemotesselection. Which remotes run live and which serve from a build is now the set of targets you start.- Runtime library control.
withModuleFederationattached a runtime plugin whenever the dev-server executor setNX_MF_DEV_REMOTES, so a live remote's copy of a shared library won over a static one. Nothing sets that variable now. - Automatic remote fallback. A host whose remote is unavailable fails the request, so add your own error boundary if you need one.
For anything beyond this migration, such as runtime plugins, promise-based remotes, or cross-version deployments, use the official configuration reference and runtime documentation.