@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.
- package/README.md +1 -3
- package/dist/angular.d.ts +1 -5
- package/dist/angular.js +7 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -2
- package/dist/rspack.d.ts +1 -5
- package/dist/rspack.js +7 -6
- package/dist/src/migrations/update-24-0-0/inventory.d.ts +11 -0
- package/dist/src/migrations/update-24-0-0/inventory.js +139 -0
- package/dist/src/migrations/update-24-0-0/migrate-angular-module-federation.d.ts +5 -0
- package/dist/src/migrations/update-24-0-0/migrate-angular-module-federation.js +18 -0
- package/dist/src/migrations/update-24-0-0/migrate-module-federation.d.ts +10 -0
- package/dist/src/migrations/update-24-0-0/migrate-module-federation.js +26 -0
- package/dist/src/migrations/update-24-0-0/migrate-module-federation.md +1931 -0
- package/dist/src/migrations/update-24-0-0/migrate-react-module-federation.d.ts +5 -0
- package/dist/src/migrations/update-24-0-0/migrate-react-module-federation.js +18 -0
- package/dist/src/stubs/models.d.ts +26 -0
- package/dist/src/stubs/stubs.d.ts +19 -0
- package/dist/src/stubs/stubs.js +43 -0
- package/dist/webpack.d.ts +1 -2
- package/dist/webpack.js +4 -3
- package/migrations.json +6 -0
- package/package.json +5 -60
- package/readme-template.md +1 -3
- package/dist/internal.d.ts +0 -2
- package/dist/internal.js +0 -13
- package/dist/src/executors/utils/build-static-remotes.d.ts +0 -4
- package/dist/src/executors/utils/build-static-remotes.js +0 -67
- package/dist/src/executors/utils/index.d.ts +0 -4
- package/dist/src/executors/utils/index.js +0 -6
- package/dist/src/executors/utils/models.d.ts +0 -30
- package/dist/src/executors/utils/start-remote-iterators.d.ts +0 -25
- package/dist/src/executors/utils/start-remote-iterators.js +0 -52
- package/dist/src/executors/utils/start-static-remotes-file-server.d.ts +0 -15
- package/dist/src/executors/utils/start-static-remotes-file-server.js +0 -61
- package/dist/src/plugins/models/index.d.ts +0 -14
- package/dist/src/plugins/models/index.js +0 -2
- package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-dev-server-plugin.d.ts +0 -13
- package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-dev-server-plugin.js +0 -75
- package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-plugin.d.ts +0 -11
- package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-plugin.js +0 -93
- package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-ssr-dev-server-plugin.d.ts +0 -15
- package/dist/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-ssr-dev-server-plugin.js +0 -107
- package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-dev-server-plugin.d.ts +0 -13
- package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-dev-server-plugin.js +0 -75
- package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-plugin.d.ts +0 -11
- package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-plugin.js +0 -80
- package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-ssr-dev-server-plugin.d.ts +0 -15
- package/dist/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-ssr-dev-server-plugin.js +0 -107
- package/dist/src/plugins/utils/build-static-remotes.d.ts +0 -3
- package/dist/src/plugins/utils/build-static-remotes.js +0 -65
- package/dist/src/plugins/utils/get-dynamic-manifest-file.d.ts +0 -2
- package/dist/src/plugins/utils/get-dynamic-manifest-file.js +0 -18
- package/dist/src/plugins/utils/get-remotes.d.ts +0 -7
- package/dist/src/plugins/utils/get-remotes.js +0 -68
- package/dist/src/plugins/utils/get-static-remotes.d.ts +0 -3
- package/dist/src/plugins/utils/get-static-remotes.js +0 -27
- package/dist/src/plugins/utils/index.d.ts +0 -7
- package/dist/src/plugins/utils/index.js +0 -10
- package/dist/src/plugins/utils/parse-remotes-config.d.ts +0 -6
- package/dist/src/plugins/utils/parse-remotes-config.js +0 -35
- package/dist/src/plugins/utils/start-remote-proxies.d.ts +0 -5
- package/dist/src/plugins/utils/start-remote-proxies.js +0 -65
- package/dist/src/plugins/utils/start-static-remotes-file-server.d.ts +0 -2
- package/dist/src/plugins/utils/start-static-remotes-file-server.js +0 -46
- package/dist/src/utils/dependencies.d.ts +0 -6
- package/dist/src/utils/dependencies.js +0 -70
- package/dist/src/utils/framework-detection.d.ts +0 -2
- package/dist/src/utils/framework-detection.js +0 -20
- package/dist/src/utils/get-remotes-for-host.d.ts +0 -17
- package/dist/src/utils/get-remotes-for-host.js +0 -154
- package/dist/src/utils/index.d.ts +0 -11
- package/dist/src/utils/index.js +0 -14
- package/dist/src/utils/is-serve-mode.d.ts +0 -2
- package/dist/src/utils/is-serve-mode.js +0 -19
- package/dist/src/utils/models/index.d.ts +0 -47
- package/dist/src/utils/models/index.js +0 -2
- package/dist/src/utils/module-federation-config.d.ts +0 -57
- package/dist/src/utils/module-federation-config.js +0 -144
- package/dist/src/utils/normalize-project-name.d.ts +0 -1
- package/dist/src/utils/normalize-project-name.js +0 -10
- package/dist/src/utils/package-json.d.ts +0 -2
- package/dist/src/utils/package-json.js +0 -12
- package/dist/src/utils/parse-static-remotes-config.d.ts +0 -13
- package/dist/src/utils/parse-static-remotes-config.js +0 -62
- package/dist/src/utils/plugins/runtime-library-control.plugin.d.ts +0 -3
- package/dist/src/utils/plugins/runtime-library-control.plugin.js +0 -54
- package/dist/src/utils/port-utils.d.ts +0 -7
- package/dist/src/utils/port-utils.js +0 -22
- package/dist/src/utils/public-api.d.ts +0 -7
- package/dist/src/utils/public-api.js +0 -18
- package/dist/src/utils/remotes.d.ts +0 -19
- package/dist/src/utils/remotes.js +0 -75
- package/dist/src/utils/rspack-version.d.ts +0 -3
- package/dist/src/utils/rspack-version.js +0 -11
- package/dist/src/utils/secondary-entry-points.d.ts +0 -12
- package/dist/src/utils/secondary-entry-points.js +0 -125
- package/dist/src/utils/share.d.ts +0 -58
- package/dist/src/utils/share.js +0 -334
- package/dist/src/utils/start-remote-proxies.d.ts +0 -5
- package/dist/src/utils/start-remote-proxies.js +0 -55
- package/dist/src/utils/start-ssr-remote-proxies.d.ts +0 -5
- package/dist/src/utils/start-ssr-remote-proxies.js +0 -69
- package/dist/src/utils/typescript.d.ts +0 -4
- package/dist/src/utils/typescript.js +0 -53
- package/dist/src/utils/url-helpers.d.ts +0 -13
- package/dist/src/utils/url-helpers.js +0 -70
- package/dist/src/with-module-federation/angular/utils.d.ts +0 -28
- package/dist/src/with-module-federation/angular/utils.js +0 -83
- package/dist/src/with-module-federation/angular/with-module-federation-ssr.d.ts +0 -2
- package/dist/src/with-module-federation/angular/with-module-federation-ssr.js +0 -86
- package/dist/src/with-module-federation/angular/with-module-federation.d.ts +0 -2
- package/dist/src/with-module-federation/angular/with-module-federation.js +0 -85
- package/dist/src/with-module-federation/react/utils.d.ts +0 -2
- package/dist/src/with-module-federation/react/utils.js +0 -17
- package/dist/src/with-module-federation/rspack/utils.d.ts +0 -10
- package/dist/src/with-module-federation/rspack/utils.js +0 -32
- package/dist/src/with-module-federation/rspack/with-module-federation-ssr.d.ts +0 -4
- package/dist/src/with-module-federation/rspack/with-module-federation-ssr.js +0 -75
- package/dist/src/with-module-federation/rspack/with-module-federation.d.ts +0 -9
- package/dist/src/with-module-federation/rspack/with-module-federation.js +0 -82
- package/dist/src/with-module-federation/webpack/utils.d.ts +0 -10
- package/dist/src/with-module-federation/webpack/utils.js +0 -32
- package/dist/src/with-module-federation/webpack/with-module-federation-ssr.d.ts +0 -2
- package/dist/src/with-module-federation/webpack/with-module-federation-ssr.js +0 -69
- package/dist/src/with-module-federation/webpack/with-module-federation.d.ts +0 -5
- package/dist/src/with-module-federation/webpack/with-module-federation.js +0 -72
- package/dist/url-helpers.d.ts +0 -13
- package/dist/url-helpers.js +0 -78
- package/executors.json +0 -4
- /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.
|