|
| 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