diff --git a/.github/actions/artifact/download/action.yml b/.github/actions/artifact/download/action.yml index 88d674dae6b8..e641fcfde4f8 100644 --- a/.github/actions/artifact/download/action.yml +++ b/.github/actions/artifact/download/action.yml @@ -18,7 +18,7 @@ runs: using: composite steps: - name: Download artifact from github - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 if: ${{ runner.environment == 'github-hosted' || inputs.force-use-github == 'true' }} with: name: ${{ inputs.name }} diff --git a/.github/actions/artifact/upload/action.yml b/.github/actions/artifact/upload/action.yml index 831b8d2fe713..67268753d334 100644 --- a/.github/actions/artifact/upload/action.yml +++ b/.github/actions/artifact/upload/action.yml @@ -32,7 +32,7 @@ runs: overwrite: true - name: Upload artifact to github - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ inputs.name }} path: ${{ inputs.path }} diff --git a/crates/rspack_plugin_mf/src/manifest/mod.rs b/crates/rspack_plugin_mf/src/manifest/mod.rs index 3356d88499cd..a515fb213e56 100644 --- a/crates/rspack_plugin_mf/src/manifest/mod.rs +++ b/crates/rspack_plugin_mf/src/manifest/mod.rs @@ -85,6 +85,65 @@ fn get_remote_entry_name(compilation: &Compilation, container_name: &str) -> Opt } None } + +fn push_unique(candidates: &mut Vec, candidate: String) { + if !candidate.is_empty() && !candidates.contains(&candidate) { + candidates.push(candidate); + } +} + +fn normalize_expose_name_part(value: &str) -> String { + value + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' || c == '-' { + c + } else { + '_' + } + }) + .collect() +} + +fn push_expose_chunk_name_candidates(candidates: &mut Vec, value: &str) { + let value = value.trim_start_matches("./").trim_matches('/'); + if value.is_empty() { + return; + } + + let normalized = normalize_expose_name_part(value); + for part in [value, normalized.as_str()] { + for prefix in ["__federation_expose_", "_federation_expose_"] { + push_unique(candidates, format!("{prefix}{part}")); + } + } +} + +fn expose_chunk_name_candidates( + expose_key: &str, + expose_name: &str, + import: &str, + configured_name: Option<&str>, +) -> Vec { + let mut candidates = Vec::new(); + + if let Some(name) = configured_name.filter(|name| !name.is_empty()) { + push_unique(&mut candidates, name.to_string()); + } + + push_expose_chunk_name_candidates(&mut candidates, expose_name); + push_expose_chunk_name_candidates(&mut candidates, expose_key); + + if let Some(file_stem) = Path::new(import) + .file_stem() + .and_then(|file_stem| file_stem.to_str()) + { + push_expose_chunk_name_candidates(&mut candidates, file_stem); + } + + candidates +} + #[plugin_hook(CompilationProcessAssets for ModuleFederationManifestPlugin)] async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { // Prepare entrypoint names @@ -213,7 +272,9 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { }; let mut exposes_map: HashMap = HashMap::default(); - let mut expose_chunk_names: HashMap = HashMap::default(); + let mut expose_chunk_keys: HashMap = HashMap::default(); + let mut expose_fallback_chunk_keys: HashMap = + HashMap::default(); let mut shared_map: HashMap = HashMap::default(); let mut shared_usage_links: Vec<(String, String)> = Vec::new(); let mut shared_module_targets: HashMap> = HashMap::default(); @@ -267,7 +328,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { path: expose_key.clone(), file: String::new(), id: id_comp, - name: expose_name, + name: expose_name.clone(), requires: Vec::new(), assets: StatsAssetsGroup::default(), }); @@ -276,18 +337,39 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { && let Some(chunk_group) = compilation .chunk_graph .get_block_chunk_group(block_id, &compilation.chunk_group_by_ukey) - && let Some(chunk_key) = chunk_group.chunks.first() - && let Some(chunk) = compilation.chunk_by_ukey.get(chunk_key) - && let Some(name) = chunk.name() { - expose_chunk_names.insert(expose_file_key.clone(), name.to_string()); - } + let candidates = expose_chunk_name_candidates( + expose_key, + &expose_name, + import, + options.name.as_deref(), + ); + if let Some(chunk_key) = candidates + .iter() + .find_map(|name| compilation.named_chunks.get(name)) + .filter(|chunk_key| chunk_group.chunks.contains(chunk_key)) + .or_else(|| { + chunk_group.chunks.iter().find(|chunk_key| { + compilation + .chunk_by_ukey + .get(chunk_key) + .and_then(|chunk| chunk.name()) + .is_some_and(|name| candidates.iter().any(|candidate| candidate == name)) + }) + }) + { + expose_chunk_keys.insert(expose_file_key.clone(), *chunk_key); + } - if !expose_chunk_names.contains_key(&expose_file_key) - && let Some(n) = &options.name - && !n.is_empty() - { - expose_chunk_names.insert(expose_file_key, n.clone()); + if let Some(chunk_key) = chunk_group.chunks.iter().find(|chunk_key| { + compilation + .chunk_by_ukey + .get(chunk_key) + .and_then(|chunk| chunk.name()) + .is_some() + }) { + expose_fallback_chunk_keys.insert(expose_file_key, *chunk_key); + } } } continue; @@ -450,9 +532,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { for (expose_file_key, expose) in exposes_map.iter_mut() { let mut assets = None; - if let Some(chunk_name) = expose_chunk_names.get(expose_file_key) - && let Some(chunk_key) = compilation.named_chunks.get(chunk_name) - { + if let Some(chunk_key) = expose_chunk_keys.get(expose_file_key) { assets = Some(collect_assets_from_chunk( compilation, chunk_key, @@ -473,6 +553,15 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { { assets = collect_assets_for_module(compilation, module_id, &entry_files); } + if assets.is_none() + && let Some(chunk_key) = expose_fallback_chunk_keys.get(expose_file_key) + { + assets = Some(collect_assets_from_chunk( + compilation, + chunk_key, + &entry_files, + )); + } let mut assets = assets.unwrap_or_else(empty_assets_group); if let Some(path) = expose_module_paths.get(expose_file_key) { expose.file = path.clone(); diff --git a/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/expose-a.js b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/expose-a.js new file mode 100644 index 000000000000..f0b3ad95e45a --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/expose-a.js @@ -0,0 +1,3 @@ +import { shared } from "./shared"; + +export const value = `a ${shared}`; diff --git a/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/expose-b.js b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/expose-b.js new file mode 100644 index 000000000000..3e477855a116 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/expose-b.js @@ -0,0 +1,3 @@ +import { shared } from "./shared"; + +export const value = `b ${shared}`; diff --git a/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/index.js b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/index.js new file mode 100644 index 000000000000..a5c9f5c7de1c --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/index.js @@ -0,0 +1,33 @@ +const fs = __non_webpack_require__("fs"); +const path = __non_webpack_require__("path"); + +const stats = JSON.parse( + fs.readFileSync(path.join(__dirname, "mf-stats.json"), "utf-8") +); +const manifest = JSON.parse( + fs.readFileSync(path.join(__dirname, "mf-manifest.json"), "utf-8") +); + +function getExpose(source, name) { + return source.exposes.find(item => item.name === name); +} + +it("should keep expose entry assets separated in stats", () => { + const exposeA = getExpose(stats, "expose-a"); + const exposeB = getExpose(stats, "expose-b"); + + expect(exposeA.assets.js.sync).toContain("__federation_expose_expose-a.js"); + expect(exposeA.assets.js.sync).not.toContain("__federation_expose_expose-b.js"); + expect(exposeB.assets.js.sync).toContain("__federation_expose_expose-b.js"); + expect(exposeB.assets.js.sync).not.toContain("__federation_expose_expose-a.js"); +}); + +it("should keep expose entry assets separated in manifest", () => { + const exposeA = getExpose(manifest, "expose-a"); + const exposeB = getExpose(manifest, "expose-b"); + + expect(exposeA.assets.js.sync).toContain("__federation_expose_expose-a.js"); + expect(exposeA.assets.js.sync).not.toContain("__federation_expose_expose-b.js"); + expect(exposeB.assets.js.sync).toContain("__federation_expose_expose-b.js"); + expect(exposeB.assets.js.sync).not.toContain("__federation_expose_expose-a.js"); +}); diff --git a/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/rspack.config.js b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/rspack.config.js new file mode 100644 index 000000000000..b7f1c7f53128 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/rspack.config.js @@ -0,0 +1,46 @@ +const path = require("path"); +const { ModuleFederationPlugin } = require("@rspack/core").container; + +const implementation = require.resolve("@module-federation/runtime-tools", { + paths: [path.dirname(require.resolve("@rspack/core/package.json"))] +}); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + optimization: { + chunkIds: "named", + moduleIds: "named", + splitChunks: { + cacheGroups: { + shared: { + test: /shared/, + name: "shared", + chunks: "all", + enforce: true + } + } + } + }, + output: { + chunkFilename: "[name].js" + }, + plugins: [ + new ModuleFederationPlugin({ + name: "container", + filename: "container.js", + library: { type: "commonjs-module" }, + implementation, + manifest: true, + exposes: { + "./expose-a": { + import: "./expose-a.js", + name: "__federation_expose_expose-a" + }, + "./expose-b": { + import: "./expose-b.js", + name: "__federation_expose_expose-b" + } + } + }) + ] +}; diff --git a/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/shared.js b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/shared.js new file mode 100644 index 000000000000..f35a91e05cd6 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/manifest-multiple-exposes/shared.js @@ -0,0 +1 @@ +export const shared = Math.random();