Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/rspack_plugin_mf/src/manifest/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ pub fn module_source_path(module: &BoxModule, compilation: &Compilation) -> Opti
if let Some(pos) = identifier.find('?') {
identifier.truncate(pos);
}
// strip aggregated suffix like " + 1 modules"
if let Some((before, _)) = identifier.split_once(" + ") {
identifier = before.to_string();
}
if identifier.starts_with("./") {
identifier.drain(..2);
}
Expand Down
2 changes: 2 additions & 0 deletions crates/rspack_plugin_mf/src/manifest/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub struct StatsBuildInfo {
#[derive(Debug, Serialize, Clone)]
pub struct StatsExpose {
pub path: String,
#[serde(default)]
pub file: String,
pub id: String,
pub name: String,
#[serde(default)]
Expand Down
66 changes: 64 additions & 2 deletions crates/rspack_plugin_mf/src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
let expose_name = expose.path.trim_start_matches("./").to_string();
StatsExpose {
path: expose.path.clone(),
file: String::new(),
id: compose_id_with_separator(&container_name, &expose_name),
name: expose_name,
requires: Vec::new(),
Expand All @@ -166,7 +167,8 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
name: shared.name.clone(),
version: shared.version.clone().unwrap_or_default(),
requiredVersion: shared.required_version.clone(),
singleton: shared.singleton,
// default singleton to true when not provided by user
singleton: shared.singleton.or(Some(true)),
assets: StatsAssetsGroup::default(),
usedIn: Vec::new(),
})
Expand Down Expand Up @@ -255,6 +257,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
let expose_file_key = strip_ext(import);
exposes_map.entry(expose_file_key).or_insert(StatsExpose {
path: expose_key.clone(),
file: String::new(),
id: id_comp,
name: expose_name,
requires: Vec::new(),
Expand All @@ -277,6 +280,18 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
if entry.version.is_empty() {
entry.version = ver;
}
// overlay user-configured shared options (singleton/requiredVersion/version)
if let Some(opt) = self.options.shared.iter().find(|s| s.name == pkg) {
if let Some(singleton) = opt.singleton {
entry.singleton = Some(singleton);
}
if entry.requiredVersion.is_none() {
entry.requiredVersion = opt.required_version.clone();
}
if let Some(cfg_ver) = opt.version.clone().filter(|_| entry.version.is_empty()) {
entry.version = cfg_ver;
}
}
let targets = shared_module_targets.entry(pkg.clone()).or_default();
for connection in module_graph.get_outgoing_connections(&module_identifier) {
let referenced = *connection.module_identifier();
Expand Down Expand Up @@ -321,6 +336,19 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
if entry.requiredVersion.is_none() && required.is_some() {
entry.requiredVersion = required;
}
// overlay user-configured shared options
if let Some(opt) = self.options.shared.iter().find(|s| s.name == pkg) {
if let Some(singleton) = opt.singleton {
entry.singleton = Some(singleton);
}
// prefer parsed requiredVersion but fill from config if still None
if entry.requiredVersion.is_none() {
entry.requiredVersion = opt.required_version.clone();
}
if let Some(cfg_ver) = opt.version.clone().filter(|_| entry.version.is_empty()) {
entry.version = cfg_ver;
}
}
record_shared_usage(
&mut shared_usage_links,
&pkg,
Expand Down Expand Up @@ -403,6 +431,9 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
} else {
empty_assets_group()
};
if let Some(path) = expose_module_paths.get(expose_file_key) {
expose.file = path.clone();
}
if !entry_name.is_empty() {
assets.js.sync.retain(|asset| asset != &entry_name);
assets.js.r#async.retain(|asset| asset != &entry_name);
Expand Down Expand Up @@ -487,7 +518,17 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
(None, alias.clone())
};
let used_in =
collect_usage_files_for_module(compilation, &module_graph, &module_id, &entry_point_names);
collect_usage_files_for_module(compilation, &module_graph, &module_id, &entry_point_names)
// keep only the file path, drop aggregated suffix like " + 1 modules"
.into_iter()
.map(|s| {
if let Some((before, _)) = s.split_once(" + ") {
before.to_string()
} else {
s
}
})
.collect();
remote_list.push(StatsRemote {
alias: alias.clone(),
consumingFederationContainerName: container_name.clone(),
Expand All @@ -509,6 +550,27 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
.collect::<Vec<_>>();
(exposes, shared, remote_list)
};
// Ensure all configured remotes exist in stats, add missing with defaults
let mut remote_list = remote_list;
for (alias, target) in self.options.remote_alias_map.iter() {
if !remote_list.iter().any(|r| r.alias == *alias) {
let remote_container_name = if target.name.is_empty() {
alias.clone()
} else {
target.name.clone()
};
remote_list.push(StatsRemote {
alias: alias.clone(),
consumingFederationContainerName: container_name.clone(),
federationContainerName: remote_container_name.clone(),
// default moduleName to "." for missing entries
moduleName: ".".to_string(),
entry: target.entry.clone(),
usedIn: vec!["UNKNOWN".to_string()],
});
}
}

let stats_root = StatsRoot {
id: container_name.clone(),
name: container_name.clone(),
Expand Down
20 changes: 15 additions & 5 deletions crates/rspack_plugin_mf/src/manifest/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ pub fn ensure_shared_entry<'a>(
name: pkg.to_string(),
version: String::new(),
requiredVersion: None,
singleton: None,
// default singleton to true
singleton: Some(true),
assets: super::data::StatsAssetsGroup::default(),
usedIn: Vec::new(),
})
Expand All @@ -56,12 +57,19 @@ pub fn record_shared_usage(
module_graph: &ModuleGraph,
compilation: &Compilation,
) {
fn strip_aggregate_suffix(s: &str) -> String {
if let Some((before, _)) = s.split_once(" + ") {
before.to_string()
} else {
s.to_string()
}
}
if let Some(issuer_module) = module_graph.get_issuer(module_identifier) {
let issuer_name = issuer_module
.readable_identifier(&compilation.options.context)
.to_string();
if !issuer_name.is_empty() {
let key = strip_ext(&issuer_name);
let key = strip_ext(&strip_aggregate_suffix(&issuer_name));
shared_usage_links.push((pkg.to_string(), key));
}
}
Expand All @@ -82,7 +90,7 @@ pub fn record_shared_usage(
.map(|dep| dep.request().to_string())
});
if let Some(request) = maybe_request {
let key = strip_ext(&request);
let key = strip_ext(&strip_aggregate_suffix(&request));
shared_usage_links.push((pkg.to_string(), key));
}
}
Expand All @@ -92,14 +100,16 @@ pub fn record_shared_usage(
pub fn parse_provide_shared_identifier(identifier: &str) -> Option<(String, String)> {
let (before_request, _) = identifier.split_once(" = ")?;
let token = before_request.split_whitespace().last()?;
let (name, version) = token.split_once('@')?;
// For scoped packages like @scope/pkg@1.0.0, split at the LAST '@'
let (name, version) = token.rsplit_once('@')?;
Some((name.to_string(), version.to_string()))
}

pub fn parse_consume_shared_identifier(identifier: &str) -> Option<(String, Option<String>)> {
let (_, rest) = identifier.split_once(") ")?;
let token = rest.split_whitespace().next()?;
let (name, version) = token.split_once('@')?;
// For scoped packages like @scope/pkg@1.0.0, split at the LAST '@'
let (name, version) = token.rsplit_once('@')?;
let version = version.trim();
let required = if version.is_empty() || version == "*" {
None
Expand Down
127 changes: 81 additions & 46 deletions tests/rspack-test/configCases/container-1-5/manifest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,47 @@ it("should emit remote entry with hash", () => {
expect(fs.existsSync(remoteEntryPath)).toBe(true);
});

// shared
it("should report shared assets in sync only", () => {
expect(stats.shared).toHaveLength(1);
expect(stats.shared[0].assets.js.sync.sort()).toEqual([
"node_modules_react_js.js"
]);
expect(stats.shared[0].assets.js.async).toEqual([]);
it("should report xreact shared assets in sync only", () => {
const xreact = stats.shared.find(item => item.name === "xreact");
expect(xreact.singleton).toBe(true);
expect(xreact).toBeDefined();
expect(xreact.assets.css.sync).toEqual([]);
expect(xreact.assets.css.async).toEqual([]);
expect(xreact.assets.js.sync).toEqual(["node_modules_xreact_index_js.js"]);
expect(xreact.assets.js.async).toEqual([]);
});

it("should materialize in manifest", () => {
expect(manifest.shared).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "react",
assets: expect.objectContaining({
js: expect.objectContaining({
sync: expect.arrayContaining([
"node_modules_react_js.js"
]),
async: []
})
})
})
])
);
it("should include scoped shared '@scope-sc/dep1' in stats", () => {
const dep1 = stats.shared.find(item => item.name === "@scope-sc/dep1");
expect(dep1).toBeDefined();
expect(dep1.singleton).toBe(true);
expect(dep1.version).toBe("1.0.0");
expect(dep1.requiredVersion).toBe("^1.0.0");
expect(dep1.assets.css.sync).toEqual([]);
expect(dep1.assets.css.async).toEqual([]);
expect(Array.isArray(dep1.assets.js.sync)).toBe(true);
expect(dep1.assets.js.async).toEqual([]);
expect(dep1.usedIn.includes("module.js")).toBe(true);
});

it("should include scoped shared '@scope-sc2/dep2' in stats", () => {
const dep2 = stats.shared.find(item => item.name === "@scope-sc2/dep2");
expect(dep2).toBeDefined();
expect(dep2.singleton).toBe(false);
expect(dep2.version).toBe("1.0.0");
expect(dep2.requiredVersion).toBe(">=1.0.0");
expect(dep2.assets.css.sync).toEqual([]);
expect(dep2.assets.css.async).toEqual([]);
expect(Array.isArray(dep2.assets.js.sync)).toBe(true);
expect(dep2.assets.js.async).toEqual([]);
expect(dep2.usedIn.includes("module.js")).toBe(true);
});


//exposes
it("should expose sync assets only", () => {
expect(stats.exposes).toHaveLength(1);
expect(stats.exposes[0].file).toBe("module.js");
expect(stats.exposes[0].assets.js.sync).toEqual(["_federation_expose_a.js"]);
expect(stats.exposes[0].assets.js.async).toEqual([
"lazy-module_js.js"
Expand Down Expand Up @@ -70,30 +81,54 @@ it("should reflect expose assets in manifest", () => {
// remotes

it("should record remote usage", () => {
expect(stats.remotes).toEqual(
expect.arrayContaining([
expect.objectContaining({
alias: "@remote/alias",
consumingFederationContainerName: "container",
federationContainerName: "remote",
moduleName: ".",
usedIn: expect.arrayContaining([
"module.js"
]),
entry: 'http://localhost:8000/remoteEntry.js'
})
])
);
expect(stats.remotes).toEqual(
expect.arrayContaining([
// actual remote usage recorded for a concrete module
expect.objectContaining({
alias: "@remote/alias",
consumingFederationContainerName: "container",
federationContainerName: "remote",
moduleName: "Button",
usedIn: expect.arrayContaining(["module.js"]),
entry: 'http://localhost:8000/remoteEntry.js'
}),
// ensured default remote record with moduleName "."
expect.objectContaining({
alias: "@remote/alias",
consumingFederationContainerName: "container",
federationContainerName: "remote",
moduleName: ".",
usedIn: expect.arrayContaining(["module.js"]),
entry: 'http://localhost:8000/remoteEntry.js'
}),
// dynamic remote ensured with default values
expect.objectContaining({
alias: "dynamic-remote",
consumingFederationContainerName: "container",
federationContainerName: "dynamic_remote",
moduleName: ".",
usedIn: expect.arrayContaining([
"UNKNOWN"
]),
entry: 'http://localhost:8001/remoteEntry.js'
})
])
);
});

it("should persist remote metadata in manifest", () => {
expect(manifest.remotes).toEqual(
expect.arrayContaining([
expect.objectContaining({
alias: "@remote/alias",
federationContainerName: "remote",
moduleName: "."
})
])
);
expect(manifest.remotes).toEqual(
expect.arrayContaining([
expect.objectContaining({
alias: "@remote/alias",
federationContainerName: "remote",
moduleName: "."
}),
expect.objectContaining({
alias: "dynamic-remote",
federationContainerName: "dynamic_remote",
moduleName: "."
})
])
);
});
15 changes: 13 additions & 2 deletions tests/rspack-test/configCases/container-1-5/manifest/module.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import react from 'react';
import { loadRemote } from 'mf';
import xreact from 'xreact';
import dep1 from '@scope-sc/dep1';
import dep2 from '@scope-sc2/dep2';
import remote from '@remote/alias';

global.react = react;
global.xreact = xreact;
global.remote = remote;
global.dep1 = dep1;
global.dep2 = dep2;

loadRemote('dynamic-remote');

import('@remote/alias/Button').then(r=>{
console.log('@remote/alias/Button: ',r)
})

import('./lazy-module').then(r=>{
console.log('lazy module: ',r)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading