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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
127 changes: 123 additions & 4 deletions crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<ConsumeSharedModule>()
&& 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::<ConsumeSharedModule>() {
// 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::<ConsumeSharedModule>()
&& 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<Option<BoxModule>> {
let dep = data.dependencies[0]
Expand Down Expand Up @@ -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(())
}
}
Original file line number Diff line number Diff line change
@@ -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');
});

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.

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": "1.0.0",
"dependencies": {
"package-2": "1.0.0",
"package-1": "1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -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' },
},
}),
],
};
Loading