Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a700783
feat(runtime): add unloadRemote teardown API
ScriptedAlchemy Feb 7, 2026
b64a4b0
fix(core): set actionlint working directory to runner temp
ScriptedAlchemy Feb 8, 2026
6119229
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 8, 2026
066c4e3
fix(rsbuild-plugin): resolve parseOptions type import
ScriptedAlchemy Feb 8, 2026
f6d5aa3
ci(core): align devtools build setup with checkout-install
ScriptedAlchemy Feb 8, 2026
02581f8
fix(runtime): gate unload teardown and move bundler cache cleanup
ScriptedAlchemy Feb 8, 2026
2f17228
refactor(runtime): move unloadRemote to optional unload entry
ScriptedAlchemy Feb 8, 2026
a9a123a
refactor(runtime): move unload internals to runtime-core
ScriptedAlchemy Feb 8, 2026
682c58f
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 9, 2026
d570a3f
Merge remote-tracking branch 'origin/main' into feat/issue-4160-unloa…
ScriptedAlchemy Feb 9, 2026
a9dd97a
Merge branch 'feat/issue-4160-unload-remote-api' of github.com:module…
ScriptedAlchemy Feb 9, 2026
916fa2a
Bundler runtime as plugin (#4393)
ScriptedAlchemy Feb 9, 2026
6415402
Merge remote-tracking branch 'origin/main' into feat/issue-4160-unloa…
ScriptedAlchemy Feb 9, 2026
5cf477e
Merge remote-tracking branch 'origin/main' into feat/issue-4160-unloa…
ScriptedAlchemy Feb 12, 2026
01ad6ad
chore(core): add changeset coverage for pr #4379
ScriptedAlchemy Feb 12, 2026
789e174
chore(core): add contextual integration changeset for unload API
ScriptedAlchemy Feb 12, 2026
218aea1
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 14, 2026
2f12a09
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 14, 2026
e0bab25
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 15, 2026
b3b32ed
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 15, 2026
a0b4286
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 16, 2026
d43cedd
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 19, 2026
7f9b14e
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 19, 2026
342c38d
fix(runtime): guard unload cache purge by loaded state
ScriptedAlchemy Feb 24, 2026
00d5a5e
Merge remote-tracking branch 'origin/main' into feat/issue-4160-unloa…
ScriptedAlchemy Feb 24, 2026
c286b42
fix(dts-plugin): align workspace entrypoints and RawSource typing
ScriptedAlchemy Feb 24, 2026
5b7e246
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 25, 2026
226bc11
fix(sdk): align package entrypoints with emitted artifacts
ScriptedAlchemy Feb 25, 2026
0cc0ae8
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 25, 2026
c1860df
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 26, 2026
68ae344
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 26, 2026
3b8ee16
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 27, 2026
11f06dd
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 27, 2026
cb37d4b
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 28, 2026
b77c420
chore: merge main into feat/issue-4160-unload-remote-api
ScriptedAlchemy Feb 28, 2026
201e1e7
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Mar 2, 2026
44b57c4
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Mar 3, 2026
5a24735
Merge branch 'main' into feat/issue-4160-unload-remote-api
ScriptedAlchemy Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lucky-fishes-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@module-federation/enhanced': patch
'@module-federation/rsbuild-plugin': patch
'@module-federation/runtime-tools': patch
---

Update enhanced and tooling integrations to consume the new optional runtime unload entrypoints.
15 changes: 15 additions & 0 deletions .changeset/soft-carpets-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@module-federation/runtime': minor
'@module-federation/runtime-core': minor
'@module-federation/webpack-bundler-runtime': minor
---

feat(runtime): move remote unload APIs to optional `@module-federation/runtime/unload` entry

- Removes `unloadRemote` from baseline `ModuleFederation` and `@module-federation/runtime` root exports to reduce default payload.
- Adds optional `@module-federation/runtime/unload` entrypoint with:
- `unloadRemote(nameOrAlias)` for the active runtime instance.
- `unloadRemoteFromInstance(instance, nameOrAlias)` for explicit instance control.
- Keeps deterministic unload behavior when using the optional entrypoint, including:
- runtime bookkeeping cleanup (`moduleCache`, manifest/snapshot markers, preloaded map entries, id-to-remote mappings),
- webpack bundler module cache cleanup (`__webpack_require__.c`/`m`) and remote load marker reset.
9 changes: 7 additions & 2 deletions .github/workflows/devtools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'

- name: Remove cached node_modules
run: rm -rf node_modules .nx

- name: Set Playwright cache status
run: |
if [ -d "$HOME/.cache/ms-playwright" ] || [ -d "$HOME/.cache/Cypress" ]; then
Expand All @@ -54,13 +57,15 @@ jobs:
uses: nrwl/nx-set-shas@v4

- name: Install Dependencies
run: pnpm install --frozen-lockfile && find . -maxdepth 6 -type d \( -name ".cache" -o -name ".modern-js" \) -exec rm -rf {} +
run: pnpm install --frozen-lockfile

- name: Install Cypress
run: npx cypress install

- name: Run Affected Build
run: npx nx run-many --targets=build --projects=tag:type:pkg
run: |
npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4 --skip-nx-cache
npx nx run-many --targets=build --projects=tag:type:pkg --parallel=4

- name: Configuration xvfb
shell: bash
Expand Down
41 changes: 41 additions & 0 deletions apps/website-new/docs/en/guide/runtime/runtime-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,47 @@ loadShare('react', {
</Tab>
</Tabs>

## unloadRemote

- Type: `unloadRemote(nameOrAlias: string): boolean`
- Removes a registered remote and clears runtime state for that remote in the current host instance.
- Returns `true` when the remote exists and is removed, otherwise `false`.

Use this API when you need deterministic teardown before switching remote entries, re-registering remotes, or cleaning up host state in long-lived sessions.

<Tabs>
<Tab label="Build Plugin(Use build plugin)">
```tsx
import { unloadRemote } from '@module-federation/enhanced/runtime';

// Remove by remote name
unloadRemote('remote');

// Remove by alias
unloadRemote('app1');
```
</Tab>
<Tab label="Pure Runtime(Not use build plugin)">
```ts
import { createInstance } from '@module-federation/enhanced/runtime';

const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'remote',
alias: 'app1',
entry: 'http://localhost:2001/mf-manifest.json',
},
],
});

mf.unloadRemote('remote');
mf.unloadRemote('app1');
```
</Tab>
</Tabs>

## preloadRemote

<Collapse>
Expand Down
41 changes: 41 additions & 0 deletions apps/website-new/docs/zh/guide/runtime/runtime-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,47 @@ loadShare('react', {
</Tab>
</Tabs>

## unloadRemote

- Type: `unloadRemote(nameOrAlias: string): boolean`
- 从当前 host 实例中移除已注册的 remote,并清理该 remote 对应的运行时状态。
- 当 remote 存在并移除成功时返回 `true`,否则返回 `false`。

当你需要在切换 remote 地址、重新注册 remote,或在长生命周期应用中做确定性清理时,可以使用该 API。

<Tabs>
<Tab label="Build Plugin(使用构建插件)">
```tsx
import { unloadRemote } from '@module-federation/enhanced/runtime';

// 按 remote name 移除
unloadRemote('remote');

// 按 alias 移除
unloadRemote('app1');
```
</Tab>
<Tab label="Pure Runtime(未使用构建插件)">
```ts
import { createInstance } from '@module-federation/enhanced/runtime';

const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'remote',
alias: 'app1',
entry: 'http://localhost:2001/mf-manifest.json',
},
],
});

mf.unloadRemote('remote');
mf.unloadRemote('app1');
```
</Tab>
</Tabs>

## preloadRemote

<Collapse>
Expand Down
1 change: 1 addition & 0 deletions packages/enhanced/src/rspack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export {
TreeShakingSharedPlugin,
PLUGIN_NAME,
} from '@module-federation/rspack/plugin';
export { parseOptions } from './lib/container/options';
export { createModuleFederationConfig } from '@module-federation/sdk';
2 changes: 1 addition & 1 deletion packages/rsbuild-plugin/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { parseOptions } from '@module-federation/enhanced';
import {
ModuleFederationPlugin,
TreeShakingSharedPlugin,
PLUGIN_NAME,
parseOptions,
} from '@module-federation/enhanced/rspack';
import { isRequiredVersion, getManifestFileName } from '@module-federation/sdk';
import pkgJson from '../../package.json';
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime-core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ export class ModuleFederation {
return this.remoteHandler.registerRemotes(remotes, options);
}

unloadRemote(nameOrAlias: string): boolean {
return this.remoteHandler.unloadRemote(nameOrAlias);
}

registerShared(shared: UserOptions['shared']) {
this.sharedHandler.registerShared(this.options, {
...this.options,
Expand Down
95 changes: 72 additions & 23 deletions packages/runtime-core/src/remote/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ export class RemoteHandler {
remote: Remote;
origin: ModuleFederation;
}>('registerRemote'),
afterRemoveRemote: new SyncHook<
[
{
remote: Remote;
origin: ModuleFederation;
loaded: boolean;
},
],
void
>('afterRemoveRemote'),
beforeRequest: new AsyncWaterfallHook<{
id: string;
options: Options;
Expand Down Expand Up @@ -194,6 +204,17 @@ export class RemoteHandler {
}
}

unloadRemote(nameOrAlias: string): boolean {
const remote = this.host.options.remotes.find(
(item) => item.name === nameOrAlias || item.alias === nameOrAlias,
);
if (!remote) {
return false;
}
this.removeRemote(remote);
return true;
}

// eslint-disable-next-line max-lines-per-function
// eslint-disable-next-line @typescript-eslint/member-ordering
async loadRemote<T>(
Expand Down Expand Up @@ -472,8 +493,16 @@ export class RemoteHandler {
host.options.remotes.splice(remoteIndex, 1);
}
const loadedModule = host.moduleCache.get(remote.name);
const remoteInfo = loadedModule
? loadedModule.remoteInfo
: getRemoteInfo(remote);

if (remoteInfo.entry) {
host.snapshotHandler.manifestCache.delete(remoteInfo.entry);
}

// Only clean global/share state when this host actually loaded the remote.
if (loadedModule) {
const remoteInfo = loadedModule.remoteInfo;
const key = remoteInfo.entryGlobalName as keyof typeof CurrentGlobal;

if (CurrentGlobal[key]) {
Expand All @@ -486,16 +515,12 @@ export class RemoteHandler {
CurrentGlobal[key] = undefined;
}
}
const remoteEntryUniqueKey = getRemoteEntryUniqueKey(
loadedModule.remoteInfo,
);
const remoteEntryUniqueKey = getRemoteEntryUniqueKey(remoteInfo);

if (globalLoading[remoteEntryUniqueKey]) {
delete globalLoading[remoteEntryUniqueKey];
}

host.snapshotHandler.manifestCache.delete(remoteInfo.entry);

// delete unloaded shared and instance
let remoteInsId = remoteInfo.buildVersion
? composeKeyWithSeparator(remoteInfo.name, remoteInfo.buildVersion)
Expand Down Expand Up @@ -574,27 +599,51 @@ export class RemoteHandler {
);
CurrentGlobal.__FEDERATION__.__INSTANCES__.splice(remoteInsIndex, 1);
}
}

const { hostGlobalSnapshot } = getGlobalRemoteInfo(remote, host);
if (hostGlobalSnapshot) {
const remoteKey =
hostGlobalSnapshot &&
'remotesInfo' in hostGlobalSnapshot &&
hostGlobalSnapshot.remotesInfo &&
getInfoWithoutType(hostGlobalSnapshot.remotesInfo, remote.name).key;
if (remoteKey) {
delete hostGlobalSnapshot.remotesInfo[remoteKey];
if (
//eslint-disable-next-line no-extra-boolean-cast
Boolean(Global.__FEDERATION__.__MANIFEST_LOADING__[remoteKey])
) {
delete Global.__FEDERATION__.__MANIFEST_LOADING__[remoteKey];
}
}
host.moduleCache.delete(remote.name);
Object.keys(this.idToRemoteMap).forEach((id) => {
if (this.idToRemoteMap[id]?.name === remote.name) {
delete this.idToRemoteMap[id];
}
});

const remotePrefixes = [remote.name, remote.alias].filter(
Boolean,
) as string[];
const preloadedMap = Global.__FEDERATION__.__PRELOADED_MAP__;
Array.from(preloadedMap.keys()).forEach((key) => {
if (
remotePrefixes.some(
(prefix) => key === prefix || key.startsWith(`${prefix}/`),
)
) {
preloadedMap.delete(key);
}
});

host.moduleCache.delete(remote.name);
const { hostGlobalSnapshot } = getGlobalRemoteInfo(remote, host);
if (hostGlobalSnapshot) {
const remoteKey =
hostGlobalSnapshot &&
'remotesInfo' in hostGlobalSnapshot &&
hostGlobalSnapshot.remotesInfo &&
getInfoWithoutType(hostGlobalSnapshot.remotesInfo, remote.name).key;
if (remoteKey) {
delete hostGlobalSnapshot.remotesInfo[remoteKey];
if (
//eslint-disable-next-line no-extra-boolean-cast
Boolean(Global.__FEDERATION__.__MANIFEST_LOADING__[remoteKey])
) {
delete Global.__FEDERATION__.__MANIFEST_LOADING__[remoteKey];
}
}
}
this.hooks.lifecycle.afterRemoveRemote.emit({
remote,
origin: host,
loaded: Boolean(loadedModule),
});
} catch (err) {
logger.log('removeRemote fail: ', err);
}
Expand Down
10 changes: 2 additions & 8 deletions packages/runtime-tools/src/webpack-bundler-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,2 @@
import webpackBundlerRuntime from '@module-federation/webpack-bundler-runtime';

const normalizedWebpackBundlerRuntime =
// Support both CJS module.exports payload and transpiled default payload.
(webpackBundlerRuntime as { default?: unknown }).default ??
webpackBundlerRuntime;

export default normalizedWebpackBundlerRuntime;
export { default } from '@module-federation/webpack-bundler-runtime';
export * from '@module-federation/webpack-bundler-runtime';
Loading
Loading