diff --git a/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs b/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs index f60aca0aa0b1..815d8c6da47b 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs @@ -93,6 +93,11 @@ impl ConsumeSharedModule { source_map_kind: SourceMapKind::empty(), } } + + /// Get the consume options + pub fn get_options(&self) -> &ConsumeOptions { + &self.options + } } impl Identifiable for ConsumeSharedModule { diff --git a/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs b/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs index f95c99e4370a..9acad36927ad 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs @@ -9,10 +9,10 @@ use regex::Regex; use rspack_cacheable::cacheable; use rspack_core::{ BoxModule, ChunkUkey, Compilation, CompilationAdditionalTreeRuntimeRequirements, - CompilationParams, CompilerThisCompilation, Context, DependencyCategory, DependencyType, - ModuleExt, ModuleFactoryCreateData, NormalModuleCreateData, NormalModuleFactoryCreateModule, - NormalModuleFactoryFactorize, Plugin, ResolveOptionsWithDependencyType, ResolveResult, Resolver, - RuntimeGlobals, + CompilationFinishModules, CompilationParams, CompilerThisCompilation, Context, DependenciesBlock, + DependencyCategory, DependencyType, ModuleExt, ModuleFactoryCreateData, NormalModuleCreateData, + NormalModuleFactoryCreateModule, NormalModuleFactoryFactorize, Plugin, + ResolveOptionsWithDependencyType, ResolveResult, Resolver, RuntimeGlobals, }; use rspack_error::{Diagnostic, Result, error}; use rspack_fs::ReadableFileSystem; @@ -397,6 +397,111 @@ async fn this_compilation( Ok(()) } +#[plugin_hook(CompilationFinishModules for ConsumeSharedPlugin, stage = 10)] +async fn finish_modules(&self, compilation: &mut Compilation) -> Result<()> { + // Add finishModules hook to copy buildMeta/buildInfo from fallback modules *after* webpack's export analysis. + // Running earlier caused parity regressions, so we intentionally execute later than plugins like FlagDependencyExportsPlugin, + // matching the behaviour in the module-federation/core repo. This still happens before seal (see webpack's Compilation.js). + + let (consume_updates, missing_fallbacks) = { + let module_graph = compilation.get_module_graph(); + let mut updates = Vec::new(); + let mut missing = Vec::new(); + + for (module_id, module) in module_graph.modules() { + let Some(consume_shared) = module.as_any().downcast_ref::() else { + continue; + }; + + let options = consume_shared.get_options(); + + let Some(import_request) = options.import.as_ref() else { + continue; + }; + + let fallback_id = if options.eager { + module.get_dependencies().iter().find_map(|dep_id| { + module_graph + .dependency_by_id(dep_id) + .filter(|dep| matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback)) + .and_then(|_| { + module_graph + .module_identifier_by_dependency_id(dep_id) + .copied() + }) + }) + } else { + module.get_blocks().iter().find_map(|block_id| { + module_graph.block_by_id(block_id).and_then(|block| { + block.get_dependencies().iter().find_map(|dep_id| { + module_graph + .dependency_by_id(dep_id) + .filter(|dep| { + matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback) + }) + .and_then(|_| { + module_graph + .module_identifier_by_dependency_id(dep_id) + .copied() + }) + }) + }) + }) + }; + + if let Some(fallback_id) = fallback_id { + if let Some(fallback_module) = module_graph.module_by_identifier(&fallback_id) { + updates.push(( + module_id, + fallback_module.build_meta().clone(), + fallback_module.build_info().clone(), + )); + } else { + missing.push((module_id, import_request.clone())); + } + } else { + missing.push((module_id, import_request.clone())); + } + } + + (updates, missing) + }; + + if !consume_updates.is_empty() { + let mut module_graph_mut = compilation.get_module_graph_mut(); + for (module_id, fallback_meta, fallback_info) in consume_updates { + if let Some(consume_module) = module_graph_mut.module_by_identifier_mut(&module_id) { + // Copy buildMeta and buildInfo following webpack's DelegatedModule pattern: this.buildMeta = { ...delegateData.buildMeta }; + // This ensures ConsumeSharedModule inherits ESM/CJS detection (exportsType) and other optimization metadata + *consume_module.build_meta_mut() = fallback_meta; + *consume_module.build_info_mut() = fallback_info; + } + // Mark all exports as provided to avoid later export analysis from pruning fallback-provided exports. + let exports_info = module_graph_mut.get_exports_info(&module_id); + exports_info.set_unknown_exports_provided( + &mut module_graph_mut, + false, + None, + None, + None, + None, + ); + } + } + + for (module_id, request) in missing_fallbacks { + compilation.push_diagnostic(Diagnostic::warn( + "ConsumeSharedFallbackMissing".into(), + format!( + "Fallback module for '{}' not found; skipping build meta copy for module '{}'", + request, module_id + ), + )); + } + + Ok(()) +} + #[plugin_hook(NormalModuleFactoryFactorize for ConsumeSharedPlugin)] async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result> { let dep = data.dependencies[0] @@ -512,6 +617,10 @@ impl Plugin for ConsumeSharedPlugin { .compilation_hooks .additional_tree_runtime_requirements .tap(additional_tree_runtime_requirements::new(self)); + ctx + .compilation_hooks + .finish_modules + .tap(finish_modules::new(self)); Ok(()) } } diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/index.js b/tests/rspack-test/configCases/container-1-5/consume-nested/index.js new file mode 100644 index 000000000000..6aed20cca35f --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/index.js @@ -0,0 +1,4 @@ +it('should be able to consume nested modules', async () => { + const { default: main } = await import('package-1'); + expect(main('test')).toEqual('test package-1 package-2'); +}); \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/esm/index.js b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/esm/index.js new file mode 100644 index 000000000000..37e2f5da7a0e --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/esm/index.js @@ -0,0 +1,6 @@ +import package2 from 'package-2'; + +export default function package1(msg) { + const result = package2(msg + ' package-1'); + return result; +} \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/esm/package.json b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/esm/package.json new file mode 100644 index 000000000000..2bd6e5099f38 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/esm/package.json @@ -0,0 +1 @@ +{"type":"module","sideEffects":false} \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/package.json b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/package.json new file mode 100644 index 000000000000..6e0e73471063 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-1/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-1", + "version": "1.0.0", + "module": "./esm/index.js" +} \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-2/index.js b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-2/index.js new file mode 100644 index 000000000000..e4af2c6244d4 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-2/index.js @@ -0,0 +1,6 @@ +function package2(msg) { + const result = msg + ' package-2'; + return result; +} + +export { package2 as default }; \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-2/package.json b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-2/package.json new file mode 100644 index 000000000000..ba72a1eec75f --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/node_modules/package-2/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-2", + "version": "1.0.0", + "module": "./index.js" +} \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/package.json b/tests/rspack-test/configCases/container-1-5/consume-nested/package.json new file mode 100644 index 000000000000..917ccb10e3d1 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/package.json @@ -0,0 +1,7 @@ +{ + "version": "1.0.0", + "dependencies": { + "package-2": "1.0.0", + "package-1": "1.0.0" + } +} \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/consume-nested/rspack.config.js b/tests/rspack-test/configCases/container-1-5/consume-nested/rspack.config.js new file mode 100644 index 000000000000..b9e1c6c7a8db --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/consume-nested/rspack.config.js @@ -0,0 +1,16 @@ +const { ModuleFederationPlugin } = require("@rspack/core").container; + +module.exports = { + mode: 'development', + devtool: false, + plugins: [ + new ModuleFederationPlugin({ + name: 'consume-nested', + filename: 'remoteEntry.js', + shared: { + 'package-2': { version: '1.0.0' }, + 'package-1': { version: '1.0.0' }, + }, + }), + ], +}; \ No newline at end of file