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..b0a7975a0b46 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,121 @@ 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 before webpack's export analysis + // This follows webpack's pattern used by FlagDependencyExportsPlugin and InferAsyncModulesPlugin + // We use finishModules with high priority stage to ensure buildMeta is available before other plugins process exports + // Based on webpack's Compilation.js: finishModules (line 2833) runs before seal (line 2920) + + let module_graph = compilation.get_module_graph(); + + // Iterate through all modules to find ConsumeShared modules with a configured fallback import + let mut consume_shared_modules = Vec::new(); + for (module_id, module) in module_graph.modules() { + if let Some(consume_shared) = module.as_any().downcast_ref::() + && consume_shared.get_options().import.is_some() + { + consume_shared_modules.push(module_id); + } + } + + // Process each ConsumeShared module + for module_id in consume_shared_modules { + // Compute fallback module id and metadata with a single immutable access to the module graph + let fallback_meta_info = { + let module_graph = compilation.get_module_graph(); + if let Some(module) = module_graph.module_by_identifier(&module_id) { + if let Some(consume_shared) = module.as_any().downcast_ref::() { + // Find the fallback dependency + let mut fallback_id = None; + + if consume_shared.get_options().eager { + // For eager mode, get the fallback directly from dependencies + for dep_id in module.get_dependencies() { + if let Some(dep) = module_graph.dependency_by_id(dep_id) + && matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback) + { + fallback_id = module_graph + .module_identifier_by_dependency_id(dep_id) + .copied(); + break; + } + } + } else { + // For async mode, get it from the async dependencies block + for block_id in module.get_blocks() { + if let Some(block) = module_graph.block_by_id(block_id) { + for dep_id in block.get_dependencies() { + if let Some(dep) = module_graph.dependency_by_id(dep_id) + && matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback) + { + fallback_id = module_graph + .module_identifier_by_dependency_id(dep_id) + .copied(); + break; + } + } + if fallback_id.is_some() { + break; + } + } + } + } + + if let Some(fallback_id) = fallback_id { + module_graph + .module_by_identifier(&fallback_id) + .map(|fallback_module| { + ( + fallback_module.build_meta().clone(), + fallback_module.build_info().clone(), + ) + }) + } else { + None + } + } else { + None + } + } else { + None + } + }; + + if let Some((fallback_meta, fallback_info)) = fallback_meta_info { + // Update the ConsumeShared module with fallback's metadata + let mut module_graph_mut = compilation.get_module_graph_mut(); + 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; + } + } else { + // No fallback module found. Emit a warning instead of silently defaulting values. + // This avoids masking potential issues where a configured fallback import cannot be resolved. + { + let module_graph = compilation.get_module_graph(); + if let Some(module) = module_graph.module_by_identifier(&module_id) + && let Some(consume_shared) = module.as_any().downcast_ref::() + && let Some(req) = &consume_shared.get_options().import + { + compilation.push_diagnostic(Diagnostic::warn( + "ConsumeSharedFallbackMissing".into(), + format!( + "Fallback module for '{}' not found; skipping build meta copy", + req + ), + )); + } + } + } + } + + Ok(()) +} + #[plugin_hook(NormalModuleFactoryFactorize for ConsumeSharedPlugin)] async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result> { let dep = data.dependencies[0] @@ -512,6 +627,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