Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .changeset/explicit-remote-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@module-federation/runtime-core': major
'@module-federation/runtime': major
'@module-federation/webpack-bundler-runtime': major
'@module-federation/modern-js-v3': minor
---

Replace forced remote registration with asynchronous `updateRemotes`. Identical registrations are idempotent; conflicting registrations and `force: true` now throw. Updates validate the complete batch, serialize cleanup and replacement, and update every attached bundler. Callers must await completion and coordinate application work.

Modern SSR updates support batch upserts, monotonic application revisions, duplicate suppression, observable publication status, and explicit recovery using retained target registrations. Static updates retain proven entry scope; dynamic or failed updates rebuild the application in the same process.
18 changes: 8 additions & 10 deletions apps/modernjs-ssr/host/src/routes/dynamic-remote/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getInstance,
loadRemote,
registerRemotes,
updateRemotes,
} from '@module-federation/modern-js-v3/runtime';

type LazyRemoteModule = { default: React.ComponentType<any> };
Expand Down Expand Up @@ -33,17 +34,14 @@ const NewRemoteCom = React.lazy(() =>
);
const Index = (): JSX.Element => {
const [showComponent, setShowComponent] = useState(false);
const replaceRemote = () => {
const replaceRemote = async () => {
console.log('replaceRemote click');
registerRemotes(
[
{
name: 'dynamic_remote',
entry: 'http://localhost:3056/mf-manifest.json',
},
],
{ force: true },
);
await updateRemotes([
{
name: 'dynamic_remote',
entry: 'http://localhost:3056/mf-manifest.json',
},
]);

setShowComponent(true);
};
Expand Down
19 changes: 8 additions & 11 deletions apps/modernjs-ssr/host/src/routes/remote/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, Suspense } from 'react';
import Comp from 'remote/Image';
import {
registerRemotes,
updateRemotes,
loadRemote,
} from '@module-federation/modern-js-v3/runtime';

Expand All @@ -16,17 +16,14 @@ const NewRemoteCom = React.lazy(() =>

const Index = (): JSX.Element => {
const [showComponent, setShowComponent] = useState(false);
const replaceRemote = () => {
const replaceRemote = async () => {
console.log('replaceRemote click');
registerRemotes(
[
{
name: 'remote',
entry: 'http://localhost:3055/mf-manifest.json',
},
],
{ force: true },
);
await updateRemotes([
{
name: 'remote',
entry: 'http://localhost:3055/mf-manifest.json',
},
]);

setShowComponent(true);
};
Expand Down
22 changes: 10 additions & 12 deletions apps/modernjs-ssr/host/src/routes/remove-remote-cache/probe.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
loadRemote,
registerRemotes,
updateRemotes,
removeRemote,
} from '@module-federation/modern-js-v3/runtime';
import { mkdirSync } from 'node:fs';
Expand Down Expand Up @@ -481,16 +482,13 @@ const collectDelayedGcSnapshots = async (delayedGcSeconds: number[]) => {
return snapshots;
};

const registerRemoteV1 = () => {
registerRemotes(
[
{
name: 'remote',
entry: remoteV1Entry,
},
],
{ force: true },
);
const registerRemoteV1 = async () => {
await updateRemotes([
{
name: 'remote',
entry: remoteV1Entry,
},
]);
};

const registerRemoteV2 = () => {
Expand All @@ -508,7 +506,7 @@ export const runLoadRemoteStep = async (): Promise<ProbeResult> => {
} catch {
// This route is the first step and should be repeatable after prior runs.
}
registerRemoteV1();
await registerRemoteV1();

const heavyStats = await loadHeavyStats();
const gcAvailable = forceGc();
Expand Down Expand Up @@ -583,7 +581,7 @@ export const runProbe = async ({
} catch {
// Reset state so the first request can be repeated locally.
}
registerRemoteV1();
await registerRemoteV1();

forceGc();
const beforeLoad = snapshot('before load');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
loadRemote,
registerRemotes,
updateRemotes,
removeRemote,
} from '@module-federation/modern-js-v3/runtime';
import { mkdirSync } from 'node:fs';
Expand Down Expand Up @@ -127,7 +127,7 @@ export const runSharedProviderProbe = async ({
} catch {
// Reset the state for a fresh SSR request sequence.
}
registerRemotes(remoteEntries, { force: true });
await updateRemotes(remoteEntries);
const consumer = (await loadRemote(
`${consumerName}/SharedConsumer`,
)) as SharedModule;
Expand Down
21 changes: 11 additions & 10 deletions apps/node-host/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import express from 'express';
import * as path from 'path';
import node_local_remote from 'node_local_remote/test';
import { registerRemotes, loadRemote } from '@module-federation/runtime';
import {
registerRemotes,
updateRemotes,
loadRemote,
} from '@module-federation/runtime';

registerRemotes([
{
Expand Down Expand Up @@ -54,15 +58,12 @@ app.get('/dynamic-remote', async (req, res) => {
});

app.get('/upgrade-remote', async (req, res) => {
registerRemotes(
[
{
name: 'node_dynamic_remote',
entry: 'http://localhost:3027/remoteEntry.js',
},
],
{ force: true },
);
await updateRemotes([
{
name: 'node_dynamic_remote',
entry: 'http://localhost:3027/remoteEntry.js',
},
]);

res.send({
message: 'Upgrade success!',
Expand Down
20 changes: 9 additions & 11 deletions apps/runtime-demo/3005-runtime-host/src/ObservabilityDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
preloadRemote,
registerPlugins,
registerRemotes,
updateRemotes,
} from '@module-federation/runtime';
import { observability } from './observability';

Expand Down Expand Up @@ -349,7 +350,7 @@ export default function ObservabilityDemo() {
setErrorMessage('');
setRemoteComponent(null);

registerRemotes([scenario.remote], { force: true });
await updateRemotes([scenario.remote]);

try {
await loadRemote(scenario.request);
Expand Down Expand Up @@ -399,16 +400,13 @@ export default function ObservabilityDemo() {
setRemoteComponent(null);

registerPlugins([observabilityRetryRecoveryPlugin]);
registerRemotes(
[
{
name: retryRecoveryRemoteName,
alias: 'observability-retry-recovered',
entry: retryRecoveryManifestEntry,
},
],
{ force: true },
);
await updateRemotes([
{
name: retryRecoveryRemoteName,
alias: 'observability-retry-recovered',
entry: retryRecoveryManifestEntry,
},
]);

try {
const remoteModule = await loadRemote(retryRecoveryRequest);
Expand Down
34 changes: 26 additions & 8 deletions apps/website-new/docs/en/guide/runtime/runtime-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -299,16 +299,34 @@ interface RemoteWithVersion {

</Collapse>

:::warning
`registerRemotes` only adds new remotes. Registering an identical configuration
again is a no-op. Conflicting configurations and `force: true` throw an error;
use `await updateRemotes(...)` to replace an existing remote.

## updateRemotes

```typescript
function updateRemotes(remotes: Remote[]): Promise<void>;
```

The list is an upsert: unknown names are added, existing names are replaced,
and unspecified remotes are preserved. The complete batch is validated before
cleanup. Updates are serialized per MF instance and reject if cleanup or
registration fails. Retry explicitly after handling the failure; a batch is not
a transaction and does not roll back mutations that already happened.

Await completion before loading the updated remote. The generic runtime does
not drain HTTP requests, reset framework renderers, or undo business side effects.
For Modern SSR, use the application owner's SSR update adapter so request
admission, entry invalidation or application rebuilding, and publication are
coordinated together. Do not await a nested update from an update lifecycle hook.

When `force: true` is set, the newly registered modules will overwrite already-registered and loaded modules, and the cache of the loaded modules will be automatically deleted. A warning will also be printed to the console to inform you that this operation is risky.

:::

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

// register new remote sub2
registerRemotes([
Expand All @@ -319,12 +337,12 @@ When `force: true` is set, the newly registered modules will overwrite already-r
]);

// override remote sub1
registerRemotes([
await updateRemotes([
{
name: 'sub1',
entry: 'http://localhost:2003/mf-manifest.json',
}
], { force: true });
]);
```
</Tab>
<Tab label="Pure Runtime (Not use build plugin)">
Expand All @@ -350,12 +368,12 @@ When `force: true` is set, the newly registered modules will overwrite already-r
]);

// override remote sub1
mf.registerRemotes([
await mf.updateRemotes([
{
name: 'sub1',
entry: 'http://localhost:2003/mf-manifest.json',
}
], { force: true });
]);
```
</Tab>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
# Dynamic Remotes

```js
import { loadRemote, init } from '@module-federation/runtime';
// if i have remotes in my federation plugin, i can pass the name of the remote
loadRemote('home/exposedModule')
// if i want to load a custom remote not known at build time.
init({
name: 'hostname',
remotes: [
{
name: 'home',
entry: 'http://somthing.com/remoteEntry.js'
}
],
force: true // may be needed to sideload remotes after the fact.
})
loadRemote('home/exposedModule')
import { loadRemote, updateRemotes } from '@module-federation/runtime';

// The build plugin initializes the host instance. Add or replace a remote,
// then wait for its previous cache cleanup before loading the new entry.
await updateRemotes([
{ name: 'home', entry: 'http://example.com/remoteEntry.js' },
]);
await loadRemote('home/exposedModule');
```
32 changes: 24 additions & 8 deletions apps/website-new/docs/pt-BR/guide/runtime/runtime-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -299,16 +299,32 @@ interface RemoteWithVersion {

</Collapse>

:::warning
`registerRemotes` adiciona apenas novos remotes. Repetir uma configuração
idêntica não altera o estado. Configurações conflitantes e `force: true`
geram um erro; use `await updateRemotes(...)` para substituir um remote.

Quando `force: true` é definir, o newly registered módulos vai overwrite já-registered e carregado módulos, e o cache do carregado módulos vai ser automatically deleted. Uma aviso vai também ser printed para o console para inform você esse este operation é risky.
## updateRemotes

:::
```typescript
function updateRemotes(remotes: Remote[]): Promise<void>;
```

A lista adiciona nomes novos e substitui nomes existentes; remotes omitidos
permanecem registrados. O lote inteiro é validado antes da limpeza. Atualizações
são serializadas por instância MF e rejeitam a Promise em caso de falha.
Trate o erro antes de tentar novamente. Um lote não é uma transação e não
desfaz alterações que já ocorreram.

Aguarde a conclusão antes de carregar o remote atualizado. O runtime genérico
não drena requisições HTTP, reinicia renderizadores ou desfaz efeitos colaterais.
No Modern SSR, use o adaptador de atualização do proprietário da aplicação
para coordenar requisições, invalidação, reconstrução e publicação.
Não aguarde uma atualização aninhada em um hook de atualização.

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

// register new remote sub2
registerRemotes([
Expand All @@ -319,12 +335,12 @@ Quando `force: true` é definir, o newly registered módulos vai overwrite já-r
]);

// override remote sub1
registerRemotes([
await updateRemotes([
{
name: 'sub1',
entry: 'http://localhost:2003/mf-manifest.json',
}
], { force: true });
]);
```
</Tab>
<Tab label="Pure Runtime (Not use build plugin)">
Expand All @@ -350,12 +366,12 @@ Quando `force: true` é definir, o newly registered módulos vai overwrite já-r
]);

// override remote sub1
mf.registerRemotes([
await mf.updateRemotes([
{
name: 'sub1',
entry: 'http://localhost:2003/mf-manifest.json',
}
], { force: true });
]);
```
</Tab>
</Tabs>
Expand Down
Loading
Loading