Skip to content

Commit 02a925c

Browse files
authored
feat(module-federation): enhance manifest generation and shared module handling (#12399)
1 parent cd97e3b commit 02a925c

File tree

19 files changed

+296
-82
lines changed

19 files changed

+296
-82
lines changed

crates/rspack_plugin_mf/src/manifest/asset.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ pub fn module_source_path(module: &BoxModule, compilation: &Compilation) -> Opti
199199
if let Some(pos) = identifier.find('?') {
200200
identifier.truncate(pos);
201201
}
202+
// strip aggregated suffix like " + 1 modules"
203+
if let Some((before, _)) = identifier.split_once(" + ") {
204+
identifier = before.to_string();
205+
}
202206
if identifier.starts_with("./") {
203207
identifier.drain(..2);
204208
}

crates/rspack_plugin_mf/src/manifest/data.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub struct StatsBuildInfo {
2727
#[derive(Debug, Serialize, Clone)]
2828
pub struct StatsExpose {
2929
pub path: String,
30+
#[serde(default)]
31+
pub file: String,
3032
pub id: String,
3133
pub name: String,
3234
#[serde(default)]

crates/rspack_plugin_mf/src/manifest/mod.rs

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
150150
let expose_name = expose.path.trim_start_matches("./").to_string();
151151
StatsExpose {
152152
path: expose.path.clone(),
153+
file: String::new(),
153154
id: compose_id_with_separator(&container_name, &expose_name),
154155
name: expose_name,
155156
requires: Vec::new(),
@@ -166,7 +167,8 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
166167
name: shared.name.clone(),
167168
version: shared.version.clone().unwrap_or_default(),
168169
requiredVersion: shared.required_version.clone(),
169-
singleton: shared.singleton,
170+
// default singleton to true when not provided by user
171+
singleton: shared.singleton.or(Some(true)),
170172
assets: StatsAssetsGroup::default(),
171173
usedIn: Vec::new(),
172174
})
@@ -207,6 +209,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
207209
};
208210

209211
let mut exposes_map: HashMap<String, StatsExpose> = HashMap::default();
212+
let mut expose_chunk_names: HashMap<String, String> = HashMap::default();
210213
let mut shared_map: HashMap<String, StatsShared> = HashMap::default();
211214
let mut shared_usage_links: Vec<(String, String)> = Vec::new();
212215
let mut shared_module_targets: HashMap<String, HashSet<ModuleIdentifier>> = HashMap::default();
@@ -253,13 +256,21 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
253256
};
254257
let id_comp = compose_id_with_separator(&container_name, &expose_name);
255258
let expose_file_key = strip_ext(import);
256-
exposes_map.entry(expose_file_key).or_insert(StatsExpose {
257-
path: expose_key.clone(),
258-
id: id_comp,
259-
name: expose_name,
260-
requires: Vec::new(),
261-
assets: StatsAssetsGroup::default(),
262-
});
259+
exposes_map
260+
.entry(expose_file_key.clone())
261+
.or_insert(StatsExpose {
262+
path: expose_key.clone(),
263+
file: String::new(),
264+
id: id_comp,
265+
name: expose_name,
266+
requires: Vec::new(),
267+
assets: StatsAssetsGroup::default(),
268+
});
269+
if let Some(n) = &options.name
270+
&& !n.is_empty()
271+
{
272+
expose_chunk_names.insert(expose_file_key, n.clone());
273+
}
263274
}
264275
continue;
265276
}
@@ -277,6 +288,18 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
277288
if entry.version.is_empty() {
278289
entry.version = ver;
279290
}
291+
// overlay user-configured shared options (singleton/requiredVersion/version)
292+
if let Some(opt) = self.options.shared.iter().find(|s| s.name == pkg) {
293+
if let Some(singleton) = opt.singleton {
294+
entry.singleton = Some(singleton);
295+
}
296+
if entry.requiredVersion.is_none() {
297+
entry.requiredVersion = opt.required_version.clone();
298+
}
299+
if let Some(cfg_ver) = opt.version.clone().filter(|_| entry.version.is_empty()) {
300+
entry.version = cfg_ver;
301+
}
302+
}
280303
let targets = shared_module_targets.entry(pkg.clone()).or_default();
281304
for connection in module_graph.get_outgoing_connections(&module_identifier) {
282305
let referenced = *connection.module_identifier();
@@ -321,6 +344,19 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
321344
if entry.requiredVersion.is_none() && required.is_some() {
322345
entry.requiredVersion = required;
323346
}
347+
// overlay user-configured shared options
348+
if let Some(opt) = self.options.shared.iter().find(|s| s.name == pkg) {
349+
if let Some(singleton) = opt.singleton {
350+
entry.singleton = Some(singleton);
351+
}
352+
// prefer parsed requiredVersion but fill from config if still None
353+
if entry.requiredVersion.is_none() {
354+
entry.requiredVersion = opt.required_version.clone();
355+
}
356+
if let Some(cfg_ver) = opt.version.clone().filter(|_| entry.version.is_empty()) {
357+
entry.version = cfg_ver;
358+
}
359+
}
324360
record_shared_usage(
325361
&mut shared_usage_links,
326362
&pkg,
@@ -395,14 +431,34 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
395431
}
396432

397433
for (expose_file_key, expose) in exposes_map.iter_mut() {
398-
let mut assets = if let Some(module_id) = module_ids_by_name.get(expose_file_key) {
399-
collect_assets_for_module(compilation, module_id, &entry_point_names)
400-
.unwrap_or_else(empty_assets_group)
401-
} else if let Some(chunk_key) = compilation.named_chunks.get(expose_file_key) {
402-
collect_assets_from_chunk(compilation, chunk_key, &entry_point_names)
403-
} else {
404-
empty_assets_group()
405-
};
434+
let mut assets = None;
435+
if let Some(chunk_name) = expose_chunk_names.get(expose_file_key)
436+
&& let Some(chunk_key) = compilation.named_chunks.get(chunk_name)
437+
{
438+
assets = Some(collect_assets_from_chunk(
439+
compilation,
440+
chunk_key,
441+
&entry_point_names,
442+
));
443+
}
444+
if assets.is_none()
445+
&& let Some(chunk_key) = compilation.named_chunks.get(expose_file_key)
446+
{
447+
assets = Some(collect_assets_from_chunk(
448+
compilation,
449+
chunk_key,
450+
&entry_point_names,
451+
));
452+
}
453+
if assets.is_none()
454+
&& let Some(module_id) = module_ids_by_name.get(expose_file_key)
455+
{
456+
assets = collect_assets_for_module(compilation, module_id, &entry_point_names);
457+
}
458+
let mut assets = assets.unwrap_or_else(empty_assets_group);
459+
if let Some(path) = expose_module_paths.get(expose_file_key) {
460+
expose.file = path.clone();
461+
}
406462
if !entry_name.is_empty() {
407463
assets.js.sync.retain(|asset| asset != &entry_name);
408464
assets.js.r#async.retain(|asset| asset != &entry_name);
@@ -441,6 +497,15 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
441497
.css
442498
.sync
443499
.retain(|asset| !shared_asset_files.contains(asset));
500+
if !entry_name.is_empty() {
501+
entry_assets.js.sync.retain(|asset| asset != &entry_name);
502+
entry_assets.js.r#async.retain(|asset| asset != &entry_name);
503+
entry_assets.css.sync.retain(|asset| asset != &entry_name);
504+
entry_assets
505+
.css
506+
.r#async
507+
.retain(|asset| asset != &entry_name);
508+
}
444509
normalize_assets_group(&mut entry_assets);
445510
for expose in exposes_map.values_mut() {
446511
let is_empty = expose.assets.js.sync.is_empty()
@@ -487,7 +552,17 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
487552
(None, alias.clone())
488553
};
489554
let used_in =
490-
collect_usage_files_for_module(compilation, module_graph, &module_id, &entry_point_names);
555+
collect_usage_files_for_module(compilation, module_graph, &module_id, &entry_point_names)
556+
// keep only the file path, drop aggregated suffix like " + 1 modules"
557+
.into_iter()
558+
.map(|s| {
559+
if let Some((before, _)) = s.split_once(" + ") {
560+
before.to_string()
561+
} else {
562+
s
563+
}
564+
})
565+
.collect();
491566
remote_list.push(StatsRemote {
492567
alias: alias.clone(),
493568
consumingFederationContainerName: container_name.clone(),
@@ -509,6 +584,27 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
509584
.collect::<Vec<_>>();
510585
(exposes, shared, remote_list)
511586
};
587+
// Ensure all configured remotes exist in stats, add missing with defaults
588+
let mut remote_list = remote_list;
589+
for (alias, target) in self.options.remote_alias_map.iter() {
590+
if !remote_list.iter().any(|r| r.alias == *alias) {
591+
let remote_container_name = if target.name.is_empty() {
592+
alias.clone()
593+
} else {
594+
target.name.clone()
595+
};
596+
remote_list.push(StatsRemote {
597+
alias: alias.clone(),
598+
consumingFederationContainerName: container_name.clone(),
599+
federationContainerName: remote_container_name.clone(),
600+
// default moduleName to "." for missing entries
601+
moduleName: ".".to_string(),
602+
entry: target.entry.clone(),
603+
usedIn: vec!["UNKNOWN".to_string()],
604+
});
605+
}
606+
}
607+
512608
let stats_root = StatsRoot {
513609
id: container_name.clone(),
514610
name: container_name.clone(),

crates/rspack_plugin_mf/src/manifest/utils.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ pub fn ensure_shared_entry<'a>(
4343
name: pkg.to_string(),
4444
version: String::new(),
4545
requiredVersion: None,
46-
singleton: None,
46+
// default singleton to true
47+
singleton: Some(true),
4748
assets: super::data::StatsAssetsGroup::default(),
4849
usedIn: Vec::new(),
4950
})
@@ -56,12 +57,19 @@ pub fn record_shared_usage(
5657
module_graph: &ModuleGraph,
5758
compilation: &Compilation,
5859
) {
60+
fn strip_aggregate_suffix(s: &str) -> String {
61+
if let Some((before, _)) = s.split_once(" + ") {
62+
before.to_string()
63+
} else {
64+
s.to_string()
65+
}
66+
}
5967
if let Some(issuer_module) = module_graph.get_issuer(module_identifier) {
6068
let issuer_name = issuer_module
6169
.readable_identifier(&compilation.options.context)
6270
.to_string();
6371
if !issuer_name.is_empty() {
64-
let key = strip_ext(&issuer_name);
72+
let key = strip_ext(&strip_aggregate_suffix(&issuer_name));
6573
shared_usage_links.push((pkg.to_string(), key));
6674
}
6775
}
@@ -80,7 +88,7 @@ pub fn record_shared_usage(
8088
.map(|dep| dep.request().to_string())
8189
});
8290
if let Some(request) = maybe_request {
83-
let key = strip_ext(&request);
91+
let key = strip_ext(&strip_aggregate_suffix(&request));
8492
shared_usage_links.push((pkg.to_string(), key));
8593
}
8694
}
@@ -90,14 +98,16 @@ pub fn record_shared_usage(
9098
pub fn parse_provide_shared_identifier(identifier: &str) -> Option<(String, String)> {
9199
let (before_request, _) = identifier.split_once(" = ")?;
92100
let token = before_request.split_whitespace().last()?;
93-
let (name, version) = token.split_once('@')?;
101+
// For scoped packages like @scope/pkg@1.0.0, split at the LAST '@'
102+
let (name, version) = token.rsplit_once('@')?;
94103
Some((name.to_string(), version.to_string()))
95104
}
96105

97106
pub fn parse_consume_shared_identifier(identifier: &str) -> Option<(String, Option<String>)> {
98107
let (_, rest) = identifier.split_once(") ")?;
99108
let token = rest.split_whitespace().next()?;
100-
let (name, version) = token.split_once('@')?;
109+
// For scoped packages like @scope/pkg@1.0.0, split at the LAST '@'
110+
let (name, version) = token.rsplit_once('@')?;
101111
let version = version.trim();
102112
let required = if version.is_empty() || version == "*" {
103113
None

0 commit comments

Comments
 (0)