Skip to content

Commit 1540f0e

Browse files
fix(mf): harden layered manifest and optimizer metadata
Preserve layer-aware shared metadata across manifest generation, used-exports optimization, and module remote runtime wiring so layered federation outputs stay correct and covered. Made-with: Cursor
1 parent 8e82836 commit 1540f0e

File tree

16 files changed

+184
-36
lines changed

16 files changed

+184
-36
lines changed

crates/node_binding/napi-binding.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,6 +2548,7 @@ export interface RawManifestSharedOption {
25482548
name: string
25492549
version?: string
25502550
requiredVersion?: string
2551+
layer?: string
25512552
singleton?: boolean
25522553
}
25532554

@@ -2690,6 +2691,7 @@ export interface RawOptimizeSharedConfig {
26902691
shareKey: string
26912692
treeShaking: boolean
26922693
usedExports?: Array<string>
2694+
layer?: string
26932695
}
26942696

26952697
export interface RawOptions {

crates/rspack_binding_api/src/raw_options/raw_builtins/raw_mf.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ pub struct RawOptimizeSharedConfig {
226226
pub share_key: String,
227227
pub tree_shaking: bool,
228228
pub used_exports: Option<Vec<String>>,
229+
pub layer: Option<String>,
229230
}
230231

231232
impl From<RawOptimizeSharedConfig> for OptimizeSharedConfig {
@@ -234,6 +235,7 @@ impl From<RawOptimizeSharedConfig> for OptimizeSharedConfig {
234235
share_key: value.share_key,
235236
tree_shaking: value.tree_shaking,
236237
used_exports: value.used_exports.unwrap_or_default(),
238+
layer: value.layer,
237239
}
238240
}
239241
}
@@ -385,6 +387,7 @@ pub struct RawManifestSharedOption {
385387
pub name: String,
386388
pub version: Option<String>,
387389
pub required_version: Option<String>,
390+
pub layer: Option<String>,
388391
pub singleton: Option<bool>,
389392
}
390393

@@ -453,6 +456,7 @@ impl From<RawModuleFederationManifestPluginOptions> for ModuleFederationManifest
453456
name: shared.name,
454457
version: shared.version,
455458
required_version: shared.required_version,
459+
layer: shared.layer,
456460
singleton: shared.singleton,
457461
})
458462
.collect(),

crates/rspack_plugin_mf/src/manifest/data.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ pub struct StatsShared {
5050
pub version: String,
5151
#[serde(default)]
5252
pub requiredVersion: Option<String>,
53+
#[serde(skip_serializing_if = "Option::is_none")]
54+
pub layer: Option<String>,
5355
#[serde(default)]
5456
pub singleton: Option<bool>,
5557
#[serde(default)]
@@ -126,6 +128,8 @@ pub struct ManifestShared {
126128
pub version: String,
127129
#[serde(default)]
128130
pub requiredVersion: Option<String>,
131+
#[serde(skip_serializing_if = "Option::is_none")]
132+
pub layer: Option<String>,
129133
#[serde(default)]
130134
pub singleton: Option<bool>,
131135
#[serde(default)]

crates/rspack_plugin_mf/src/manifest/mod.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ use rspack_hook::{plugin, plugin_hook};
3131
use rspack_util::fx_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
3232
use utils::{
3333
collect_entry_files, collect_expose_requirements, compose_id_with_separator,
34-
ensure_configured_remotes, ensure_shared_entry, filter_assets, is_hot_file,
35-
parse_consume_shared_identifier, parse_provide_shared_identifier, record_shared_usage, strip_ext,
34+
compose_shared_map_key, ensure_configured_remotes, ensure_shared_entry, filter_assets,
35+
is_hot_file, parse_consume_shared_identifier, parse_provide_shared_identifier,
36+
record_shared_usage, strip_ext,
3637
};
3738

3839
use crate::container::{container_entry_module::ContainerEntryModule, remote_module::RemoteModule};
@@ -183,6 +184,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
183184
name: shared.name.clone(),
184185
version: shared.version.clone().unwrap_or_default(),
185186
requiredVersion: shared.required_version.clone(),
187+
layer: shared.layer.clone(),
186188
// default singleton to true when not provided by user
187189
singleton: shared.singleton.or(Some(true)),
188190
assets: StatsAssetsGroup::default(),
@@ -232,6 +234,20 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
232234
let mut module_ids_by_name: HashMap<String, ModuleIdentifier> = HashMap::default();
233235
let mut remote_module_ids: Vec<ModuleIdentifier> = Vec::new();
234236
let mut container_entry_module: Option<ModuleIdentifier> = None;
237+
let find_shared_option = |name: &str, layer: Option<&str>| {
238+
self
239+
.options
240+
.shared
241+
.iter()
242+
.find(|s| s.name == name && s.layer.as_deref() == layer)
243+
.or_else(|| {
244+
self
245+
.options
246+
.shared
247+
.iter()
248+
.find(|s| s.name == name && s.layer.is_none())
249+
})
250+
};
235251
for (_, module) in module_graph.modules() {
236252
let module_identifier = module.identifier();
237253
if let Some(path) = module_source_path(module, compilation) {
@@ -322,12 +338,20 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
322338

323339
if matches!(module_type, ModuleType::ProvideShared) {
324340
if let Some((pkg, ver)) = parse_provide_shared_identifier(&identifier) {
325-
let entry = ensure_shared_entry(&mut shared_map, &container_name, &pkg);
341+
let layer = module.get_layer().map(ToString::to_string);
342+
let shared_key = compose_shared_map_key(&pkg, layer.as_deref());
343+
let entry = ensure_shared_entry(
344+
&mut shared_map,
345+
&shared_key,
346+
&container_name,
347+
&pkg,
348+
layer.clone(),
349+
);
326350
if entry.version.is_empty() {
327351
entry.version = ver;
328352
}
329353
// overlay user-configured shared options (singleton/requiredVersion/version)
330-
if let Some(opt) = self.options.shared.iter().find(|s| s.name == pkg) {
354+
if let Some(opt) = find_shared_option(&pkg, layer.as_deref()) {
331355
if let Some(singleton) = opt.singleton {
332356
entry.singleton = Some(singleton);
333357
}
@@ -338,7 +362,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
338362
entry.version = cfg_ver;
339363
}
340364
}
341-
let targets = shared_module_targets.entry(pkg.clone()).or_default();
365+
let targets = shared_module_targets.entry(shared_key.clone()).or_default();
342366
for connection in module_graph.get_outgoing_connections(&module_identifier) {
343367
let referenced = *connection.module_identifier();
344368
if should_collect_module(&referenced) {
@@ -351,7 +375,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
351375
}
352376
record_shared_usage(
353377
&mut shared_usage_links,
354-
&pkg,
378+
&shared_key,
355379
&module_identifier,
356380
module_graph,
357381
compilation,
@@ -363,6 +387,8 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
363387
if matches!(module_type, ModuleType::ConsumeShared)
364388
&& let Some((pkg, required)) = parse_consume_shared_identifier(&identifier)
365389
{
390+
let layer = module.get_layer().map(ToString::to_string);
391+
let shared_key = compose_shared_map_key(&pkg, layer.as_deref());
366392
let mut target_ids: IdentifierSet = IdentifierSet::default();
367393
for connection in module_graph.get_outgoing_connections(&module_identifier) {
368394
let module_id = *connection.module_identifier();
@@ -375,15 +401,21 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
375401
}
376402
}
377403
shared_module_targets
378-
.entry(pkg.clone())
404+
.entry(shared_key.clone())
379405
.or_default()
380406
.extend(target_ids.into_iter());
381-
let entry = ensure_shared_entry(&mut shared_map, &container_name, &pkg);
407+
let entry = ensure_shared_entry(
408+
&mut shared_map,
409+
&shared_key,
410+
&container_name,
411+
&pkg,
412+
layer.clone(),
413+
);
382414
if entry.requiredVersion.is_none() && required.is_some() {
383415
entry.requiredVersion = required;
384416
}
385417
// overlay user-configured shared options
386-
if let Some(opt) = self.options.shared.iter().find(|s| s.name == pkg) {
418+
if let Some(opt) = find_shared_option(&pkg, layer.as_deref()) {
387419
if let Some(singleton) = opt.singleton {
388420
entry.singleton = Some(singleton);
389421
}
@@ -397,7 +429,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
397429
}
398430
record_shared_usage(
399431
&mut shared_usage_links,
400-
&pkg,
432+
&shared_key,
401433
&module_identifier,
402434
module_graph,
403435
compilation,
@@ -662,6 +694,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
662694
name: s.name,
663695
version: s.version,
664696
requiredVersion: s.requiredVersion,
697+
layer: s.layer,
665698
singleton: s.singleton,
666699
assets: s.assets,
667700
})

crates/rspack_plugin_mf/src/manifest/options.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub struct ManifestSharedOption {
1919
pub name: String,
2020
pub version: Option<String>,
2121
pub required_version: Option<String>,
22+
pub layer: Option<String>,
2223
pub singleton: Option<bool>,
2324
}
2425

crates/rspack_plugin_mf/src/manifest/utils.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ pub fn compose_id_with_separator(container: &str, name: &str) -> String {
133133
format!("{container}:{name}")
134134
}
135135

136+
pub fn compose_shared_map_key(pkg: &str, layer: Option<&str>) -> String {
137+
match layer {
138+
Some(layer) => format!("{pkg}\u{0000}{layer}"),
139+
None => pkg.to_string(),
140+
}
141+
}
142+
136143
pub fn is_hot_file(file: &str) -> bool {
137144
file.contains(HOT_UPDATE_SUFFIX)
138145
}
@@ -155,16 +162,22 @@ pub fn strip_ext(path: &str) -> String {
155162

156163
pub fn ensure_shared_entry<'a>(
157164
shared_map: &'a mut HashMap<String, StatsShared>,
165+
shared_key: &str,
158166
container_name: &str,
159167
pkg: &str,
168+
layer: Option<String>,
160169
) -> &'a mut StatsShared {
161170
shared_map
162-
.entry(pkg.to_string())
171+
.entry(shared_key.to_string())
163172
.or_insert_with(|| StatsShared {
164-
id: compose_id_with_separator(container_name, pkg),
173+
id: match &layer {
174+
Some(layer) => compose_id_with_separator(container_name, &format!("{pkg}:{layer}")),
175+
None => compose_id_with_separator(container_name, pkg),
176+
},
165177
name: pkg.to_string(),
166178
version: String::new(),
167179
requiredVersion: None,
180+
layer,
168181
// default singleton to true
169182
singleton: Some(true),
170183
assets: super::data::StatsAssetsGroup::default(),
@@ -175,7 +188,7 @@ pub fn ensure_shared_entry<'a>(
175188

176189
pub fn record_shared_usage(
177190
shared_usage_links: &mut Vec<(String, String)>,
178-
pkg: &str,
191+
shared_key: &str,
179192
module_identifier: &ModuleIdentifier,
180193
module_graph: &ModuleGraph,
181194
compilation: &Compilation,
@@ -193,7 +206,7 @@ pub fn record_shared_usage(
193206
.to_string();
194207
if !issuer_name.is_empty() {
195208
let key = strip_ext(&strip_aggregate_suffix(&issuer_name));
196-
shared_usage_links.push((pkg.to_string(), key));
209+
shared_usage_links.push((shared_key.to_string(), key));
197210
}
198211
}
199212
if let Some(mgm) = module_graph.module_graph_module_by_identifier(module_identifier) {
@@ -212,7 +225,7 @@ pub fn record_shared_usage(
212225
});
213226
if let Some(request) = maybe_request {
214227
let key = strip_ext(&strip_aggregate_suffix(&request));
215-
shared_usage_links.push((pkg.to_string(), key));
228+
shared_usage_links.push((shared_key.to_string(), key));
216229
}
217230
}
218231
}
@@ -227,7 +240,11 @@ pub fn parse_provide_shared_identifier(identifier: &str) -> Option<(String, Stri
227240
}
228241

229242
pub fn parse_consume_shared_identifier(identifier: &str) -> Option<(String, Option<String>)> {
230-
let (_, rest) = identifier.split_once(") ")?;
243+
let (_, mut rest) = identifier.split_once(") ")?;
244+
if rest.starts_with('(') {
245+
let (_, after_layer) = rest.split_once(") ")?;
246+
rest = after_layer;
247+
}
231248
let token = rest.split_whitespace().next()?;
232249
// For scoped packages like @scope/pkg@1.0.0, split at the LAST '@'
233250
let (name, version) = token.rsplit_once('@')?;
@@ -247,12 +264,12 @@ pub fn collect_expose_requirements(
247264
expose_module_paths: &HashMap<String, String>,
248265
) {
249266
#[cfg(debug_assertions)]
250-
for (pkg, expose_key) in links {
267+
for (shared_key, expose_key) in links {
251268
if let Some(expose) = exposes_map.get_mut(&expose_key) {
252-
if !expose.requires.contains(&pkg) {
253-
expose.requires.push(pkg.clone());
254-
}
255-
if let Some(shared) = shared_map.get_mut(&pkg) {
269+
if let Some(shared) = shared_map.get_mut(&shared_key) {
270+
if !expose.requires.contains(&shared.name) {
271+
expose.requires.push(shared.name.clone());
272+
}
256273
let target = expose_module_paths
257274
.get(&expose_key)
258275
.cloned()

0 commit comments

Comments
 (0)