Skip to content

Commit aacfe60

Browse files
authored
docs: add multiple share scope usage (#4472)
1 parent 8f8bfdf commit aacfe60

File tree

27 files changed

+897
-3
lines changed

27 files changed

+897
-3
lines changed

apps/website-new/docs/en/configure/_meta.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
"name": "remotes",
2020
"label": "Remotes"
2121
},
22+
{
23+
"type": "file",
24+
"name": "shareScope",
25+
"label": "shareScope"
26+
},
2227
{
2328
"type": "file",
2429
"name": "exposes",

apps/website-new/docs/en/configure/index.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The current page lists all the configuration options for `Module Federation`. Pl
66
type ModuleFederationOptions = {
77
// Name for module federation
88
name: string;
9+
// Share scope(s) for the current app (default: 'default')
10+
shareScope?: string | string[];
911
// Name for the remoteEntry file
1012
filename?: string;
1113
// Configuration for remote modules and entry information in module federation

apps/website-new/docs/en/configure/remotes.mdx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ The `PluginRemoteOptions` type is as follows:
1414

1515
```tsx
1616
type ModuleFederationInfo = string;
17+
type ShareScope = string | string[];
18+
19+
type RemoteConfig = {
20+
external: ModuleFederationInfo | ModuleFederationInfo[];
21+
shareScope?: ShareScope;
22+
};
23+
1724
interface PluginRemoteOptions {
18-
[remoteAlias: string]: ModuleFederationInfo;
25+
[remoteAlias: string]: ModuleFederationInfo | RemoteConfig;
1926
}
2027
```
2128

@@ -44,3 +51,28 @@ module.exports = {
4451
],
4552
};
4653
```
54+
55+
## shareScope
56+
57+
- Type: `string | string[]`
58+
- Required: No
59+
- Default: `'default'`
60+
61+
Defines which share scopes (shared dependency pools) the host aligns with a given remote. This is useful for isolating certain shared dependencies away from the default pool (for example, putting an internal design system into `scope1` while keeping React in `default`).
62+
63+
```ts
64+
new ModuleFederationPlugin({
65+
name: 'host',
66+
remotes: {
67+
remote: {
68+
external: 'app_remote@http://localhost:2001/remoteEntry.js',
69+
shareScope: ['default', 'scope1'],
70+
},
71+
},
72+
});
73+
```
74+
75+
Related configuration:
76+
77+
- The producer (remote) should declare which scopes it initializes via [shareScope](./shareScope).
78+
- Each shared dependency chooses its scope via [shared.shareScope](./shared#sharescope).
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# shareScope
2+
3+
`shareScope` specifies which shared dependency pools (share scopes) a producer (remote) participates in. You can think of a share scope as a named shared-dependency pool: dependencies are only reused within the same scope.
4+
5+
- Type: `string | string[]`
6+
- Required: No
7+
- Default: `'default'`
8+
9+
## What It Does
10+
11+
- Controls which share scopes a remote initializes at runtime.
12+
- Works with `shared[*].shareScope`: a shared dependency only participates in the scope it is assigned to.
13+
- Works with `remotes[remote].shareScope`: the host must align the scopes it wants to reuse with the remote, otherwise missing scopes are treated as empty and cannot be reused.
14+
15+
## Examples
16+
17+
### Single Scope (Default)
18+
19+
```ts
20+
new ModuleFederationPlugin({
21+
name: 'app_remote',
22+
shareScope: 'default',
23+
});
24+
```
25+
26+
### Multiple Scopes (Isolated Shared Pools)
27+
28+
```ts
29+
new ModuleFederationPlugin({
30+
name: 'app_remote',
31+
shareScope: ['default', 'scope1'],
32+
shared: {
33+
react: {
34+
singleton: true,
35+
requiredVersion: false,
36+
shareScope: 'default',
37+
},
38+
'@company/design-system': {
39+
singleton: true,
40+
requiredVersion: false,
41+
shareScope: 'scope1',
42+
},
43+
},
44+
});
45+
```
46+
47+
## Notes
48+
49+
- `shareScope` declares which share scopes this remote initializes. It does not automatically put dependencies into those scopes; that is controlled by each `shared` entry's `shareScope`.
50+
- To actually reuse dependencies across apps, the host and remote typically need to agree on the same share scopes for that remote. See [remotes.shareScope](./remotes#sharescope).
51+

apps/website-new/docs/en/guide/_meta.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
"name": "performance",
3030
"label": "Performance"
3131
},
32+
{
33+
"type": "dir-section-header",
34+
"name": "advanced",
35+
"label": "Advanced"
36+
},
3237
{
3338
"type": "dir-section-header",
3439
"name": "deployment",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
{
3+
"type": "file",
4+
"name": "multiple-shared-scope",
5+
"label": "Multiple Share Scopes"
6+
}
7+
]
8+
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Shared Dependency Isolation: Multiple Share Scopes
2+
3+
> Split `shared` dependencies into multiple named shared pools (for example `default`, `scope1`). Dependencies are only reused within the same pool.
4+
5+
## Background
6+
7+
In {props.name || 'Module Federation'}, `shared` dependencies are registered into the `default` share scope by default. A single scope is often not enough when:
8+
9+
* You want to isolate part of your shared dependencies from the default pool (for example, running two React ecosystems side-by-side, gradual upgrades, or domain isolation in micro-frontends).
10+
* You want the same package to use different versions or strategies in different domains, while still being shared within each domain (singleton/reuse still works within a domain).
11+
12+
The key idea of multiple share scopes is: **move shared dependency registration and resolution into different namespaces (scopes)**, so you can isolate shared pools and layer policies.
13+
14+
## Configuration Quick Map
15+
16+
The easiest mental model is to focus on what you configure on producer, consumer, and each shared entry:
17+
18+
* **Producer (remote)**: use [shareScope](../../configure/shareScope) to declare which share scopes this remote initializes (default: `default`, supports `string | string[]`).
19+
* **Consumer (host)**: use [remotes[remote].shareScope](../../configure/remotes#sharescope) to declare which share scopes the host aligns with that remote (default: `default`).
20+
* **Shared entry**: use `shared[pkg].shareScope` to decide which pool a dependency is registered/resolved in (see [shared.shareScope](../../configure/shared#sharescope)).
21+
22+
## What Happens for Different Combinations
23+
24+
When the host initializes a remote, it first aligns the share scopes based on both sides’ `shareScope` settings (so the remote knows which shared pools can reuse the host’s), then the remote initializes shared dependencies according to its own `shareScope`.
25+
26+
Below, **HostShareScope** refers to `remotes[remote].shareScope` on the host, and **RemoteShareScope** refers to `shareScope` on the remote.
27+
28+
| HostShareScope (remotes[remote].shareScope) | RemoteShareScope (shareScope) | Share Pool Alignment (pseudo code) | Outcome (key points) |
29+
| --- | --- | --- | --- |
30+
| `'default'` | `'default'` | `remote['default'] = host['default']` | The `default` shared pool is fully shared between host and remote. |
31+
| `['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. |
32+
| `'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). |
33+
| `['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`). |
34+
35+
Notes:
36+
37+
* 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.
38+
* With multiple share scopes, the remote will try to initialize all scopes listed in RemoteShareScope.
39+
40+
## Build Plugin Configuration
41+
42+
You typically configure share scopes at three levels:
43+
44+
1. **Remote (producer)**: which scopes the remote initializes ([shareScope](../../configure/shareScope)).
45+
2. **Host (consumer)**: which scopes the host aligns with that remote ([remotes[remote].shareScope](../../configure/remotes#sharescope)).
46+
3. **Shared entry**: which scope a dependency belongs to (`shared[pkg].shareScope`, see [shared.shareScope](../../configure/shared#sharescope)).
47+
48+
### Producer
49+
50+
```ts title="remote/rspack.config.ts"
51+
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
52+
53+
export default {
54+
plugins: [
55+
new ModuleFederationPlugin({
56+
name: 'app_remote',
57+
filename: 'remoteEntry.js',
58+
exposes: {
59+
'./Button': './src/Button',
60+
},
61+
shareScope: ['default', 'scope1'],
62+
shared: {
63+
react: {
64+
singleton: true,
65+
requiredVersion: false,
66+
shareScope: 'default',
67+
},
68+
'react-dom': {
69+
singleton: true,
70+
requiredVersion: false,
71+
shareScope: 'default',
72+
},
73+
'@company/design-system': {
74+
singleton: true,
75+
requiredVersion: false,
76+
shareScope: 'scope1',
77+
},
78+
},
79+
}),
80+
],
81+
};
82+
```
83+
84+
Key points:
85+
86+
* `shareScope: ['default','scope1']` controls which pools the remote initializes at runtime.
87+
* `shared[pkg].shareScope` decides which pool a dependency participates in. If `@company/design-system` is in `scope1`, it only participates in version selection and reuse within the `scope1` pool.
88+
89+
### Consumer
90+
91+
```ts title="host/rspack.config.ts"
92+
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
93+
94+
export default {
95+
plugins: [
96+
new ModuleFederationPlugin({
97+
name: 'app_host',
98+
remotes: {
99+
app_remote: {
100+
external: 'app_remote@http://localhost:2001/remoteEntry.js',
101+
shareScope: ['default', 'scope1'],
102+
},
103+
},
104+
}),
105+
],
106+
};
107+
```
108+
109+
Key points:
110+
111+
* `remotes[remote].shareScope` controls which pools the host aligns when initializing that remote.
112+
* If the host uses multiple scopes but the remote uses a single scope, the pools are aligned but the remote only initializes shared deps for its single scope. For multi-scope reuse to “really work”, the host and remote usually need to agree on the same scopes.
113+
114+
## Pure Runtime (Runtime API)
115+
116+
If you don’t rely on build-time `remotes` (or you want to register `remotes/shared` dynamically at runtime), you can configure the same idea via runtime API:
117+
118+
* Multi-scope remotes: use `registerRemotes` / `createInstance({ remotes })` and set `shareScope: string | string[]` in the remote config.
119+
* Shared entry scope: use `registerShared` / `createInstance({ shared })` and set `scope: string | string[]` in the shared config (note the field name is `scope` here).
120+
121+
```ts title="host/runtime.ts"
122+
import React from 'react';
123+
import { registerRemotes, registerShared } from '@module-federation/enhanced/runtime';
124+
125+
registerRemotes([
126+
{
127+
name: 'app_remote',
128+
alias: 'remote',
129+
entry: 'http://localhost:2001/mf-manifest.json',
130+
shareScope: ['default', 'scope1'],
131+
},
132+
]);
133+
134+
registerShared({
135+
react: {
136+
version: '18.0.0',
137+
scope: 'default',
138+
lib: () => React,
139+
shareConfig: {
140+
singleton: true,
141+
requiredVersion: '^18.0.0',
142+
},
143+
},
144+
'@company/design-system': {
145+
version: '1.2.3',
146+
scope: 'scope1',
147+
lib: () => require('@company/design-system'),
148+
shareConfig: {
149+
singleton: true,
150+
requiredVersion: false,
151+
},
152+
},
153+
});
154+
```
155+
156+
## Fine-grained Control with Runtime Hooks
157+
158+
Multiple share scopes essentially group shared pools by name. If you need finer control over scope selection, alignment, and fallback strategies, you can use runtime hooks (runtime plugins) to intervene during remote initialization or shared resolution.
159+
160+
### 1) Force a remote to use a specific scope (beforeInitContainer)
161+
162+
The example below forces `legacy_remote` to always use the `legacy` scope:
163+
164+
```ts title="multi-scope-policy-plugin.ts"
165+
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';
166+
167+
export function multiScopePolicyPlugin(): ModuleFederationRuntimePlugin {
168+
return {
169+
name: 'multi-scope-policy',
170+
async beforeInitContainer(args) {
171+
if (args.remoteInfo.name !== 'legacy_remote') return args;
172+
173+
const hostShareScopeMap = args.origin.shareScopeMap;
174+
if (!hostShareScopeMap.legacy) hostShareScopeMap.legacy = {};
175+
176+
args.remoteEntryInitOptions.shareScopeKeys = ['legacy'];
177+
178+
return {
179+
...args,
180+
shareScope: hostShareScopeMap.legacy,
181+
};
182+
},
183+
};
184+
}
185+
```
186+
187+
### 2) Fallback / alias when a scope is missing (initContainerShareScopeMap / resolveShare)
188+
189+
* `initContainerShareScopeMap`: adjust the share pool mapping during remote initialization.
190+
* `resolveShare`: intervene when selecting a specific shared version, useful for “fallback to default if not found in current scope”.
191+
192+
Example: if a package is not found in `scope1`, fall back to `default`:
193+
194+
```ts title="scope-fallback-plugin.ts"
195+
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';
196+
197+
export function scopeFallbackPlugin(): ModuleFederationRuntimePlugin {
198+
return {
199+
name: 'scope-fallback',
200+
resolveShare(args) {
201+
const hasPkg = Boolean(args.shareScopeMap[args.scope]?.[args.pkgName]);
202+
if (hasPkg) return args;
203+
return { ...args, scope: 'default' };
204+
},
205+
};
206+
}
207+
```
208+
209+
You can also alias one scope to another (so they share the same pool object):
210+
211+
```ts title="scope-alias-plugin.ts"
212+
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';
213+
214+
export function scopeAliasPlugin(): ModuleFederationRuntimePlugin {
215+
return {
216+
name: 'scope-alias',
217+
initContainerShareScopeMap(args) {
218+
if (args.scopeName !== 'scope1') return args;
219+
if (!args.hostShareScopeMap?.default) return args;
220+
221+
args.hostShareScopeMap.scope1 = args.hostShareScopeMap.default;
222+
return {
223+
...args,
224+
shareScope: args.hostShareScopeMap.default,
225+
};
226+
},
227+
};
228+
}
229+
```
230+

0 commit comments

Comments
 (0)