Skip to content

Commit 203c950

Browse files
authored
feat: array share scope (module-federation#4477)
1 parent 987716a commit 203c950

13 files changed

Lines changed: 413 additions & 16 deletions

File tree

.changeset/eighty-vans-unite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/runtime-core': patch
3+
---
4+
5+
fix(runtime-core): preserve init scope across module init and avoid self-load init loops

.changeset/jolly-cougars-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/webpack-bundler-runtime': patch
3+
---
4+
5+
feat(webpack-bundler-runtime): support multiple share scopes for remotes

apps/website-new/docs/en/guide/advanced/multiple-shared-scope.mdx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import React from 'react';
2+
import ShareScopeAlignmentPlayground from '@components/common/ShareScopeAlignmentPlayground';
3+
14
# Shared Dependency Isolation: Multiple Share Scopes
25

36
> Split `shared` dependencies into multiple named shared pools (for example `default`, `scope1`). Dependencies are only reused within the same pool.
@@ -13,7 +16,7 @@ The key idea of multiple share scopes is: **move shared dependency registration
1316

1417
## Configuration Quick Map
1518

16-
The easiest mental model is to focus on what you configure on producer, consumer, and each shared entry:
19+
The simplest way to understand multiple share scopes is to focus on what you configure on the producer, the consumer, and each shared entry. You don’t need to learn runtime internal data structures or variable names.
1720

1821
* **Producer (remote)**: use [shareScope](../../configure/shareScope) to declare which share scopes this remote initializes (default: `default`, supports `string | string[]`).
1922
* **Consumer (host)**: use [remotes[remote].shareScope](../../configure/remotes#sharescope) to declare which share scopes the host aligns with that remote (default: `default`).
@@ -25,13 +28,27 @@ When the host initializes a remote, it first aligns the share scopes based on bo
2528

2629
Below, **HostShareScope** refers to `remotes[remote].shareScope` on the host, and **RemoteShareScope** refers to `shareScope` on the remote.
2730

31+
:::warning Note
32+
33+
Although both `shareScope` and `remotes[remote].shareScope` accept `string | string[]`, don’t use arrays casually, especially `['default']` or `[]`:
34+
35+
* **Single scope**: use a string (for example `'default'`), not `['default']`. Based on the runtime implementation, `'default'` and `['default']` follow different branches for alignment/initialization. If the host uses an array and the remote uses a string, the remote aligns scopes using the host’s list; if the remote uses an array, it only processes the remote’s list.
36+
* **Empty array `[]`**: results in no scopes being initialized (no `default`, no alignment), which is almost certainly a misconfiguration.
37+
38+
:::
39+
2840
| HostShareScope (remotes[remote].shareScope) | RemoteShareScope (shareScope) | Share Pool Alignment (pseudo code) | Outcome (key points) |
2941
| --- | --- | --- | --- |
3042
| `'default'` | `'default'` | `remote['default'] = host['default']` | The `default` shared pool is fully shared between host and remote. |
3143
| `['default','scope1']` | `'default'` | `remote['default'] = host['default']; remote['scope1'] = host['scope1'] ?? {}` | The remote prepares both pools, but **only initializes shared deps for RemoteShareScope** (so only `default` is initialized). To actually resolve shared deps from `scope1`, the remote must also be configured with multiple share scopes. |
3244
| `'default'` | `['default','scope1']` | `remote['default'] = host['default']; remote['scope1'] = {}` | The remote initializes both `default`/`scope1`, but the host only provides `default`. `scope1` becomes `{}`, so deps assigned to `scope1` **cannot be reused from the host** (typically falling back to the remote’s own provided/local deps). |
3345
| `['scope1','default']` | `['scope1','scope2']` | `remote['scope1'] = host['scope1']; remote['scope2'] = host['scope2'] ?? {}` | `scope1` is aligned (reuses the host’s `scope1`). If the host has no `scope2`, it becomes `{}`. The remote initializes shared deps for its RemoteShareScope (so it will try to initialize `scope1/scope2`). |
3446

47+
### Playground
48+
49+
{props.shareScopeAlignmentPlayground ||
50+
React.createElement(ShareScopeAlignmentPlayground)}
51+
3552
Notes:
3653

3754
* If a scope does not exist on the host, the host will treat it as `{}` for this initialization, so it won’t crash due to missing scope names.
@@ -227,4 +244,3 @@ export function scopeAliasPlugin(): ModuleFederationRuntimePlugin {
227244
};
228245
}
229246
```
230-

apps/website-new/docs/zh/guide/advanced/multiple-shared-scope.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import React from 'react';
2+
import ShareScopeAlignmentPlayground from '@components/common/ShareScopeAlignmentPlayground';
3+
14
# 共享依赖隔离:多共享池(Share Scope)
25

36
> `shared` 分到多个命名的“共享池”(例如 `default``scope1`),不同池之间互不共享。
@@ -25,13 +28,28 @@ host 初始化某个 remote 时,会先根据双方的 `shareScope` 配置把
2528

2629
下面用 **HostShareScope** 表示 host(消费者)侧为某个 remote 配置的 `remotes[remote].shareScope`,用 **RemoteShareScope** 表示 remote(生产者)侧配置的 `shareScope`
2730

31+
:::warning 注意
32+
33+
`shareScope` / `remotes[remote].shareScope` 虽然都支持 `string | string[]`,但不要随意用数组,尤其不要写成 `['default']``[]`
34+
35+
* **单个 scope**:请用字符串(例如 `'default'`),不要写成 `['default']`。根据运行时实现,`'default'``['default']` 在“共享池对齐/初始化”的分支上是不同的:当 host 配的是数组而 remote 配的是字符串时,remote 会按 host 的列表去对齐 scope;而 remote 配成数组时,则只会按 remote 的列表处理。
36+
* **空数组 `[]`**:会导致没有任何 scope 被初始化(既不会初始化 `default`,也不会对齐其它 scope),基本一定是错误配置。
37+
38+
:::
39+
40+
2841
| HostShareScope(remotes[remote].shareScope) | RemoteShareScope(shareScope) | 共享池对齐(伪代码) | 最终效果(关键点) |
2942
| --- | --- | --- | --- |
3043
| `'default'` | `'default'` | `remote['default'] = host['default']` | remote 的 `shareScopeMap['default']` 指向 host 的 `shareScopeMap['default']`,共享池完全一致。 |
3144
| `['default','scope1']` | `'default'` | `remote['default'] = host['default']; remote['scope1'] = host['scope1'] ?? {}` | remote 会把 `default/scope1` 都准备好,但 **只会按 RemoteShareScope 初始化共享依赖**(因此只会初始化 `default` 共享池)。要让 remote 真正使用 `scope1` 去解析 shared,需要把 remote(生产者)也配置为多共享池。 |
3245
| `'default'` | `['default','scope1']` | `remote['default'] = host['default']; remote['scope1'] = {}` | remote 会初始化 `default/scope1` 两个 scope;但 host 只提供 `default``scope1` 会被补成空对象 `{}`,因此 remote 中声明在 `scope1` 下的 shared **无法从 host 复用**(通常会回退到自身提供/本地依赖)。 |
3346
| `['scope1','default']` | `['scope1','scope2']` | `remote['scope1'] = host['scope1']; remote['scope2'] = host['scope2'] ?? {}` | remote 会对齐 `scope1`(复用 host 的 `scope1`),`scope2` 若 host 没有会被补空 `{}`;remote 会按 RemoteShareScope 初始化共享依赖(因此会尝试初始化 `scope1/scope2` 两个共享池)。 |
3447

48+
### Playground
49+
50+
{props.shareScopeAlignmentPlayground ||
51+
React.createElement(ShareScopeAlignmentPlayground)}
52+
3553
补充说明:
3654

3755
* 当某个 scope 在 host 的 `shareScopeMap` 中不存在时,host 会先补齐为 `{}` 后再参与 init(因此不会出现 scopeName 找不到导致崩溃)。

apps/website-new/module-federation.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ const exposes = {
6262
// blog
6363
[`./error-load-remote-${LANGUAGE}`]: `./docs/${LANGUAGE}/blog/error-load-remote.mdx`,
6464

65-
// performance
66-
// [`./shared-tree-shaking-overview-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/performance/shared-treeshaking.mdx`,
6765
[`./node-${LANGUAGE}`]: `./docs/${LANGUAGE}/blog/node.mdx`,
66+
67+
// advanced
68+
[`./advanced-multiple-shared-scope-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/advanced/multiple-shared-scope.mdx`,
6869
};
6970

7071
export default createModuleFederationConfig({
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import React, { useMemo, useState } from 'react';
2+
import { NoSSR } from '../NoSSR';
3+
import type { ShareScopeAlignmentItem } from './computeInitContainerShareScopeAlignment';
4+
import { computeInitContainerShareScopeAlignment } from './computeInitContainerShareScopeAlignment';
5+
6+
type InputKind = 'unset' | 'string' | 'array';
7+
8+
function parseStringArray(raw: string): string[] {
9+
if (!raw.trim()) return [];
10+
return raw
11+
.split(',')
12+
.map((s) => s.trim())
13+
.filter(Boolean);
14+
}
15+
16+
function toShareScopeKey(
17+
kind: InputKind,
18+
raw: string,
19+
): string | string[] | undefined {
20+
if (kind === 'unset') return undefined;
21+
if (kind === 'string') {
22+
const v = raw.trim();
23+
return v ? v : undefined;
24+
}
25+
return parseStringArray(raw);
26+
}
27+
28+
export default function ShareScopeAlignmentPlayground() {
29+
const [hostKind, setHostKind] = useState<InputKind>('array');
30+
const [hostRaw, setHostRaw] = useState('default,scope1');
31+
32+
const [remoteKind, setRemoteKind] = useState<InputKind>('string');
33+
const [remoteRaw, setRemoteRaw] = useState('default');
34+
35+
const hostShareScopeKeys = useMemo(
36+
() => toShareScopeKey(hostKind, hostRaw),
37+
[hostKind, hostRaw],
38+
);
39+
const remoteShareScopeKey = useMemo(
40+
() => toShareScopeKey(remoteKind, remoteRaw),
41+
[remoteKind, remoteRaw],
42+
);
43+
44+
const result = useMemo(() => {
45+
return computeInitContainerShareScopeAlignment({
46+
hostShareScopeKeys,
47+
remoteShareScopeKey,
48+
});
49+
}, [hostShareScopeKeys, remoteShareScopeKey]);
50+
51+
const inputStyle: React.CSSProperties = {
52+
width: '100%',
53+
maxWidth: 520,
54+
padding: '8px 10px',
55+
border: '1px solid var(--rp-c-divider, #e5e7eb)',
56+
borderRadius: 8,
57+
background: 'var(--rp-c-bg, #fff)',
58+
color: 'var(--rp-c-text-1, #111827)',
59+
};
60+
61+
const selectStyle: React.CSSProperties = {
62+
...inputStyle,
63+
maxWidth: 200,
64+
};
65+
66+
const cardStyle: React.CSSProperties = {
67+
border: '1px solid var(--rp-c-divider, #e5e7eb)',
68+
borderRadius: 12,
69+
padding: 14,
70+
background: 'var(--rp-c-bg, #fff)',
71+
margin: '12px 0',
72+
};
73+
74+
const labelStyle: React.CSSProperties = {
75+
fontSize: 13,
76+
color: 'var(--rp-c-text-2, #374151)',
77+
marginBottom: 6,
78+
};
79+
80+
const rowStyle: React.CSSProperties = {
81+
display: 'flex',
82+
gap: 12,
83+
flexWrap: 'wrap',
84+
alignItems: 'center',
85+
};
86+
87+
const blockTitleStyle: React.CSSProperties = {
88+
fontSize: 13,
89+
fontWeight: 600,
90+
color: 'var(--rp-c-text-1, #111827)',
91+
margin: '14px 0 8px',
92+
};
93+
94+
return (
95+
<NoSSR>
96+
<div style={cardStyle}>
97+
<div style={rowStyle}>
98+
<div style={{ flex: '1 1 320px' }}>
99+
<div style={labelStyle}>
100+
HostShareScope (remotes[remote].shareScope)
101+
</div>
102+
<div style={rowStyle}>
103+
<select
104+
value={hostKind}
105+
onChange={(e) => setHostKind(e.target.value as InputKind)}
106+
style={selectStyle}
107+
>
108+
<option value="unset">unset</option>
109+
<option value="string">string</option>
110+
<option value="array">array</option>
111+
</select>
112+
<input
113+
value={hostRaw}
114+
onChange={(e) => setHostRaw(e.target.value)}
115+
placeholder="default,scope1"
116+
style={inputStyle}
117+
disabled={hostKind === 'unset'}
118+
/>
119+
</div>
120+
</div>
121+
122+
<div style={{ flex: '1 1 320px' }}>
123+
<div style={labelStyle}>RemoteShareScope (shareScope)</div>
124+
<div style={rowStyle}>
125+
<select
126+
value={remoteKind}
127+
onChange={(e) => setRemoteKind(e.target.value as InputKind)}
128+
style={selectStyle}
129+
>
130+
<option value="unset">unset</option>
131+
<option value="string">string</option>
132+
<option value="array">array</option>
133+
</select>
134+
<input
135+
value={remoteRaw}
136+
onChange={(e) => setRemoteRaw(e.target.value)}
137+
placeholder="default"
138+
style={inputStyle}
139+
disabled={remoteKind === 'unset'}
140+
/>
141+
</div>
142+
</div>
143+
</div>
144+
145+
{result.warnings.length > 0 ? (
146+
<div style={{ marginTop: 12 }}>
147+
<div style={blockTitleStyle}>Warnings</div>
148+
<ul style={{ margin: 0, paddingLeft: 18 }}>
149+
{result.warnings.map((w: string) => (
150+
<li key={w} style={{ color: 'var(--rp-c-text-2, #374151)' }}>
151+
{w}
152+
</li>
153+
))}
154+
</ul>
155+
</div>
156+
) : null}
157+
158+
<div style={blockTitleStyle}>Alignment (pseudo code)</div>
159+
<pre
160+
style={{
161+
margin: 0,
162+
padding: 12,
163+
borderRadius: 10,
164+
border: '1px solid var(--rp-c-divider, #e5e7eb)',
165+
background: 'var(--rp-code-bg, rgba(0,0,0,0.03))',
166+
overflowX: 'auto',
167+
}}
168+
>
169+
{result.alignment.length > 0
170+
? result.alignment
171+
.map((a: ShareScopeAlignmentItem) => {
172+
if ('note' in a) return `${a.expression}\n${a.note}`;
173+
return a.expression;
174+
})
175+
.join('\n')
176+
: '(no alignment)'}
177+
</pre>
178+
179+
<div style={blockTitleStyle}>Initialized Scopes</div>
180+
<pre
181+
style={{
182+
margin: 0,
183+
padding: 12,
184+
borderRadius: 10,
185+
border: '1px solid var(--rp-c-divider, #e5e7eb)',
186+
background: 'var(--rp-code-bg, rgba(0,0,0,0.03))',
187+
overflowX: 'auto',
188+
}}
189+
>
190+
{JSON.stringify(result.initializedScopes, null, 2)}
191+
</pre>
192+
</div>
193+
</NoSSR>
194+
);
195+
}

0 commit comments

Comments
 (0)