@nx/module-federation 24.0.0-beta.2 → 24.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +1 -3
  2. package/dist/angular.d.ts +1 -5
  3. package/dist/angular.js +7 -6
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +0 -2
  6. package/dist/rspack.d.ts +1 -5
  7. package/dist/rspack.js +7 -6
  8. package/dist/src/migrations/update-24-0-0/inventory.d.ts +11 -0
  9. package/dist/src/migrations/update-24-0-0/inventory.js +139 -0
  10. package/dist/src/migrations/update-24-0-0/migrate-angular-module-federation.d.ts +5 -0
  11. package/dist/src/migrations/update-24-0-0/migrate-angular-module-federation.js +18 -0
  12. package/dist/src/migrations/update-24-0-0/migrate-module-federation.d.ts +10 -0
  13. package/dist/src/migrations/update-24-0-0/migrate-module-federation.js +26 -0
  14. package/dist/src/migrations/update-24-0-0/migrate-module-federation.md +1931 -0
  15. package/dist/src/migrations/update-24-0-0/migrate-react-module-federation.d.ts +5 -0
  16. package/dist/src/migrations/update-24-0-0/migrate-react-module-federation.js +18 -0
  17. package/dist/src/stubs/models.d.ts +26 -0
  18. package/dist/src/stubs/stubs.d.ts +19 -0
  19. package/dist/src/stubs/stubs.js +43 -0
  20. package/dist/webpack.d.ts +1 -2
  21. package/dist/webpack.js +4 -3
  22. package/migrations.json +6 -0
  23. package/package.json +5 -60
  24. package/readme-template.md +1 -3
  25. package/dist/internal.d.ts +0 -2
  26. package/dist/internal.js +0 -13
  27. package/dist/src/executors/utils/build-static-remotes.d.ts +0 -4
  28. package/dist/src/executors/utils/build-static-remotes.js +0 -67
  29. package/dist/src/executors/utils/index.d.ts +0 -4
  30. package/dist/src/executors/utils/index.js +0 -6
  31. package/dist/src/executors/utils/models.d.ts +0 -30
  32. package/dist/src/executors/utils/start-remote-iterators.d.ts +0 -25
  33. package/dist/src/executors/utils/start-remote-iterators.js +0 -52
  34. package/dist/src/executors/utils/start-static-remotes-file-server.d.ts +0 -15
  35. package/dist/src/executors/utils/start-static-remotes-file-server.js +0 -61
  36. package/dist/src/plugins/models/index.d.ts +0 -14
  37. package/dist/src/plugins/models/index.js +0 -2
  38. package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-dev-server-plugin.d.ts +0 -13
  39. package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-dev-server-plugin.js +0 -75
  40. package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-plugin.d.ts +0 -11
  41. package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-plugin.js +0 -93
  42. package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-ssr-dev-server-plugin.d.ts +0 -15
  43. package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-ssr-dev-server-plugin.js +0 -107
  44. package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-dev-server-plugin.d.ts +0 -13
  45. package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-dev-server-plugin.js +0 -75
  46. package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-plugin.d.ts +0 -11
  47. package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-plugin.js +0 -80
  48. package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-ssr-dev-server-plugin.d.ts +0 -15
  49. package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-ssr-dev-server-plugin.js +0 -107
  50. package/dist/src/plugins/utils/build-static-remotes.d.ts +0 -3
  51. package/dist/src/plugins/utils/build-static-remotes.js +0 -65
  52. package/dist/src/plugins/utils/get-dynamic-manifest-file.d.ts +0 -2
  53. package/dist/src/plugins/utils/get-dynamic-manifest-file.js +0 -18
  54. package/dist/src/plugins/utils/get-remotes.d.ts +0 -7
  55. package/dist/src/plugins/utils/get-remotes.js +0 -68
  56. package/dist/src/plugins/utils/get-static-remotes.d.ts +0 -3
  57. package/dist/src/plugins/utils/get-static-remotes.js +0 -27
  58. package/dist/src/plugins/utils/index.d.ts +0 -7
  59. package/dist/src/plugins/utils/index.js +0 -10
  60. package/dist/src/plugins/utils/parse-remotes-config.d.ts +0 -6
  61. package/dist/src/plugins/utils/parse-remotes-config.js +0 -35
  62. package/dist/src/plugins/utils/start-remote-proxies.d.ts +0 -5
  63. package/dist/src/plugins/utils/start-remote-proxies.js +0 -65
  64. package/dist/src/plugins/utils/start-static-remotes-file-server.d.ts +0 -2
  65. package/dist/src/plugins/utils/start-static-remotes-file-server.js +0 -46
  66. package/dist/src/utils/dependencies.d.ts +0 -6
  67. package/dist/src/utils/dependencies.js +0 -70
  68. package/dist/src/utils/framework-detection.d.ts +0 -2
  69. package/dist/src/utils/framework-detection.js +0 -20
  70. package/dist/src/utils/get-remotes-for-host.d.ts +0 -17
  71. package/dist/src/utils/get-remotes-for-host.js +0 -154
  72. package/dist/src/utils/index.d.ts +0 -11
  73. package/dist/src/utils/index.js +0 -14
  74. package/dist/src/utils/is-serve-mode.d.ts +0 -2
  75. package/dist/src/utils/is-serve-mode.js +0 -19
  76. package/dist/src/utils/models/index.d.ts +0 -47
  77. package/dist/src/utils/models/index.js +0 -2
  78. package/dist/src/utils/module-federation-config.d.ts +0 -57
  79. package/dist/src/utils/module-federation-config.js +0 -144
  80. package/dist/src/utils/normalize-project-name.d.ts +0 -1
  81. package/dist/src/utils/normalize-project-name.js +0 -10
  82. package/dist/src/utils/package-json.d.ts +0 -2
  83. package/dist/src/utils/package-json.js +0 -12
  84. package/dist/src/utils/parse-static-remotes-config.d.ts +0 -13
  85. package/dist/src/utils/parse-static-remotes-config.js +0 -62
  86. package/dist/src/utils/plugins/runtime-library-control.plugin.d.ts +0 -3
  87. package/dist/src/utils/plugins/runtime-library-control.plugin.js +0 -54
  88. package/dist/src/utils/port-utils.d.ts +0 -7
  89. package/dist/src/utils/port-utils.js +0 -22
  90. package/dist/src/utils/public-api.d.ts +0 -7
  91. package/dist/src/utils/public-api.js +0 -18
  92. package/dist/src/utils/remotes.d.ts +0 -19
  93. package/dist/src/utils/remotes.js +0 -75
  94. package/dist/src/utils/rspack-version.d.ts +0 -3
  95. package/dist/src/utils/rspack-version.js +0 -11
  96. package/dist/src/utils/secondary-entry-points.d.ts +0 -12
  97. package/dist/src/utils/secondary-entry-points.js +0 -125
  98. package/dist/src/utils/share.d.ts +0 -58
  99. package/dist/src/utils/share.js +0 -334
  100. package/dist/src/utils/start-remote-proxies.d.ts +0 -5
  101. package/dist/src/utils/start-remote-proxies.js +0 -55
  102. package/dist/src/utils/start-ssr-remote-proxies.d.ts +0 -5
  103. package/dist/src/utils/start-ssr-remote-proxies.js +0 -69
  104. package/dist/src/utils/typescript.d.ts +0 -4
  105. package/dist/src/utils/typescript.js +0 -53
  106. package/dist/src/utils/url-helpers.d.ts +0 -13
  107. package/dist/src/utils/url-helpers.js +0 -70
  108. package/dist/src/with-module-federation/angular/utils.d.ts +0 -28
  109. package/dist/src/with-module-federation/angular/utils.js +0 -83
  110. package/dist/src/with-module-federation/angular/with-module-federation-ssr.d.ts +0 -2
  111. package/dist/src/with-module-federation/angular/with-module-federation-ssr.js +0 -86
  112. package/dist/src/with-module-federation/angular/with-module-federation.d.ts +0 -2
  113. package/dist/src/with-module-federation/angular/with-module-federation.js +0 -85
  114. package/dist/src/with-module-federation/react/utils.d.ts +0 -2
  115. package/dist/src/with-module-federation/react/utils.js +0 -17
  116. package/dist/src/with-module-federation/rspack/utils.d.ts +0 -10
  117. package/dist/src/with-module-federation/rspack/utils.js +0 -32
  118. package/dist/src/with-module-federation/rspack/with-module-federation-ssr.d.ts +0 -4
  119. package/dist/src/with-module-federation/rspack/with-module-federation-ssr.js +0 -75
  120. package/dist/src/with-module-federation/rspack/with-module-federation.d.ts +0 -9
  121. package/dist/src/with-module-federation/rspack/with-module-federation.js +0 -82
  122. package/dist/src/with-module-federation/webpack/utils.d.ts +0 -10
  123. package/dist/src/with-module-federation/webpack/utils.js +0 -32
  124. package/dist/src/with-module-federation/webpack/with-module-federation-ssr.d.ts +0 -2
  125. package/dist/src/with-module-federation/webpack/with-module-federation-ssr.js +0 -69
  126. package/dist/src/with-module-federation/webpack/with-module-federation.d.ts +0 -5
  127. package/dist/src/with-module-federation/webpack/with-module-federation.js +0 -72
  128. package/dist/url-helpers.d.ts +0 -13
  129. package/dist/url-helpers.js +0 -78
  130. package/executors.json +0 -4
  131. /package/dist/src/{executors/utils → stubs}/models.js +0 -0
@@ -0,0 +1,1931 @@
1
+ # Migrate apps off Nx Module Federation
2
+
3
+ Nx v24 removed Nx Module Federation. `@nx/module-federation` now only ships stubs that keep
4
+ old configs loadable while Nx builds the project graph. They do not configure Module Federation,
5
+ and the Nx MF executors, generators, and runtime helpers are gone. Migrate every project listed in
6
+ the migration's agent context to the official Module Federation plugins, keeping each app's bundler.
7
+
8
+ ## Before you edit
9
+
10
+ - The workspace is already on Nx v24, so the old setup no longer builds here. To capture the
11
+ baseline the steps below ask for, check out the commit before this upgrade in a separate git
12
+ worktree, install with its lockfile, and build there. Do not downgrade this workspace. If you
13
+ cannot build a baseline, say so in your report instead of calling the comparison a pass.
14
+ - Work only on the projects the agent context lists, plus any file they import from.
15
+ - The agent context groups projects by framework. Follow the React guide below for React projects
16
+ and the Angular guide for Angular projects, and skip a guide when no project needs it. The React
17
+ guide's instruction to stop on Angular apps does not apply here.
18
+ - Remove `@nx/module-federation` from `package.json` only after no file in the workspace imports
19
+ it and every migrated app, React and Angular, builds.
20
+
21
+ # Migrate React apps off Nx Module Federation
22
+
23
+ ## Rules
24
+
25
+ Migrate this workspace off the Nx Module Federation APIs that Nx v24 removes, keeping each app's current bundler. Follow every step at https://nx.dev/docs/kb/migrate-from-nx-module-federation in order. Scope is React on Rspack and webpack. Stop and report if you find Angular apps.
26
+
27
+ Rules that override any shortcut you are tempted to take:
28
+
29
+ 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. If a host build fails with `Cannot find remote "<kebab-name>"`, rename the generated remote entries to the project names first. If webpack production builds fail with `Path variable [contenthash:20] not implemented`, pin webpack to 5.110 or earlier.
30
+ 2. Replace `withModuleFederation` and `NxModuleFederationPlugin` with `ModuleFederationPlugin` from `@module-federation/enhanced/rspack` or `/webpack`, and drop `NxModuleFederationDevServerPlugin`. Keep `NxAppRspackPlugin`, `NxAppWebpackPlugin`, `NxReactRspackPlugin`, `NxReactWebpackPlugin`. Do not switch bundlers or regenerate apps.
31
+ 3. Set `name` to the project name with every character outside `[a-zA-Z0-9_$]` replaced by `_`, plus `filename: 'remoteEntry.js'`, `dts: false`, and `remoteType: 'script'` on webpack. Drop any `library` entry.
32
+ 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`, the stats value if it has no `package.json`. A workspace library imported by more than one app but missing from the stats was never shared. Sharing it now changes behavior. Report it.
33
+ 5. Move a static host to a runtime manifest: `registerRemotes` with `type: 'global'` and full `remoteEntry.js` URLs, `loadRemote('<container-name>/<exposed>')` at the call sites (map a named export to `default`), and `implicitDependencies` on the host. A host that already calls `registerRemotes` is done as it is, including `mf-manifest.json` entries with no `type`. Keep static `remotes` entries only if the user asks to stay static.
34
+ 6. Restore the bundler settings the wrapper used to set: `output.uniqueName`, `output.publicPath: 'auto'`, `output.clean: true`, app-plugin `commonChunk: false`, `devServer.hot: true` with an `Access-Control-Allow-Origin` header, `lazyCompilation: false` on Rspack, and `output.scriptType: 'text/javascript'` on webpack. `optimization.runtimeChunk` and the app-plugin `runtimeChunk` are `false` on remotes and on host production builds, and `'single'` (app-plugin `true`) on host development builds. Keep `extractLicenses` on for production only. Delete the old `module-federation.config.ts`, `rspack.config.ts` or `webpack.config.ts`, and `*.config.prod.*` in the same step you write the `.js` replacements.
35
+ 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`. Point e2e web servers at `nx run-many -t serve-static -p <host> <remotes>`. For `@nx/cypress:cypress`, point `devServerTarget` at a continuous `nx:run-commands` target on the e2e project that runs that command with a `readyWhen` per app, and remove the e2e target's `port` option.
36
+ 8. Remove `@nx/module-federation` only after the new setup builds and runs. On v23, `@nx/rspack` depends on it, so on Rspack rebuild uncached with the `node_modules` copy renamed aside, then put it back.
37
+ 9. Do not claim hot-update parity, shared-state parity, or deployment-path correctness from a green build. Serve one remote alone with `nx serve <remote>`, and check that the host boots and that remote's route renders. Run the development flow with at least one remote served statically, and edit a host file and a live remote file to check hot updates. Report what you verified and what you did not.
38
+ 10. For apps with a server build (a `server` target or a `*.server.config.*` file), also follow https://nx.dev/docs/kb/migrate-react-module-federation-ssr.
39
+
40
+ Nx v23 deprecates the Module Federation wrappers, runtime helpers, dev-server executors, and
41
+ generators that `@nx/module-federation`, `@nx/react`, `@nx/rspack`, and `@nx/angular` ship today.
42
+ These APIs wrap the official [Module Federation](https://module-federation.io) plugins. Nx v24
43
+ removes the wrappers, so migrate your apps to the official plugins directly and keep each app on
44
+ its current bundler.
45
+
46
+ Migrate before you upgrade to v24, while the old setup still builds and you can compare against
47
+ it. Nx will also ship migrations for this later.
48
+
49
+ > **Scope:** React apps on Rspack or webpack, keeping the same bundler. Server-side rendered apps follow these
50
+ > steps for the browser build, then
51
+ > [migrate server-side rendered apps](https://nx.dev/docs/kb/migrate-react-module-federation-ssr). For Angular, see
52
+ > [migrate Angular Module Federation](https://nx.dev/docs/kb/migrate-angular-module-federation).
53
+
54
+ ## What replaces what
55
+
56
+ | Going away in v24 | Replacement |
57
+ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
58
+ | `withModuleFederation` from `@nx/module-federation/rspack` | `ModuleFederationPlugin` from `@module-federation/enhanced/rspack` |
59
+ | `withModuleFederation` from `@nx/module-federation/webpack` | `ModuleFederationPlugin` from `@module-federation/enhanced/webpack` |
60
+ | `NxModuleFederationPlugin` from `@nx/module-federation/rspack` | `ModuleFederationPlugin` from `@module-federation/enhanced/rspack` |
61
+ | `setRemoteDefinitions`, `setRemoteDefinition` from `@nx/react/mf` | `registerRemotes` from `@module-federation/enhanced/runtime` |
62
+ | `loadRemoteModule` from `@nx/react/mf` | `loadRemote` from `@module-federation/enhanced/runtime` |
63
+ | `@nx/react:module-federation-dev-server` and the Rspack equivalent | `nx:run-commands` running your bundler dev server |
64
+ | `NxModuleFederationDevServerPlugin` | Not supported |
65
+ | `@nx/react:module-federation-static-server` and the Rspack equivalent | Not supported |
66
+ | `@nx/react/module-federation` and `@nx/rspack/module-federation` re-exports | Same as the `@nx/module-federation` entry they re-export |
67
+ | `sharePackages`, `shareWorkspaceLibraries`, `mapRemotes` from `@nx/module-federation` | Not supported. Write the resolved values out, Step 4 |
68
+
69
+ The two unsupported entries ran processes, not builds. `NxModuleFederationDevServerPlugin` is the
70
+ Rspack form of the dev-server executor, which started the host and every remote from one
71
+ `nx serve`. `@nx/react:module-federation-static-server` served the remotes you weren't working on
72
+ from a single port. Now you start the processes you want, and the runtime manifest from Step 5
73
+ lets the host boot without them.
74
+
75
+ Keep `NxAppRspackPlugin`, `NxAppWebpackPlugin`, `NxReactRspackPlugin`, and
76
+ `NxReactWebpackPlugin`. They are general bundler plugins and are not part of this removal.
77
+
78
+ > **The other route:** The `@nx/react:consumer` and `@nx/react:provider` generators create a new setup on Vite, Rsbuild,
79
+ > or Rspack, covered in [consumer and provider](https://nx.dev/docs/kb/consumer-and-provider). They generate
80
+ > new apps. Use them for a greenfield app or as a reference. For a workspace you already ship,
81
+ > update the configs in place with the steps below.
82
+
83
+ ## Capture a baseline first
84
+
85
+ A remote can render correctly while the host and the remote hold separate copies of a shared
86
+ library. Write down what works now:
87
+
88
+ - Serve the host and its remotes, and visit the routes that load each remote.
89
+ - Edit a file in the host and in a remote. Both should refresh the app.
90
+ - Run a production build and note the artifacts you depend on, such as each remote's
91
+ `remoteEntry.js`. Chunk names and order will change, so reproduce the federation config rather
92
+ than the exact output.
93
+ - Copy each `dist/apps/<app>/mf-stats.json` out of `dist` before you rebuild. Steps 3 and 4
94
+ reproduce its `shared` map, `exposes`, and container name.
95
+ - Note each app's `serve` port for the remote URLs in Step 5.
96
+
97
+ ## Step 1: inventory the workspace
98
+
99
+ Search for every consumer before you edit one:
100
+
101
+ ```shell
102
+ grep -rE "@nx/module-federation|@nx/react/module-federation|@nx/rspack/module-federation|@nx/react/mf|module-federation-dev-server|module-federation-static-server|ssr-dev-server|withModuleFederationForSSR|NxModuleFederationSSRDevServerPlugin|NX_MF_DEV_REMOTES" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" --exclude-dir=node_modules --exclude-dir=.nx --exclude-dir=dist .
103
+ ```
104
+
105
+ On Windows:
106
+
107
+ ```powershell
108
+ Get-ChildItem -Recurse -File -Include *.ts,*.tsx,*.js,*.json |
109
+ Where-Object FullName -notmatch 'node_modules|dist|\.nx' |
110
+ 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|ssr-dev-server|withModuleFederationForSSR|NxModuleFederationSSRDevServerPlugin|NX_MF_DEV_REMOTES'
111
+ ```
112
+
113
+ Record, per app, whether it exposes modules, consumes them, or does both, along with its
114
+ production remote URLs, custom upstream options, and any runtime plugins.
115
+
116
+ Apps with a `server` target or a `*.server.config.*` file are server-side rendered. Migrate their
117
+ browser build here, then their server build with
118
+ [migrate server-side rendered apps](https://nx.dev/docs/kb/migrate-react-module-federation-ssr). Angular apps
119
+ follow [migrate Angular Module Federation](https://nx.dev/docs/kb/migrate-angular-module-federation) instead.
120
+
121
+ ## Step 2: install the official packages
122
+
123
+ Workspaces created by the `host` generator already list `@module-federation/enhanced`. Application
124
+ code imports its runtime, so move it to `dependencies` if it sits in `devDependencies`, then
125
+ reinstall to update the lockfile. If it's missing, install it:
126
+
127
+ ```shell
128
+ npm add --save-prod @module-federation/enhanced
129
+ ```
130
+
131
+ Step 7 calls the bundler CLI. Rspack workspaces already declare `@rspack/cli`, but webpack
132
+ workspaces that only built through the Nx executor usually lack `webpack-cli`:
133
+
134
+ ```shell
135
+ npm add -D webpack-cli
136
+ ```
137
+
138
+ Use `pnpm add`, `yarn add`, or `bun add` in place of `npm add` throughout. On pnpm, pass `-P`
139
+ to move an existing devDependency to `dependencies`.
140
+
141
+ ## Step 3: translate the federation config
142
+
143
+ Nx resolved each remote project name to a URL from the project graph. The official plugin needs
144
+ explicit values.
145
+
146
+ Rspack apps from the Nx 23 `host` and `remote` generators use a plain `rspack.config.ts` with
147
+ `NxModuleFederationPlugin` and `NxModuleFederationDevServerPlugin` on the inferred
148
+ `@nx/rspack/plugin` targets. Translate the `config` passed to `NxModuleFederationPlugin` and drop
149
+ the dev-server plugin. These apps have no build-target options to move in Step 6. Ignore the
150
+ generated `rspack.config.prod.ts`, which the inferred build never loads.
151
+
152
+ ### Container name and import alias
153
+
154
+ The container name is normalized: every character outside `[a-zA-Z0-9_$]` becomes `_`, as does
155
+ a leading character that can't start an identifier, so `webpack-host` publishes as
156
+ `webpack_host`. Use that form for `name`.
157
+
158
+ The import alias stays the original project name. The key in `remotes` is the alias, and
159
+ `webpackRemoteA/Module` keeps working.
160
+
161
+ ### Options Nx passed for you
162
+
163
+ Nx set `filename` to `remoteEntry.js`, set `remoteType` to `script` on webpack, and passed
164
+ through the `dts: false` override from your `withModuleFederation` call. Set all three yourself.
165
+ Without `dts: false`, every remote build logs a type-generation error.
166
+
167
+ Drop any `library` entry from the browser config. Nx never passed it to the plugin, and the
168
+ generated `{ type: 'var', name: '<project-name>' }` publishes an un-normalized global. On Rspack,
169
+ a `var` or `window` library type also turned on `remoteType: 'script'` and
170
+ `output.scriptType: 'text/javascript'`. Set both by hand if your Rspack config had one.
171
+
172
+ ### Remote URLs
173
+
174
+ List only direct remotes. A remote that also consumes remotes keeps its `exposes` and gets a
175
+ `remotes` map too.
176
+
177
+ Generated apps also carry a `webpack.config.prod.*` file wired through
178
+ `build.configurations.production.webpackConfig`, holding tuples such as
179
+ `['webpackRemoteA', 'http://localhost:4701/']`. Nx appended `remoteEntry.js` to any tuple URL
180
+ without a filename, with or without a trailing slash. Fold both sets into one config keyed on
181
+ `NODE_ENV`, with exactly one slash before `remoteEntry.js`.
182
+
183
+ **Before:**
184
+
185
+ ```ts
186
+ // apps/webpack-host/module-federation.config.ts
187
+ import type { ModuleFederationConfig } from '@nx/module-federation';
188
+
189
+ const config: ModuleFederationConfig = {
190
+ name: 'webpack-host',
191
+ remotes: ['webpackRemoteA', 'webpackRemoteB'],
192
+ };
193
+
194
+ export default config;
195
+ ```
196
+
197
+ **After:**
198
+
199
+ ```js
200
+ // apps/webpack-host/module-federation.config.js
201
+ const isProd = process.env.NODE_ENV === 'production';
202
+
203
+ module.exports = {
204
+ name: 'webpack_host', // normalized
205
+ filename: 'remoteEntry.js',
206
+ dts: false,
207
+ remoteType: 'script', // webpack only, Rspack defaults to script
208
+ remotes: {
209
+ webpackRemoteA: isProd
210
+ ? 'webpackRemoteA@https://remote-a.example.com/remoteEntry.js'
211
+ : 'webpackRemoteA@http://localhost:4701/remoteEntry.js',
212
+ webpackRemoteB: isProd
213
+ ? 'webpackRemoteB@https://remote-b.example.com/remoteEntry.js'
214
+ : 'webpackRemoteB@http://localhost:4702/remoteEntry.js',
215
+ },
216
+ shared: {
217
+ // ... see Step 4
218
+ },
219
+ };
220
+ ```
221
+
222
+ Remote configs keep their `exposes` map unchanged. A top-level host needs these URLs only until
223
+ Step 5 moves them to a runtime manifest. A remote that consumes other remotes keeps its map.
224
+
225
+ > **JavaScript config files:** The samples use CommonJS `.js`, which both CLIs load without a TypeScript loader. Recent Node
226
+ > versions run TypeScript configs directly, so you can rename them back to `.ts` once the migration
227
+ > works.
228
+
229
+ ## Step 4: write the shared map
230
+
231
+ Nx built `shared` from the project graph. It covered framework packages, their secondary entry
232
+ points, every workspace library the app imports, and those libraries' npm dependencies. The
233
+ official plugin shares only what you list.
234
+
235
+ Copy the values from the baseline `mf-stats.json`, including `singleton`, `strictVersion`,
236
+ `requiredVersion`, and `eager` per entry:
237
+
238
+ ```js
239
+ // apps/webpack-host/module-federation.config.js
240
+ module.exports = {
241
+ // ... name, filename, dts, remotes
242
+ shared: {
243
+ '@myorg/state': {
244
+ singleton: true,
245
+ requiredVersion: false,
246
+ version: '1.0.0',
247
+ },
248
+ react: {
249
+ singleton: true,
250
+ strictVersion: true,
251
+ requiredVersion: '^19.0.0',
252
+ eager: true,
253
+ },
254
+ 'react-dom': {
255
+ singleton: true,
256
+ strictVersion: true,
257
+ requiredVersion: '^19.0.0',
258
+ eager: true,
259
+ },
260
+ 'react-dom/client': {
261
+ singleton: true,
262
+ strictVersion: true,
263
+ requiredVersion: '^19.0.0',
264
+ },
265
+ 'react/jsx-runtime': {
266
+ singleton: true,
267
+ strictVersion: true,
268
+ requiredVersion: '^19.0.0',
269
+ },
270
+ },
271
+ };
272
+ ```
273
+
274
+ Check these against the stats.
275
+
276
+ - **Secondary entry points are separate keys.** `pkg` does not cover `pkg/subpath`. List the
277
+ subpaths the stats show.
278
+ - **`additionalShared` entries set their own values.** The same package can appear with a
279
+ different range and no `strictVersion`, so copy each entry as it is and drop any the stats don't
280
+ list.
281
+ - **The stats rewrite one field.** A callback that set `requiredVersion: false` shows up as
282
+ `requiredVersion: '^<version>'`. For a workspace library, keep the `false` and your callback's
283
+ `version`.
284
+ - **A source-only workspace library needs an explicit `version`.** `singleton: true` alone did not
285
+ give host and remotes one copy in testing. Use the library's declared version or one your team
286
+ agrees on, and never give two implementations the same version. A library with no
287
+ `package.json` shows `0.0.0` in the stats. Copy that.
288
+
289
+ Nx found workspace libraries only through `tsconfig.base.json` paths. In a workspace linked
290
+ through package manager workspaces, it never shared them, and each app bundled its own copy. If a
291
+ library that several apps import is missing from the stats, sharing it now changes behavior. Give
292
+ it an explicit `version` and note the change.
293
+
294
+ > **Imports have to use the library's package name:** A `shared` key matches the request string. A library imported by a relative path into its
295
+ > source resolves to a separate copy. Nx rewrote those requests, and the official plugin does not.
296
+
297
+ Replace each call to `sharePackages`, `shareWorkspaceLibraries`, or `mapRemotes` with the values
298
+ it returned. The stats list them.
299
+
300
+ ```js
301
+ // before: shared: sharePackages(['react', 'react-dom'])
302
+ // after:
303
+ const sharedNpm = (names, version) =>
304
+ Object.fromEntries(
305
+ names.map((name) => [
306
+ name,
307
+ { singleton: true, strictVersion: true, requiredVersion: version },
308
+ ])
309
+ );
310
+
311
+ module.exports = {
312
+ // ...
313
+ shared: sharedNpm(['react', 'react-dom'], '^19.0.0'),
314
+ };
315
+ ```
316
+
317
+ ## Step 5: move remotes to a runtime manifest
318
+
319
+ A runtime manifest holds the remote URLs in a file the host fetches at startup. You can change
320
+ a URL without rebuilding, and the host boots even when a remote is down. It's the upstream
321
+ default, described in the [manifest reference](https://module-federation.io/configure/manifest),
322
+ and it replaces the static server this migration removes.
323
+
324
+ Register the remotes before the async boundary that imports your bootstrap, and translate
325
+ `loadRemoteModule(name, './Module')` into `loadRemote(name + '/Module')`.
326
+
327
+ **Before:**
328
+
329
+ ```tsx
330
+ // apps/webpack-host/src/app/app.tsx
331
+ const RemoteA = React.lazy(() => import('webpackRemoteA/Module'));
332
+ ```
333
+
334
+ **After:**
335
+
336
+ ```ts
337
+ // apps/webpack-host/src/main.ts
338
+ import { registerRemotes } from '@module-federation/enhanced/runtime';
339
+
340
+ fetch('/assets/module-federation.manifest.json')
341
+ .then((res) => res.json())
342
+ .then((remotes: Record<string, string>) =>
343
+ registerRemotes(
344
+ Object.entries(remotes).map(([name, entry]) => ({
345
+ name,
346
+ entry,
347
+ type: 'global' as const,
348
+ }))
349
+ )
350
+ )
351
+ .then(() => import('./bootstrap'));
352
+ ```
353
+
354
+ ```tsx
355
+ // apps/webpack-host/src/app/app.tsx
356
+ import { loadRemote } from '@module-federation/enhanced/runtime';
357
+
358
+ const RemoteA = React.lazy(
359
+ () =>
360
+ loadRemote<{ default: React.ComponentType }>(
361
+ 'webpackRemoteA/Module'
362
+ ) as Promise<{ default: React.ComponentType }>
363
+ );
364
+
365
+ // a module with a named export, such as one added by federate-module
366
+ const Greeting = React.lazy(() =>
367
+ loadRemote<{ Greeting: React.ComponentType }>('webpackRemoteD/Greeting').then(
368
+ (m) => ({ default: m!.Greeting })
369
+ )
370
+ );
371
+ ```
372
+
373
+ ```json
374
+ // apps/webpack-host/src/assets/module-federation.manifest.json
375
+ {
376
+ "webpackRemoteA": "http://localhost:4701/remoteEntry.js",
377
+ "webpackRemoteB": "http://localhost:4702/remoteEntry.js"
378
+ }
379
+ ```
380
+
381
+ - **Every value is a full entry URL.** `loadRemoteModule` appended `/remoteEntry.mjs` to a bare
382
+ origin such as `http://localhost:4701`. `registerRemotes` uses the string as given. With
383
+ `output.publicPath: 'auto'`, each remote's chunks load from wherever its entry came from. Put a
384
+ deployment path prefix in the manifest you ship, not your local one.
385
+ - **Each key is the remote's normalized container name.** `type: 'global'` makes the runtime read
386
+ the container from `globalThis[name]`, which matches how Nx built these remotes. The key must
387
+ match `name` from Step 3, and it's also the prefix `loadRemote` takes. A project named
388
+ `my-remote` is `my_remote` in the manifest and in `loadRemote('my_remote/Module')`.
389
+ - **The host loses its project-graph edge.** Static `import('<remote>/Module')` calls gave Nx
390
+ that edge. Add `implicitDependencies` on the host so remotes still build first and
391
+ `nx affected` still includes the host. In a package manager workspace, a `workspace:*`
392
+ dependency on each remote gives the same edge. Drop the host's `<remote>/Module` entries from
393
+ `tsconfig.base.json` and any `remotes.d.ts`.
394
+ - **Per-environment URLs move into the manifest.** The `NODE_ENV` ternary from Step 3 has no
395
+ equivalent. Ship the right manifest for each environment, with a `fileReplacements` pair or by
396
+ writing the file at deploy time.
397
+
398
+ Only a top-level host converts. A remote that consumes other remotes keeps its static `remotes`
399
+ map. Hosts load its exposed module, not its `main.ts`, and a registration there never runs.
400
+
401
+ > **Staying with static remotes:** Keep the `remotes` map from Step 3 and the `tsconfig.base.json` paths, and skip this step.
402
+ > Every remote then has to be running before the host boots, so serve them together:
403
+ > `nx run-many -t serve -p webpack-host webpackRemoteA webpackRemoteB`.
404
+
405
+ A host that already calls `init` or `registerRemotes` from the upstream runtime is done. That
406
+ includes the Nx 23 `--dynamic` host, which registers `mf-manifest.json` entries without a `type`.
407
+ The official plugin still emits `mf-manifest.json`. That host never had a graph edge to its
408
+ remotes. Add `implicitDependencies` for them.
409
+
410
+ A host that passes a fetched manifest to `setRemoteDefinitions` still needs the conversion above.
411
+ A host that used `setRemoteUrlResolver` from `@nx/react/mf` keeps its URL logic and passes each
412
+ resolved URL as the `entry` in `registerRemotes`.
413
+
414
+ ## Step 6: swap the wrapper in the bundler config
415
+
416
+ `composePlugins(withNx(), withReact(), withModuleFederation(config))` returns an Nx-specific
417
+ config function that the Rspack and webpack CLIs can't run. Replace it with a standard config
418
+ object that adds `ModuleFederationPlugin`, then move `build` and `serve` to the CLI in Step 7.
419
+
420
+ The options on your old `build` target become `NxAppRspackPlugin` or `NxAppWebpackPlugin`
421
+ options. When you move them:
422
+
423
+ - Paths such as `main`, `index`, `tsConfig`, `assets`, and `styles` resolve from the project
424
+ root. `apps/webpack-host/src/main.ts` becomes `./src/main.ts`.
425
+ - `fileReplacements` paths resolve from the workspace root. Drop a pair whose files do not exist.
426
+ - Values that differed per named configuration become
427
+ `process.env.NODE_ENV === 'production'` ternaries. Step 7 sets `NODE_ENV` per configuration.
428
+ - Drop the generator's per-configuration `optimization`, `outputHashing`, `sourceMap`,
429
+ `namedChunks`, and `vendorChunk` values. The plugin derives them from `NODE_ENV`. It doesn't
430
+ derive `extractLicenses`, so keep that on for production only.
431
+ - Leave no build options on the target. The plugin merges in the running target's `project.json`
432
+ options, and they win.
433
+
434
+ The wrapper also changed these bundler settings, which you now set yourself.
435
+
436
+ | Setting | Value |
437
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
438
+ | `output.uniqueName` | The project name, not the normalized container name |
439
+ | `output.publicPath` | `'auto'` |
440
+ | `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 |
441
+ | `output.scriptType` | `'text/javascript'` on webpack |
442
+ | `resolve.modules` | `node_modules` plus the workspace root, as the wrapper set. `exposes` paths written from the workspace root need it |
443
+ | `optimization.runtimeChunk` | `false` on remotes and host production builds. `'single'` on host development builds, or host edits never re-render. Set the app-plugin `runtimeChunk` to match. It overwrites the top-level value |
444
+ | `commonChunk` | `false` in the app-plugin options. `NxAppWebpackPlugin` defaults it to `true` and adds a `common.<hash>.js` per remote |
445
+ | `splitChunks.cacheGroups` | `default` and `common` set to `false`, only if the old config used `NxModuleFederationPlugin`. `withModuleFederation` did not set them |
446
+ | `lazyCompilation` (Rspack) | `false`. rspack-cli turns it on in dev mode, and federation breaks |
447
+ | `devServer.hot` | `true` on every app, remotes included. Remote edits then update the host without resetting its state |
448
+ | `devServer.headers` | `Access-Control-Allow-Origin`, so a host on one port can fetch `remoteEntry.js` from another |
449
+
450
+ **Rspack:**
451
+
452
+ ```js
453
+ // apps/rspack-host/rspack.config.js
454
+ const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
455
+ const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
456
+ const {
457
+ ModuleFederationPlugin,
458
+ } = require('@module-federation/enhanced/rspack');
459
+ const { join } = require('node:path');
460
+ const mf = require('./module-federation.config');
461
+
462
+ const isProd = process.env.NODE_ENV === 'production';
463
+
464
+ module.exports = {
465
+ lazyCompilation: false,
466
+ output: {
467
+ path: join(__dirname, '../../dist/apps/rspack-host'),
468
+ publicPath: 'auto',
469
+ uniqueName: 'rspack-host',
470
+ clean: true,
471
+ },
472
+ resolve: { modules: ['node_modules', join(__dirname, '../..')] },
473
+ optimization: {
474
+ runtimeChunk: isProd ? false : 'single', // host only, remotes use false
475
+ // only if the old config used NxModuleFederationPlugin
476
+ splitChunks: { cacheGroups: { default: false, common: false } },
477
+ },
478
+ devServer: {
479
+ port: 4703,
480
+ hot: true,
481
+ static: false,
482
+ headers: { 'Access-Control-Allow-Origin': '*' },
483
+ historyApiFallback: { index: '/index.html', disableDotRule: true },
484
+ },
485
+ plugins: [
486
+ new NxAppRspackPlugin({
487
+ // ... your existing options, minus outputPath
488
+ runtimeChunk: !isProd, // host only, remotes use false
489
+ commonChunk: false,
490
+ extractLicenses: isProd,
491
+ }),
492
+ new NxReactRspackPlugin(),
493
+ new ModuleFederationPlugin(mf),
494
+ ],
495
+ };
496
+ ```
497
+
498
+ **webpack:**
499
+
500
+ ```js
501
+ // apps/webpack-host/webpack.config.js
502
+ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
503
+ const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
504
+ const {
505
+ ModuleFederationPlugin,
506
+ } = require('@module-federation/enhanced/webpack');
507
+ const { join } = require('node:path');
508
+ const mf = require('./module-federation.config');
509
+
510
+ const isProd = process.env.NODE_ENV === 'production';
511
+
512
+ module.exports = {
513
+ output: {
514
+ path: join(__dirname, '../../dist/apps/webpack-host'),
515
+ publicPath: 'auto',
516
+ uniqueName: 'webpack-host',
517
+ scriptType: 'text/javascript',
518
+ clean: true,
519
+ },
520
+ resolve: { modules: ['node_modules', join(__dirname, '../..')] },
521
+ optimization: { runtimeChunk: isProd ? false : 'single' }, // host only, remotes use false
522
+ devServer: {
523
+ port: 4700,
524
+ hot: true,
525
+ static: false,
526
+ headers: { 'Access-Control-Allow-Origin': '*' },
527
+ historyApiFallback: { index: '/index.html', disableDotRule: true },
528
+ },
529
+ plugins: [
530
+ new NxAppWebpackPlugin({
531
+ // ... your existing options, minus outputPath
532
+ runtimeChunk: !isProd, // host only, remotes use false
533
+ commonChunk: false,
534
+ extractLicenses: isProd,
535
+ }),
536
+ new NxReactWebpackPlugin(),
537
+ new ModuleFederationPlugin(mf),
538
+ ],
539
+ };
540
+ ```
541
+
542
+ > **These configs still run through Nx:** The app plugins read `NX_TASK_TARGET_PROJECT` and related variables to find the project. The
543
+ > config works under `nx run` and fails under a bare `npx webpack serve` from the app folder.
544
+
545
+ The Nx dev server also derived `devMiddleware.publicPath` from `baseHref`, mapped `publicHost` to
546
+ `client.webSocketURL`, and passed through `proxyConfig`, `allowedHosts`, and the `ssl` options.
547
+ If you served under a sub-path, behind a proxy, or over HTTPS, add those to `devServer` yourself.
548
+
549
+ Delete the replaced files in this same step: `module-federation.config.ts`, `rspack.config.ts` or
550
+ `webpack.config.ts`, and any `*.config.prod.*`. While the old `.ts` config exists, an inferred
551
+ build still loads it, and its `./module-federation.config` import now picks up your new `.js`
552
+ file.
553
+
554
+ ## Step 7: update the targets
555
+
556
+ The official plugin only covers the build. The host's `serve` no longer starts its remotes, so
557
+ you start the ones you need.
558
+
559
+ Point `build` and `serve` at the bundler CLI, with `NODE_ENV` in place of the named
560
+ configurations the executor passed:
561
+
562
+ ```jsonc
563
+ // apps/rspack-host/project.json
564
+ {
565
+ "targets": {
566
+ "build": {
567
+ "executor": "nx:run-commands",
568
+ "outputs": ["{workspaceRoot}/dist/apps/rspack-host"],
569
+ "defaultConfiguration": "production",
570
+ "options": {
571
+ "command": "rspack build --config rspack.config.js",
572
+ "cwd": "apps/rspack-host",
573
+ },
574
+ "configurations": {
575
+ "production": { "env": { "NODE_ENV": "production" } },
576
+ "development": { "env": { "NODE_ENV": "development" } },
577
+ },
578
+ },
579
+ "serve": {
580
+ "executor": "nx:run-commands",
581
+ "continuous": true,
582
+ "defaultConfiguration": "development",
583
+ "options": {
584
+ "command": "rspack serve --config rspack.config.js",
585
+ "cwd": "apps/rspack-host",
586
+ },
587
+ "configurations": {
588
+ "development": { "env": { "NODE_ENV": "development" } },
589
+ "production": { "env": { "NODE_ENV": "production" } },
590
+ },
591
+ },
592
+ "serve-static": {
593
+ "executor": "@nx/web:file-server",
594
+ "continuous": true,
595
+ "dependsOn": ["build"],
596
+ "options": {
597
+ "staticFilePath": "dist/apps/rspack-host",
598
+ "port": 4703,
599
+ "spa": true,
600
+ "watch": false,
601
+ },
602
+ },
603
+ },
604
+ }
605
+ ```
606
+
607
+ On webpack, the commands are `webpack build --config webpack.config.js` and
608
+ `webpack serve --config webpack.config.js`.
609
+
610
+ With a runtime manifest from Step 5, serve the host alone and add remotes as you need them, live
611
+ or from their last build:
612
+
613
+ ```shell
614
+ nx serve rspack-host
615
+ nx run-many -t serve-static -p rspackRemoteA rspackRemoteB
616
+ ```
617
+
618
+ `nx serve rspack-host --devRemotes=rspackRemoteA` becomes one command for the apps you edit and
619
+ one for the rest:
620
+
621
+ ```shell
622
+ nx run-many -t serve -p rspackRemoteA rspack-host
623
+ nx run-many -t serve-static -p rspackRemoteB
624
+ ```
625
+
626
+ When the host runs in development, build the remotes you serve statically with the development
627
+ configuration too, or serve them live. A development host with production-built remotes can render
628
+ a blank page.
629
+
630
+ ```shell
631
+ nx run-many -t build -p rspackRemoteB -c development
632
+ nx run-many -t serve-static -p rspackRemoteB --excludeTaskDependencies
633
+ ```
634
+
635
+ The blank page shows `dispatcher.getOwner is not a function`. The runtime picked the remote's
636
+ production `react` while the host's `react/jsx-dev-runtime` stayed development.
637
+ `shareStrategy: 'loaded-first'` avoids it but stops remote edits from updating the host. Do not
638
+ use it.
639
+
640
+ > **Inferred targets:** If `nx.json` lists `@nx/rspack/plugin` or `@nx/webpack/plugin`, the inferred `build` and `serve`
641
+ > targets work as they are. Only `serve-static` needs the shape above. Nx merges your `project.json`
642
+ > options over the inferred ones key by key, and the inferred `buildTarget` comes along with your
643
+ > `staticFilePath`. Add `"buildTarget": ""` to clear it.
644
+
645
+ Leave `buildTarget` unset on `serve-static`. With it set, the file server rebuilds the app and
646
+ duplicates the `dependsOn` build, which fails inside an e2e run. Keep `spa: true` on every app, or
647
+ a refresh on a deep route returns a 404.
648
+
649
+ Point an e2e project's web server command at `nx run-many -t serve-static -p <host> <remotes>`,
650
+ with its URL on the host's `serve-static` port.
651
+
652
+ An e2e target on the `@nx/cypress:cypress` executor names a `devServerTarget` instead. The app's
653
+ own `serve` starts only the host, and inside the e2e task the app plugin resolves paths against the
654
+ e2e project. Add a continuous target to the e2e project that starts every app, with one `readyWhen`
655
+ line per app, and point `devServerTarget` at it:
656
+
657
+ ```jsonc
658
+ // apps/webpack-host-e2e/project.json
659
+ {
660
+ "targets": {
661
+ "serve-mf": {
662
+ "executor": "nx:run-commands",
663
+ "continuous": true,
664
+ "options": {
665
+ "command": "nx run-many -t serve-static -p webpack-host webpackRemoteA webpackRemoteB",
666
+ "readyWhen": [
667
+ "Unhandled requests will be served from: http://localhost:4700",
668
+ "Unhandled requests will be served from: http://localhost:4701",
669
+ "Unhandled requests will be served from: http://localhost:4702",
670
+ ],
671
+ },
672
+ },
673
+ "e2e": {
674
+ "executor": "@nx/cypress:cypress",
675
+ "options": {
676
+ "cypressConfig": "apps/webpack-host-e2e/cypress.config.ts",
677
+ "devServerTarget": "webpack-host-e2e:serve-mf",
678
+ "testingType": "e2e",
679
+ },
680
+ },
681
+ },
682
+ }
683
+ ```
684
+
685
+ Remove the e2e target's `port` option, which Cypress passes on to the command. Remove the
686
+ `production` and `ci` configurations that pointed at the old targets too.
687
+
688
+ Last, remove the `targetDefaults` entries in `nx.json` keyed on `@nx/rspack:rspack` or
689
+ `@nx/webpack:webpack`. Their `NX_MF_DEV_REMOTES` input no longer does anything. Move their other
690
+ settings, such as `cache`, `inputs`, and `dependsOn`, to the generic `build` default. A `filter`
691
+ scopes them to a set of projects:
692
+
693
+ ```jsonc
694
+ // nx.json
695
+ {
696
+ "targetDefaults": {
697
+ "build": [
698
+ {
699
+ "filter": { "projects": ["apps/*", "!apps/legacy"] },
700
+ "cache": true,
701
+ "inputs": ["production", "^production"],
702
+ },
703
+ ],
704
+ },
705
+ }
706
+ ```
707
+
708
+ See the [task pipeline reference](https://nx.dev/docs/reference/nx-json#task-pipelines) for the full syntax.
709
+
710
+ ## Step 8: validate the result
711
+
712
+ A green build isn't enough. Everything you recorded in the baseline has to work again, along
713
+ with these checks.
714
+
715
+ - Production and development configurations both build with `--skipNxCache`. Build production
716
+ last, since both write the same `mf-stats.json`.
717
+ - `tsc -p apps/<app>/tsconfig.app.json --noEmit` passes for each app. The app plugin type-checks
718
+ asynchronously, and a build exits 0 on type errors.
719
+ - Each app's production `mf-stats.json` lists the same container name, exposes, and `shared`
720
+ entries as the baseline. A host on a runtime manifest has an empty build-time `remotes` by
721
+ design. Check its manifest against the baseline URLs.
722
+ - The e2e suite passes against the replacement web server.
723
+ - With only `nx serve webpackRemoteA` running, the host boots and the `webpackRemoteA` route
724
+ renders.
725
+ - In development, with at least one remote served statically, an edit to a host file and an edit
726
+ to a live remote both update the page without a reload.
727
+
728
+ Record baseline failures separately instead of weakening an assertion to get a pass.
729
+
730
+ ## Step 9: remove the package
731
+
732
+ Once nothing references the removed APIs and the new setup builds and runs, remove the package:
733
+
734
+ ```shell
735
+ npm remove @nx/module-federation
736
+ ```
737
+
738
+ Keep `@module-federation/enhanced` as a direct dependency and commit the lockfile.
739
+
740
+ ## What the official plugins do not do
741
+
742
+ - **`--devRemotes` selection.** The targets you start decide which remotes run live and which
743
+ serve from a build.
744
+ - **Runtime library control.** `withModuleFederation` attached a runtime plugin when the
745
+ dev-server executor set `NX_MF_DEV_REMOTES`. It let a live remote's copy of a shared library win
746
+ over a static one. Nothing sets that variable now.
747
+ - **Automatic remote fallback.** A host fails the request when a remote is unavailable. Add your
748
+ own error boundary if you need one.
749
+
750
+ For anything beyond this migration, such as runtime plugins, promise-based remotes, or
751
+ cross-version deployments, use the official
752
+ [configuration reference](https://module-federation.io/configure/) and
753
+ [runtime documentation](https://module-federation.io/guide/basic/runtime).
754
+
755
+ # Migrate server-side rendered React Module Federation apps
756
+
757
+ ## Rules
758
+
759
+ Migrate the server build of each server-side rendered React Module Federation app in this workspace off the Nx Module Federation APIs that Nx v24 removes. An app with a `server` target or a `*.server.config.*` file is server-side rendered. Migrate its browser build first with https://nx.dev/docs/kb/migrate-from-nx-module-federation, then follow every step at https://nx.dev/docs/kb/migrate-react-module-federation-ssr.
760
+
761
+ Rules that override any shortcut you are tempted to take:
762
+
763
+ 1. Before editing, keep the baseline browser stats from `dist/apps/<app>/browser/mf-stats.json`. Record the server-rendered HTML of each host route that renders a remote.
764
+ 2. Replace `withModuleFederationForSSR` with `ModuleFederationPlugin` from `@module-federation/enhanced/webpack` or `/rspack`, and drop `NxModuleFederationSSRDevServerPlugin`. Keep `@module-federation/node` as a dependency.
765
+ 3. The server container sets `library: { type: 'commonjs-module' }`, `remoteType: 'script'` on both bundlers, `experiments: { asyncStartup: true }`, and `runtimePlugins: [require.resolve('@module-federation/node/runtimePlugin')]`. Its remote URLs keep the `name@` prefix and point at `<origin>/server/remoteEntry.js`.
766
+ 4. Server builds write no stats. Reuse the browser `shared` map, with every entry `eager: true` on Rspack.
767
+ 5. A host on a runtime manifest also calls `registerRemotes` in `server.ts` with the `/server` URLs. On webpack keep `@module-federation/*` inside the server bundle, or the server fails with `#RUNTIME-009`.
768
+ 6. Replace the SSR dev server with watch builds plus `node --watch-path`. A static run is `node dist/apps/<app>/server/server.js`, never `@nx/web:file-server`. On Rspack, run it from `apps/<app>` (`node ../../dist/apps/<app>/server/server.js`), since the generated `server.ts` resolves `dist` from the working directory.
769
+ 7. Accept the migration only when each host route's HTML holds the remote's markup and client scripts, and the page hydrates in a browser without errors. Report what you verified and what you did not.
770
+
771
+ > **Migrate the browser build first:** This guide covers only the server build. Finish
772
+ > [migrating the browser build](https://nx.dev/docs/kb/migrate-from-nx-module-federation) of every app, then
773
+ > come back here.
774
+
775
+ A host or remote generated with `--ssr` builds the same container twice, once for the browser and
776
+ once for Node. The server build needs its own container format, runtime plugin, and serve targets.
777
+
778
+ ## What replaces what
779
+
780
+ | Going away in v24 | Replacement |
781
+ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
782
+ | `withModuleFederationForSSR` from `@nx/module-federation/webpack` or `/rspack` | `ModuleFederationPlugin` with the `@module-federation/node` runtime |
783
+ | `@nx/react:module-federation-ssr-dev-server`, `@nx/webpack:ssr-dev-server`, and `NxModuleFederationSSRDevServerPlugin` | `nx:run-commands` running watch builds and `node --watch-path` |
784
+
785
+ ## Capture the server baseline
786
+
787
+ With `--ssr`, copy the browser stats from `dist/apps/<app>/browser/mf-stats.json`, since server
788
+ builds write none. Also save the HTML each host route returns with `curl` to compare against at
789
+ the end.
790
+
791
+ ## Server container settings
792
+
793
+ The server container is CommonJS and loads remotes over HTTP through the Node runtime plugin.
794
+ Each remote's Express server serves its server build under `/server`, and the host's server-side
795
+ remote URLs point there. Keep that static mount in every remote's `server.ts`.
796
+
797
+ The server config needs:
798
+
799
+ - `library: { type: 'commonjs-module' }`. The browser guide drops `library` from the browser
800
+ config only.
801
+ - `remoteType: 'script'`, on Rspack too.
802
+ - `experiments: { asyncStartup: true }`.
803
+ - `runtimePlugins: [require.resolve('@module-federation/node/runtimePlugin')]`.
804
+ - Remote URLs such as `remoteA@http://localhost:4751/server/remoteEntry.js`, with the `name@`
805
+ prefix.
806
+
807
+ Browser and server builds each need their own production URLs.
808
+
809
+ ## Server federation config
810
+
811
+ ```js
812
+ // apps/shell/module-federation.server.config.js
813
+ const browser = require('./module-federation.config');
814
+
815
+ module.exports = {
816
+ ...browser,
817
+ remotes: {
818
+ remoteA: 'remoteA@http://localhost:4751/server/remoteEntry.js',
819
+ },
820
+ library: { type: 'commonjs-module' },
821
+ remoteType: 'script',
822
+ runtimePlugins: [require.resolve('@module-federation/node/runtimePlugin')],
823
+ experiments: { asyncStartup: true },
824
+ };
825
+ ```
826
+
827
+ The spread reuses the browser `shared` map. On Rspack, set `eager: true` on every entry, as the
828
+ old server config did.
829
+
830
+ ## Server bundler config
831
+
832
+ **Rspack:**
833
+
834
+ The generated `rspack.config.ts` already exports a `[browser, server]` array, with a second
835
+ `NxModuleFederationPlugin({ config, isServer: true })` and an `NxModuleFederationSSRDevServerPlugin`.
836
+ Keep the array in a new `rspack.config.js`. Both compilers get the browser guide's Step 6
837
+ settings, and the server compiler also gets `target: 'async-node'`:
838
+
839
+ ```js
840
+ // apps/shell/rspack.config.js
841
+ const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
842
+ const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
843
+ const {
844
+ ModuleFederationPlugin,
845
+ } = require('@module-federation/enhanced/rspack');
846
+ const { join } = require('node:path');
847
+
848
+ const workspaceRoot = join(__dirname, '../..');
849
+ const shared = {
850
+ resolve: { modules: ['node_modules', workspaceRoot] },
851
+ optimization: {
852
+ runtimeChunk: false,
853
+ splitChunks: { cacheGroups: { default: false, common: false } },
854
+ },
855
+ };
856
+
857
+ const browser = {
858
+ name: 'browser',
859
+ lazyCompilation: false,
860
+ ...shared,
861
+ output: {
862
+ path: join(workspaceRoot, 'dist/apps/shell/browser'),
863
+ publicPath: 'auto',
864
+ uniqueName: 'shell',
865
+ clean: true,
866
+ },
867
+ devServer: {
868
+ port: 4757, // the Express server owns the app port
869
+ hot: true,
870
+ headers: { 'Access-Control-Allow-Origin': '*' },
871
+ devMiddleware: {
872
+ writeToDisk: (file) => !file.includes('.hot-update.'),
873
+ },
874
+ },
875
+ plugins: [
876
+ new NxAppRspackPlugin({
877
+ // ... your existing browser options
878
+ runtimeChunk: false,
879
+ commonChunk: false,
880
+ }),
881
+ new NxReactRspackPlugin(),
882
+ new ModuleFederationPlugin(require('./module-federation.config')),
883
+ ],
884
+ };
885
+
886
+ const server = {
887
+ name: 'server',
888
+ target: 'async-node',
889
+ ...shared,
890
+ output: {
891
+ path: join(workspaceRoot, 'dist/apps/shell/server'),
892
+ filename: 'server.js',
893
+ uniqueName: 'shell',
894
+ clean: true,
895
+ },
896
+ plugins: [
897
+ new NxAppRspackPlugin({
898
+ outputPath: join(workspaceRoot, 'dist/apps/shell/server'),
899
+ outputFileName: 'server.js',
900
+ tsConfig: './tsconfig.app.json',
901
+ main: './server.ts',
902
+ runtimeChunk: false,
903
+ commonChunk: false,
904
+ }),
905
+ new ModuleFederationPlugin(require('./module-federation.server.config')),
906
+ ],
907
+ };
908
+
909
+ module.exports = [browser, server];
910
+ ```
911
+
912
+ **webpack:**
913
+
914
+ The old server build is `webpack.server.config.ts` with `withModuleFederationForSSR`. Replace it
915
+ with a `webpack.server.config.js` next to the browser one:
916
+
917
+ ```js
918
+ // apps/shell/webpack.server.config.js
919
+ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
920
+ const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
921
+ const {
922
+ ModuleFederationPlugin,
923
+ } = require('@module-federation/enhanced/webpack');
924
+ const { join } = require('node:path');
925
+
926
+ module.exports = {
927
+ target: 'async-node',
928
+ entry: {}, // otherwise webpack's default ./src entry fails the build
929
+ output: {
930
+ path: join(__dirname, '../../dist/apps/shell/server'),
931
+ uniqueName: 'shell',
932
+ clean: true,
933
+ },
934
+ resolve: { modules: ['node_modules', join(__dirname, '../..')] },
935
+ optimization: { runtimeChunk: false },
936
+ plugins: [
937
+ new NxAppWebpackPlugin({
938
+ target: 'node', // turns on externalDependencies
939
+ compiler: 'babel',
940
+ main: './server.ts',
941
+ tsConfig: './tsconfig.server.json',
942
+ outputFileName: 'server.js',
943
+ externalDependencies: 'all',
944
+ outputHashing: 'none',
945
+ ssr: true,
946
+ }),
947
+ new NxReactWebpackPlugin(),
948
+ new ModuleFederationPlugin(require('./module-federation.server.config')),
949
+ ],
950
+ };
951
+ ```
952
+
953
+ ## Runtime manifest on the server
954
+
955
+ A host moved to a runtime manifest renders `loadRemote` on the server too, where `main.ts` never
956
+ runs. Register the server-side URLs in `server.ts` before `app.listen`:
957
+
958
+ ```ts
959
+ // apps/shell/server.ts
960
+ import { registerRemotes } from '@module-federation/enhanced/runtime';
961
+
962
+ registerRemotes([
963
+ { name: 'remoteA', entry: 'http://localhost:4751/server/remoteEntry.js' },
964
+ ]);
965
+ ```
966
+
967
+ On webpack, keep `@module-federation/*` inside the server bundle. Switch to
968
+ `externalDependencies: 'none'` and `mergeExternals: true`, and write an `externals` function that
969
+ skips that scope.
970
+
971
+ ## Targets
972
+
973
+ Replace the SSR dev server with a `serve` that watches the browser and server builds and restarts
974
+ Node when the server bundle changes.
975
+
976
+ **Rspack:**
977
+
978
+ `rspack serve` builds both compilers and writes them to disk:
979
+
980
+ ```jsonc
981
+ // apps/shell/project.json
982
+ {
983
+ "targets": {
984
+ "serve": {
985
+ "executor": "nx:run-commands",
986
+ "continuous": true,
987
+ "options": {
988
+ "cwd": "apps/shell",
989
+ "env": { "PORT": "4755" },
990
+ "parallel": true,
991
+ "commands": [
992
+ "rspack serve --node-env=development",
993
+ "until [ -f ../../dist/apps/shell/server/server.js ]; do sleep 1; done; node --watch-path=../../dist/apps/shell/server ../../dist/apps/shell/server/server.js",
994
+ ],
995
+ },
996
+ },
997
+ },
998
+ }
999
+ ```
1000
+
1001
+ **webpack:**
1002
+
1003
+ Add a `server` target that runs `webpack.server.config.js`, with `dependsOn: ["build"]`. The
1004
+ `serve` target also watches each remote's server output:
1005
+
1006
+ ```jsonc
1007
+ // apps/shell/project.json
1008
+ {
1009
+ "targets": {
1010
+ "serve": {
1011
+ "executor": "nx:run-commands",
1012
+ "continuous": true,
1013
+ "dependsOn": ["server"],
1014
+ "options": {
1015
+ "parallel": true,
1016
+ "commands": [
1017
+ "webpack build --watch --config apps/shell/webpack.config.js",
1018
+ "webpack build --watch --config apps/shell/webpack.server.config.js",
1019
+ "PORT=4750 node --watch-path=dist/apps/shell/server --watch-path=dist/apps/remoteA/server dist/apps/shell/server/server.js",
1020
+ ],
1021
+ },
1022
+ },
1023
+ },
1024
+ }
1025
+ ```
1026
+
1027
+ Start host and remotes together with `nx run-many -t serve -p shell remoteA -c development`.
1028
+ `serve-static` becomes `node dist/apps/<app>/server/server.js` in place of `@nx/web:file-server`,
1029
+ depending on the build that writes the server bundle. On Rspack, set `"cwd": "apps/<app>"` and run
1030
+ `node ../../dist/apps/<app>/server/server.js`, since the generated `server.ts` resolves `dist` from the
1031
+ working directory.
1032
+
1033
+ ## Validate the result
1034
+
1035
+ Request each host route that renders a remote, for example with `curl`, and check that the
1036
+ response holds the remote's markup and the client `<script>` tags. Then load it in a browser and
1037
+ check that it hydrates without errors.
1038
+
1039
+ Keep `@module-federation/node` in `dependencies` when you remove `@nx/module-federation`.
1040
+
1041
+ ## What the official plugins do not do
1042
+
1043
+ - **Dev orchestration.** The SSR dev-server executors started the remotes with the host and
1044
+ proxied the static ones. Start each app's `serve` yourself.
1045
+
1046
+ # Migrate Angular apps off Nx Module Federation
1047
+
1048
+ ## Rules
1049
+
1050
+ 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 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.
1051
+
1052
+ Rules 2 to 7 cover Path A. On Path B, follow that section of the page instead:
1053
+
1054
+ 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.
1055
+ 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.
1056
+ 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.
1057
+ 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.
1058
+ 5. Move every host to a runtime manifest so it boots without every remote running. A host that already calls `registerRemotes` from `@module-federation/enhanced/runtime` is done, including `mf-manifest.json` entries with no `type`. For static hosts and hosts on `@nx/angular/mf`, fetch `module-federation.manifest.json` in `main.ts` and pass it to `registerRemotes` with `type: 'module'`, set `remotes: {}` in the federation config, replace `import('<remote>/Routes')` and `loadRemoteModule(name, './Routes')` with `loadRemote('<container-name>/Routes')`, add `implicitDependencies` on the host, and drop the `<remote>/<exposed>` paths from `tsconfig.base.json`. Map keys and `loadRemote` prefixes are the normalized container name. Map values must be full `remoteEntry.mjs` URLs (`remoteEntry.js` on Rspack). Keep static `remotes` only if the user asks to stay static.
1059
+ 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>`.
1060
+ 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.
1061
+ 8. Serve one remote alone with `nx serve <remote>`, and check that the host boots and that remote's route renders. 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.
1062
+ 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')`.
1063
+ 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.
1064
+
1065
+ Nx v23 deprecates the Angular Module Federation generators, executors, runtime helpers, and the
1066
+ `withModuleFederation` and `NxModuleFederationPlugin` wrappers in `@nx/module-federation/angular`.
1067
+ These APIs wrap the official [Module Federation](https://module-federation.io) plugin, which
1068
+ supports both webpack and Rspack. Nx v24 removes the wrappers, so migrate your apps to the official
1069
+ plugin directly.
1070
+
1071
+ Migrate before you upgrade to v24. The old packages still build, which gives you a baseline to
1072
+ compare against. Nx will also ship migrations for this later.
1073
+
1074
+ You have one decision to make before you start:
1075
+
1076
+ - **Path A** keeps your bundler and swaps the wrapper for the official plugin. Your build target
1077
+ and deployment model stay the same, and hosts load remotes from a runtime manifest. It is the
1078
+ conservative option and the smallest diff.
1079
+ - **Path B** moves to [Native Federation](https://www.npmjs.com/package/@angular-architects/native-federation)
1080
+ and onto the esbuild-based `@angular/build:application` builder. It is the larger change, and
1081
+ the one more likely to track Angular's own direction.
1082
+
1083
+ > **Scope:** Static and runtime-loaded remotes. For server-side rendered (`--ssr`) apps, follow Path A, then
1084
+ > [migrate server-side rendered apps](https://nx.dev/docs/kb/migrate-angular-module-federation-ssr). Path B does
1085
+ > not cover them. For React, see the
1086
+ > [React guide](https://nx.dev/docs/kb/migrate-from-nx-module-federation).
1087
+
1088
+ ## What replaces what
1089
+
1090
+ | Going away in v24 | Replacement |
1091
+ | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
1092
+ | `withModuleFederation` from `@nx/module-federation/angular` | `ModuleFederationPlugin` from `@module-federation/enhanced/webpack` |
1093
+ | `NxModuleFederationPlugin` from `@nx/module-federation/angular` | `ModuleFederationPlugin` from `@module-federation/enhanced/rspack` |
1094
+ | `setRemoteDefinitions`, `setRemoteDefinition` from `@nx/angular/mf` | `registerRemotes` from `@module-federation/enhanced/runtime` |
1095
+ | `loadRemoteModule` from `@nx/angular/mf` | `loadRemote` from `@module-federation/enhanced/runtime` |
1096
+ | `@nx/angular:module-federation-dev-server` | `@nx/angular:dev-server`, plus the targets you start yourself |
1097
+ | `NxModuleFederationDevServerPlugin` | Not supported |
1098
+ | `@nx/angular:module-federation-static-server` | Not supported |
1099
+
1100
+ The last two ran processes and built nothing. `NxModuleFederationDevServerPlugin` was the Rspack
1101
+ form of the dev-server executor, which started the host and every remote from one `nx serve`.
1102
+ `module-federation-static-server` served the remotes you weren't working on from one port. Step 6
1103
+ replaces both with targets you start yourself.
1104
+
1105
+ ## Capture a baseline first
1106
+
1107
+ A remote can render correctly while it and the host hold separate copies of a shared library.
1108
+ Record what works now:
1109
+
1110
+ - Serve one host at a time with its remotes and visit the routes that load each remote.
1111
+ - Edit a file in the host and in a remote, and note how each change reaches the page.
1112
+ - Before you rebuild, copy each app's `mf-stats.json` to a folder such as `baseline/`. It's at
1113
+ `dist/apps/<app>/mf-stats.json` on webpack and `dist/apps/<app>/browser/mf-stats.json` on
1114
+ Rspack. Both paths reproduce its `shared` map, `exposes`, and container name.
1115
+ - Note each app's `serve` port for the remote URLs in Step 3.
1116
+
1117
+ ## Step 1: inventory the workspace
1118
+
1119
+ ```shell
1120
+ 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 .
1121
+ ```
1122
+
1123
+ On Windows:
1124
+
1125
+ ```powershell
1126
+ Get-ChildItem -Recurse -File -Include *.ts,*.js,*.json |
1127
+ Where-Object FullName -notmatch 'node_modules|dist|\.nx' |
1128
+ Select-String -Pattern '@nx/module-federation|@nx/angular/mf|module-federation-dev-server|module-federation-static-server|module-federation-dev-ssr|withModuleFederationForSSR'
1129
+ ```
1130
+
1131
+ A generated webpack host has `withModuleFederation` in `webpack.config.ts` and
1132
+ `webpack.prod.config.ts`, a `module-federation.config.ts`, a `build` target on
1133
+ `@nx/angular:webpack-browser` with `customWebpackConfig`, and a `serve` target on
1134
+ `@nx/angular:module-federation-dev-server`. A generated Rspack host has inferred targets and an
1135
+ `rspack.config.ts` that merges a `webpack.config.ts` holding `NxModuleFederationPlugin` and
1136
+ `NxModuleFederationDevServerPlugin`, plus a `webpack.prod.config.ts` that nothing loads. Remotes
1137
+ expose `./Routes` (`./Module` for NgModule remotes) and any path `federate-module` added. Their
1138
+ `serve` target depends on the host's `serve`.
1139
+
1140
+ Sort your hosts into two groups by reading each `src/main.ts`. Step 5 moves both to a runtime
1141
+ manifest.
1142
+
1143
+ - **Static hosts** have no federation call in `main.ts`. They reach remotes through an
1144
+ `import('remoteA/Routes')` in the routes file, resolved by a `tsconfig.base.json` path. Step 5
1145
+ converts them.
1146
+ - **Runtime-loaded hosts** fetch a manifest in `main.ts`. If that chain ends in `init` or
1147
+ `registerRemotes` from `@module-federation/enhanced/runtime`, the host needs no source change
1148
+ on Path A. Nx 23.2 generates `--dynamic` hosts this way. If it ends in `setRemoteDefinitions`
1149
+ from `@nx/angular/mf`, Step 5 converts it.
1150
+
1151
+ Server-side rendered apps (a `server` target or a `webpack.server.config.ts`) follow Path A, then
1152
+ [migrate server-side rendered apps](https://nx.dev/docs/kb/migrate-angular-module-federation-ssr). Their server
1153
+ build fails in between.
1154
+
1155
+ ## Path A, the official plugin on webpack or Rspack
1156
+
1157
+ ### Step 2: install the official package
1158
+
1159
+ Workspaces from the `host` generator already list `@module-federation/enhanced`, usually under
1160
+ `devDependencies`. Application code imports its runtime. Move it to `dependencies`, or install it
1161
+ if it's missing:
1162
+
1163
+ **npm:**
1164
+
1165
+ ```shell
1166
+ npm add --save-prod @module-federation/enhanced
1167
+ ```
1168
+
1169
+ **pnpm:**
1170
+
1171
+ ```shell
1172
+ pnpm add -P @module-federation/enhanced
1173
+ ```
1174
+
1175
+ **yarn:**
1176
+
1177
+ ```shell
1178
+ yarn add @module-federation/enhanced
1179
+ ```
1180
+
1181
+ **bun:**
1182
+
1183
+ ```shell
1184
+ bun add @module-federation/enhanced
1185
+ ```
1186
+
1187
+ ### Step 3: translate the federation config
1188
+
1189
+ Nx accepted a list of project names and resolved each one to a URL from the project graph. The
1190
+ official plugin needs the values written out.
1191
+
1192
+ #### Container name and import alias
1193
+
1194
+ The container name is normalized. Every character outside `[a-zA-Z0-9_$]`, and a leading
1195
+ character that cannot start an identifier, becomes `_`. `dyn-shell` publishes as `dyn_shell`,
1196
+ and that is the form `name` takes.
1197
+
1198
+ The import alias stays the project name, and so do the `remoteA/Routes` entries in
1199
+ `tsconfig.base.json` that a static host's `import()` resolves through:
1200
+
1201
+ ```js
1202
+ // a project named dyn-shell that consumes a project named remote-a
1203
+ module.exports = {
1204
+ name: 'dyn_shell', // container, normalized
1205
+ remotes: {
1206
+ 'remote-a': 'http://localhost:4201/remoteEntry.mjs', // key is the project name
1207
+ },
1208
+ };
1209
+ ```
1210
+
1211
+ #### The shared map
1212
+
1213
+ Every app needs its own map, copied from that app's baseline `mf-stats.json`. Nx shared every npm
1214
+ package the app depends on in the graph, plus each package's secondary entry points, so expect
1215
+ entries such as `@angular/common/http` and `@angular/core/primitives/signals` that your source
1216
+ never imports.
1217
+
1218
+ Take `singleton`, `strictVersion`, `requiredVersion`, and `eager` from each entry:
1219
+
1220
+ ```jsonc
1221
+ // dist/apps/shell/mf-stats.json, one entry of the shared array
1222
+ {
1223
+ "name": "@angular/core",
1224
+ "version": "22.1.7",
1225
+ "singleton": true,
1226
+ "requiredVersion": "~22.1.0",
1227
+ "strictVersion": true,
1228
+ "eager": false,
1229
+ }
1230
+ ```
1231
+
1232
+ ```js
1233
+ // apps/shell/module-federation.config.js
1234
+ '@angular/core': { singleton: true, strictVersion: true, requiredVersion: '~22.1.0' },
1235
+ ```
1236
+
1237
+ `eager: false` is the plugin default and can be left out, but Rspack stats mark most
1238
+ `@angular/*` entries `eager: true` and those need copying.
1239
+
1240
+ Drop the `version` from npm entries. Keep it on source-only workspace libraries, which have no
1241
+ `package.json` for the plugin to read. The stats show them as `version: '0.0.0'` with
1242
+ `requiredVersion: '^0.0.0'` and no `strictVersion`.
1243
+
1244
+ #### Remote URLs
1245
+
1246
+ Each remote gets a full URL on its `serve` port. The host's baseline `mf-stats.json` has these
1247
+ under `remotes[].federationContainerName`, one row per exposed module the host uses. Write one
1248
+ entry per remote. Angular containers are ES modules, and their URLs take no `name@` prefix.
1249
+
1250
+ **Before:**
1251
+
1252
+ ```ts
1253
+ // apps/shell/module-federation.config.ts
1254
+ import type { ModuleFederationConfig } from '@nx/module-federation';
1255
+
1256
+ const config: ModuleFederationConfig = {
1257
+ name: 'shell',
1258
+ remotes: ['remoteA', 'remoteB'],
1259
+ };
1260
+
1261
+ export default config;
1262
+ ```
1263
+
1264
+ **After:**
1265
+
1266
+ ```js
1267
+ // apps/shell/module-federation.config.js
1268
+ module.exports = {
1269
+ name: 'shell', // normalized, so dyn-shell becomes dyn_shell
1270
+ filename: 'remoteEntry.mjs', // remoteEntry.js on Rspack
1271
+ library: { type: 'module' },
1272
+ dts: false,
1273
+ remotes: {
1274
+ remoteA: 'http://localhost:4201/remoteEntry.mjs', // .js on Rspack
1275
+ remoteB: 'http://localhost:4202/remoteEntry.mjs',
1276
+ },
1277
+ shared: {
1278
+ '@angular/core': {
1279
+ singleton: true,
1280
+ strictVersion: true,
1281
+ requiredVersion: '~22.1.0',
1282
+ },
1283
+ '@angular/core/primitives/signals': {
1284
+ singleton: true,
1285
+ strictVersion: true,
1286
+ requiredVersion: '~22.1.0',
1287
+ },
1288
+ // ... every entry the baseline mf-stats.json lists
1289
+ '@myorg/state': {
1290
+ singleton: true,
1291
+ requiredVersion: '^0.0.0',
1292
+ version: '0.0.0',
1293
+ },
1294
+ },
1295
+ };
1296
+ ```
1297
+
1298
+ Every app's config carries `name`, `filename`, `library`, and `dts`, and hosts add `remotes`.
1299
+ Remotes keep their existing `exposes` map, whose workspace-root paths resolve once Step 4 adds the
1300
+ workspace root to `resolve.modules`.
1301
+
1302
+ Production remote URLs live in `webpack.prod.config.ts` as
1303
+ `['remoteA', 'http://remote-a.example.com/']` tuples. On webpack the `production` configuration
1304
+ loads that file through `customWebpackConfig`. Rspack never loads it. Fold those URLs into the one
1305
+ config, adding the `/remoteEntry.mjs` that Nx appended for you. If the file only has the
1306
+ generator's commented example, you have one URL set for every configuration.
1307
+
1308
+ Key the URLs on the Nx task configuration, because the Angular builders don't set `NODE_ENV`.
1309
+
1310
+ ```js
1311
+ // apps/shell/module-federation.config.js
1312
+ const isProd = process.env.NX_TASK_TARGET_CONFIGURATION === 'production';
1313
+
1314
+ module.exports = {
1315
+ // ... name, filename, library, dts, shared
1316
+ remotes: {
1317
+ remoteA: isProd
1318
+ ? 'https://remote-a.example.com/remoteEntry.mjs'
1319
+ : 'http://localhost:4201/remoteEntry.mjs',
1320
+ },
1321
+ };
1322
+ ```
1323
+
1324
+ > **JavaScript config files:** The samples use CommonJS `.js`. TypeScript configs also work, and you can rename them back once
1325
+ > the migration is verified.
1326
+
1327
+ ### Step 4: replace the wrapper in the bundler config
1328
+
1329
+ **webpack:**
1330
+
1331
+ `@nx/angular:webpack-browser` passes the Angular build's webpack config to the function your
1332
+ `customWebpackConfig` exports. That function was `withModuleFederation`. Replace it with one that
1333
+ adds the plugin and the settings the wrapper applied.
1334
+
1335
+ **Before:**
1336
+
1337
+ ```ts
1338
+ // apps/shell/webpack.config.ts
1339
+ import { withModuleFederation } from '@nx/module-federation/angular';
1340
+ import config from './module-federation.config';
1341
+
1342
+ export default withModuleFederation(config, { dts: false });
1343
+ ```
1344
+
1345
+ **After:**
1346
+
1347
+ ```js
1348
+ // apps/shell/webpack.config.js
1349
+ const {
1350
+ ModuleFederationPlugin,
1351
+ } = require('@module-federation/enhanced/webpack');
1352
+ const { join } = require('node:path');
1353
+ const mf = require('./module-federation.config.js');
1354
+
1355
+ module.exports = (config) => ({
1356
+ ...config,
1357
+ output: { ...config.output, uniqueName: mf.name, publicPath: 'auto' },
1358
+ experiments: { ...(config.experiments ?? {}), outputModule: true },
1359
+ resolve: {
1360
+ ...config.resolve,
1361
+ modules: [
1362
+ ...(config.resolve?.modules ?? ['node_modules']),
1363
+ join(__dirname, '../..'),
1364
+ ],
1365
+ alias: {
1366
+ ...(config.resolve?.alias ?? {}),
1367
+ '@myorg/state': join(__dirname, '../../libs/state/src/index.ts'),
1368
+ },
1369
+ },
1370
+ optimization: { ...config.optimization, runtimeChunk: false },
1371
+ plugins: [...(config.plugins ?? []), new ModuleFederationPlugin(mf)],
1372
+ });
1373
+ ```
1374
+
1375
+ Point `customWebpackConfig.path` at this file in `project.json`, delete the `production`
1376
+ configuration's override of it, and keep `@nx/angular:webpack-browser` as the executor.
1377
+
1378
+ Add the `resolve.alias` entries in every federated app, hosts included. The federation plugin
1379
+ resolves exposed modules outside the Angular compilation, which is the only place `tsconfig`
1380
+ paths apply. Alias workspace libraries only. Aliasing a `<remote>/<exposed>` path such as
1381
+ `remoteA/Routes`, `remoteNg/Module`, or a `federate-module` path bundles that remote into the
1382
+ host.
1383
+
1384
+ **Rspack:**
1385
+
1386
+ The generated `rspack.config.ts` calls `createConfig` from `@nx/angular-rspack` and merges in a
1387
+ `webpack.config.ts` holding `NxModuleFederationPlugin` and `NxModuleFederationDevServerPlugin`.
1388
+ Keep that structure. Replace the merged file's contents and drop the dev-server plugin:
1389
+
1390
+ **Before:**
1391
+
1392
+ ```ts
1393
+ // apps/shell/webpack.config.ts
1394
+ import { NxModuleFederationPlugin } from '@nx/module-federation/angular';
1395
+ import config from './module-federation.config';
1396
+
1397
+ export default {
1398
+ plugins: [new NxModuleFederationPlugin({ config }, { dts: false })],
1399
+ };
1400
+ ```
1401
+
1402
+ **After:**
1403
+
1404
+ ```ts
1405
+ // apps/shell/webpack.config.ts
1406
+ import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
1407
+ import { join } from 'node:path';
1408
+ import mf from './module-federation.config.js';
1409
+
1410
+ export default {
1411
+ output: { uniqueName: mf.name, publicPath: 'auto', module: true },
1412
+ resolve: { modules: ['node_modules', join(__dirname, '../..')] },
1413
+ optimization: {
1414
+ // hosts only, remotes set runtimeChunk: false unconditionally
1415
+ ...(process.env['RSPACK_SERVE'] || process.env['WEBPACK_SERVE']
1416
+ ? {}
1417
+ : { runtimeChunk: false }),
1418
+ // keeps the wrapper's chunk layout, without it one chunk is merged away
1419
+ splitChunks: { cacheGroups: { default: false, common: false } },
1420
+ },
1421
+ plugins: [new ModuleFederationPlugin(mf)],
1422
+ };
1423
+ ```
1424
+
1425
+ Under the dev server, a host keeps the runtime chunk `createConfig` gives it, as the wrapper did.
1426
+
1427
+ Rspack containers keep `filename: 'remoteEntry.js'`, so host `remotes` URLs end in
1428
+ `/remoteEntry.js` rather than `.mjs`. Skip `resolve.alias`, since `createConfig` reads `tsconfig`
1429
+ paths itself.
1430
+
1431
+ > **Do not add \:** Rspack builds print a `MODULE_TYPELESS_PACKAGE_JSON` warning that predates this migration. Leave
1432
+ > it alone, because adding `"type": "module"` breaks the CommonJS federation config.
1433
+
1434
+ ### Step 5: move hosts to a runtime manifest
1435
+
1436
+ A runtime manifest holds the remote URLs in a file the host fetches at startup. A static host loads
1437
+ every remote's entry before it renders, so one remote that isn't running leaves a blank page. With
1438
+ a manifest the host boots on its own, and only the routes of stopped remotes fail. You can also
1439
+ change a URL without rebuilding.
1440
+
1441
+ Hosts that already call `registerRemotes` or `init` from `@module-federation/enhanced/runtime`
1442
+ skip this step, including the Nx 23.2 `--dynamic` host. Convert static hosts and hosts that call
1443
+ `setRemoteDefinitions` from `@nx/angular/mf`.
1444
+
1445
+ Move the `remotes` URLs from Step 3 into `public/module-federation.manifest.json`. The host keeps
1446
+ `ModuleFederationPlugin` in its build with `remotes: {}`, since `registerRemotes` and
1447
+ `loadRemote` do nothing without the runtime it creates.
1448
+
1449
+ **Static host:**
1450
+
1451
+ ```ts
1452
+ // apps/shell/src/app/app.routes.ts
1453
+ export const appRoutes: Route[] = [
1454
+ {
1455
+ path: 'remoteA',
1456
+ loadChildren: () => import('remoteA/Routes').then((m) => m!.remoteRoutes),
1457
+ },
1458
+ ];
1459
+ ```
1460
+
1461
+ **Runtime-loaded host:**
1462
+
1463
+ ```ts
1464
+ // apps/dyn-shell/src/main.ts
1465
+ import { setRemoteDefinitions } from '@nx/angular/mf';
1466
+
1467
+ fetch('/module-federation.manifest.json')
1468
+ .then((res) => res.json())
1469
+ .then(setRemoteDefinitions)
1470
+ .then(() => import('./bootstrap'));
1471
+ ```
1472
+
1473
+ ```ts
1474
+ // apps/dyn-shell/src/app/app.routes.ts
1475
+ import { loadRemoteModule } from '@nx/angular/mf';
1476
+
1477
+ export const appRoutes: Route[] = [
1478
+ {
1479
+ path: 'remoteA',
1480
+ loadChildren: () =>
1481
+ loadRemoteModule('remoteA', './Routes').then((m) => m.remoteRoutes),
1482
+ },
1483
+ ];
1484
+ ```
1485
+
1486
+ **After:**
1487
+
1488
+ ```ts
1489
+ // apps/shell/src/main.ts
1490
+ import { registerRemotes } from '@module-federation/enhanced/runtime';
1491
+
1492
+ fetch('/module-federation.manifest.json')
1493
+ .then((res) => res.json())
1494
+ .then((remotes: Record<string, string>) =>
1495
+ registerRemotes(
1496
+ Object.entries(remotes).map(([name, entry]) => ({
1497
+ name,
1498
+ entry,
1499
+ type: 'module' as const,
1500
+ }))
1501
+ )
1502
+ )
1503
+ .then(() => import('./bootstrap'));
1504
+ ```
1505
+
1506
+ ```ts
1507
+ // apps/shell/src/app/app.routes.ts
1508
+ import { loadRemote } from '@module-federation/enhanced/runtime';
1509
+
1510
+ export const appRoutes: Route[] = [
1511
+ {
1512
+ path: 'remoteA',
1513
+ loadChildren: () =>
1514
+ loadRemote<{ remoteRoutes: Route[] }>('remoteA/Routes').then(
1515
+ (m) => m!.remoteRoutes
1516
+ ),
1517
+ },
1518
+ ];
1519
+ ```
1520
+
1521
+ ```json
1522
+ // apps/shell/public/module-federation.manifest.json
1523
+ {
1524
+ "remoteA": "http://localhost:4201/remoteEntry.mjs",
1525
+ "remoteB": "http://localhost:4202/remoteEntry.mjs"
1526
+ }
1527
+ ```
1528
+
1529
+ Map values must be full URLs. Append `/remoteEntry.mjs` (`/remoteEntry.js` on Rspack) to any
1530
+ bare origin, which `setRemoteDefinitions` used to complete for you. A remote's `mf-manifest.json`
1531
+ URL works too. Register `remoteEntry.mjs` values with `type: 'module'`, while manifest values
1532
+ carry their own type.
1533
+
1534
+ Map keys and `loadRemote` prefixes use the normalized container name. A project named `remote-a`
1535
+ is `remote_a` in the manifest and `loadRemote('remote_a/Routes')` at the call site.
1536
+
1537
+ A static host loses its project-graph edge, which its `import('remoteA/Routes')` calls gave Nx.
1538
+ Add `implicitDependencies` on the host so remotes still build first and `nx affected` still
1539
+ includes the host. Drop the host's `<remote>/<exposed>` paths from `tsconfig.base.json`.
1540
+
1541
+ The `isProd` ternary from Step 3 has no equivalent in the manifest. Ship the right manifest for
1542
+ each environment, with a `fileReplacements` pair or by writing the file at deploy time.
1543
+
1544
+ > **Staying with static remotes:** Keep the `remotes` map from Step 3 and the `tsconfig.base.json` paths, and skip this step.
1545
+ > Every remote then has to be running before the host renders, so serve them together:
1546
+ > `nx run-many -t serve -p shell remoteA remoteB`.
1547
+
1548
+ ### Step 6: replace the serve orchestration
1549
+
1550
+ `@nx/angular:module-federation-dev-server` built and served the remotes along with the host, and
1551
+ `--devRemotes` picked which ones ran live. The official plugin only handles the build, so you
1552
+ start the processes yourself.
1553
+
1554
+ #### Dev servers
1555
+
1556
+ On webpack, switch the host's `serve` executor to `@nx/angular:dev-server`. Keep `port`,
1557
+ `publicHost`, and the per-configuration `buildTarget` options. Remove `devRemotes`,
1558
+ `skipRemotes`, `static`, `isInitialHost`, and `pathToManifestFile`, which the dev server rejects.
1559
+ On Rspack, keep the inferred `build` and `serve` targets.
1560
+
1561
+ Every remote needs `headers: { 'Access-Control-Allow-Origin': '*' }`, on the `serve` target on
1562
+ webpack or under `devServer.headers` in `rspack.config.ts` on Rspack. The host loads
1563
+ `remoteEntry.mjs` cross-origin, and the Angular dev server sends no CORS header by default.
1564
+
1565
+ Keep each remote's `dependsOn: ['shell:serve']` and start everything you want live from one
1566
+ `run-many`, naming each app:
1567
+
1568
+ ```shell
1569
+ nx run-many -t serve -p shell remoteA remoteB
1570
+ ```
1571
+
1572
+ #### Static servers
1573
+
1574
+ Replace every app's generated `serve-static` target, hosts included, with one that serves the
1575
+ build output directly:
1576
+
1577
+ ```jsonc
1578
+ // apps/remoteB/project.json
1579
+ {
1580
+ "targets": {
1581
+ "serve-static": {
1582
+ "executor": "@nx/web:file-server",
1583
+ "continuous": true,
1584
+ "dependsOn": ["build"],
1585
+ "options": {
1586
+ "staticFilePath": "dist/apps/remoteB", // dist/apps/remoteB/browser on Rspack
1587
+ "port": 4202,
1588
+ "spa": true,
1589
+ "watch": false,
1590
+ },
1591
+ },
1592
+ },
1593
+ }
1594
+ ```
1595
+
1596
+ Replace the whole target, including `configurations` and `defaultConfiguration`, and leave
1597
+ `buildTarget` unset. With it set, the file server rebuilds the app on top of the `dependsOn`
1598
+ build, and that nested build fails inside e2e runs. Keep `spa: true` so a refresh on a deep route
1599
+ doesn't 404.
1600
+
1601
+ Each remote's `serve-static` port must match the port in the host's `remotes` URL.
1602
+
1603
+ > **Inferred targets on Rspack:** On Rspack, `@nx/rspack/plugin` infers these targets and Nx merges your `project.json` options
1604
+ > into them key by key. Add `"buildTarget": ""` to clear the inferred one.
1605
+
1606
+ #### End-to-end projects
1607
+
1608
+ Generated Playwright projects start the host with `nx run shell:serve`, which no longer starts
1609
+ the remotes. Point `webServer.command` at `nx run-many -t serve-static -p shell remoteA remoteB`.
1610
+ A remote's own e2e project needs only `-p remoteA`, with `webServer.url` and `baseURL` on its
1611
+ `serve-static` port.
1612
+
1613
+ Cypress projects set these commands under `nxE2EPreset` in `cypress.config.ts`:
1614
+
1615
+ ```ts
1616
+ // apps/shell-e2e/cypress.config.ts
1617
+ nxE2EPreset(__filename, {
1618
+ cypressDir: 'src',
1619
+ webServerCommands: {
1620
+ default: 'nx run-many -t serve-static -p shell remoteA remoteB',
1621
+ production: 'nx run-many -t serve-static -p shell remoteA remoteB',
1622
+ },
1623
+ ciWebServerCommand: 'nx run-many -t serve-static -p shell remoteA remoteB',
1624
+ });
1625
+ ```
1626
+
1627
+ #### Target defaults
1628
+
1629
+ In `nx.json`, drop the `{ "env": "NX_MF_DEV_REMOTES" }` input from the
1630
+ `@nx/angular:webpack-browser` target default and keep the rest of the entry.
1631
+
1632
+ ### Step 7: validate the result
1633
+
1634
+ Check the result in a browser against your baseline:
1635
+
1636
+ - Each remote route renders in the host.
1637
+ - With only `nx serve remoteA` running, which starts the host through `dependsOn`, the host
1638
+ boots and the `remoteA` route renders.
1639
+ - A shared mutable value written in the host reads back in each remote after a client-side route
1640
+ change.
1641
+ - Editing a remote's component shows in the host after a reload, as in the baseline.
1642
+ - The e2e suite passes on the replacement web server.
1643
+
1644
+ Then check the builds:
1645
+
1646
+ - Production and development both build with `--skipNxCache` after you delete `dist/apps/<app>`
1647
+ and `.angular/cache`. Build production last because both configurations write the same
1648
+ `mf-stats.json`. On Rspack, build development as
1649
+ `NGRS_CONFIG=development nx build <app> --configuration=development`.
1650
+ - Each app's production `mf-stats.json` lists the same container name, exposes, and `shared`
1651
+ entries as the baseline. Compare only those fields, since asset hashes and `usedIn` text
1652
+ change. A host on a runtime manifest has an empty build-time `remotes`, so check its manifest
1653
+ against the baseline URLs.
1654
+
1655
+ ### Step 8: clean up the old setup
1656
+
1657
+ Once the new setup builds and runs, delete `module-federation.config.ts`,
1658
+ `webpack.prod.config.ts`, and on webpack `webpack.config.ts`. Rspack keeps `webpack.config.ts`
1659
+ because `rspack.config.ts` imports it.
1660
+
1661
+ Then remove the package and rebuild uncached:
1662
+
1663
+ ```shell
1664
+ npm remove @nx/module-federation
1665
+ ```
1666
+
1667
+ ## Path B, rewrite to native federation
1668
+
1669
+ Native Federation does the same runtime composition with ES modules and import maps, on the
1670
+ esbuild `@angular/build:application` builder. Expect to rewrite the federation layer, starting
1671
+ from an `init` generator that expects an Angular CLI app.
1672
+
1673
+ ### Step 2: install and initialize
1674
+
1675
+ Install the release matching your Angular major and minor. On Angular 22.1:
1676
+
1677
+ ```shell
1678
+ npm add @angular-architects/native-federation@~22.1.0
1679
+ ```
1680
+
1681
+ It belongs in `dependencies`.
1682
+
1683
+ Migrate remotes together with every host that loads them. A Module Federation host can't load a
1684
+ Native Federation remote. If a host can't move yet, keep its remotes on Path A until it can.
1685
+
1686
+ The generator refuses an app that already has a `bootstrap.ts`. Fold `bootstrap.ts` back into
1687
+ `main.ts`, delete it, and run the generator per app. On a runtime-loaded host, also drop the
1688
+ `fetch(...)` chain that ends in `setRemoteDefinitions` or `registerRemotes`, keeping only the
1689
+ bootstrap body. The `dynamic-host` type writes and reads its own `public/federation.manifest.json`.
1690
+ Delete the old `module-federation.manifest.json`.
1691
+
1692
+ ```shell
1693
+ nx g @angular-architects/native-federation:init --project=shell --port=4200 --type=host
1694
+ nx g @angular-architects/native-federation:init --project=remoteA --port=4201 --type=remote
1695
+ ```
1696
+
1697
+ Use `--type=host` for a static host and `--type=dynamic-host` for one that reads a manifest.
1698
+
1699
+ The generator creates `federation.config.mjs`, `tsconfig.federation.json`, a new `bootstrap.ts`
1700
+ and `main.ts`, moves the app onto an `esbuild` target running `@angular/build:application`, wraps
1701
+ `build` and `serve` in `@angular-architects/native-federation:build`, and adds `es-module-shims`
1702
+ to the polyfills. It also adds `es-module-shims` under `dependencies` and
1703
+ `@softarc/native-federation-orchestrator` under `devDependencies`, and runs an install.
1704
+
1705
+ The host's remote map comes from every other application's `serve` port, or `serve-original`
1706
+ once that app has moved. Run the generator in any order. Apps with no port land at 4200, and
1707
+ Step 3 prunes them from the map. The wrapped `serve` gets `port: 0` and falls through to the port
1708
+ on `serve-original`.
1709
+
1710
+ Delete `dist/apps/<app>` before the first build. The new output goes under `browser/`, next to
1711
+ the old webpack files. The first build also rewrites the `files` list in
1712
+ `tsconfig.federation.json`. Commit after the first build that follows Step 3.
1713
+
1714
+ ### Step 3: finish what the generator left
1715
+
1716
+ On an Nx Module Federation app, the generator leaves this cleanup:
1717
+
1718
+ - Delete `module-federation.config.ts`, `webpack.config.ts`, and `webpack.prod.config.ts`, and
1719
+ remove the `customWebpackConfig` it copied onto the `esbuild` target and its `production`
1720
+ configuration.
1721
+ - Replace the placeholder `'./Component'` entry in each remote's `exposes` with the route file,
1722
+ as a path from the workspace root:
1723
+ `exposes: { './Routes': './apps/remoteA/src/app/remote-entry/entry.routes.ts' }`.
1724
+ - Share workspace libraries reached through `tsconfig` paths with
1725
+ `sharedMappings: ['@myorg/state']` in every app's `federation.config.mjs`. `shareAll` only
1726
+ covers `package.json` dependencies.
1727
+ - Prune the host's remote map, which lists every application with `build` and `serve` targets
1728
+ under a camel-cased key, other hosts included. With `--type=host` the map is the first argument
1729
+ of `initFederation` in `main.ts`, and with `--type=dynamic-host` it is
1730
+ `public/federation.manifest.json`. Keep the `hostRemoteEntry` option the generator writes next
1731
+ to it.
1732
+ - On every app's `serve-original` target, swap the executor to `@angular/build:dev-server` and
1733
+ drop `publicHost`, which that builder does not accept. Keep a remote's `headers`.
1734
+ - Point `extract-i18n` at `<app>:esbuild` on `@angular/build:extract-i18n`.
1735
+ - Mark the wrapped `serve` `continuous: true`. It has no configurations, so use `serve-static`
1736
+ for a production-like local run. Give the wrapped `build` `cache: true` and `outputs`
1737
+ (`{workspaceRoot}/dist/apps/<app>`). Cache hits start from the second build.
1738
+ - Move the `dependsOn: ['shell:serve']` the generator renamed onto `serve-original` back to the
1739
+ new `serve`. When a second host uses the same remotes, name every app in the `run-many`.
1740
+ - Replace each app's whole `serve-static` target, `defaultConfiguration` included, with the one
1741
+ from Path A Step 6 using `staticFilePath: dist/apps/<app>/browser`, and point the e2e
1742
+ `webServer` commands at `nx run-many -t serve-static` as in that step.
1743
+ - Remove the `@nx/angular:webpack-browser` entry from `targetDefaults` in `nx.json` once every
1744
+ app has moved.
1745
+ - Rewrite host routes to use Native Federation's `loadRemoteModule`. Static hosts replace each
1746
+ `import('remoteA/Routes')` with the call below, and so do hosts calling
1747
+ `loadRemote('remoteA/Routes')`. Hosts on `@nx/angular/mf` swap only the import:
1748
+
1749
+ ```ts
1750
+ // apps/shell/src/app/app.routes.ts
1751
+ import { loadRemoteModule } from '@angular-architects/native-federation';
1752
+
1753
+ export const appRoutes: Route[] = [
1754
+ {
1755
+ path: 'remoteA',
1756
+ loadChildren: () =>
1757
+ loadRemoteModule('remoteA', './Routes').then((m) => m.remoteRoutes),
1758
+ },
1759
+ ];
1760
+ ```
1761
+
1762
+ - Last, once every app has moved and no target names a webpack executor, drop the
1763
+ `remoteA/Routes` entries from `tsconfig.base.json` paths and
1764
+ `npm remove @nx/module-federation @module-federation/enhanced @nx/webpack webpack-merge`.
1765
+ Removing them earlier breaks the graph commands the other steps need.
1766
+
1767
+ > **loadRemoteModule is deprecated:** Native Federation 22.1 deprecates the module-level `loadRemoteModule` in favor of the one the
1768
+ > `initFederation` promise resolves to. The module-level form still works with one host per page.
1769
+
1770
+ Builds land under `dist/apps/<app>/browser/` with a `remoteEntry.json` per app and no
1771
+ `mf-stats.json`. Validate as in Path A Step 7, running the parity check against
1772
+ `remoteEntry.json`:
1773
+
1774
+ - Every baseline npm `shared` entry appears as a singleton with the same `requiredVersion` range.
1775
+ - `sharedMappings` libraries show up as `~<version>` with `strictVersion`.
1776
+ - The container `name` is the project name as written, not normalized.
1777
+ - Remote URLs are in `main.ts` or the manifest.
1778
+
1779
+ `shareAll` adds more shared packages than the old `mf-stats.json` listed. Trim them with its
1780
+ `skip` option if they matter to you.
1781
+
1782
+ ## What the official plugin does not do
1783
+
1784
+ - **`--devRemotes` selection.** Which remotes run live and which serve from a build is now the
1785
+ set of targets you start.
1786
+ - **Automatic remote fallback.** A host fails the request when a remote is down. Add your own
1787
+ error handling if you need it.
1788
+
1789
+ For anything beyond this migration, use the official
1790
+ [Module Federation configuration reference](https://module-federation.io/configure/) and, for
1791
+ Path B, the
1792
+ [Native Federation documentation](https://github.com/angular-architects/module-federation-plugin#readme).
1793
+
1794
+ # Migrate server-side rendered Angular Module Federation apps
1795
+
1796
+ ## Rules
1797
+
1798
+ Migrate the server build of each server-side rendered Angular Module Federation app in this workspace off the Nx Module Federation APIs that Nx v24 removes. An app with a `server` target or a `webpack.server.config.*` file is server-side rendered. Complete Path A of https://nx.dev/docs/kb/migrate-angular-module-federation for the browser build first, then follow every step at https://nx.dev/docs/kb/migrate-angular-module-federation-ssr. Native Federation is not covered, so stop and report if the user asks for it on a server-side rendered app.
1799
+
1800
+ Rules that override any shortcut you are tempted to take:
1801
+
1802
+ 1. Before editing, keep the baseline stats from `dist/apps/<app>/browser/mf-stats.json` and `dist/apps/<app>/server/mf-stats.json`. Record the server-rendered HTML of each host route that renders a remote.
1803
+ 2. Replace `withModuleFederationForSSR` from `@nx/module-federation/angular` with `ModuleFederationPlugin` from `@module-federation/enhanced/webpack`. Keep `@module-federation/node` as a dependency.
1804
+ 3. The server container sets `library: { type: 'commonjs-module' }`, `remoteType: 'script'`, `experiments: { asyncStartup: true }`, and `runtimePlugins: [require.resolve('@module-federation/node/runtimePlugin')]`. Its remote URLs keep the `name@` prefix and point at `<origin>/server/remoteEntry.js`, even though the browser container is `.mjs`.
1805
+ 4. Copy the server `shared` map from the server stats.
1806
+ 5. The server build fails after Path A until the server configs are replaced, because the old `webpack.server.config.ts` imports the new browser `module-federation.config.js`. Only validate the server build at the end.
1807
+ 6. Swap `@nx/angular:module-federation-dev-ssr` for `@angular-devkit/build-angular:ssr-dev-server` with an explicit `port`. A static run is `node dist/apps/<app>/server/main.js`, never `@nx/web:file-server`.
1808
+ 7. Accept the migration only when each host route's HTML holds the remote's markup and client scripts, and the page hydrates in a browser without errors. Report what you verified and what you did not.
1809
+
1810
+ > **Migrate the browser build first:** This guide covers only the server build. Complete Path A of
1811
+ > [migrating Angular Module Federation](https://nx.dev/docs/kb/migrate-angular-module-federation) for every app,
1812
+ > then come back here.
1813
+
1814
+ A host or remote generated with `--ssr` builds the same container twice, once for the browser and
1815
+ once for Node. The server build needs its own container format, runtime plugin, and serve targets.
1816
+ Native Federation server-side rendering is not covered.
1817
+
1818
+ ## What replaces what
1819
+
1820
+ | Going away in v24 | Replacement |
1821
+ | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
1822
+ | `withModuleFederationForSSR` from `@nx/module-federation/angular` | `ModuleFederationPlugin` with the `@module-federation/node` runtime |
1823
+ | `@nx/angular:module-federation-dev-ssr` | `@angular-devkit/build-angular:ssr-dev-server` |
1824
+
1825
+ ## Capture the server baseline
1826
+
1827
+ With `--ssr`, copy the browser stats from `dist/apps/<app>/browser/mf-stats.json` and the server
1828
+ stats from `dist/apps/<app>/server/mf-stats.json`.
1829
+
1830
+ Also save the HTML each host route returns with `curl` to compare against at the end.
1831
+
1832
+ ## Server container settings
1833
+
1834
+ The server container is CommonJS and loads remotes over HTTP through the Node runtime plugin.
1835
+ Each remote's Express server serves its server build under `/server`, and the host's server-side
1836
+ remote URLs point there. Keep that static mount in every remote's `server.ts`.
1837
+
1838
+ A host or remote generated with `--ssr` has a `server` target on `@nx/angular:webpack-server`
1839
+ whose `webpack.server.config.ts` calls `withModuleFederationForSSR`. That config imports the
1840
+ browser federation config, so the server build fails after Path A until you finish the steps
1841
+ below.
1842
+
1843
+ ## Server federation config
1844
+
1845
+ Write a `module-federation.server.config.js` per app. It keeps the browser config's `name`,
1846
+ `exposes`, and `dts: false`. Unlike the browser container, it is CommonJS and its remote URLs
1847
+ keep the `name@` prefix and end in `.js`:
1848
+
1849
+ ```js
1850
+ // apps/shell/module-federation.server.config.js
1851
+ const isProd = process.env.NX_TASK_TARGET_CONFIGURATION === 'production';
1852
+
1853
+ module.exports = {
1854
+ name: 'shell',
1855
+ filename: 'remoteEntry.js',
1856
+ library: { type: 'commonjs-module' },
1857
+ remoteType: 'script',
1858
+ dts: false,
1859
+ remotes: {
1860
+ remoteA: isProd
1861
+ ? 'remoteA@https://remote-a.example.com/server/remoteEntry.js'
1862
+ : 'remoteA@http://localhost:4201/server/remoteEntry.js',
1863
+ },
1864
+ shared: {
1865
+ // ... every entry the baseline server/mf-stats.json lists
1866
+ },
1867
+ experiments: { asyncStartup: true },
1868
+ runtimePlugins: [require.resolve('@module-federation/node/runtimePlugin')],
1869
+ };
1870
+ ```
1871
+
1872
+ Copy `shared` from the server stats. They list `@angular/ssr`, `@angular/ssr/node`, `express`,
1873
+ and `cors` on top of the browser map.
1874
+
1875
+ ## Server bundler config
1876
+
1877
+ Point `server.options.customWebpackConfig.path` at a `webpack.server.config.js`:
1878
+
1879
+ ```js
1880
+ // apps/shell/webpack.server.config.js
1881
+ const {
1882
+ ModuleFederationPlugin,
1883
+ } = require('@module-federation/enhanced/webpack');
1884
+ const { join } = require('node:path');
1885
+ const mf = require('./module-federation.server.config.js');
1886
+
1887
+ module.exports = (config) => ({
1888
+ ...config,
1889
+ target: 'async-node',
1890
+ output: { ...config.output, uniqueName: mf.name },
1891
+ resolve: {
1892
+ ...config.resolve,
1893
+ modules: [
1894
+ ...(config.resolve?.modules ?? ['node_modules']),
1895
+ join(__dirname, '../..'),
1896
+ ],
1897
+ },
1898
+ optimization: { ...config.optimization, runtimeChunk: false },
1899
+ plugins: [...(config.plugins ?? []), new ModuleFederationPlugin(mf)],
1900
+ });
1901
+ ```
1902
+
1903
+ `main.server.ts`, `bootstrap.server.ts`, and the routes need no change. Delete
1904
+ `webpack.server.config.ts`.
1905
+
1906
+ ## Targets
1907
+
1908
+ Swap the host's `serve-ssr` from `@nx/angular:module-federation-dev-ssr` to
1909
+ `@angular-devkit/build-angular:ssr-dev-server`. Keep `browserTarget` and `serverTarget`, and add
1910
+ an explicit `port`. Remotes keep their `serve-ssr` target. Start them together:
1911
+
1912
+ ```shell
1913
+ nx run-many -t serve-ssr -p shell remoteA
1914
+ ```
1915
+
1916
+ Restart the host's `serve-ssr` to pick up a remote's server-side changes.
1917
+
1918
+ For a production-like run, start `node dist/apps/<app>/server/main.js` with `PORT` set.
1919
+
1920
+ ## Validate the result
1921
+
1922
+ Request each host route that renders a remote, for example with `curl`, and check that the
1923
+ response holds the remote's markup and the client `<script>` tags. Then load it in a browser and
1924
+ check that it hydrates without errors.
1925
+
1926
+ Keep `@module-federation/node` in `dependencies` when you remove `@nx/module-federation`.
1927
+
1928
+ ## What the official plugin does not do
1929
+
1930
+ - **Dev orchestration.** The SSR dev-server executor started the remotes with the host and
1931
+ proxied the static ones. Start each app's `serve-ssr` yourself.