Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5694178
docs(spec): add fetchOptions ESM remote loading design
Jun 16, 2026
4fd6b31
docs(plan): add fetchOptions ESM remote loading implementation plan
Jun 16, 2026
bd36254
feat(sdk): add blob-loader import rewriter and resolveSpec
Jun 16, 2026
5265f3d
test(sdk): cover BlobDep splice fields and side-effect imports
Jun 16, 2026
8748e5a
feat(sdk): add fetch+blob ESM loader (loadEsmEntryWithFetch, loadCssW…
Jun 16, 2026
f0e831e
fix(sdk): normalize headers, evict failed loads, refresh context, ded…
Jun 16, 2026
033ca3c
feat(runtime-core): thread per-remote fetchOptions through registerRe…
Jun 16, 2026
1f23f1e
feat(runtime-core): route ESM remotes with fetchOptions to blob loader
Jun 16, 2026
82b7e6e
feat(runtime-core): authenticate manifest fetch with remote fetchOptions
Jun 16, 2026
b9c816a
feat(runtime-core): fetch manifest CSS with headers for fetchOptions …
Jun 16, 2026
0d1841b
fix(sdk): dedupe in-flight css loads; harden preload css test
Jun 16, 2026
5ae69d6
chore: changeset + document deferred limitations for fetchOptions ESM…
Jun 16, 2026
6c9b7a8
refactor(sdk): rename js module cache to jsCache for clarity
Jun 17, 2026
f5a3b2d
refactor(sdk): simplify fetchText init construction to single spread
Jun 17, 2026
f56a462
chore(ts): add dom.iterable to base lib; drop Headers cast in blobLoad
Jun 17, 2026
60cfc38
refactor(sdk): clean up blobLoad (rename shim, clarify comments)
Jun 17, 2026
af04afe
feat(runtime-core): merge call-level and per-remote fetchOptions
Jun 17, 2026
796aa05
docs(runtime-core): tidy comments in preload css fetch path
Jun 17, 2026
2593a4d
fix(runtime-core): forward fetchOptions to remoteEntry preload
Jun 17, 2026
220b946
docs(runtime): document registerRemotes fetchOptions; remove scratch …
Jun 17, 2026
b4d95c2
fix(runtime,sdk): address PR review on remote fetchOptions
Jun 17, 2026
cd28a33
refactor(sdk): hang blob-load context registry off __mfDyn
Jun 17, 2026
4afa878
refactor(sdk): drop dynImportInstalled flag from blob loader
Jun 17, 2026
89b99c4
refactor(sdk): rename blob loader symbols for clarity
Jun 17, 2026
bb56552
refactor(runtime-core): collapse preload CSS branches into one helper
Jun 17, 2026
7e65201
refactor(runtime-core): drive remote header auth via fetch hook, not …
Jun 23, 2026
090f153
test(runtime-core): drop manifest-fetch-hook spec covering pre-existi…
Jun 23, 2026
72d432c
test(runtime-core): tidy fetch-hook spec naming and helpers
Jun 23, 2026
d06e3bf
docs(runtime): simplify fetch-hook header docs in en and zh
Jun 23, 2026
fb0b299
docs(changeset): clarify and reorganize remote-fetch-options entry
Jun 23, 2026
59af18f
rebase test to use rstest
Jul 7, 2026
9783266
fix occurance of vi
Jul 7, 2026
d5d9ab2
test: stub fetch and blob primitives instead of spying on sdk exports
Jul 7, 2026
6061b9b
docs(runtime): add pt-BR translation for fetch-hook header docs
Jul 10, 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
10 changes: 10 additions & 0 deletions .changeset/remote-fetch-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@module-federation/runtime-core': minor
'@module-federation/sdk': minor
---

Support loading remote assets in the manifest with custom headers through the existing runtime `fetch` hook.

- When a `fetch` hook plugin is registered, `module`/`esm` remotes load their remote entry and manifest-declared CSS through a fetch + blob loader, instead of native `import()` / `<link>`.
- The hook receives `remoteInfo`, so headers can be set per remote and support dynamic cases such as token refresh.
- When no `fetch` hook is present, remotes load exactly as before.
39 changes: 39 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 @@ -305,6 +305,45 @@ When `force: true` is set, the newly registered modules will overwrite already-r

:::

### Loading remotes with custom headers via the `fetch` hook

Some remotes require specific headers to be accessed. In that case you can configure the headers through the runtime [`fetch` hook](./runtime-hooks#fetch). You can handle dynamic cases such as token refresh or per-request auth logic inside this hook.

For **`module` / `esm`** remotes, when a `fetch` hook is registered the remote entry and its manifest-declared CSS are loaded through a `fetch` + blob loader so the headers you add reach every asset request. With no `fetch` hook registered, remotes load as before via native `import()` / `<link>` (which cannot carry custom headers).

```ts
import { createInstance } from '@module-federation/enhanced/runtime';

const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'sub1',
entry: 'https://localhost:2001/mf-manifest.json',
type: 'module',
},
],
plugins: [
{
name: 'auth-fetch',
// `remoteInfo` lets you use different request options for specific remotes
fetch(url, init, remoteInfo) {
return fetch(url, {
...init,
headers: { ...init.headers, Authorization: `Bearer ${token}` },
});
},
},
],
});
```

:::info

Native preload hints (`<link rel="modulepreload">` / `<link rel="preload">`) cannot carry headers, so they may fail on an authenticated origin; the actual loads still go through the `fetch` hook when the asset is used. Authenticated CSS is injected directly and does not emit the `createLink` hook, so plugin-added `nonce`/SRI attributes are not applied to it.

:::

<Tabs>
<Tab label="Build Plugin (Use build plugin)">
```tsx
Expand Down
3 changes: 2 additions & 1 deletion apps/website-new/docs/en/guide/runtime/runtime-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,8 @@ const changeScriptAttributePlugin: () => ModuleFederationRuntimePlugin =

`AsyncHook`

The `fetch` function allows customizing the request that fetches the manifest JSON. A successful `Response` must yield a valid JSON.
The `fetch` function allows customizing the request used to fetch the manifest JSON; a successfully loaded manifest `Response` must yield a valid JSON.
You can also use the `fetch` function to apply custom `headers` (e.g. `Authorization`) to the remote entry and manifest-declared CSS resources of **`module` / `esm`** remotes.

```typescript
function fetch(
Expand Down
39 changes: 39 additions & 0 deletions apps/website-new/docs/pt-BR/guide/runtime/runtime-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,45 @@ Quando `force: true` é definir, o newly registered módulos vai overwrite já-r

:::

### Carregando remotes com cabeçalhos personalizados via hook `fetch`

Alguns remotes exigem cabeçalhos específicos para serem acessados. Nesse caso, você pode configurar os cabeçalhos através do [hook `fetch`](./runtime-hooks#fetch) do runtime. Você pode lidar com casos dinâmicos, como atualização de token ou lógica de autenticação por requisição, dentro desse hook.

Para remotes do tipo **`module` / `esm`**, quando um hook `fetch` é registrado, o remote entry e o CSS declarado no manifest são carregados através de um carregador `fetch` + blob, de modo que os cabeçalhos que você adiciona alcancem todas as requisições de recursos. Sem um hook `fetch` registrado, os remotes carregam como antes, via `import()` / `<link>` nativos (que não podem carregar cabeçalhos personalizados).

```ts
import { createInstance } from '@module-federation/enhanced/runtime';

const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'sub1',
entry: 'https://localhost:2001/mf-manifest.json',
type: 'module',
},
],
plugins: [
{
name: 'auth-fetch',
// `remoteInfo` permite usar diferentes opções de requisição para remotes específicos
fetch(url, init, remoteInfo) {
return fetch(url, {
...init,
headers: { ...init.headers, Authorization: `Bearer ${token}` },
});
},
},
],
});
```

:::info

As dicas de preload nativas (`<link rel="modulepreload">` / `<link rel="preload">`) não podem carregar cabeçalhos, portanto podem falhar em uma origem autenticada; os carregamentos reais ainda passam pelo hook `fetch` quando o recurso é utilizado. O CSS autenticado é injetado diretamente e não emite o hook `createLink`, então atributos `nonce`/SRI adicionados por plugins não são aplicados a ele.

:::

<Tabs>
<Tab label="Build Plugin (Use build plugin)">
```tsx
Expand Down
3 changes: 2 additions & 1 deletion apps/website-new/docs/pt-BR/guide/runtime/runtime-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,8 @@ const changeScriptAttributePlugin: () => ModuleFederationRuntimePlugin =

`AsyncHook`

O `fetch` função allows customizing o request esse fetches o manifest JSON. Uma successful `Response` deve yield uma valid JSON.
O `fetch` função allows customizing o request esse fetches o manifest JSON; uma manifest `Response` carregada com sucesso deve retornar um JSON válido.
Você também pode usar a função `fetch` para aplicar `headers` personalizados (por exemplo, `Authorization`) ao remote entry e aos recursos CSS declarados no manifest de remotes do tipo **`module` / `esm`**.

```typescript
function fetch(
Expand Down
39 changes: 39 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 @@ -305,6 +305,45 @@ interface RemoteWithVersion {

:::

### 通过 fetch 钩子使用自定义请求头加载 remote

某些 remote 需要特定的请求头才能访问,此时可以通过运行时的 [`fetch` 钩子](./runtime-hooks#fetch) 配置请求头。你可以在该钩子中处理诸如 token 刷新、按请求鉴权等动态场景。

对于 **`module` / `esm`** 类型的 remote:当注册了 `fetch` 钩子时,其 remote entry 以及 manifest 中声明的 CSS 都会通过 `fetch` + blob 加载器加载,从而让你添加的请求头能够应用到每一个资源请求上。未注册 `fetch` 钩子时,remote 仍按原有方式通过原生 `import()` / `<link>` 加载(这些方式无法携带自定义请求头)。

```ts
import { createInstance } from '@module-federation/enhanced/runtime';

const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'sub1',
entry: 'https://localhost:2001/mf-manifest.json',
type: 'module',
},
],
plugins: [
{
name: 'auth-fetch',
// 可通过 `remoteInfo` 对特定 remote 使用不同的请求配置
fetch(url, init, remoteInfo) {
return fetch(url, {
...init,
headers: { ...init.headers, Authorization: `Bearer ${token}` },
});
},
},
],
});
```

:::info

原生的预加载提示(`<link rel="modulepreload">` / `<link rel="preload">`)无法携带请求头,因此在需要鉴权的源上可能会失败;但最终使用时仍会通过 `fetch` 钩子加载。带鉴权的 CSS 是直接注入的,不会触发 `createLink` 钩子,因此插件添加的 `nonce`/SRI 属性不会作用于这类 CSS。

:::

<Tabs>
<Tab label="Build Plugin(使用构建插件)">
```tsx
Expand Down
3 changes: 2 additions & 1 deletion apps/website-new/docs/zh/guide/runtime/runtime-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,8 @@ const changeScriptAttributePlugin: () => ModuleFederationRuntimePlugin =

`AsyncHook`

`fetch` 函数允许自定义获取清单(manifest)JSON 的请求。成功的 `Response` 必须返回一个有效的 JSON。
`fetch` 函数允许自定义获取清单(manifest)JSON 的请求,成功加载的清单 `Response` 必须返回一个有效的 JSON。
你还可通过 `fetch` 函数将自定义的 `headers`(如 `Authorization`)应用到 **`module` / `esm`** 类型 remote 的 remote entry 及 manifest 中声明的 CSS 资源上。

```typescript
function fetch(
Expand Down
110 changes: 110 additions & 0 deletions packages/runtime-core/__tests__/load-entry-fetch-hook.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, it, expect, rs, beforeEach, afterEach } from '@rstest/core';
import { loadModule } from '@module-federation/sdk';
import { __loadEntryDomForTest } from '../src/utils/load';

// Create a mocked fetch lifecycle loader hook
function createLoaderHook(hasFetchListener: boolean) {
const listeners = new Set<any>();
if (hasFetchListener) {
listeners.add(() => undefined);
}
return {
lifecycle: {
fetch: { emit: rs.fn(), listeners },
},
} as any;
}

// Create a mocked remote info
function createRemoteInfo(name: string, entry: string) {
return {
name,
entry,
type: 'module',
entryGlobalName: name,
shareScope: 'default',
};
}

describe('loadEntryDom ESM with fetch lifecycle loader hook', () => {
let originalFetch: typeof globalThis.fetch;
let originalCreateObjectURL: typeof URL.createObjectURL;
let fetchMock: ReturnType<typeof rs.fn>;

beforeEach(() => {
rs.clearAllMocks();
loadModule.clearCache();
originalFetch = globalThis.fetch;
originalCreateObjectURL = URL.createObjectURL;
fetchMock = rs.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
text: () => Promise.resolve('export const ok = 1;'),
});
globalThis.fetch = fetchMock as any;
URL.createObjectURL = rs.fn(
() => 'data:text/javascript,export const ok = 1;',
);
});

afterEach(() => {
globalThis.fetch = originalFetch;
URL.createObjectURL = originalCreateObjectURL;
});

it('uses the blob loader for module remotes when a fetch hook is registered', async () => {
const loaderHook = createLoaderHook(true);
const resourceContext: any = {
initiator: 'loadRemote',
id: 'a/say',
resourceType: 'remoteEntry',
};
const result = await __loadEntryDomForTest({
remoteInfo: createRemoteInfo('a', 'http://x/e.js'),
loaderHook,
resourceContext,
});
expect(fetchMock).toHaveBeenCalledWith('http://x/e.js', expect.anything());
// The loader's customFetch forwards remoteInfo and resourceContext as
// additional arguments so the plugin can add different headers per remote/resource.
expect(loaderHook.lifecycle.fetch.emit).toHaveBeenCalledWith(
'http://x/e.js',
{ headers: {} },
expect.objectContaining({ name: 'a' }),
resourceContext,
);
expect(result).toEqual(expect.objectContaining({ ok: 1 }));
});

it('wraps blob loader failures as RUNTIME_008 so loadEntryError recovery can fire', async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: () => Promise.resolve(''),
});
const err = await __loadEntryDomForTest({
remoteInfo: createRemoteInfo('a', 'http://x/e.js'),
loaderHook: createLoaderHook(true),
}).then(
() => undefined,
(e: unknown) => e,
);
expect(err).toBeInstanceOf(Error);
// RUNTIME_008 = 'RUNTIME-008'; getRemoteEntry keys recovery off this code.
expect((err as Error).message).toContain('RUNTIME-008');
// The original failure is preserved for diagnostics.
expect((err as Error).message).toContain('401 Unauthorized');
});

it('does NOT use the blob loader for module remotes when no fetch hook is registered', async () => {
const loaderHook = createLoaderHook(false);
await __loadEntryDomForTest({
remoteInfo: createRemoteInfo('b', 'http://x/e2.js'),
loaderHook,
}).catch(() => undefined);
expect(fetchMock).not.toHaveBeenCalled();
expect(loaderHook.lifecycle.fetch.emit).not.toHaveBeenCalled();
});
});
Loading