diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index 7e632cf67197..ce49eeb88765 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -548,10 +548,13 @@ export declare enum BuiltinPluginName { SplitChunksPlugin = 'SplitChunksPlugin', RemoveDuplicateModulesPlugin = 'RemoveDuplicateModulesPlugin', ShareRuntimePlugin = 'ShareRuntimePlugin', + SharedUsedExportsOptimizerPlugin = 'SharedUsedExportsOptimizerPlugin', ContainerPlugin = 'ContainerPlugin', ContainerReferencePlugin = 'ContainerReferencePlugin', ProvideSharedPlugin = 'ProvideSharedPlugin', ConsumeSharedPlugin = 'ConsumeSharedPlugin', + CollectSharedEntryPlugin = 'CollectSharedEntryPlugin', + SharedContainerPlugin = 'SharedContainerPlugin', ModuleFederationRuntimePlugin = 'ModuleFederationRuntimePlugin', ModuleFederationManifestPlugin = 'ModuleFederationManifestPlugin', NamedModuleIdsPlugin = 'NamedModuleIdsPlugin', @@ -1876,6 +1879,11 @@ export interface RawCircularDependencyRspackPluginOptions { onEnd?: () => void } +export interface RawCollectShareEntryPluginOptions { + consumes: Array + filename?: string +} + export interface RawCompilerPlatform { web?: boolean | null browser?: boolean | null @@ -1896,6 +1904,7 @@ export interface RawConsumeOptions { strictVersion: boolean singleton: boolean eager: boolean + treeShakingMode?: string } export interface RawConsumeSharedPluginOptions { @@ -2613,6 +2622,12 @@ export interface RawOptimizationOptions { avoidEntryIife: boolean } +export interface RawOptimizeSharedConfig { + shareKey: string + treeShaking: boolean + usedExports?: Array +} + export interface RawOptions { name?: string mode?: undefined | 'production' | 'development' | 'none' @@ -2712,6 +2727,7 @@ export interface RawProvideOptions { singleton?: boolean requiredVersion?: string | false | undefined strictVersion?: boolean + treeShakingMode?: string } export interface RawRelated { @@ -2846,6 +2862,21 @@ export interface RawRuntimeChunkOptions { name: string | ((entrypoint: { name: string }) => string) } +export interface RawSharedContainerPluginOptions { + name: string + request: string + version: string + fileName?: string + library: JsLibraryOptions +} + +export interface RawSharedUsedExportsOptimizerPluginOptions { + shared: Array + injectTreeShakingUsedExports?: boolean + manifestFileName?: string + statsFileName?: string +} + export interface RawSizeLimitsPluginOptions { assetFilter?: (assetFilename: string) => boolean hints?: "error" | "warning" @@ -2889,6 +2920,8 @@ export interface RawSplitChunksOptions { export interface RawStatsBuildInfo { buildVersion: string buildName?: string + target?: Array + plugins?: Array } export interface RawStatsOptions { diff --git a/crates/rspack_binding_api/src/plugins/interceptor.rs b/crates/rspack_binding_api/src/plugins/interceptor.rs index cb690bbb50aa..aa1856b82b7f 100644 --- a/crates/rspack_binding_api/src/plugins/interceptor.rs +++ b/crates/rspack_binding_api/src/plugins/interceptor.rs @@ -81,6 +81,7 @@ use crate::{ JsContextModuleFactoryAfterResolveDataWrapper, JsContextModuleFactoryAfterResolveResult, JsContextModuleFactoryBeforeResolveDataWrapper, JsContextModuleFactoryBeforeResolveResult, }, + dependency::DependencyWrapper, html::{ JsAfterEmitData, JsAfterTemplateExecutionData, JsAlterAssetTagGroupsData, JsAlterAssetTagsData, JsBeforeAssetTagGenerationData, JsBeforeEmitData, @@ -97,7 +98,7 @@ use crate::{ runtime::{ JsAdditionalTreeRuntimeRequirementsArg, JsAdditionalTreeRuntimeRequirementsResult, JsCreateLinkData, JsCreateScriptData, JsLinkPrefetchData, JsLinkPreloadData, JsRuntimeGlobals, - JsRuntimeRequirementInTreeArg, JsRuntimeRequirementInTreeResult, + JsRuntimeRequirementInTreeArg, JsRuntimeRequirementInTreeResult, JsRuntimeSpec, }, source::JsSourceToJs, }; diff --git a/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs b/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs index ee98fe31b7d5..fd289d1bd63e 100644 --- a/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs +++ b/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs @@ -32,7 +32,11 @@ use napi_derive::napi; use raw_dll::{RawDllReferenceAgencyPluginOptions, RawFlagAllModulesAsUsedPluginOptions}; use raw_ids::RawOccurrenceChunkIdsPluginOptions; use raw_lightning_css_minimizer::RawLightningCssMinimizerRspackPluginOptions; -use raw_mf::{RawModuleFederationManifestPluginOptions, RawModuleFederationRuntimePluginOptions}; +use raw_mf::{ + RawCollectShareEntryPluginOptions, RawModuleFederationManifestPluginOptions, + RawModuleFederationRuntimePluginOptions, RawProvideOptions, + RawSharedUsedExportsOptimizerPluginOptions, +}; use raw_sri::RawSubresourceIntegrityPluginOptions; use rspack_core::{BoxPlugin, Plugin, PluginExt}; use rspack_error::{Result, ToStringResultToRspackResultExt}; @@ -76,8 +80,9 @@ use rspack_plugin_lightning_css_minimizer::LightningCssMinimizerRspackPlugin; use rspack_plugin_limit_chunk_count::LimitChunkCountPlugin; use rspack_plugin_merge_duplicate_chunks::MergeDuplicateChunksPlugin; use rspack_plugin_mf::{ - ConsumeSharedPlugin, ContainerPlugin, ContainerReferencePlugin, ModuleFederationManifestPlugin, - ModuleFederationRuntimePlugin, ProvideSharedPlugin, ShareRuntimePlugin, + CollectSharedEntryPlugin, ConsumeSharedPlugin, ContainerPlugin, ContainerReferencePlugin, + ModuleFederationManifestPlugin, ModuleFederationRuntimePlugin, ProvideSharedPlugin, + ShareRuntimePlugin, SharedContainerPlugin, SharedUsedExportsOptimizerPlugin, }; use rspack_plugin_module_info_header::ModuleInfoHeaderPlugin; use rspack_plugin_module_replacement::{ContextReplacementPlugin, NormalModuleReplacementPlugin}; @@ -118,7 +123,7 @@ use self::{ raw_limit_chunk_count::RawLimitChunkCountPluginOptions, raw_mf::{ RawConsumeSharedPluginOptions, RawContainerPluginOptions, RawContainerReferencePluginOptions, - RawProvideOptions, + RawSharedContainerPluginOptions, }, raw_normal_replacement::RawNormalModuleReplacementPluginOptions, raw_runtime_chunk::RawRuntimeChunkOptions, @@ -171,10 +176,13 @@ pub enum BuiltinPluginName { SplitChunksPlugin, RemoveDuplicateModulesPlugin, ShareRuntimePlugin, + SharedUsedExportsOptimizerPlugin, ContainerPlugin, ContainerReferencePlugin, ProvideSharedPlugin, ConsumeSharedPlugin, + CollectSharedEntryPlugin, + SharedContainerPlugin, ModuleFederationRuntimePlugin, ModuleFederationManifestPlugin, NamedModuleIdsPlugin, @@ -471,6 +479,12 @@ impl<'a> BuiltinPlugin<'a> { ) .boxed(), ), + BuiltinPluginName::SharedUsedExportsOptimizerPlugin => { + let options = downcast_into::(self.options) + .map_err(|report| napi::Error::from_reason(report.to_string()))? + .into(); + plugins.push(SharedUsedExportsOptimizerPlugin::new(options).boxed()); + } BuiltinPluginName::ContainerPlugin => { plugins.push( ContainerPlugin::new( @@ -500,6 +514,18 @@ impl<'a> BuiltinPlugin<'a> { provides.sort_unstable_by_key(|(k, _)| k.to_string()); plugins.push(ProvideSharedPlugin::new(provides).boxed()) } + BuiltinPluginName::CollectSharedEntryPlugin => { + let options = downcast_into::(self.options) + .map_err(|report| napi::Error::from_reason(report.to_string()))? + .into(); + plugins.push(CollectSharedEntryPlugin::new(options).boxed()) + } + BuiltinPluginName::SharedContainerPlugin => { + let options = downcast_into::(self.options) + .map_err(|report| napi::Error::from_reason(report.to_string()))? + .into(); + plugins.push(SharedContainerPlugin::new(options).boxed()) + } BuiltinPluginName::ConsumeSharedPlugin => plugins.push( ConsumeSharedPlugin::new( downcast_into::(self.options) diff --git a/crates/rspack_binding_api/src/raw_options/raw_builtins/raw_mf.rs b/crates/rspack_binding_api/src/raw_options/raw_builtins/raw_mf.rs index 809658340a20..0b5fe627df49 100644 --- a/crates/rspack_binding_api/src/raw_options/raw_builtins/raw_mf.rs +++ b/crates/rspack_binding_api/src/raw_options/raw_builtins/raw_mf.rs @@ -3,11 +3,12 @@ use std::{collections::HashMap, sync::Arc}; use napi::Either; use napi_derive::napi; use rspack_plugin_mf::{ - ConsumeOptions, ConsumeSharedPluginOptions, ConsumeVersion, ContainerPluginOptions, - ContainerReferencePluginOptions, ExposeOptions, ManifestExposeOption, ManifestSharedOption, - ModuleFederationManifestPluginOptions, ModuleFederationRuntimeExperimentsOptions, - ModuleFederationRuntimePluginOptions, ProvideOptions, ProvideVersion, RemoteAliasTarget, - RemoteOptions, StatsBuildInfo, + CollectSharedEntryPluginOptions, ConsumeOptions, ConsumeSharedPluginOptions, ConsumeVersion, + ContainerPluginOptions, ContainerReferencePluginOptions, ExposeOptions, ManifestExposeOption, + ManifestSharedOption, ModuleFederationManifestPluginOptions, + ModuleFederationRuntimeExperimentsOptions, ModuleFederationRuntimePluginOptions, + OptimizeSharedConfig, ProvideOptions, ProvideVersion, RemoteAliasTarget, RemoteOptions, + SharedContainerPluginOptions, SharedUsedExportsOptimizerPluginOptions, StatsBuildInfo, }; use crate::options::{ @@ -115,6 +116,7 @@ pub struct RawProvideOptions { #[napi(ts_type = "string | false | undefined")] pub required_version: Option, pub strict_version: Option, + pub tree_shaking_mode: Option, } impl From for (String, ProvideOptions) { @@ -129,11 +131,57 @@ impl From for (String, ProvideOptions) { singleton: value.singleton, required_version: value.required_version.map(|v| RawVersionWrapper(v).into()), strict_version: value.strict_version, + tree_shaking_mode: value.tree_shaking_mode, }, ) } } +#[derive(Debug)] +#[napi(object)] +pub struct RawCollectShareEntryPluginOptions { + pub consumes: Vec, + pub filename: Option, +} + +impl From for CollectSharedEntryPluginOptions { + fn from(value: RawCollectShareEntryPluginOptions) -> Self { + Self { + consumes: value + .consumes + .into_iter() + .map(|provide| { + let (key, consume_options): (String, ConsumeOptions) = provide.into(); + (key, std::sync::Arc::new(consume_options)) + }) + .collect(), + filename: value.filename, + } + } +} + +#[derive(Debug)] +#[napi(object)] +pub struct RawSharedContainerPluginOptions { + pub name: String, + pub request: String, + pub version: String, + pub file_name: Option, + pub library: JsLibraryOptions, +} + +impl From for SharedContainerPluginOptions { + fn from(value: RawSharedContainerPluginOptions) -> Self { + SharedContainerPluginOptions { + name: value.name, + request: value.request, + version: value.version, + library: value.library.into(), + file_name: value.file_name.clone().map(Into::into), + } + } +} + #[derive(Debug)] #[napi(object)] pub struct RawConsumeSharedPluginOptions { @@ -155,6 +203,52 @@ impl From for ConsumeSharedPluginOptions { } } +#[derive(Debug)] +#[napi(object)] +pub struct RawOptimizeSharedConfig { + pub share_key: String, + pub tree_shaking: bool, + pub used_exports: Option>, +} + +impl From for OptimizeSharedConfig { + fn from(value: RawOptimizeSharedConfig) -> Self { + Self { + share_key: value.share_key, + tree_shaking: value.tree_shaking, + used_exports: value.used_exports.unwrap_or_default(), + } + } +} + +#[derive(Debug)] +#[napi(object)] +pub struct RawSharedUsedExportsOptimizerPluginOptions { + pub shared: Vec, + pub inject_tree_shaking_used_exports: Option, + pub manifest_file_name: Option, + pub stats_file_name: Option, +} + +impl From for SharedUsedExportsOptimizerPluginOptions { + fn from(value: RawSharedUsedExportsOptimizerPluginOptions) -> Self { + Self { + shared: value + .shared + .into_iter() + .map(|config| config.into()) + .collect(), + inject_tree_shaking_used_exports: value.inject_tree_shaking_used_exports.unwrap_or(true), + manifest_file_name: value + .manifest_file_name + .and_then(|s| if s.trim().is_empty() { None } else { Some(s) }), + stats_file_name: value + .stats_file_name + .and_then(|s| if s.trim().is_empty() { None } else { Some(s) }), + } + } +} + #[derive(Debug)] #[napi(object)] pub struct RawConsumeOptions { @@ -169,6 +263,7 @@ pub struct RawConsumeOptions { pub strict_version: bool, pub singleton: bool, pub eager: bool, + pub tree_shaking_mode: Option, } impl From for (String, ConsumeOptions) { @@ -185,6 +280,7 @@ impl From for (String, ConsumeOptions) { strict_version: value.strict_version, singleton: value.singleton, eager: value.eager, + tree_shaking_mode: value.tree_shaking_mode, }, ) } @@ -274,6 +370,9 @@ pub struct RawManifestSharedOption { pub struct RawStatsBuildInfo { pub build_version: String, pub build_name: Option, + // only appear when enable tree_shaking + pub target: Option>, + pub plugins: Option>, } #[derive(Debug)] @@ -337,6 +436,8 @@ impl From for ModuleFederationManifest build_info: value.build_info.map(|info| StatsBuildInfo { build_version: info.build_version, build_name: info.build_name, + target: info.target, + plugins: info.plugins, }), } } diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index 562ad923c57e..9975e7fcac41 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -71,10 +71,10 @@ use crate::{ ChunkRenderCacheArtifact, ChunkRenderResult, ChunkUkey, CodeGenerateCacheArtifact, CodeGenerationJob, CodeGenerationResult, CodeGenerationResults, CompilationLogger, CompilationLogging, CompilerOptions, CompilerPlatform, ConcatenationScope, - DependenciesDiagnosticsArtifact, DependencyCodeGeneration, DependencyTemplate, + DependenciesDiagnosticsArtifact, DependencyCodeGeneration, DependencyId, DependencyTemplate, DependencyTemplateType, DependencyType, DerefOption, Entry, EntryData, EntryOptions, - EntryRuntime, Entrypoint, ExecuteModuleId, Filename, ImportPhase, ImportVarMap, - ImportedByDeferModulesArtifact, MemoryGCStorage, ModuleFactory, ModuleGraph, + EntryRuntime, Entrypoint, ExecuteModuleId, ExtendedReferencedExport, Filename, ImportPhase, + ImportVarMap, ImportedByDeferModulesArtifact, MemoryGCStorage, ModuleFactory, ModuleGraph, ModuleGraphCacheArtifact, ModuleIdentifier, ModuleIdsArtifact, ModuleStaticCache, PathData, ProcessRuntimeRequirementsCacheArtifact, ResolverFactory, RuntimeGlobals, RuntimeKeyMap, RuntimeMode, RuntimeModule, RuntimeSpec, RuntimeSpecMap, RuntimeTemplate, SharedPluginDriver, @@ -97,6 +97,13 @@ define_hook!(CompilationExecuteModule: Series(module: &ModuleIdentifier, runtime_modules: &IdentifierSet, code_generation_results: &BindingCell, execute_module_id: &ExecuteModuleId)); define_hook!(CompilationFinishModules: Series(compilation: &mut Compilation, async_modules_artifact: &mut AsyncModulesArtifact)); define_hook!(CompilationSeal: Series(compilation: &mut Compilation)); +define_hook!(CompilationDependencyReferencedExports: Series( + compilation: &Compilation, + dependency: &DependencyId, + referenced_exports: &Option>, + runtime: Option<&RuntimeSpec>, + module_graph: Option<&ModuleGraph> +)); define_hook!(CompilationConcatenationScope: SeriesBail(compilation: &Compilation, curr_module: ModuleIdentifier) -> ConcatenationScope); define_hook!(CompilationOptimizeDependencies: SeriesBail(compilation: &Compilation, side_effects_optimize_artifact: &mut SideEffectsOptimizeArtifact, build_module_graph_artifact: &mut BuildModuleGraphArtifact, diagnostics: &mut Vec) -> bool); @@ -136,6 +143,7 @@ pub struct CompilationHooks { pub succeed_module: CompilationSucceedModuleHook, pub execute_module: CompilationExecuteModuleHook, pub finish_modules: CompilationFinishModulesHook, + pub dependency_referenced_exports: CompilationDependencyReferencedExportsHook, pub seal: CompilationSealHook, pub optimize_dependencies: CompilationOptimizeDependenciesHook, pub optimize_modules: CompilationOptimizeModulesHook, diff --git a/crates/rspack_core/src/dependency/dependency_type.rs b/crates/rspack_core/src/dependency/dependency_type.rs index e0994a0ccdf6..fd62282c1795 100644 --- a/crates/rspack_core/src/dependency/dependency_type.rs +++ b/crates/rspack_core/src/dependency/dependency_type.rs @@ -99,6 +99,10 @@ pub enum DependencyType { ContainerExposed, /// container entry, ContainerEntry, + /// share container entry + ShareContainerEntry, + /// share container fallback + ShareContainerFallback, /// remote to external, RemoteToExternal, /// fallback @@ -185,6 +189,8 @@ impl DependencyType { DependencyType::ImportMetaResolve => "import.meta.resolve", DependencyType::ContainerExposed => "container exposed", DependencyType::ContainerEntry => "container entry", + DependencyType::ShareContainerEntry => "share container entry", + DependencyType::ShareContainerFallback => "share container fallback", DependencyType::DllEntry => "dll entry", DependencyType::RemoteToExternal => "remote to external", DependencyType::RemoteToFallback => "fallback", diff --git a/crates/rspack_core/src/dependency/mod.rs b/crates/rspack_core/src/dependency/mod.rs index f03d3c04fde2..c885bbbd1463 100644 --- a/crates/rspack_core/src/dependency/mod.rs +++ b/crates/rspack_core/src/dependency/mod.rs @@ -40,10 +40,16 @@ pub use static_exports_dependency::{StaticExportsDependency, StaticExportsSpec}; use swc_core::ecma::atoms::Atom; use crate::{ - ConnectionState, EvaluatedInlinableValue, ModuleGraph, ModuleGraphCacheArtifact, - ModuleGraphConnection, ModuleIdentifier, RuntimeSpec, + ConnectionState, EvaluatedInlinableValue, ExtendedReferencedExport, ModuleGraph, + ModuleGraphCacheArtifact, ModuleGraphConnection, ModuleIdentifier, RuntimeSpec, }; +#[derive(Debug, Clone)] +pub enum ProcessModuleReferencedExports { + Map(FxHashMap), + ExtendRef(Vec), +} + #[derive(Debug, Default)] pub struct ExportSpec { pub name: Atom, diff --git a/crates/rspack_core/src/lib.rs b/crates/rspack_core/src/lib.rs index 2afceabe8977..b1f489f811c5 100644 --- a/crates/rspack_core/src/lib.rs +++ b/crates/rspack_core/src/lib.rs @@ -122,6 +122,7 @@ pub enum SourceType { Remote, ShareInit, ConsumeShared, + ShareContainerShared, Custom(#[cacheable(with=AsPreset)] Ustr), #[default] Unknown, @@ -141,6 +142,7 @@ impl std::fmt::Display for SourceType { SourceType::Remote => write!(f, "remote"), SourceType::ShareInit => write!(f, "share-init"), SourceType::ConsumeShared => write!(f, "consume-shared"), + SourceType::ShareContainerShared => write!(f, "share-container-shared"), SourceType::Unknown => write!(f, "unknown"), SourceType::CssImport => write!(f, "css-import"), SourceType::Custom(source_type) => f.write_str(source_type), @@ -160,6 +162,7 @@ impl From<&str> for SourceType { "remote" => Self::Remote, "share-init" => Self::ShareInit, "consume-shared" => Self::ConsumeShared, + "share-container-shared" => Self::ShareContainerShared, "unknown" => Self::Unknown, "css-import" => Self::CssImport, other => SourceType::Custom(other.into()), @@ -175,6 +178,7 @@ impl From<&ModuleType> for SourceType { ModuleType::WasmSync | ModuleType::WasmAsync => Self::Wasm, ModuleType::Asset | ModuleType::AssetInline | ModuleType::AssetResource => Self::Asset, ModuleType::ConsumeShared => Self::ConsumeShared, + ModuleType::ShareContainerShared => Self::ShareContainerShared, _ => Self::Unknown, } } @@ -202,6 +206,7 @@ pub enum ModuleType { Fallback, ProvideShared, ConsumeShared, + ShareContainerShared, SelfReference, Custom(#[cacheable(with=AsPreset)] Ustr), } @@ -267,6 +272,7 @@ impl ModuleType { ModuleType::Fallback => "fallback-module", ModuleType::ProvideShared => "provide-module", ModuleType::ConsumeShared => "consume-shared-module", + ModuleType::ShareContainerShared => "share-container-shared-module", ModuleType::SelfReference => "self-reference-module", ModuleType::Custom(custom) => custom.as_str(), diff --git a/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs index 7a32bd75f74a..f44f73457998 100644 --- a/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs @@ -52,7 +52,7 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { } } - fn apply(&mut self) { + async fn apply(&mut self) { let mut module_graph = self.build_module_graph_artifact.get_module_graph_mut(); module_graph.active_all_exports_info(); module_graph.reset_all_exports_info_used(); @@ -113,20 +113,19 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { self.compilation.module_graph_cache_artifact.freeze(); // collect referenced exports from modules by calling `dependency.get_referenced_exports` - // and also added referenced modules to the queue for further processing - let batch_res = batch - .into_par_iter() - .map(|(block_id, runtime, force_side_effects)| { - let (referenced_exports, module_tasks) = - self.process_module(block_id, runtime.as_ref(), force_side_effects, self.global); - ( - runtime, - force_side_effects, - referenced_exports, - module_tasks, - ) - }) - .collect::>(); + // and also added referenced modules to queue for further processing + let mut batch_res = vec![]; + for (block_id, runtime, force_side_effects) in batch { + let (referenced_exports, module_tasks) = self + .process_module(block_id, runtime.as_ref(), force_side_effects, self.global) + .await; + batch_res.push(( + runtime, + force_side_effects, + referenced_exports, + module_tasks, + )); + } let mut nested_tasks = vec![]; let mut non_nested_tasks: IdentifierMap> = IdentifierMap::default(); @@ -252,7 +251,7 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { } } - fn process_module( + async fn process_module( &self, block_id: ModuleOrAsyncDependenciesBlock, runtime: Option<&RuntimeSpec>, @@ -276,17 +275,31 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { for (dep_id, module_id) in dependencies { let old_referenced_exports = map.remove(&module_id); - let Some(referenced_exports) = get_dependency_referenced_exports( + + let referenced_exports_result = get_dependency_referenced_exports( dep_id, self.build_module_graph_artifact.get_module_graph(), &self.compilation.module_graph_cache_artifact, runtime, - ) else { - continue; - }; + ); + + self + .compilation + .plugin_driver + .compilation_hooks + .dependency_referenced_exports + .call( + self.compilation, + &dep_id, + &referenced_exports_result, + runtime, + Some(self.build_module_graph_artifact.get_module_graph()), + ) + .await; - if let Some(new_referenced_exports) = - merge_referenced_exports(old_referenced_exports, referenced_exports) + if let Some(mut referenced_exports) = referenced_exports_result + && let Some(new_referenced_exports) = + merge_referenced_exports(old_referenced_exports, referenced_exports) { map.insert(module_id, new_referenced_exports); } @@ -517,7 +530,7 @@ async fn optimize_dependencies( let mut proxy = FlagDependencyUsagePluginProxy::new(self.global, compilation, build_module_graph_artifact); - proxy.apply(); + proxy.apply().await; Ok(None) } diff --git a/crates/rspack_plugin_mf/src/container/container_entry_dependency.rs b/crates/rspack_plugin_mf/src/container/container_entry_dependency.rs index 276da8957b39..11600e882f47 100644 --- a/crates/rspack_plugin_mf/src/container/container_entry_dependency.rs +++ b/crates/rspack_plugin_mf/src/container/container_entry_dependency.rs @@ -13,8 +13,11 @@ pub struct ContainerEntryDependency { pub name: String, pub exposes: Vec<(String, ExposeOptions)>, pub share_scope: String, + pub request: Option, + pub version: Option, resource_identifier: ResourceIdentifier, pub(crate) enhanced: bool, + dependency_type: DependencyType, factorize_info: FactorizeInfo, } @@ -31,8 +34,27 @@ impl ContainerEntryDependency { name, exposes, share_scope, + request: None, + version: None, resource_identifier, enhanced, + dependency_type: DependencyType::ContainerEntry, + factorize_info: Default::default(), + } + } + + pub fn new_share_container_entry(name: String, request: String, version: String) -> Self { + let resource_identifier = format!("share-container-entry-{}", &name).into(); + Self { + id: DependencyId::new(), + name, + exposes: vec![], + share_scope: String::new(), + request: Some(request), + version: Some(version), + resource_identifier, + enhanced: false, + dependency_type: DependencyType::ShareContainerEntry, factorize_info: Default::default(), } } @@ -49,7 +71,7 @@ impl Dependency for ContainerEntryDependency { } fn dependency_type(&self) -> &DependencyType { - &DependencyType::ContainerEntry + &self.dependency_type } fn resource_identifier(&self) -> Option<&str> { @@ -64,7 +86,11 @@ impl Dependency for ContainerEntryDependency { #[cacheable_dyn] impl ModuleDependency for ContainerEntryDependency { fn request(&self) -> &str { - &self.resource_identifier + if self.dependency_type == DependencyType::ShareContainerEntry { + self.request.as_deref().unwrap_or_default() + } else { + &self.resource_identifier + } } fn factorize_info(&self) -> &FactorizeInfo { diff --git a/crates/rspack_plugin_mf/src/container/container_entry_module.rs b/crates/rspack_plugin_mf/src/container/container_entry_module.rs index 64058e725e1d..d7dbaa763e9a 100644 --- a/crates/rspack_plugin_mf/src/container/container_entry_module.rs +++ b/crates/rspack_plugin_mf/src/container/container_entry_module.rs @@ -6,11 +6,11 @@ use rspack_collections::{Identifiable, Identifier}; use rspack_core::{ AsyncDependenciesBlock, AsyncDependenciesBlockIdentifier, BoxDependency, BuildContext, BuildInfo, BuildMeta, BuildMetaExportsType, BuildResult, ChunkGroupOptions, CodeGenerationResult, - Compilation, Context, DependenciesBlock, Dependency, DependencyId, ExportsArgument, FactoryMeta, - GroupOptions, LibIdentOptions, Module, ModuleCodeGenerationContext, ModuleCodegenRuntimeTemplate, - ModuleDependency, ModuleGraph, ModuleIdentifier, ModuleType, RuntimeGlobals, RuntimeSpec, - SourceType, StaticExportsDependency, StaticExportsSpec, impl_module_meta_info, - impl_source_map_config, module_update_hash, + Compilation, Context, DependenciesBlock, Dependency, DependencyId, DependencyType, + ExportsArgument, FactoryMeta, GroupOptions, LibIdentOptions, Module, ModuleCodeGenerationContext, + ModuleCodegenRuntimeTemplate, ModuleDependency, ModuleGraph, ModuleIdentifier, ModuleType, + RuntimeGlobals, RuntimeSpec, SourceType, StaticExportsDependency, StaticExportsSpec, + impl_module_meta_info, impl_source_map_config, module_update_hash, rspack_sources::{BoxSource, RawStringSource, SourceExt}, }; use rspack_error::{Result, impl_empty_diagnosable_trait}; @@ -37,6 +37,10 @@ pub struct ContainerEntryModule { build_info: BuildInfo, build_meta: BuildMeta, enhanced: bool, + request: Option, + version: Option, + dependency_type: DependencyType, + name: String, } impl ContainerEntryModule { @@ -68,14 +72,50 @@ impl ContainerEntryModule { exports_type: BuildMetaExportsType::Namespace, ..Default::default() }, - source_map_kind: SourceMapKind::empty(), enhanced, + request: None, + version: None, + dependency_type: DependencyType::ContainerEntry, + source_map_kind: SourceMapKind::empty(), + name, + } + } + + pub fn new_share_container_entry(name: String, request: String, version: String) -> Self { + let lib_ident = format!("webpack/share/container/{}", &name); + Self { + blocks: Vec::new(), + dependencies: Vec::new(), + identifier: ModuleIdentifier::from(format!("share container entry {}@{}", &name, &version,)), + lib_ident, + exposes: vec![], + share_scope: String::new(), + factory_meta: None, + build_info: BuildInfo { + strict: true, + top_level_declarations: Some(FxHashSet::default()), + ..Default::default() + }, + build_meta: BuildMeta { + exports_type: BuildMetaExportsType::Namespace, + ..Default::default() + }, + enhanced: false, + request: Some(request), + version: Some(version), + dependency_type: DependencyType::ShareContainerEntry, + source_map_kind: SourceMapKind::empty(), + name, } } pub fn exposes(&self) -> &[(String, ExposeOptions)] { &self.exposes } + + pub fn name(&self) -> &str { + &self.name + } } impl Identifiable for ContainerEntryModule { @@ -116,7 +156,11 @@ impl Module for ContainerEntryModule { } fn module_type(&self) -> &ModuleType { - &ModuleType::JsDynamic + if self.dependency_type == DependencyType::ShareContainerEntry { + &ModuleType::ShareContainerShared + } else { + &ModuleType::JsDynamic + } } fn source_types(&self, _module_graph: &ModuleGraph) -> &[SourceType] { @@ -128,7 +172,11 @@ impl Module for ContainerEntryModule { } fn readable_identifier(&self, _context: &Context) -> Cow<'_, str> { - "container entry".into() + if self.dependency_type == DependencyType::ShareContainerEntry { + "share container entry".into() + } else { + "container entry".into() + } } fn lib_ident(&self, _options: LibIdentOptions) -> Option> { @@ -142,32 +190,49 @@ impl Module for ContainerEntryModule { ) -> Result { let mut blocks = vec![]; let mut dependencies: Vec = vec![]; - for (name, options) in &self.exposes { - let mut block = AsyncDependenciesBlock::new( - self.identifier, - None, - Some(name), - options - .import - .iter() - .map(|request| { - Box::new(ContainerExposedDependency::new( - name.clone(), - request.clone(), - )) as Box - }) - .collect(), - None, - ); - block.set_group_options(GroupOptions::ChunkGroup( - ChunkGroupOptions::default().name_optional(options.name.clone()), - )); - blocks.push(Box::new(block)); + + if self.dependency_type == DependencyType::ShareContainerEntry { + // Shared Container logic + dependencies.push(Box::new(StaticExportsDependency::new( + StaticExportsSpec::Array(vec!["get".into(), "init".into()]), + false, + ))); + if let Some(request) = &self.request { + let dep = ContainerExposedDependency::new_shared_fallback(request.clone()); + dependencies.push(Box::new(dep)); + } + } else { + // Container logic + for (name, options) in &self.exposes { + let mut block = AsyncDependenciesBlock::new( + self.identifier, + None, + Some(name), + options + .import + .iter() + .map(|request| { + Box::new(ContainerExposedDependency::new( + name.clone(), + request.clone(), + )) as Box + }) + .collect(), + None, + ); + block.set_group_options(GroupOptions::ChunkGroup( + ChunkGroupOptions::default().name_optional(options.name.clone()), + )); + blocks.push(Box::new(block)); + } + dependencies.push(Box::new(StaticExportsDependency::new( + StaticExportsSpec::Array(vec!["get".into(), "init".into()]), + false, + ))); } - dependencies.push(Box::new(StaticExportsDependency::new( - StaticExportsSpec::Array(vec!["get".into(), "init".into()]), - false, - ))); + + // I need `name` for SharedContainer logic. + // I will add `name` field to struct. Ok(BuildResult { dependencies, @@ -188,9 +253,88 @@ impl Module for ContainerEntryModule { } = code_generation_context; let mut code_generation_result = CodeGenerationResult::default(); + + if self.dependency_type == DependencyType::ShareContainerEntry { + let module_graph = compilation.get_module_graph(); + let mut factory = String::new(); + for dependency_id in self.get_dependencies() { + let dependency = module_graph.dependency_by_id(dependency_id); + if let Some(dependency) = dependency + .as_any() + .downcast_ref::() + && *dependency.dependency_type() == DependencyType::ShareContainerFallback + { + let request: &str = dependency.user_request(); + let module_expr = runtime_template.module_raw(compilation, dependency_id, request, false); + factory = runtime_template.returning_function(&module_expr, ""); + } + } + + let federation_global = format!( + "{}.federation", + runtime_template.render_runtime_globals(&RuntimeGlobals::REQUIRE) + ); + + // Generate installInitialConsumes function using returning_function + let install_initial_consumes_call = r#"localBundlerRuntime.installInitialConsumes({ + installedModules: localInstalledModules, + initialConsumes: __webpack_require__.consumesLoadingData.initialConsumes, + moduleToHandlerMapping: __webpack_require__.federation.consumesLoadingModuleToHandlerMapping || {}, + webpackRequire: __webpack_require__, + asyncLoad: true + })"#; + let install_initial_consumes_fn = + runtime_template.returning_function(install_initial_consumes_call, ""); + + // Create initShareContainer function using basic_function, supporting multi-statement body + let init_body = format!( + r#" + var installedModules = {{}}; + {federation_global}.instance = mfInstance; + {federation_global}.bundlerRuntime = bundlerRuntime; + + // Save parameters to local variables to avoid closure issues + var localBundlerRuntime = bundlerRuntime; + var localInstalledModules = installedModules; + + if(!__webpack_require__.consumesLoadingData){{return; }} + {federation_global}.installInitialConsumes = {install_initial_consumes_fn}; + + return {federation_global}.installInitialConsumes(); + "# + ); + let init_share_container_fn = + runtime_template.basic_function("mfInstance, bundlerRuntime", &init_body); + + // Generate the final source string + let source = format!( + r#" + __webpack_require__.federation = {{ instance: undefined,bundlerRuntime: undefined }} + var factory = ()=>{factory}; + var initShareContainer = {init_share_container_fn}; + {runtime}({exports}, {{ + get: function() {{ return factory;}}, + init: function() {{ return initShareContainer;}} + }}); + "#, + runtime = runtime_template.render_runtime_globals(&RuntimeGlobals::DEFINE_PROPERTY_GETTERS), + exports = runtime_template.render_exports_argument(ExportsArgument::Exports), + factory = factory, + init_share_container_fn = init_share_container_fn + ); + + // Update the code generation result with the generated source + code_generation_result = + code_generation_result.with_javascript(RawStringSource::from(source).boxed()); + code_generation_result.add(SourceType::Expose, RawStringSource::from_static("").boxed()); + return Ok(code_generation_result); + } + + // Normal Container Logic runtime_template .runtime_requirements_mut() .insert(RuntimeGlobals::CURRENT_REMOTE_GET_SCOPE); + let module_map = ExposeModuleMap::new(compilation, self, runtime_template); let module_map_str = module_map.render(runtime_template); let source = if self.enhanced { diff --git a/crates/rspack_plugin_mf/src/container/container_entry_module_factory.rs b/crates/rspack_plugin_mf/src/container/container_entry_module_factory.rs index d0f348dae927..8b54667082f8 100644 --- a/crates/rspack_plugin_mf/src/container/container_entry_module_factory.rs +++ b/crates/rspack_plugin_mf/src/container/container_entry_module_factory.rs @@ -1,5 +1,8 @@ use async_trait::async_trait; -use rspack_core::{ModuleExt, ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult}; +use rspack_core::{ + Dependency, DependencyType, ModuleExt, ModuleFactory, ModuleFactoryCreateData, + ModuleFactoryResult, +}; use rspack_error::Result; use super::{ @@ -16,14 +19,25 @@ impl ModuleFactory for ContainerEntryModuleFactory { let dep = data.dependencies[0] .downcast_ref::() .expect("dependency of ContainerEntryModuleFactory should be ContainerEntryDependency"); - Ok(ModuleFactoryResult::new_with_module( - ContainerEntryModule::new( - dep.name.clone(), - dep.exposes.clone(), - dep.share_scope.clone(), - dep.enhanced, - ) - .boxed(), - )) + if *dep.dependency_type() == DependencyType::ShareContainerEntry { + Ok(ModuleFactoryResult::new_with_module( + ContainerEntryModule::new_share_container_entry( + dep.name.clone(), + dep.request.clone().expect("should have request"), + dep.version.clone().expect("should have version"), + ) + .boxed(), + )) + } else { + Ok(ModuleFactoryResult::new_with_module( + ContainerEntryModule::new( + dep.name.clone(), + dep.exposes.clone(), + dep.share_scope.clone(), + dep.enhanced, + ) + .boxed(), + )) + } } } diff --git a/crates/rspack_plugin_mf/src/container/container_exposed_dependency.rs b/crates/rspack_plugin_mf/src/container/container_exposed_dependency.rs index 729792bf3622..f903355c5882 100644 --- a/crates/rspack_plugin_mf/src/container/container_exposed_dependency.rs +++ b/crates/rspack_plugin_mf/src/container/container_exposed_dependency.rs @@ -11,6 +11,7 @@ pub struct ContainerExposedDependency { request: String, pub exposed_name: String, resource_identifier: ResourceIdentifier, + dependency_type: DependencyType, factorize_info: FactorizeInfo, } @@ -22,6 +23,19 @@ impl ContainerExposedDependency { request, exposed_name, resource_identifier, + dependency_type: DependencyType::ContainerExposed, + factorize_info: Default::default(), + } + } + + pub fn new_shared_fallback(request: String) -> Self { + let resource_identifier = format!("share-container-fallback:{request}").into(); + Self { + id: DependencyId::new(), + request, + exposed_name: String::new(), + resource_identifier, + dependency_type: DependencyType::ShareContainerFallback, factorize_info: Default::default(), } } @@ -38,7 +52,7 @@ impl Dependency for ContainerExposedDependency { } fn dependency_type(&self) -> &DependencyType { - &DependencyType::ContainerExposed + &self.dependency_type } fn resource_identifier(&self) -> Option<&str> { diff --git a/crates/rspack_plugin_mf/src/lib.rs b/crates/rspack_plugin_mf/src/lib.rs index fab355eb5ce8..942686419b2a 100644 --- a/crates/rspack_plugin_mf/src/lib.rs +++ b/crates/rspack_plugin_mf/src/lib.rs @@ -18,6 +18,7 @@ pub use manifest::{ ModuleFederationManifestPluginOptions, RemoteAliasTarget, StatsBuildInfo, }; pub use sharing::{ + collect_shared_entry_plugin::{CollectSharedEntryPlugin, CollectSharedEntryPluginOptions}, consume_shared_module::ConsumeSharedModule, consume_shared_plugin::{ ConsumeOptions, ConsumeSharedPlugin, ConsumeSharedPluginOptions, ConsumeVersion, @@ -28,6 +29,10 @@ pub use sharing::{ CodeGenerationDataShareInit, DataInitStage, ShareInitData, ShareRuntimeModule, }, share_runtime_plugin::ShareRuntimePlugin, + shared_container_plugin::{SharedContainerPlugin, SharedContainerPluginOptions}, + shared_used_exports_optimizer_plugin::{ + OptimizeSharedConfig, SharedUsedExportsOptimizerPlugin, SharedUsedExportsOptimizerPluginOptions, + }, }; mod utils { diff --git a/crates/rspack_plugin_mf/src/manifest/data.rs b/crates/rspack_plugin_mf/src/manifest/data.rs index ff99209845ba..8d60ba3e58ab 100644 --- a/crates/rspack_plugin_mf/src/manifest/data.rs +++ b/crates/rspack_plugin_mf/src/manifest/data.rs @@ -1,6 +1,6 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct StatsAssetsGroup { #[serde(default)] pub js: AssetsSplit, @@ -8,7 +8,7 @@ pub struct StatsAssetsGroup { pub css: AssetsSplit, } -#[derive(Debug, Serialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct AssetsSplit { #[serde(default)] pub sync: Vec, @@ -16,15 +16,19 @@ pub struct AssetsSplit { pub r#async: Vec, } -#[derive(Debug, Serialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct StatsBuildInfo { #[serde(rename = "buildVersion")] pub build_version: String, #[serde(rename = "buildName", skip_serializing_if = "Option::is_none")] pub build_name: Option, + #[serde(rename = "target", skip_serializing_if = "Option::is_none")] + pub target: Option>, + #[serde(rename = "plugins", skip_serializing_if = "Option::is_none")] + pub plugins: Option>, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct StatsExpose { pub path: String, #[serde(default)] @@ -37,7 +41,7 @@ pub struct StatsExpose { pub assets: StatsAssetsGroup, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct StatsShared { pub id: String, pub name: String, @@ -50,9 +54,11 @@ pub struct StatsShared { pub assets: StatsAssetsGroup, #[serde(default)] pub usedIn: Vec, + #[serde(default)] + pub usedExports: Vec, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct StatsRemote { pub alias: String, pub consumingFederationContainerName: String, @@ -64,7 +70,7 @@ pub struct StatsRemote { pub usedIn: Vec, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct BasicStatsMetaData { pub name: String, pub globalName: String, @@ -78,7 +84,7 @@ pub struct BasicStatsMetaData { pub r#type: Option, } -#[derive(Debug, Serialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct RemoteEntryMeta { #[serde(default)] pub name: String, @@ -88,7 +94,7 @@ pub struct RemoteEntryMeta { pub r#type: String, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct StatsRoot { pub id: String, pub name: String, diff --git a/crates/rspack_plugin_mf/src/manifest/mod.rs b/crates/rspack_plugin_mf/src/manifest/mod.rs index 901fb8c765df..563ba4051a25 100644 --- a/crates/rspack_plugin_mf/src/manifest/mod.rs +++ b/crates/rspack_plugin_mf/src/manifest/mod.rs @@ -11,11 +11,11 @@ use asset::{ collect_assets_for_module, collect_assets_from_chunk, collect_usage_files_for_module, empty_assets_group, module_source_path, normalize_assets_group, }; -pub use data::StatsBuildInfo; use data::{ BasicStatsMetaData, ManifestExpose, ManifestRemote, ManifestRoot, ManifestShared, - RemoteEntryMeta, StatsAssetsGroup, StatsExpose, StatsRemote, StatsRoot, StatsShared, + RemoteEntryMeta, StatsAssetsGroup, StatsExpose, StatsRemote, StatsShared, }; +pub use data::{StatsBuildInfo, StatsRoot}; pub use options::{ ManifestExposeOption, ManifestSharedOption, ModuleFederationManifestPluginOptions, RemoteAliasTarget, @@ -85,7 +85,7 @@ fn get_remote_entry_name(compilation: &Compilation, container_name: &str) -> Opt } None } -#[plugin_hook(CompilationProcessAssets for ModuleFederationManifestPlugin)] +#[plugin_hook(CompilationProcessAssets for ModuleFederationManifestPlugin, stage = 0)] async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { // Prepare entrypoint names let entry_point_names: HashSet = compilation.entrypoints.keys().cloned().collect(); @@ -170,6 +170,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { singleton: shared.singleton.or(Some(true)), assets: StatsAssetsGroup::default(), usedIn: Vec::new(), + usedExports: Vec::new(), }) .collect::>(); let remote_list = self @@ -597,10 +598,15 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { ), ); // Build manifest from stats + let mut manifest_meta = stats_root.metaData.clone(); + if let Some(build_info) = &mut manifest_meta.build_info { + build_info.target = None; + build_info.plugins = None; + } let manifest = ManifestRoot { id: stats_root.id.clone(), name: stats_root.name.clone(), - metaData: stats_root.metaData.clone(), + metaData: manifest_meta, exposes: exposes .into_iter() .map(|e| ManifestExpose { diff --git a/crates/rspack_plugin_mf/src/manifest/utils.rs b/crates/rspack_plugin_mf/src/manifest/utils.rs index fe90ee536ba2..c87ae681b634 100644 --- a/crates/rspack_plugin_mf/src/manifest/utils.rs +++ b/crates/rspack_plugin_mf/src/manifest/utils.rs @@ -151,6 +151,7 @@ pub fn ensure_shared_entry<'a>( singleton: Some(true), assets: super::data::StatsAssetsGroup::default(), usedIn: Vec::new(), + usedExports: Vec::new(), }) } diff --git a/crates/rspack_plugin_mf/src/sharing/collect_shared_entry_plugin.rs b/crates/rspack_plugin_mf/src/sharing/collect_shared_entry_plugin.rs new file mode 100644 index 000000000000..9aa2493b5c59 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/collect_shared_entry_plugin.rs @@ -0,0 +1,230 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use regex::Regex; +use rspack_core::{ + Compilation, CompilationAsset, CompilationProcessAssets, Context, DependenciesBlock, Module, + Plugin, + rspack_sources::{RawStringSource, SourceExt}, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; +use rustc_hash::FxHashMap; +use serde::Serialize; + +use super::consume_shared_plugin::ConsumeOptions; + +const DEFAULT_FILENAME: &str = "collect-shared-entries.json"; + +#[derive(Debug, Serialize)] +struct CollectSharedEntryAssetItem<'a> { + #[serde(rename = "shareScope")] + share_scope: &'a str, + requests: &'a [[String; 2]], +} + +#[derive(Debug)] +pub struct CollectSharedEntryPluginOptions { + pub consumes: Vec<(String, Arc)>, + pub filename: Option, +} + +#[plugin] +#[derive(Debug)] +pub struct CollectSharedEntryPlugin { + options: CollectSharedEntryPluginOptions, +} + +impl CollectSharedEntryPlugin { + pub fn new(options: CollectSharedEntryPluginOptions) -> Self { + Self::new_inner(options) + } + + /// Infer package version from a module request path + /// Example: ../../../.eden-mono/temp/node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/index.js + /// It locates react-dom's package.json and reads the version field + async fn infer_version(&self, request: &str) -> Option { + // 1) Try pnpm store path pattern: .pnpm/@_ + let pnpm_re = Regex::new(r"/\\.pnpm/[^/]*@([^/_]+)").ok(); + if let Some(re) = pnpm_re + && let Some(caps) = re.captures(request) + && let Some(m) = caps.get(1) + { + return Some(m.as_str().to_string()); + } + + // 2) Fallback: read version from the deepest node_modules//package.json + let path = Path::new(request); + let comps: Vec = path + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect(); + if let Some(idx) = comps.iter().rposition(|c| c == "node_modules") { + let mut pkg_parts: Vec<&str> = Vec::new(); + if let Some(next) = comps.get(idx + 1) { + if next.starts_with('@') { + if let Some(next2) = comps.get(idx + 2) { + pkg_parts.push(next.as_str()); + pkg_parts.push(next2.as_str()); + } + } else { + pkg_parts.push(next.as_str()); + } + } + if !pkg_parts.is_empty() { + let mut package_json_path = PathBuf::new(); + for c in comps.iter().take(idx + 1) { + package_json_path.push(c); + } + for p in &pkg_parts { + package_json_path.push(p); + } + package_json_path.push("package.json"); + if package_json_path.exists() + && let Ok(content) = std::fs::read_to_string(&package_json_path) + && let Ok(json) = serde_json::from_str::(&content) + && let Some(version) = json.get("version").and_then(|v| v.as_str()) + { + return Some(version.to_string()); + } + } + } + + None + } +} + +#[plugin_hook(CompilationProcessAssets for CollectSharedEntryPlugin)] +async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { + // Traverse ConsumeSharedModule in the graph and collect real resolved module paths from fallback + let module_graph = compilation.get_module_graph(); + let mut ordered_requests: FxHashMap> = FxHashMap::default(); + let mut share_scopes: FxHashMap = FxHashMap::default(); + + for (_id, module) in module_graph.modules() { + let module_type = module.module_type(); + if !matches!(module_type, rspack_core::ModuleType::ConsumeShared) { + continue; + } + + if let Some(consume) = module + .as_any() + .downcast_ref::() + { + // Parse share_scope and share_key from readable_identifier + let ident = consume.readable_identifier(&Context::default()).to_string(); + // Format: "consume shared module ({scope}) {share_key}@..." + let (scope, key) = { + let mut scope = String::new(); + let mut key = String::new(); + if let Some(start) = ident.find("(") + && let Some(end) = ident.find(")") + && end > start + { + scope = ident[start + 1..end].to_string(); + } + if let Some(pos) = ident.find(") ") { + let rest = &ident[pos + 2..]; + // Limit to the segment before any suffixes like " (strict)", " (fallback: ...)" or " (eager)" + let suffix_start = rest.find(" (").unwrap_or(rest.len()); + let head = &rest[..suffix_start]; + // Use the LAST '@' within the head to split "{share_key}@{version}", + // so scoped names like "@scope/pkg@1.0.0" are handled correctly. + let at = head.rfind('@').unwrap_or(head.len()); + key = head[..at].to_string(); + } + (scope, key) + }; + if key.is_empty() { + continue; + } + // Collect target modules from dependencies and async blocks + let mut target_modules = Vec::new(); + for dep_id in consume.get_dependencies() { + if let Some(target_id) = module_graph.module_identifier_by_dependency_id(dep_id) { + target_modules.push(*target_id); + } + } + for block_id in consume.get_blocks() { + if let Some(block) = module_graph.block_by_id(block_id) { + for dep_id in block.get_dependencies() { + if let Some(target_id) = module_graph.module_identifier_by_dependency_id(dep_id) { + target_modules.push(*target_id); + } + } + } + } + + // Add real module resource paths to the map and infer version + let mut reqs = ordered_requests.remove(&key).unwrap_or_default(); + for target_id in target_modules { + if let Some(target) = module_graph.module_by_identifier(&target_id) + && let Some(name) = target.name_for_condition() + { + let resource: String = name.into(); + let version = self + .infer_version(&resource) + .await + .unwrap_or_else(String::new); + let pair = [resource, version]; + if !reqs.iter().any(|p| p[0] == pair[0] && p[1] == pair[1]) { + reqs.push(pair); + } + } + } + reqs.sort_by(|a, b| a[0].cmp(&b[0]).then(a[1].cmp(&b[1]))); + ordered_requests.insert(key.clone(), reqs); + if !scope.is_empty() { + share_scopes.insert(key.clone(), scope); + } + } + } + + // Build asset content + let mut shared: FxHashMap<&str, CollectSharedEntryAssetItem<'_>> = FxHashMap::default(); + for (share_key, requests) in ordered_requests.iter() { + let scope = share_scopes.get(share_key).map_or("", |s| s.as_str()); + shared.insert( + share_key.as_str(), + CollectSharedEntryAssetItem { + share_scope: scope, + requests: requests.as_slice(), + }, + ); + } + + let json = serde_json::to_string_pretty(&shared) + .expect("CollectSharedEntryPlugin: failed to serialize share entries"); + + // Get filename, or use default when absent + let filename = self + .options + .filename + .clone() + .unwrap_or_else(|| DEFAULT_FILENAME.to_string()); + + compilation.emit_asset( + filename, + CompilationAsset::new( + Some(RawStringSource::from(json).boxed()), + Default::default(), + ), + ); + Ok(()) +} + +impl Plugin for CollectSharedEntryPlugin { + fn name(&self) -> &'static str { + "rspack.CollectSharedEntryPlugin" + } + + fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> { + ctx + .compilation_hooks + .process_assets + .tap(process_assets::new(self)); + Ok(()) + } +} 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 d7a766397c50..b90b51b8c457 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs @@ -239,6 +239,7 @@ impl Module for ConsumeSharedModule { singleton: self.options.singleton, eager: self.options.eager, fallback: factory, + tree_shaking_mode: self.options.tree_shaking_mode.clone(), }); Ok(code_generation_result) } 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 e3e569cbac4e..32edf21ccd42 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs @@ -36,6 +36,7 @@ pub struct ConsumeOptions { pub strict_version: bool, pub singleton: bool, pub eager: bool, + pub tree_shaking_mode: Option, } #[cacheable] @@ -54,21 +55,21 @@ impl fmt::Display for ConsumeVersion { } } -static RELATIVE_REQUEST: LazyLock = +pub static RELATIVE_REQUEST: LazyLock = LazyLock::new(|| Regex::new(r"^\.\.?(\/|$)").expect("Invalid regex")); -static ABSOLUTE_REQUEST: LazyLock = +pub static ABSOLUTE_REQUEST: LazyLock = LazyLock::new(|| Regex::new(r"^(\/|[A-Za-z]:\\|\\\\)").expect("Invalid regex")); -static PACKAGE_NAME: LazyLock = +pub static PACKAGE_NAME: LazyLock = LazyLock::new(|| Regex::new(r"^((?:@[^\\/]+[\\/])?[^\\/]+)").expect("Invalid regex")); #[derive(Debug)] -struct MatchedConsumes { - resolved: FxHashMap>, - unresolved: FxHashMap>, - prefixed: FxHashMap>, +pub struct MatchedConsumes { + pub resolved: FxHashMap>, + pub unresolved: FxHashMap>, + pub prefixed: FxHashMap>, } -async fn resolve_matched_configs( +pub async fn resolve_matched_configs( compilation: &mut Compilation, resolver: Arc, configs: &[(String, Arc)], @@ -104,7 +105,7 @@ async fn resolve_matched_configs( } } -async fn get_description_file( +pub async fn get_description_file( fs: Arc, mut dir: &Utf8Path, satisfies_description_file_data: Option) -> bool>, @@ -137,7 +138,7 @@ async fn get_description_file( } } -fn get_required_version_from_description_file( +pub fn get_required_version_from_description_file( data: serde_json::Value, package_name: &str, ) -> Option { @@ -368,6 +369,7 @@ impl ConsumeSharedPlugin { strict_version: config.strict_version, singleton: config.singleton, eager: config.eager, + tree_shaking_mode: config.tree_shaking_mode.clone(), }, ) } @@ -404,12 +406,15 @@ async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result Result Result> { if matches!( data.dependencies[0].dependency_type(), - DependencyType::ConsumeSharedFallback | DependencyType::ProvideModuleForShared + DependencyType::ConsumeSharedFallback + | DependencyType::ProvideModuleForShared + | DependencyType::ShareContainerFallback ) { return Ok(None); } let resource = create_data.resource_resolve_data.resource(); let consumes = self.get_matched_consumes(); + if let Some(options) = consumes.resolved.get(resource) { let module = self .create_consume_shared_module(&data.context, resource, options.clone(), |d| { diff --git a/crates/rspack_plugin_mf/src/sharing/consume_shared_runtime_module.rs b/crates/rspack_plugin_mf/src/sharing/consume_shared_runtime_module.rs index 564137f89593..f5ff882546b5 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_runtime_module.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_runtime_module.rs @@ -92,7 +92,7 @@ impl RuntimeModule for ConsumeSharedRuntimeModule { .get(&module, Some(chunk.runtime())); if let Some(data) = code_gen.data.get::() { module_id_to_consume_data_mapping.insert(id, format!( - "{{ shareScope: {}, shareKey: {}, import: {}, requiredVersion: {}, strictVersion: {}, singleton: {}, eager: {}, fallback: {} }}", + "{{ shareScope: {}, shareKey: {}, import: {}, requiredVersion: {}, strictVersion: {}, singleton: {}, eager: {}, fallback: {}, treeShakingMode: {} }}", json_stringify(&data.share_scope), json_stringify(&data.share_key), json_stringify(&data.import), @@ -101,6 +101,7 @@ impl RuntimeModule for ConsumeSharedRuntimeModule { json_stringify(&data.singleton), json_stringify(&data.eager), data.fallback.as_deref().unwrap_or("undefined"), + json_stringify(&data.tree_shaking_mode), )); } }; @@ -220,4 +221,5 @@ pub struct CodeGenerationDataConsumeShared { pub singleton: bool, pub eager: bool, pub fallback: Option, + pub tree_shaking_mode: Option, } diff --git a/crates/rspack_plugin_mf/src/sharing/mod.rs b/crates/rspack_plugin_mf/src/sharing/mod.rs index a2f9e246e08b..941eb4345f82 100644 --- a/crates/rspack_plugin_mf/src/sharing/mod.rs +++ b/crates/rspack_plugin_mf/src/sharing/mod.rs @@ -1,3 +1,4 @@ +pub mod collect_shared_entry_plugin; pub mod consume_shared_fallback_dependency; pub mod consume_shared_module; pub mod consume_shared_plugin; @@ -9,3 +10,7 @@ pub mod provide_shared_module_factory; pub mod provide_shared_plugin; pub mod share_runtime_module; pub mod share_runtime_plugin; +pub mod shared_container_plugin; +pub mod shared_container_runtime_module; +pub mod shared_used_exports_optimizer_plugin; +pub mod shared_used_exports_optimizer_runtime_module; diff --git a/crates/rspack_plugin_mf/src/sharing/provide_shared_dependency.rs b/crates/rspack_plugin_mf/src/sharing/provide_shared_dependency.rs index 2c68d7d3deff..b11ae5324644 100644 --- a/crates/rspack_plugin_mf/src/sharing/provide_shared_dependency.rs +++ b/crates/rspack_plugin_mf/src/sharing/provide_shared_dependency.rs @@ -19,6 +19,7 @@ pub struct ProvideSharedDependency { pub singleton: Option, pub required_version: Option, pub strict_version: Option, + pub tree_shaking_mode: Option, resource_identifier: ResourceIdentifier, factorize_info: FactorizeInfo, } @@ -34,6 +35,7 @@ impl ProvideSharedDependency { singleton: Option, required_version: Option, strict_version: Option, + tree_shaking_mode: Option, ) -> Self { let resource_identifier = format!( "provide module ({}) {} as {} @ {} {}", @@ -54,6 +56,7 @@ impl ProvideSharedDependency { singleton, required_version, strict_version, + tree_shaking_mode, resource_identifier, factorize_info: Default::default(), } diff --git a/crates/rspack_plugin_mf/src/sharing/provide_shared_module.rs b/crates/rspack_plugin_mf/src/sharing/provide_shared_module.rs index e3ea52c1dde3..5cacc602c517 100644 --- a/crates/rspack_plugin_mf/src/sharing/provide_shared_module.rs +++ b/crates/rspack_plugin_mf/src/sharing/provide_shared_module.rs @@ -40,6 +40,7 @@ pub struct ProvideSharedModule { singleton: Option, required_version: Option, strict_version: Option, + tree_shaking_mode: Option, factory_meta: Option, build_info: BuildInfo, build_meta: BuildMeta, @@ -56,6 +57,7 @@ impl ProvideSharedModule { singleton: Option, required_version: Option, strict_version: Option, + tree_shaking_mode: Option, ) -> Self { let identifier = format!( "provide shared module ({}) {}@{} = {}", @@ -75,6 +77,7 @@ impl ProvideSharedModule { singleton, required_version, strict_version, + tree_shaking_mode, factory_meta: None, build_info: BuildInfo { strict: true, @@ -84,6 +87,10 @@ impl ProvideSharedModule { source_map_kind: SourceMapKind::empty(), } } + + pub fn share_key(&self) -> &str { + &self.name + } } impl Identifiable for ProvideSharedModule { @@ -199,6 +206,7 @@ impl Module for ProvideSharedModule { singleton: self.singleton, strict_version: self.strict_version, required_version: self.required_version.clone(), + tree_shaking_mode: self.tree_shaking_mode.clone(), }), }], }); diff --git a/crates/rspack_plugin_mf/src/sharing/provide_shared_module_factory.rs b/crates/rspack_plugin_mf/src/sharing/provide_shared_module_factory.rs index 108a7b0fad6e..990f37787363 100644 --- a/crates/rspack_plugin_mf/src/sharing/provide_shared_module_factory.rs +++ b/crates/rspack_plugin_mf/src/sharing/provide_shared_module_factory.rs @@ -31,6 +31,7 @@ impl ModuleFactory for ProvideSharedModuleFactory { dep.singleton, dep.required_version.clone(), dep.strict_version, + dep.tree_shaking_mode.clone(), ) .boxed(), )) diff --git a/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs b/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs index 2865bb206d05..d01ec623b966 100644 --- a/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs +++ b/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs @@ -35,6 +35,7 @@ pub struct ProvideOptions { pub singleton: Option, pub required_version: Option, pub strict_version: Option, + pub tree_shaking_mode: Option, } #[derive(Debug, Clone)] @@ -46,6 +47,7 @@ pub struct VersionedProvideOptions { pub singleton: Option, pub required_version: Option, pub strict_version: Option, + pub tree_shaking_mode: Option, } impl ProvideOptions { @@ -58,6 +60,7 @@ impl ProvideOptions { singleton: self.singleton, required_version: self.required_version.clone(), strict_version: self.strict_version, + tree_shaking_mode: self.tree_shaking_mode.clone(), } } } @@ -109,6 +112,7 @@ impl ProvideSharedPlugin { singleton: Option, required_version: Option, strict_version: Option, + tree_shaking_mode: Option, resource: &str, resource_data: &ResourceData, mut add_diagnostic: impl FnMut(Diagnostic), @@ -126,6 +130,7 @@ impl ProvideSharedPlugin { singleton, strict_version, required_version, + tree_shaking_mode: tree_shaking_mode.clone(), }, ); } else if let Some(description) = resource_data.description() { @@ -143,6 +148,7 @@ impl ProvideSharedPlugin { singleton, strict_version, required_version, + tree_shaking_mode: tree_shaking_mode.clone(), }, ); } else { @@ -213,6 +219,7 @@ async fn finish_make(&self, compilation: &mut Compilation) -> Result<()> { config.singleton, config.required_version.clone(), config.strict_version, + config.tree_shaking_mode.clone(), )) as BoxDependency, EntryOptions { name: None, @@ -256,6 +263,7 @@ async fn normal_module_factory_module( config.singleton, config.required_version.clone(), config.strict_version, + config.tree_shaking_mode.clone(), resource, resource_data, |d| data.diagnostics.push(d), @@ -276,6 +284,7 @@ async fn normal_module_factory_module( config.singleton, config.required_version.clone(), config.strict_version, + config.tree_shaking_mode.clone(), resource, resource_data, |d| data.diagnostics.push(d), diff --git a/crates/rspack_plugin_mf/src/sharing/share_runtime_module.rs b/crates/rspack_plugin_mf/src/sharing/share_runtime_module.rs index 3f9c0793592e..d3d284cf0b16 100644 --- a/crates/rspack_plugin_mf/src/sharing/share_runtime_module.rs +++ b/crates/rspack_plugin_mf/src/sharing/share_runtime_module.rs @@ -78,11 +78,12 @@ impl RuntimeModule for ShareRuntimeModule { DataInitInfo::ExternalModuleId(Some(id)) => json_stringify(&id), DataInitInfo::ProvideSharedInfo(info) => { let mut stage = format!( - "{{ name: {}, version: {}, factory: {}, eager: {}", + "{{ name: {}, version: {}, factory: {}, eager: {}, treeShakingMode: {}", json_stringify(&info.name), json_stringify(&info.version.to_string()), info.factory, if info.eager { "1" } else { "0" }, + json_stringify(&info.tree_shaking_mode), ); if self.enhanced { if let Some(singleton) = info.singleton { @@ -172,4 +173,5 @@ pub struct ProvideSharedInfo { pub singleton: Option, pub required_version: Option, pub strict_version: Option, + pub tree_shaking_mode: Option, } diff --git a/crates/rspack_plugin_mf/src/sharing/shared_container_plugin.rs b/crates/rspack_plugin_mf/src/sharing/shared_container_plugin.rs new file mode 100644 index 000000000000..87cf4cead02f --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/shared_container_plugin.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use rspack_core::{ + ChunkUkey, Compilation, CompilationAdditionalTreeRuntimeRequirements, CompilationParams, + CompilerCompilation, CompilerMake, DependencyType, Filename, LibraryOptions, Plugin, + RuntimeGlobals, RuntimeModule, RuntimeModuleExt, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; + +use crate::{ + container::{ + container_entry_dependency::ContainerEntryDependency, + container_entry_module_factory::ContainerEntryModuleFactory, + }, + sharing::shared_container_runtime_module::ShareContainerRuntimeModule, +}; + +#[derive(Debug)] +pub struct SharedContainerPluginOptions { + pub name: String, + pub request: String, + pub version: String, + pub file_name: Option, + pub library: LibraryOptions, +} + +#[plugin] +#[derive(Debug)] +pub struct SharedContainerPlugin { + options: SharedContainerPluginOptions, +} + +impl SharedContainerPlugin { + pub fn new(options: SharedContainerPluginOptions) -> Self { + Self::new_inner(options) + } +} + +#[plugin_hook(CompilerCompilation for SharedContainerPlugin)] +async fn compilation( + &self, + compilation: &mut Compilation, + params: &mut CompilationParams, +) -> Result<()> { + compilation.set_dependency_factory( + DependencyType::ShareContainerEntry, + Arc::new(ContainerEntryModuleFactory), + ); + compilation.set_dependency_factory( + DependencyType::ShareContainerFallback, + params.normal_module_factory.clone(), + ); + Ok(()) +} + +#[plugin_hook(CompilerMake for SharedContainerPlugin)] +async fn make(&self, compilation: &mut Compilation) -> Result<()> { + let dep = ContainerEntryDependency::new_share_container_entry( + self.options.name.clone(), + self.options.request.clone(), + self.options.version.clone(), + ); + + compilation + .add_entry( + Box::new(dep), + rspack_core::EntryOptions { + name: Some(self.options.name.clone()), + filename: self.options.file_name.clone(), + library: Some(self.options.library.clone()), + ..Default::default() + }, + ) + .await?; + Ok(()) +} + +#[plugin_hook(CompilationAdditionalTreeRuntimeRequirements for SharedContainerPlugin)] +async fn additional_tree_runtime_requirements( + &self, + compilation: &Compilation, + chunk_ukey: &ChunkUkey, + _runtime_requirements: &mut RuntimeGlobals, + runtime_modules: &mut Vec>, +) -> Result<()> { + let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey); + if let Some(name) = chunk.name() + && name == self.options.name + { + runtime_modules.push(ShareContainerRuntimeModule::new(&compilation.runtime_template).boxed()); + } + Ok(()) +} + +impl Plugin for SharedContainerPlugin { + fn name(&self) -> &'static str { + "rspack.SharedContainerPlugin" + } + + fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> { + ctx.compiler_hooks.compilation.tap(compilation::new(self)); + ctx.compiler_hooks.make.tap(make::new(self)); + ctx + .compilation_hooks + .additional_tree_runtime_requirements + .tap(additional_tree_runtime_requirements::new(self)); + Ok(()) + } +} diff --git a/crates/rspack_plugin_mf/src/sharing/shared_container_runtime_module.rs b/crates/rspack_plugin_mf/src/sharing/shared_container_runtime_module.rs new file mode 100644 index 000000000000..cce7571b2209 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/shared_container_runtime_module.rs @@ -0,0 +1,27 @@ +use rspack_core::{ + Compilation, RuntimeModule, RuntimeModuleStage, RuntimeTemplate, impl_runtime_module, +}; + +#[impl_runtime_module] +#[derive(Debug)] +pub struct ShareContainerRuntimeModule {} + +impl ShareContainerRuntimeModule { + pub fn new(runtime_template: &RuntimeTemplate) -> Self { + Self::with_name(runtime_template, "share_container_federation") + } +} + +#[async_trait::async_trait] +impl RuntimeModule for ShareContainerRuntimeModule { + async fn generate(&self, _compilation: &Compilation) -> rspack_error::Result { + Ok( + "__webpack_require__.federation = { instance: undefined,bundlerRuntime: undefined };" + .to_string(), + ) + } + + fn stage(&self) -> RuntimeModuleStage { + RuntimeModuleStage::Attach + } +} diff --git a/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_plugin.rs b/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_plugin.rs new file mode 100644 index 000000000000..189c94549937 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_plugin.rs @@ -0,0 +1,516 @@ +use std::sync::{Arc, RwLock}; + +use rspack_core::{ + AsyncDependenciesBlockIdentifier, ChunkUkey, Compilation, + CompilationAdditionalTreeRuntimeRequirements, CompilationDependencyReferencedExports, + CompilationOptimizeDependencies, CompilationProcessAssets, DependenciesBlock, Dependency, + DependencyId, DependencyType, ExtendedReferencedExport, Module, ModuleGraph, ModuleIdentifier, + Plugin, RuntimeGlobals, RuntimeModule, RuntimeModuleExt, RuntimeSpec, + SideEffectsOptimizeArtifact, + build_module_graph::BuildModuleGraphArtifact, + rspack_sources::{RawStringSource, SourceExt, SourceValue}, +}; +use rspack_error::{Diagnostic, Result}; +use rspack_hook::{plugin, plugin_hook}; +use rspack_plugin_javascript::dependency::{ESMImportSpecifierDependency, ImportDependency}; +use rspack_util::atom::Atom; +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{ + consume_shared_module::ConsumeSharedModule, provide_shared_module::ProvideSharedModule, + shared_used_exports_optimizer_runtime_module::SharedUsedExportsOptimizerRuntimeModule, +}; +use crate::{container::container_entry_module::ContainerEntryModule, manifest::StatsRoot}; +#[derive(Debug, Clone)] +pub struct OptimizeSharedConfig { + pub share_key: String, + pub tree_shaking: bool, + pub used_exports: Vec, +} + +#[derive(Debug, Clone)] +pub struct SharedUsedExportsOptimizerPluginOptions { + pub shared: Vec, + pub inject_tree_shaking_used_exports: bool, + pub stats_file_name: Option, + pub manifest_file_name: Option, +} + +#[derive(Debug, Clone)] +struct SharedEntryData { + used_exports: Vec, +} + +#[plugin] +#[derive(Debug, Clone)] +pub struct SharedUsedExportsOptimizerPlugin { + shared_map: FxHashMap, + shared_referenced_exports: Arc>>>, + inject_tree_shaking_used_exports: bool, + stats_file_name: Option, + manifest_file_name: Option, +} + +impl SharedUsedExportsOptimizerPlugin { + pub fn new(options: SharedUsedExportsOptimizerPluginOptions) -> Self { + let mut shared_map = FxHashMap::default(); + let inject_tree_shaking_used_exports = options.inject_tree_shaking_used_exports; + for config in options.shared.into_iter().filter(|c| c.tree_shaking) { + let atoms = config + .used_exports + .into_iter() + .map(Atom::from) + .collect::>(); + shared_map.insert( + config.share_key, + SharedEntryData { + used_exports: atoms, + }, + ); + } + + let shared_referenced_exports = Arc::new(RwLock::new( + FxHashMap::>::default(), + )); + + Self::new_inner( + shared_map, + shared_referenced_exports, + inject_tree_shaking_used_exports, + options.stats_file_name, + options.manifest_file_name, + ) + } + + fn apply_custom_exports(&self) { + let mut shared_referenced_exports = self + .shared_referenced_exports + .write() + .expect("lock poisoned"); + for (share_key, shared_entry_data) in &self.shared_map { + let export_set = shared_referenced_exports + .entry(share_key.clone()) + .or_default(); + for used_export in &shared_entry_data.used_exports { + export_set.insert(used_export.to_string()); + } + } + } +} + +fn collect_processed_modules( + module_graph: &ModuleGraph, + module_blocks: &[AsyncDependenciesBlockIdentifier], + module_deps: &[DependencyId], + out: &mut Vec, +) { + for dep_id in module_deps { + if let Some(target_id) = module_graph.module_identifier_by_dependency_id(dep_id) { + out.push(*target_id); + } + } + + for block_id in module_blocks { + if let Some(block) = module_graph.block_by_id(block_id) { + for dep_id in block.get_dependencies() { + if let Some(target_id) = module_graph.module_identifier_by_dependency_id(dep_id) { + out.push(*target_id); + } + } + } + } +} + +#[plugin_hook( + CompilationOptimizeDependencies for SharedUsedExportsOptimizerPlugin, + stage = 1 +)] +async fn optimize_dependencies( + &self, + _compilation: &Compilation, + _side_effects_optimize_artifact: &mut SideEffectsOptimizeArtifact, + build_module_graph_artifact: &mut BuildModuleGraphArtifact, + _diagnostics: &mut Vec, +) -> Result> { + let module_ids: Vec<_> = { + let module_graph = build_module_graph_artifact.get_module_graph(); + module_graph.modules().keys().copied().collect() + }; + self.apply_custom_exports(); + for module_id in module_ids { + let module_graph = build_module_graph_artifact.get_module_graph(); + let share_info = { + let module = module_graph.module_by_identifier(&module_id); + module.and_then(|module| { + let module_type = module.module_type(); + if !matches!( + module_type, + rspack_core::ModuleType::ConsumeShared + | rspack_core::ModuleType::ProvideShared + | rspack_core::ModuleType::ShareContainerShared + ) { + return None; + } + let mut modules_to_process = Vec::new(); + let share_key = match module_type { + rspack_core::ModuleType::ConsumeShared => { + let consume_shared_module = module.as_any().downcast_ref::()?; + // Use the readable_identifier to extract the share key + // The share key is part of the identifier string in format "consume shared module ({share_scope}) {share_key}@..." + let identifier = + consume_shared_module.readable_identifier(&rspack_core::Context::default()); + let identifier_str = identifier.to_string(); + let parts: Vec<&str> = identifier_str.split(") ").collect(); + if parts.len() < 2 { + return None; + } + let share_key_part = parts[1]; + let share_key_end = if let Some(stripped) = share_key_part.strip_prefix('@') { + stripped.find('@').map_or(share_key_part.len(), |i| i + 1) + } else { + share_key_part.find('@').unwrap_or(share_key_part.len()) + }; + let sk: String = share_key_part[..share_key_end].to_string(); + collect_processed_modules( + module_graph, + consume_shared_module.get_blocks(), + consume_shared_module.get_dependencies(), + &mut modules_to_process, + ); + sk + } + rspack_core::ModuleType::ProvideShared => { + let provide_shared_module = module.as_any().downcast_ref::()?; + let sk = provide_shared_module.share_key().to_string(); + collect_processed_modules( + module_graph, + provide_shared_module.get_blocks(), + provide_shared_module.get_dependencies(), + &mut modules_to_process, + ); + sk + } + rspack_core::ModuleType::ShareContainerShared => { + let share_container_entry_module = + module.as_any().downcast_ref::()?; + let sk = share_container_entry_module.name().to_string(); + collect_processed_modules( + module_graph, + share_container_entry_module.get_blocks(), + share_container_entry_module.get_dependencies(), + &mut modules_to_process, + ); + sk + } + _ => return None, + }; + Some((share_key, modules_to_process)) + }) + }; + + let (share_key, modules_to_process) = match share_info { + Some(result) => result, + None => continue, + }; + + if share_key.is_empty() { + continue; + } + + // Get the runtime referenced exports for this share key + let runtime_reference_exports = { + self + .shared_referenced_exports + .read() + .expect("lock poisoned") + .get(&share_key) + .cloned() + }; + // Check if this share key is in our shared map and has tree_shaking enabled + if !self.shared_map.contains_key(&share_key) { + continue; + } + if let Some(runtime_reference_exports) = runtime_reference_exports { + if runtime_reference_exports.is_empty() { + continue; + } + + let real_shared_identifier = modules_to_process.first().copied(); + + // Check if the real shared module is side effect free + if let Some(real_shared_identifier) = real_shared_identifier { + let is_side_effect_free = { + module_graph + .module_by_identifier(&real_shared_identifier) + .and_then(|module| module.factory_meta().and_then(|meta| meta.side_effect_free)) + .unwrap_or(false) + }; + + if !is_side_effect_free { + // Clear referenced exports for this share_key when module is not side-effect free + if let Ok(mut shared_referenced_exports) = self.shared_referenced_exports.write() + && let Some(set) = shared_referenced_exports.get_mut(&share_key) + { + set.clear(); + } + continue; + } + + let module_graph_mut = build_module_graph_artifact.get_module_graph_mut(); + module_graph_mut.active_all_exports_info(); + // mark used for collected modules + for module_id in &modules_to_process { + let exports_info = module_graph_mut.get_exports_info(module_id); + let exports_info_data = exports_info.as_data_mut(module_graph_mut); + + for export_name in runtime_reference_exports.iter() { + let export_atom = Atom::from(export_name.as_str()); + if let Some(export_info) = exports_info_data.named_exports_mut(&export_atom) { + // export_info.set_used(rspack_core::UsageState::Used, Some(&runtime_spec)); + export_info.set_used(rspack_core::UsageState::Used, None); + } + } + } + + // find if can update real share module + let exports_info = module_graph_mut.get_exports_info(&real_shared_identifier); + let exports_info_data = exports_info.as_data_mut(module_graph_mut); + let can_update_module_used_stage = { + let exports_view = exports_info_data.exports(); + if exports_view.is_empty() { + false + } else { + // Check if all used exports are in the runtime_reference_exports set + exports_view.iter().all(|(name, export_info)| { + let used = export_info.get_used(None); + if used != rspack_core::UsageState::Unknown && used != rspack_core::UsageState::Unused + { + runtime_reference_exports.contains(&name.to_string()) + } else { + true + } + }) + } + }; + if can_update_module_used_stage { + // mark used exports per runtime + // Mark used exports + for export_info in exports_info_data.exports_mut().values_mut() { + export_info.set_used_conditionally( + Box::new(|used| *used == rspack_core::UsageState::Unknown), + rspack_core::UsageState::Unused, + None, + ); + export_info.set_can_mangle_provide(Some(false)); + export_info.set_can_mangle_use(Some(false)); + } + } + } + } + } + + Ok(None) +} + +#[plugin_hook(CompilationProcessAssets for SharedUsedExportsOptimizerPlugin, stage = 1)] +async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { + let file_names = vec![ + self.stats_file_name.clone(), + self.manifest_file_name.clone(), + ]; + for file_name in file_names { + if let Some(file_name) = &file_name + && let Some(file) = compilation.assets().get(file_name) + && let Some(source) = file.get_source() + && let SourceValue::String(content) = source.source() + && let Ok(mut stats_root) = serde_json::from_str::(&content) + { + let shared_referenced_exports = self + .shared_referenced_exports + .read() + .expect("lock poisoned"); + + for shared in &mut stats_root.shared { + if let Some(exports_set) = shared_referenced_exports.get(&shared.name) { + shared.usedExports = exports_set.iter().cloned().collect::>(); + } + } + + let updated_content = serde_json::to_string_pretty(&stats_root) + .map_err(|e| rspack_error::error!("Failed to serialize stats root: {}", e))?; + + compilation.update_asset(file_name, |_, info| { + Ok((RawStringSource::from(updated_content).boxed(), info)) + })?; + } + } + + Ok(()) +} + +#[plugin_hook( + CompilationAdditionalTreeRuntimeRequirements for SharedUsedExportsOptimizerPlugin +)] +async fn additional_tree_runtime_requirements( + &self, + compilation: &Compilation, + _chunk_ukey: &ChunkUkey, + runtime_requirements: &mut RuntimeGlobals, + runtime_modules: &mut Vec>, +) -> Result<()> { + if self.shared_map.is_empty() { + return Ok(()); + } + + runtime_requirements.insert(RuntimeGlobals::RUNTIME_ID); + runtime_modules.push( + SharedUsedExportsOptimizerRuntimeModule::new( + &compilation.runtime_template, + Arc::new( + self + .shared_referenced_exports + .read() + .expect("lock poisoned") + .clone(), + ), + ) + .boxed(), + ); + + Ok(()) +} + +#[plugin_hook(CompilationDependencyReferencedExports for SharedUsedExportsOptimizerPlugin)] +async fn dependency_referenced_exports( + &self, + compilation: &Compilation, + dependency_id: &DependencyId, + referenced_exports: &Option>, + _runtime: Option<&RuntimeSpec>, + module_graph: Option<&ModuleGraph>, +) -> Result<()> { + let module_graph = module_graph.unwrap_or_else(|| compilation.get_module_graph()); + if referenced_exports.is_none() { + return Ok(()); + } + let Some(exports) = referenced_exports else { + return Ok(()); + }; + + let dependency = module_graph.dependency_by_id(dependency_id); + + let Some(module_dependency) = dependency.as_module_dependency() else { + return Ok(()); + }; + + let share_key: &str = module_dependency.request(); + + // Check if dependency type is EsmImportSpecifier and share_key is in shared_map + if !self.shared_map.contains_key(share_key) { + return Ok(()); + } + let mut final_exports = exports.clone(); + + // If it's an import dependency and referenced exports indicate "exports object referenced", + // clear any recorded shared referenced exports for this share key and stop here. + let is_exports_object = matches!( + final_exports.as_slice(), + [ExtendedReferencedExport::Array(arr)] if arr.is_empty() + ); + if dependency + .as_any() + .downcast_ref::() + .is_some() + && is_exports_object + { + let mut shared_referenced_exports = self + .shared_referenced_exports + .write() + .expect("lock poisoned"); + shared_referenced_exports.remove(share_key); + return Ok(()); + } + if (final_exports.is_empty() || is_exports_object) + && dependency.dependency_type() == &DependencyType::EsmImportSpecifier + && let Some(esm_dep) = dependency + .as_any() + .downcast_ref::() + { + let ids: &[Atom] = esm_dep.get_ids(module_graph); + if ids.is_empty() { + return Ok(()); + } + if let Some(first) = ids.first() + && *first == "default" + { + final_exports = esm_dep.get_referenced_exports_in_destructuring(Some(ids)); + } else { + final_exports = esm_dep.get_referenced_exports( + module_graph, + &compilation.module_graph_cache_artifact, + _runtime, + ); + } + } + + // Process each referenced export + if self.shared_map.contains_key(share_key) { + let mut shared_referenced_exports = self + .shared_referenced_exports + .write() + .expect("lock poisoned"); + let export_set = shared_referenced_exports + .entry(share_key.to_string()) + .or_default(); + + for referenced_export in &final_exports { + match referenced_export { + ExtendedReferencedExport::Array(exports_array) => { + for export in exports_array { + export_set.insert(export.to_string()); + } + } + ExtendedReferencedExport::Export(referenced) => { + if referenced.name.is_empty() { + continue; + } + for atom in &referenced.name { + export_set.insert(atom.to_string()); + } + } + } + } + } + Ok(()) +} + +impl Plugin for SharedUsedExportsOptimizerPlugin { + fn name(&self) -> &'static str { + "rspack.sharing.SharedUsedExportsOptimizerPlugin" + } + + fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> { + if self.shared_map.is_empty() { + return Ok(()); + } + ctx + .compilation_hooks + .dependency_referenced_exports + .tap(dependency_referenced_exports::new(self)); + ctx + .compilation_hooks + .optimize_dependencies + .tap(optimize_dependencies::new(self)); + ctx + .compilation_hooks + .process_assets + .tap(process_assets::new(self)); + if self.inject_tree_shaking_used_exports { + ctx + .compilation_hooks + .additional_tree_runtime_requirements + .tap(additional_tree_runtime_requirements::new(self)); + } + Ok(()) + } +} diff --git a/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_runtime_module.rs b/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_runtime_module.rs new file mode 100644 index 000000000000..a0bc98447c09 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_runtime_module.rs @@ -0,0 +1,69 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use async_trait::async_trait; +use rspack_core::{ + Compilation, RuntimeGlobals, RuntimeModule, RuntimeModuleStage, RuntimeTemplate, + impl_runtime_module, +}; +use rspack_error::{Result, error}; +use rustc_hash::{FxHashMap, FxHashSet}; + +#[impl_runtime_module] +#[derive(Debug)] +pub struct SharedUsedExportsOptimizerRuntimeModule { + // Keep type consistent with plugin: FxHashMap> + shared_used_exports: Arc>>, +} + +impl SharedUsedExportsOptimizerRuntimeModule { + pub fn new( + runtime_template: &RuntimeTemplate, + shared_used_exports: Arc>>, + ) -> Self { + Self::with_name( + runtime_template, + "module_federation/shared_used_exports", + shared_used_exports, + ) + } +} + +#[async_trait] +impl RuntimeModule for SharedUsedExportsOptimizerRuntimeModule { + fn stage(&self) -> RuntimeModuleStage { + RuntimeModuleStage::Attach + } + + async fn generate(&self, compilation: &Compilation) -> Result { + if self.shared_used_exports.is_empty() { + return Ok(String::new()); + } + let federation_global = format!( + "{}.federation", + compilation + .runtime_template + .render_runtime_globals(&RuntimeGlobals::REQUIRE) + ); + // Convert set to vec for JSON serialization stability + let stable_map: BTreeMap> = self + .shared_used_exports + .iter() + .map(|(share_key, set)| { + let mut v: Vec = set.iter().cloned().collect(); + v.sort(); + (share_key.clone(), v) + }) + .collect(); + let used_exports_json = serde_json::to_string(&stable_map).map_err(|err| { + error!( + "OptimizeDependencyReferencedExportsRuntimeModule: failed to serialize used exports: {err}" + ) + })?; + Ok(format!( + r#" +if(!{federation_global}){{return;}} +{federation_global}.usedExports = {used_exports_json}; +"# + )) + } +} diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index 9ac69c6a847b..5dcd73ebdccd 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -1526,6 +1526,7 @@ export type ConsumesConfig = { shareScope?: string; singleton?: boolean; strictVersion?: boolean; + treeShakingMode?: 'server-calc' | 'runtime-infer'; }; // @public (undocumented) @@ -1544,6 +1545,7 @@ class ConsumeSharedPlugin extends RspackBuiltinPlugin { packageName: string | undefined; singleton: boolean; eager: boolean; + treeShakingMode: "server-calc" | "runtime-infer" | undefined; }][]; enhanced: boolean; }; @@ -3538,6 +3540,18 @@ type IntermediateFileSystemExtras = { close: (arg0: number, arg1: (arg0: null | NodeJS.ErrnoException) => void) => void; }; +// @public (undocumented) +type InternalManifestPluginOptions = { + name?: string; + globalName?: string; + filePath?: string; + disableAssetsAnalyze?: boolean; + fileName?: string; + remoteAliasMap?: RemoteAliasMap; + exposes?: ManifestExposeOption[]; + shared?: ManifestSharedOption[]; +}; + // @public (undocumented) interface Invalid extends Node_4, HasSpan { // (undocumented) @@ -4943,16 +4957,7 @@ type ModuleDeclaration = ImportDeclaration | ExportDeclaration | ExportNamedDecl type ModuleExportName = Identifier | StringLiteral; // @public (undocumented) -type ModuleFederationManifestPluginOptions = { - name?: string; - globalName?: string; - filePath?: string; - disableAssetsAnalyze?: boolean; - fileName?: string; - remoteAliasMap?: RemoteAliasMap; - exposes?: ManifestExposeOption[]; - shared?: ManifestSharedOption[]; -}; +type ModuleFederationManifestPluginOptions = boolean | Pick; // @public (undocumented) class ModuleFederationPlugin { @@ -4968,11 +4973,19 @@ export interface ModuleFederationPluginOptions extends Omit; + injectTreeShakingUsedExports?: boolean; + // (undocumented) + manifest?: ModuleFederationManifestPluginOptions; // (undocumented) runtimePlugins?: RuntimePlugins; // (undocumented) shareStrategy?: 'version-first' | 'loaded-first'; + // (undocumented) + treeShakingSharedDir?: string; + // (undocumented) + treeShakingSharedExcludePlugins?: string[]; + // (undocumented) + treeShakingSharedPlugins?: string[]; } // @public (undocumented) @@ -5364,6 +5377,9 @@ export type NoParseOption = NoParseOptionSingle | NoParseOptionSingle[]; // @public (undocumented) type NoParseOptionSingle = string | RegExp | ((request: string) => boolean); +// @public (undocumented) +type NormalizedSharedOptions = [string, SharedConfig][]; + // @public (undocumented) type NormalizedStatsOptions = KnownNormalizedStatsOptions & Omit & Record; @@ -6144,6 +6160,7 @@ type ProvidesEnhancedExtraConfig = { singleton?: boolean; strictVersion?: boolean; requiredVersion?: false | string; + treeShakingMode?: 'server-calc' | 'runtime-infer'; }; // @public (undocumented) @@ -6964,6 +6981,7 @@ declare namespace rspackExports { SharedItem, SharedObject, SharePluginOptions, + TreeshakingSharedPluginOptions, sharing, LightningcssFeatureOptions, LightningcssLoaderOptions, @@ -7611,6 +7629,7 @@ export type SharedConfig = { singleton?: boolean; strictVersion?: boolean; version?: false | string; + treeShaking?: TreeShakingConfig; }; // @public (undocumented) @@ -7639,6 +7658,9 @@ type SharedOptimizationSplitChunksCacheGroup = { automaticNameDelimiter?: string; }; +// @public (undocumented) +type ShareFallback = Record; + // @public (undocumented) class SharePlugin { constructor(options: SharePluginOptions); @@ -7655,6 +7677,7 @@ class SharePlugin { singleton: boolean | undefined; packageName: string | undefined; eager: boolean | undefined; + treeShakingMode: "server-calc" | "runtime-infer" | undefined; }; }[]; // (undocumented) @@ -7669,9 +7692,12 @@ class SharePlugin { singleton: boolean | undefined; requiredVersion: string | false | undefined; strictVersion: boolean | undefined; + treeShakingMode: "server-calc" | "runtime-infer" | undefined; }; }[]; // (undocumented) + _sharedOptions: NormalizedSharedOptions; + // (undocumented) _shareScope: string | undefined; } @@ -7685,6 +7711,7 @@ export type SharePluginOptions = { // @public (undocumented) export const sharing: { ProvideSharedPlugin: typeof ProvideSharedPlugin; + TreeShakingSharedPlugin: typeof TreeShakingSharedPlugin; ConsumeSharedPlugin: typeof ConsumeSharedPlugin; SharePlugin: typeof SharePlugin; }; @@ -8765,6 +8792,38 @@ interface TransformConfig { // @public (undocumented) function transformSync(source: string, options?: Options): TransformOutput; +// @public (undocumented) +type TreeShakingConfig = { + usedExports?: string[]; + mode?: 'server-calc' | 'runtime-infer'; + filename?: string; +}; + +// @public (undocumented) +class TreeShakingSharedPlugin { + constructor(options: TreeshakingSharedPluginOptions); + // (undocumented) + apply(compiler: Compiler): void; + // (undocumented) + get buildAssets(): ShareFallback; + // (undocumented) + mfConfig: ModuleFederationPluginOptions; + // (undocumented) + name: string; + // (undocumented) + outputDir: string; + // (undocumented) + secondary?: boolean; +} + +// @public (undocumented) +export interface TreeshakingSharedPluginOptions { + // (undocumented) + mfConfig: ModuleFederationPluginOptions; + // (undocumented) + secondary?: boolean; +} + // @public (undocumented) type TruePlusMinus = true | "+" | "-"; diff --git a/packages/rspack/package.json b/packages/rspack/package.json index 3eb5322912b8..61488931efcd 100644 --- a/packages/rspack/package.json +++ b/packages/rspack/package.json @@ -70,7 +70,7 @@ "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { - "@module-federation/runtime-tools": ">=0.22.0", + "@module-federation/runtime-tools": "0.24.1", "@swc/helpers": ">=0.5.1" }, "peerDependenciesMeta": { diff --git a/packages/rspack/src/container/ContainerPlugin.ts b/packages/rspack/src/container/ContainerPlugin.ts index e8cf0aac9594..d7a30f71ac24 100644 --- a/packages/rspack/src/container/ContainerPlugin.ts +++ b/packages/rspack/src/container/ContainerPlugin.ts @@ -42,7 +42,7 @@ export class ContainerPlugin extends RspackBuiltinPlugin { name: options.name, shareScope: options.shareScope || 'default', library: options.library || { - type: 'var', + type: 'global', name: options.name, }, runtime: options.runtime, diff --git a/packages/rspack/src/container/ModuleFederationManifestPlugin.ts b/packages/rspack/src/container/ModuleFederationManifestPlugin.ts index 328165d92fd6..122ddc22af4d 100644 --- a/packages/rspack/src/container/ModuleFederationManifestPlugin.ts +++ b/packages/rspack/src/container/ModuleFederationManifestPlugin.ts @@ -10,6 +10,16 @@ import { RspackBuiltinPlugin, } from '../builtin-plugin/base'; import type { Compiler } from '../Compiler'; +import { + normalizeSharedOptions, + type SharedConfig, +} from '../sharing/SharePlugin'; +import { isRequiredVersion } from '../sharing/utils'; +import { + getRemoteInfos, + type ModuleFederationPluginOptions, +} from './ModuleFederationPlugin'; +import { parseOptions } from './options'; const MANIFEST_FILE_NAME = 'mf-manifest.json'; const STATS_FILE_NAME = 'mf-stats.json'; @@ -58,20 +68,43 @@ function readPKGJson(root?: string): Record { return {}; } -function getBuildInfo(isDev: boolean, root?: string): StatsBuildInfo { - const rootPath = root || process.cwd(); +function getBuildInfo( + isDev: boolean, + compiler: Compiler, + mfConfig: ModuleFederationPluginOptions, +): StatsBuildInfo { + const rootPath = compiler.options.context || process.cwd(); const pkg = readPKGJson(rootPath); const buildVersion = isDev ? LOCAL_BUILD_VERSION : pkg?.version; - return { + const statsBuildInfo: StatsBuildInfo = { buildVersion: process.env.MF_BUILD_VERSION || buildVersion || 'UNKNOWN', buildName: process.env.MF_BUILD_NAME || pkg?.name || 'UNKNOWN', }; + + const normalizedShared = normalizeSharedOptions(mfConfig.shared || {}); + const enableTreeShaking = Object.values(normalizedShared).some( + (config) => config[1].treeShaking, + ); + if (enableTreeShaking) { + statsBuildInfo.target = Array.isArray(compiler.options.target) + ? compiler.options.target + : []; + statsBuildInfo.plugins = mfConfig.treeShakingSharedPlugins || []; + statsBuildInfo.excludePlugins = + mfConfig.treeShakingSharedExcludePlugins || []; + } + + return statsBuildInfo; } interface StatsBuildInfo { buildVersion: string; buildName?: string; + // only appear when enable tree shaking + target?: string[]; + excludePlugins?: string[]; + plugins?: string[]; } export type RemoteAliasMap = Record; @@ -88,7 +121,7 @@ export type ManifestSharedOption = { singleton?: boolean; }; -export type ModuleFederationManifestPluginOptions = { +type InternalManifestPluginOptions = { name?: string; globalName?: string; filePath?: string; @@ -99,11 +132,27 @@ export type ModuleFederationManifestPluginOptions = { shared?: ManifestSharedOption[]; }; -function getFileName(manifestOptions: ModuleFederationManifestPluginOptions): { +export type ModuleFederationManifestPluginOptions = + | boolean + | Pick< + InternalManifestPluginOptions, + 'disableAssetsAnalyze' | 'filePath' | 'fileName' + >; + +export function getFileName( + manifestOptions: ModuleFederationManifestPluginOptions, +): { statsFileName: string; manifestFileName: string; } { if (!manifestOptions) { + return { + statsFileName: '', + manifestFileName: '', + }; + } + + if (typeof manifestOptions === 'boolean') { return { statsFileName: STATS_FILE_NAME, manifestFileName: MANIFEST_FILE_NAME, @@ -135,19 +184,144 @@ function getFileName(manifestOptions: ModuleFederationManifestPluginOptions): { }; } +function resolveLibraryGlobalName( + library: ModuleFederationPluginOptions['library'], +): string | undefined { + if (!library) { + return undefined; + } + const libName = library.name; + if (!libName) { + return undefined; + } + if (typeof libName === 'string') { + return libName; + } + if (Array.isArray(libName)) { + return libName[0]; + } + if (typeof libName === 'object') { + return libName.root?.[0] ?? libName.amd ?? libName.commonjs ?? undefined; + } + return undefined; +} + +function collectManifestExposes( + exposes: ModuleFederationPluginOptions['exposes'], +): ManifestExposeOption[] | undefined { + if (!exposes) return undefined; + type NormalizedExpose = { import: string[]; name?: string }; + type ExposesConfigInput = { import: string | string[]; name?: string }; + const parsed = parseOptions( + exposes, + (value) => ({ + import: Array.isArray(value) ? value : [value], + name: undefined, + }), + (value) => ({ + import: Array.isArray(value.import) ? value.import : [value.import], + name: value.name ?? undefined, + }), + ); + const result = parsed.map(([exposeKey, info]) => { + const exposeName = info.name ?? exposeKey.replace(/^\.\//, ''); + return { + path: exposeKey, + name: exposeName, + }; + }); + return result.length > 0 ? result : undefined; +} + +function collectManifestShared( + shared: ModuleFederationPluginOptions['shared'], +): ManifestSharedOption[] | undefined { + if (!shared) return undefined; + const parsed = parseOptions( + shared, + (item, key) => { + if (typeof item !== 'string') { + throw new Error('Unexpected array in shared'); + } + return item === key || !isRequiredVersion(item) + ? { import: item } + : { import: key, requiredVersion: item }; + }, + (item) => item, + ); + const result = parsed.map(([key, config]) => { + const name = config.shareKey || key; + const version = + typeof config.version === 'string' ? config.version : undefined; + const requiredVersion = + typeof config.requiredVersion === 'string' + ? config.requiredVersion + : undefined; + return { + name, + version, + requiredVersion, + singleton: config.singleton, + }; + }); + return result.length > 0 ? result : undefined; +} + +function normalizeManifestOptions(mfConfig: ModuleFederationPluginOptions) { + const manifestOptions: InternalManifestPluginOptions = + mfConfig.manifest === true ? {} : { ...mfConfig.manifest }; + const containerName = mfConfig.name; + const globalName = + resolveLibraryGlobalName(mfConfig.library) ?? containerName; + const remoteAliasMap: RemoteAliasMap = Object.entries( + getRemoteInfos(mfConfig), + ).reduce((sum, cur) => { + if (cur[1].length > 1) { + // no support multiple remotes + return sum; + } + const remoteInfo = cur[1][0]; + const { entry, alias, name } = remoteInfo; + if (entry && name) { + sum[alias] = { + name, + entry, + }; + } + return sum; + }, {}); + + const manifestExposes = collectManifestExposes(mfConfig.exposes); + if (manifestOptions.exposes === undefined && manifestExposes) { + manifestOptions.exposes = manifestExposes; + } + const manifestShared = collectManifestShared(mfConfig.shared); + if (manifestOptions.shared === undefined && manifestShared) { + manifestOptions.shared = manifestShared; + } + + return { + ...manifestOptions, + remoteAliasMap, + globalName, + name: containerName, + }; +} + /** * JS-side post-processing plugin: reads mf-manifest.json and mf-stats.json, executes additionalData callback and merges/overwrites manifest. * To avoid cross-NAPI callback complexity, this plugin runs at the afterProcessAssets stage to ensure Rust-side MfManifestPlugin has already output its artifacts. */ export class ModuleFederationManifestPlugin extends RspackBuiltinPlugin { name = BuiltinPluginName.ModuleFederationManifestPlugin; - private opts: ModuleFederationManifestPluginOptions; - constructor(opts: ModuleFederationManifestPluginOptions) { + private rawOpts: ModuleFederationPluginOptions; + constructor(opts: ModuleFederationPluginOptions) { super(); - this.opts = opts; + this.rawOpts = opts; } raw(compiler: Compiler): BuiltinPlugin { + const opts = normalizeManifestOptions(this.rawOpts); const { fileName, filePath, @@ -155,12 +329,12 @@ export class ModuleFederationManifestPlugin extends RspackBuiltinPlugin { remoteAliasMap, exposes, shared, - } = this.opts; - const { statsFileName, manifestFileName } = getFileName(this.opts); + } = opts; + const { statsFileName, manifestFileName } = getFileName(opts); const rawOptions: RawModuleFederationManifestPluginOptions = { - name: this.opts.name, - globalName: this.opts.globalName, + name: opts.name, + globalName: opts.globalName, fileName, filePath, manifestFileName, @@ -171,7 +345,8 @@ export class ModuleFederationManifestPlugin extends RspackBuiltinPlugin { shared, buildInfo: getBuildInfo( compiler.options.mode === 'development', - compiler.context, + compiler, + this.rawOpts, ), }; return createBuiltinPlugin(this.name, rawOptions); diff --git a/packages/rspack/src/container/ModuleFederationPlugin.ts b/packages/rspack/src/container/ModuleFederationPlugin.ts index 153fe0afde18..40d5b7c31978 100644 --- a/packages/rspack/src/container/ModuleFederationPlugin.ts +++ b/packages/rspack/src/container/ModuleFederationPlugin.ts @@ -1,14 +1,13 @@ import { createRequire } from 'node:module'; import type { Compiler } from '../Compiler'; import type { ExternalsType } from '../config'; +import type { ShareFallback } from '../sharing/IndependentSharedPlugin'; import type { SharedConfig } from '../sharing/SharePlugin'; +import { TreeShakingSharedPlugin } from '../sharing/TreeShakingSharedPlugin'; import { isRequiredVersion } from '../sharing/utils'; import { - type ManifestExposeOption, - type ManifestSharedOption, ModuleFederationManifestPlugin, type ModuleFederationManifestPluginOptions, - type RemoteAliasMap, } from './ModuleFederationManifestPlugin'; import type { ModuleFederationPluginV1Options } from './ModuleFederationPluginV1'; import { @@ -28,17 +27,18 @@ export interface ModuleFederationPluginOptions extends Omit< runtimePlugins?: RuntimePlugins; implementation?: string; shareStrategy?: 'version-first' | 'loaded-first'; - manifest?: - | boolean - | Omit< - ModuleFederationManifestPluginOptions, - 'remoteAliasMap' | 'globalName' | 'name' | 'exposes' | 'shared' - >; + manifest?: ModuleFederationManifestPluginOptions; + injectTreeShakingUsedExports?: boolean; + treeShakingSharedDir?: string; + treeShakingSharedExcludePlugins?: string[]; + treeShakingSharedPlugins?: string[]; experiments?: ModuleFederationRuntimeExperimentsOptions; } export type RuntimePlugins = string[] | [string, Record][]; export class ModuleFederationPlugin { + private _treeShakingSharedPlugin?: TreeShakingSharedPlugin; + constructor(private _options: ModuleFederationPluginOptions) {} apply(compiler: Compiler) { @@ -50,19 +50,66 @@ export class ModuleFederationPlugin { ...compiler.options.resolve.alias, }; - // Generate the runtime entry content - const entryRuntime = getDefaultEntryRuntime(paths, this._options, compiler); + const sharedOptions = getSharedOptions(this._options); + const treeShakingEntries = sharedOptions.filter( + ([, config]) => config.treeShaking, + ); + if (treeShakingEntries.length > 0) { + this._treeShakingSharedPlugin = new TreeShakingSharedPlugin({ + mfConfig: this._options, + secondary: false, + }); + this._treeShakingSharedPlugin.apply(compiler); + } const asyncStartup = this._options.experiments?.asyncStartup ?? false; const runtimeExperiments: ModuleFederationRuntimeExperimentsOptions = { asyncStartup, }; - // Pass only the entry runtime to the Rust-side plugin - new ModuleFederationRuntimePlugin({ - entryRuntime, - experiments: runtimeExperiments, - }).apply(compiler); + // need to wait treeShakingSharedPlugin buildAssets + let runtimePluginApplied = false; + compiler.hooks.beforeRun.tap( + { + name: 'ModuleFederationPlugin', + stage: 100, + }, + () => { + if (runtimePluginApplied) return; + runtimePluginApplied = true; + const entryRuntime = getDefaultEntryRuntime( + paths, + this._options, + compiler, + this._treeShakingSharedPlugin?.buildAssets, + ); + new ModuleFederationRuntimePlugin({ + entryRuntime, + experiments: runtimeExperiments, + }).apply(compiler); + }, + ); + compiler.hooks.watchRun.tap( + { + name: 'ModuleFederationPlugin', + stage: 100, + }, + () => { + if (runtimePluginApplied) return; + runtimePluginApplied = true; + const entryRuntime = getDefaultEntryRuntime( + paths, + this._options, + compiler, + this._treeShakingSharedPlugin?.buildAssets || {}, + ); + // Pass only the entry runtime to the Rust-side plugin + new ModuleFederationRuntimePlugin({ + entryRuntime, + experiments: runtimeExperiments, + }).apply(compiler); + }, + ); // Keep v1 options isolated from v2-only fields like `experiments`. const v1Options: ModuleFederationPluginV1Options = { @@ -80,46 +127,7 @@ export class ModuleFederationPlugin { new webpack.container.ModuleFederationPluginV1(v1Options).apply(compiler); if (this._options.manifest) { - const manifestOptions: ModuleFederationManifestPluginOptions = - this._options.manifest === true ? {} : { ...this._options.manifest }; - const containerName = manifestOptions.name ?? this._options.name; - const globalName = - manifestOptions.globalName ?? - resolveLibraryGlobalName(this._options.library) ?? - containerName; - const remoteAliasMap: RemoteAliasMap = Object.entries( - getRemoteInfos(this._options), - ).reduce((sum, cur) => { - if (cur[1].length > 1) { - // no support multiple remotes - return sum; - } - const remoteInfo = cur[1][0]; - const { entry, alias, name } = remoteInfo; - if (entry && name) { - sum[alias] = { - name, - entry, - }; - } - return sum; - }, {}); - - const manifestExposes = collectManifestExposes(this._options.exposes); - if (manifestOptions.exposes === undefined && manifestExposes) { - manifestOptions.exposes = manifestExposes; - } - const manifestShared = collectManifestShared(this._options.shared); - if (manifestOptions.shared === undefined && manifestShared) { - manifestOptions.shared = manifestShared; - } - - new ModuleFederationManifestPlugin({ - ...manifestOptions, - name: containerName, - globalName, - remoteAliasMap, - }).apply(compiler); + new ModuleFederationManifestPlugin(this._options).apply(compiler); } } } @@ -140,90 +148,9 @@ interface RemoteInfo { type RemoteInfos = Record; -function collectManifestExposes( - exposes: ModuleFederationPluginOptions['exposes'], -): ManifestExposeOption[] | undefined { - if (!exposes) return undefined; - type NormalizedExpose = { import: string[]; name?: string }; - type ExposesConfigInput = { import: string | string[]; name?: string }; - const parsed = parseOptions( - exposes, - (value) => ({ - import: Array.isArray(value) ? value : [value], - name: undefined, - }), - (value) => ({ - import: Array.isArray(value.import) ? value.import : [value.import], - name: value.name ?? undefined, - }), - ); - const result = parsed.map(([exposeKey, info]) => { - const exposeName = info.name ?? exposeKey.replace(/^\.\//, ''); - return { - path: exposeKey, - name: exposeName, - }; - }); - return result.length > 0 ? result : undefined; -} - -function collectManifestShared( - shared: ModuleFederationPluginOptions['shared'], -): ManifestSharedOption[] | undefined { - if (!shared) return undefined; - const parsed = parseOptions( - shared, - (item, key) => { - if (typeof item !== 'string') { - throw new Error('Unexpected array in shared'); - } - return item === key || !isRequiredVersion(item) - ? { import: item } - : { import: key, requiredVersion: item }; - }, - (item) => item, - ); - const result = parsed.map(([key, config]) => { - const name = config.shareKey || key; - const version = - typeof config.version === 'string' ? config.version : undefined; - const requiredVersion = - typeof config.requiredVersion === 'string' - ? config.requiredVersion - : undefined; - return { - name, - version, - requiredVersion, - singleton: config.singleton, - }; - }); - return result.length > 0 ? result : undefined; -} - -function resolveLibraryGlobalName( - library: ModuleFederationPluginOptions['library'], -): string | undefined { - if (!library) { - return undefined; - } - const libName = library.name; - if (!libName) { - return undefined; - } - if (typeof libName === 'string') { - return libName; - } - if (Array.isArray(libName)) { - return libName[0]; - } - if (typeof libName === 'object') { - return libName.root?.[0] ?? libName.amd ?? libName.commonjs ?? undefined; - } - return undefined; -} - -function getRemoteInfos(options: ModuleFederationPluginOptions): RemoteInfos { +export function getRemoteInfos( + options: ModuleFederationPluginOptions, +): RemoteInfos { if (!options.remotes) { return {}; } @@ -306,6 +233,24 @@ function getRuntimePlugins(options: ModuleFederationPluginOptions) { return options.runtimePlugins ?? []; } +function getSharedOptions( + options: ModuleFederationPluginOptions, +): [string, SharedConfig][] { + if (!options.shared) return []; + return parseOptions( + options.shared, + (item, key) => { + if (typeof item !== 'string') { + throw new Error('Unexpected array in shared'); + } + return item === key || !isRequiredVersion(item) + ? { import: item } + : { import: key, requiredVersion: item }; + }, + (item) => item, + ); +} + function getPaths( options: ModuleFederationPluginOptions, compiler: Compiler, @@ -353,11 +298,13 @@ function getDefaultEntryRuntime( paths: RuntimePaths, options: ModuleFederationPluginOptions, compiler: Compiler, + treeShakingShareFallbacks?: ShareFallback, ) { const runtimePlugins = getRuntimePlugins(options); const remoteInfos = getRemoteInfos(options); const runtimePluginImports = []; const runtimePluginVars = []; + const libraryType = options.library?.type || 'var'; for (let i = 0; i < runtimePlugins.length; i++) { const runtimePluginVar = `__module_federation_runtime_plugin_${i}__`; const pluginSpec = runtimePlugins[i]; @@ -389,6 +336,10 @@ function getDefaultEntryRuntime( `const __module_federation_share_strategy__ = ${JSON.stringify( options.shareStrategy ?? 'version-first', )}`, + `const __module_federation_share_fallbacks__ = ${JSON.stringify( + treeShakingShareFallbacks, + )}`, + `const __module_federation_library_type__ = ${JSON.stringify(libraryType)}`, IS_BROWSER ? MF_RUNTIME_CODE : compiler.webpack.Template.getFunctionContent( diff --git a/packages/rspack/src/exports.ts b/packages/rspack/src/exports.ts index 40a501498dd6..1f48093f7ea8 100644 --- a/packages/rspack/src/exports.ts +++ b/packages/rspack/src/exports.ts @@ -274,6 +274,7 @@ export const container = { import { ConsumeSharedPlugin } from './sharing/ConsumeSharedPlugin'; import { ProvideSharedPlugin } from './sharing/ProvideSharedPlugin'; import { SharePlugin } from './sharing/SharePlugin'; +import { TreeShakingSharedPlugin } from './sharing/TreeShakingSharedPlugin'; export type { ConsumeSharedPluginOptions, @@ -296,8 +297,10 @@ export type { SharedObject, SharePluginOptions, } from './sharing/SharePlugin'; +export type { TreeshakingSharedPluginOptions } from './sharing/TreeShakingSharedPlugin'; export const sharing = { ProvideSharedPlugin, + TreeShakingSharedPlugin, ConsumeSharedPlugin, SharePlugin, }; diff --git a/packages/rspack/src/runtime/moduleFederationDefaultRuntime.js b/packages/rspack/src/runtime/moduleFederationDefaultRuntime.js index dc220bbb1e73..db0364465795 100644 --- a/packages/rspack/src/runtime/moduleFederationDefaultRuntime.js +++ b/packages/rspack/src/runtime/moduleFederationDefaultRuntime.js @@ -3,7 +3,9 @@ var __module_federation_bundler_runtime__, __module_federation_runtime_plugins__, __module_federation_remote_infos__, __module_federation_container_name__, - __module_federation_share_strategy__; + __module_federation_share_strategy__, + __module_federation_share_fallbacks__, + __module_federation_library_type__; export default function () { if ( (__webpack_require__.initializeSharingData || @@ -51,6 +53,17 @@ export default function () { __module_federation_bundler_runtime__[key]; } + early( + __webpack_require__.federation, + 'libraryType', + () => __module_federation_library_type__, + ); + early( + __webpack_require__.federation, + 'sharedFallback', + () => __module_federation_share_fallbacks__, + ); + const sharedFallback = __webpack_require__.federation.sharedFallback; early( __webpack_require__.federation, 'consumesLoadingModuleToHandlerMapping', @@ -60,7 +73,17 @@ export default function () { consumesLoadingModuleToConsumeDataMapping, )) { consumesLoadingModuleToHandlerMapping[moduleId] = { - getter: data.fallback, + getter: sharedFallback + ? __webpack_require__.federation.bundlerRuntime?.getSharedFallbackGetter( + { + shareKey: data.shareKey, + factory: data.fallback, + webpackRequire: __webpack_require__, + libraryType: __webpack_require__.federation.libraryType, + }, + ) + : data.fallback, + treeShakingGetter: sharedFallback ? data.fallback : undefined, shareInfo: { shareConfig: { fixedDependencies: false, @@ -72,6 +95,12 @@ export default function () { scope: [data.shareScope], }, shareKey: data.shareKey, + treeShaking: __webpack_require__.federation.sharedFallback + ? { + get: data.fallback, + mode: data.treeShakingMode, + } + : undefined, }; } return consumesLoadingModuleToHandlerMapping; @@ -104,6 +133,7 @@ export default function () { singleton, requiredVersion, strictVersion, + treeShakingMode, } = stage; const shareConfig = {}; const isValidValue = function (val) { @@ -126,6 +156,11 @@ export default function () { scope: [scope], shareConfig, get: factory, + treeShaking: treeShakingMode + ? { + mode: treeShakingMode, + } + : undefined, }; if (shared[name]) { shared[name].push(options); @@ -273,9 +308,9 @@ export default function () { }); __webpack_require__.federation.instance = - __webpack_require__.federation.runtime.init( - __webpack_require__.federation.initOptions, - ); + __webpack_require__.federation.bundlerRuntime.init({ + webpackRequire: __webpack_require__, + }); if (__webpack_require__.consumesLoadingData?.initialConsumes) { __webpack_require__.federation.bundlerRuntime.installInitialConsumes({ diff --git a/packages/rspack/src/sharing/CollectSharedEntryPlugin.ts b/packages/rspack/src/sharing/CollectSharedEntryPlugin.ts new file mode 100644 index 000000000000..45f712e1440c --- /dev/null +++ b/packages/rspack/src/sharing/CollectSharedEntryPlugin.ts @@ -0,0 +1,92 @@ +import { + type BuiltinPlugin, + BuiltinPluginName, + type RawCollectShareEntryPluginOptions, +} from '@rspack/binding'; +import { + createBuiltinPlugin, + RspackBuiltinPlugin, +} from '../builtin-plugin/base'; +import type { Compiler } from '../Compiler'; +import { normalizeConsumeShareOptions } from './ConsumeSharedPlugin'; +import { + createConsumeShareOptions, + type NormalizedSharedOptions, +} from './SharePlugin'; + +export type CollectSharedEntryPluginOptions = { + sharedOptions: NormalizedSharedOptions; + shareScope?: string; +}; + +export type ShareRequestsMap = Record< + string, + { + shareScope: string; + requests: [string, string][]; + } +>; + +const SHARE_ENTRY_ASSET = 'collect-shared-entries.json'; +export class CollectSharedEntryPlugin extends RspackBuiltinPlugin { + name = BuiltinPluginName.CollectSharedEntryPlugin; + sharedOptions: NormalizedSharedOptions; + private _collectedEntries: ShareRequestsMap; + + constructor(options: CollectSharedEntryPluginOptions) { + super(); + const { sharedOptions } = options; + + this.sharedOptions = sharedOptions; + this._collectedEntries = {}; + } + + getData() { + return this._collectedEntries; + } + + getFilename() { + return SHARE_ENTRY_ASSET; + } + + apply(compiler: Compiler) { + super.apply(compiler); + + compiler.hooks.thisCompilation.tap( + 'Collect shared entry', + (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'CollectSharedEntry', + stage: + compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE, + }, + () => { + compilation.getAssets().forEach((asset) => { + if (asset.name === SHARE_ENTRY_ASSET) { + this._collectedEntries = JSON.parse( + asset.source.source().toString(), + ); + } + compilation.deleteAsset(asset.name); + }); + }, + ); + }, + ); + } + + raw(): BuiltinPlugin { + const consumeShareOptions = createConsumeShareOptions(this.sharedOptions); + const normalizedConsumeShareOptions = + normalizeConsumeShareOptions(consumeShareOptions); + const rawOptions: RawCollectShareEntryPluginOptions = { + consumes: normalizedConsumeShareOptions.map(([key, v]) => ({ + key, + ...v, + })), + filename: this.getFilename(), + }; + return createBuiltinPlugin(this.name, rawOptions); + } +} diff --git a/packages/rspack/src/sharing/ConsumeSharedPlugin.ts b/packages/rspack/src/sharing/ConsumeSharedPlugin.ts index 34564ca6610e..ed92eeeda526 100644 --- a/packages/rspack/src/sharing/ConsumeSharedPlugin.ts +++ b/packages/rspack/src/sharing/ConsumeSharedPlugin.ts @@ -31,8 +31,63 @@ export type ConsumesConfig = { shareScope?: string; singleton?: boolean; strictVersion?: boolean; + treeShakingMode?: 'server-calc' | 'runtime-infer'; }; +export function normalizeConsumeShareOptions( + consumes: Consumes, + shareScope?: string, +) { + return parseOptions( + consumes, + (item, key) => { + if (Array.isArray(item)) throw new Error('Unexpected array in options'); + const result = + item === key || !isRequiredVersion(item) + ? // item is a request/key + { + import: key, + shareScope: shareScope || 'default', + shareKey: key, + requiredVersion: undefined, + packageName: undefined, + strictVersion: false, + singleton: false, + eager: false, + treeShakingMode: undefined, + } + : // key is a request/key + // item is a version + { + import: key, + shareScope: shareScope || 'default', + shareKey: key, + requiredVersion: item, + strictVersion: true, + packageName: undefined, + singleton: false, + eager: false, + treeShakingMode: undefined, + }; + return result; + }, + (item, key) => ({ + import: item.import === false ? undefined : item.import || key, + shareScope: item.shareScope || shareScope || 'default', + shareKey: item.shareKey || key, + requiredVersion: item.requiredVersion, + strictVersion: + typeof item.strictVersion === 'boolean' + ? item.strictVersion + : item.import !== false && !item.singleton, + packageName: item.packageName, + singleton: !!item.singleton, + eager: !!item.eager, + treeShakingMode: item.treeShakingMode, + }), + ); +} + export class ConsumeSharedPlugin extends RspackBuiltinPlugin { name = BuiltinPluginName.ConsumeSharedPlugin; _options; @@ -40,51 +95,9 @@ export class ConsumeSharedPlugin extends RspackBuiltinPlugin { constructor(options: ConsumeSharedPluginOptions) { super(); this._options = { - consumes: parseOptions( + consumes: normalizeConsumeShareOptions( options.consumes, - (item, key) => { - if (Array.isArray(item)) - throw new Error('Unexpected array in options'); - const result = - item === key || !isRequiredVersion(item) - ? // item is a request/key - { - import: key, - shareScope: options.shareScope || 'default', - shareKey: key, - requiredVersion: undefined, - packageName: undefined, - strictVersion: false, - singleton: false, - eager: false, - } - : // key is a request/key - // item is a version - { - import: key, - shareScope: options.shareScope || 'default', - shareKey: key, - requiredVersion: item, - strictVersion: true, - packageName: undefined, - singleton: false, - eager: false, - }; - return result; - }, - (item, key) => ({ - import: item.import === false ? undefined : item.import || key, - shareScope: item.shareScope || options.shareScope || 'default', - shareKey: item.shareKey || key, - requiredVersion: item.requiredVersion, - strictVersion: - typeof item.strictVersion === 'boolean' - ? item.strictVersion - : item.import !== false && !item.singleton, - packageName: item.packageName, - singleton: !!item.singleton, - eager: !!item.eager, - }), + options.shareScope, ), enhanced: options.enhanced ?? false, }; diff --git a/packages/rspack/src/sharing/IndependentSharedPlugin.ts b/packages/rspack/src/sharing/IndependentSharedPlugin.ts new file mode 100644 index 000000000000..0775146d67d6 --- /dev/null +++ b/packages/rspack/src/sharing/IndependentSharedPlugin.ts @@ -0,0 +1,469 @@ +import { join, resolve } from 'node:path'; + +import type { Compiler } from '../Compiler'; +import type { LibraryOptions, Plugins, RspackOptions } from '../config'; +import { + getFileName, + type ModuleFederationManifestPluginOptions, +} from '../container/ModuleFederationManifestPlugin'; +import { parseOptions } from '../container/options'; +import { + CollectSharedEntryPlugin, + type ShareRequestsMap, +} from './CollectSharedEntryPlugin'; +import { ConsumeSharedPlugin } from './ConsumeSharedPlugin'; +import { + SharedContainerPlugin, + type SharedContainerPluginOptions, +} from './SharedContainerPlugin'; +import { SharedUsedExportsOptimizerPlugin } from './SharedUsedExportsOptimizerPlugin'; +import type { Shared, SharedConfig } from './SharePlugin'; +import { encodeName, isRequiredVersion } from './utils'; + +const VIRTUAL_ENTRY = './virtual-entry.js'; +const VIRTUAL_ENTRY_NAME = 'virtual-entry'; + +export type MakeRequired = Required> & + Omit; + +const filterPlugin = (plugin: Plugins[0], excludedPlugins: string[] = []) => { + if (!plugin) { + return true; + } + const pluginName = plugin.name || plugin.constructor?.name; + if (!pluginName) { + return true; + } + return ![ + 'TreeShakingSharedPlugin', + 'IndependentSharedPlugin', + 'ModuleFederationPlugin', + 'SharedUsedExportsOptimizerPlugin', + 'HtmlWebpackPlugin', + 'HtmlRspackPlugin', + 'RsbuildHtmlPlugin', + ...excludedPlugins, + ].includes(pluginName); +}; + +export interface IndependentSharePluginOptions { + name: string; + shared: Shared; + library?: LibraryOptions; + outputDir?: string; + plugins?: Plugins; + treeShaking?: boolean; + manifest?: ModuleFederationManifestPluginOptions; + injectTreeShakingUsedExports?: boolean; + treeShakingSharedExcludePlugins?: string[]; +} + +// { react: [ [ react/19.0.0/index.js , 19.0.0, react_global_name ] ] } +export type ShareFallback = Record; + +class VirtualEntryPlugin { + sharedOptions: [string, SharedConfig][]; + collectShared = false; + constructor(sharedOptions: [string, SharedConfig][], collectShared: boolean) { + this.sharedOptions = sharedOptions; + this.collectShared = collectShared; + } + createEntry() { + const { sharedOptions, collectShared } = this; + const entryContent = sharedOptions.reduce((acc, cur, index) => { + const importLine = `import shared_${index} from '${cur[0]}';\n`; + // Always mark the import as used to prevent tree-shaking removal + // Optional console for debugging: reference the variable, not a string + const logLine = collectShared ? `console.log(shared_${index});\n` : ''; + return acc + importLine + logLine; + }, ''); + return entryContent; + } + + static entry() { + return { + [VIRTUAL_ENTRY_NAME]: VIRTUAL_ENTRY, + }; + } + + apply(compiler: Compiler) { + new compiler.rspack.experiments.VirtualModulesPlugin({ + [VIRTUAL_ENTRY]: this.createEntry(), + }).apply(compiler); + + compiler.hooks.thisCompilation.tap( + 'RemoveVirtualEntryAsset', + (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'RemoveVirtualEntryAsset', + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE, + }, + () => { + try { + const chunk = compilation.namedChunks.get(VIRTUAL_ENTRY_NAME); + + chunk?.files.forEach((f) => { + compilation.deleteAsset(f); + }); + } catch (_e) { + console.error('Failed to remove virtual entry file!'); + } + }, + ); + }, + ); + } +} + +const resolveOutputDir = (outputDir: string, shareName?: string) => { + return shareName ? join(outputDir, encodeName(shareName)) : outputDir; +}; + +export class IndependentSharedPlugin { + mfName: string; + shared: Shared; + library?: LibraryOptions; + sharedOptions: [string, SharedConfig][]; + outputDir: string; + plugins: Plugins; + treeShaking?: boolean; + manifest?: ModuleFederationManifestPluginOptions; + buildAssets: ShareFallback = {}; + injectTreeShakingUsedExports?: boolean; + treeShakingSharedExcludePlugins?: string[]; + + name = 'IndependentSharedPlugin'; + constructor(options: IndependentSharePluginOptions) { + const { + outputDir, + plugins, + treeShaking, + shared, + name, + manifest, + injectTreeShakingUsedExports, + library, + treeShakingSharedExcludePlugins, + } = options; + this.shared = shared; + this.mfName = name; + this.outputDir = outputDir || 'independent-packages'; + this.plugins = plugins || []; + this.treeShaking = treeShaking; + this.manifest = manifest; + this.injectTreeShakingUsedExports = injectTreeShakingUsedExports ?? true; + this.library = library; + this.treeShakingSharedExcludePlugins = + treeShakingSharedExcludePlugins || []; + this.sharedOptions = parseOptions( + shared, + (item, key) => { + if (typeof item !== 'string') + throw new Error( + `Unexpected array in shared configuration for key "${key}"`, + ); + const config: SharedConfig = + item === key || !isRequiredVersion(item) + ? { + import: item, + } + : { + import: key, + requiredVersion: item, + }; + + return config; + }, + (item) => { + return item; + }, + ); + } + + apply(compiler: Compiler) { + const { manifest } = this; + let runCount = 0; + + compiler.hooks.beforeRun.tapPromise('IndependentSharedPlugin', async () => { + if (runCount) { + return; + } + await this.createIndependentCompilers(compiler); + runCount++; + }); + + compiler.hooks.watchRun.tapPromise('IndependentSharedPlugin', async () => { + if (runCount) { + return; + } + await this.createIndependentCompilers(compiler); + runCount++; + }); + + // inject buildAssets to stats + if (manifest) { + compiler.hooks.compilation.tap( + 'IndependentSharedPlugin', + (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'injectBuildAssets', + stage: (compilation.constructor as any) + .PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER, + }, + () => { + const { statsFileName, manifestFileName } = getFileName(manifest); + const injectBuildAssetsIntoStatsOrManifest = ( + filename: string, + ) => { + const stats = compilation.getAsset(filename); + if (!stats) { + return; + } + const statsContent = JSON.parse( + stats.source.source().toString(), + ) as { + shared: { + name: string; + version: string; + fallback?: string; + fallbackName?: string; + }[]; + }; + + const { shared } = statsContent; + Object.entries(this.buildAssets).forEach(([key, item]) => { + const targetShared = shared.find((s) => s.name === key); + if (!targetShared) { + return; + } + item.forEach(([entry, version, globalName]) => { + if (version === targetShared.version) { + targetShared.fallback = entry; + targetShared.fallbackName = globalName; + } + }); + }); + + compilation.updateAsset( + filename, + new compiler.webpack.sources.RawSource( + JSON.stringify(statsContent), + ), + ); + }; + + injectBuildAssetsIntoStatsOrManifest(statsFileName); + injectBuildAssetsIntoStatsOrManifest(manifestFileName); + }, + ); + }, + ); + } + } + + private async createIndependentCompilers(parentCompiler: Compiler) { + const { sharedOptions, buildAssets, outputDir } = this; + console.log('Start building shared fallback resources ...'); + + // collect share requests for each shareName and then build share container + const shareRequestsMap: ShareRequestsMap = + await this.createIndependentCompiler(parentCompiler); + + await Promise.all( + sharedOptions.map(async ([shareName, shareConfig]) => { + if (!shareConfig.treeShaking || shareConfig.import === false) { + return; + } + const shareRequests = shareRequestsMap[shareName].requests; + await Promise.all( + shareRequests.map(async ([request, version]) => { + const sharedConfig = sharedOptions.find( + ([name]) => name === shareName, + )?.[1]; + const [shareFileName, globalName, sharedVersion] = + await this.createIndependentCompiler(parentCompiler, { + shareRequestsMap, + currentShare: { + shareName, + version, + request, + independentShareFileName: sharedConfig?.treeShaking?.filename, + }, + }); + if (typeof shareFileName === 'string') { + buildAssets[shareName] ||= []; + buildAssets[shareName].push([ + join(resolveOutputDir(outputDir, shareName), shareFileName), + sharedVersion, + globalName, + ]); + } + }), + ); + }), + ); + + console.log('All shared fallback have been compiled successfully!'); + } + + private async createIndependentCompiler( + parentCompiler: Compiler, + extraOptions?: { + currentShare: Omit; + shareRequestsMap: ShareRequestsMap; + }, + ) { + const { + mfName, + plugins, + outputDir, + sharedOptions, + treeShaking, + library, + treeShakingSharedExcludePlugins, + } = this; + + const outputDirWithShareName = resolveOutputDir( + outputDir, + extraOptions?.currentShare?.shareName || '', + ); + const parentConfig = parentCompiler.options; + + const finalPlugins = []; + const rspack = parentCompiler.rspack; + let extraPlugin: CollectSharedEntryPlugin | SharedContainerPlugin; + if (!extraOptions) { + extraPlugin = new CollectSharedEntryPlugin({ + sharedOptions, + shareScope: 'default', + }); + } else { + extraPlugin = new SharedContainerPlugin({ + mfName: `${mfName}_${treeShaking ? 't' : 'f'}`, + library, + ...extraOptions.currentShare, + }); + } + (parentConfig.plugins || []).forEach((plugin) => { + if ( + plugin !== undefined && + typeof plugin !== 'string' && + filterPlugin(plugin, treeShakingSharedExcludePlugins) + ) { + finalPlugins.push(plugin); + } + }); + plugins.forEach((plugin) => { + finalPlugins.push(plugin); + }); + finalPlugins.push(extraPlugin); + + finalPlugins.push( + new ConsumeSharedPlugin({ + consumes: sharedOptions + .filter( + ([key, options]) => + extraOptions?.currentShare.shareName !== + (options.shareKey || key), + ) + .map(([key, options]) => ({ + [key]: { + import: !extraOptions ? options.import : false, + shareKey: options.shareKey || key, + shareScope: options.shareScope, + requiredVersion: options.requiredVersion, + strictVersion: options.strictVersion, + singleton: options.singleton, + packageName: options.packageName, + eager: options.eager, + }, + })), + enhanced: true, + }), + ); + + if (treeShaking) { + finalPlugins.push( + new SharedUsedExportsOptimizerPlugin( + sharedOptions, + this.injectTreeShakingUsedExports, + ), + ); + } + finalPlugins.push( + new VirtualEntryPlugin(sharedOptions, !extraOptions), + // new rspack.experiments.VirtualModulesPlugin({ + // [VIRTUAL_ENTRY]: this.createEntry() + // }) + ); + const fullOutputDir = resolve( + parentCompiler.outputPath, + outputDirWithShareName, + ); + const compilerConfig: RspackOptions = { + ...parentConfig, + module: { + ...parentConfig.module, + rules: [ + { + test: /virtual-entry\.js$/, + type: 'javascript/auto', + resolve: { fullySpecified: false }, + use: { + loader: 'builtin:swc-loader', + }, + }, + ...(parentConfig.module?.rules || []), + ], + }, + mode: parentConfig.mode || 'development', + + entry: VirtualEntryPlugin.entry, + + output: { + path: fullOutputDir, + clean: true, + publicPath: parentConfig.output?.publicPath || 'auto', + }, + + plugins: finalPlugins, + + optimization: { + ...parentConfig.optimization, + splitChunks: false, + }, + }; + + const compiler = rspack.rspack(compilerConfig); + + compiler.inputFileSystem = parentCompiler.inputFileSystem; + compiler.outputFileSystem = parentCompiler.outputFileSystem; + compiler.intermediateFileSystem = parentCompiler.intermediateFileSystem; + + const { currentShare } = extraOptions || {}; + + return new Promise((resolve, reject) => { + compiler.run((err: any, stats: any) => { + if (err || stats?.hasErrors()) { + const target = currentShare ? currentShare.shareName : 'Collect deps'; + console.error( + `${target} Compile failed:`, + err || + stats + .toJson() + .errors.map((e: Error) => e.message) + .join('\n'), + ); + reject(err || new Error(`${target} Compile failed`)); + return; + } + + currentShare && + console.log(`${currentShare.shareName} Compile success`); + resolve(extraPlugin.getData()); + }); + }); + } +} diff --git a/packages/rspack/src/sharing/ProvideSharedPlugin.ts b/packages/rspack/src/sharing/ProvideSharedPlugin.ts index 32c91c3ce739..73d9ebb17831 100644 --- a/packages/rspack/src/sharing/ProvideSharedPlugin.ts +++ b/packages/rspack/src/sharing/ProvideSharedPlugin.ts @@ -37,8 +37,49 @@ type ProvidesEnhancedExtraConfig = { singleton?: boolean; strictVersion?: boolean; requiredVersion?: false | string; + /** + * Tree shaking strategy for the shared module. + */ + treeShakingMode?: 'server-calc' | 'runtime-infer'; }; +export function normalizeProvideShareOptions( + options: Provides, + shareScope?: string, + enhanced?: boolean, +) { + return parseOptions( + options, + (item) => { + if (Array.isArray(item)) throw new Error('Unexpected array of provides'); + return { + shareKey: item, + version: undefined, + shareScope: shareScope || 'default', + eager: false, + }; + }, + (item) => { + const raw = { + shareKey: item.shareKey, + version: item.version, + shareScope: item.shareScope || shareScope || 'default', + eager: !!item.eager, + }; + if (enhanced) { + const enhancedItem: ProvidesConfig = item; + return { + ...raw, + singleton: enhancedItem.singleton, + requiredVersion: enhancedItem.requiredVersion, + strictVersion: enhancedItem.strictVersion, + treeShakingMode: enhancedItem.treeShakingMode, + }; + } + return raw; + }, + ); +} export class ProvideSharedPlugin< Enhanced extends boolean = false, > extends RspackBuiltinPlugin { @@ -48,36 +89,10 @@ export class ProvideSharedPlugin< constructor(options: ProvideSharedPluginOptions) { super(); - this._provides = parseOptions( + this._provides = normalizeProvideShareOptions( options.provides, - (item) => { - if (Array.isArray(item)) - throw new Error('Unexpected array of provides'); - return { - shareKey: item, - version: undefined, - shareScope: options.shareScope || 'default', - eager: false, - }; - }, - (item) => { - const raw = { - shareKey: item.shareKey, - version: item.version, - shareScope: item.shareScope || options.shareScope || 'default', - eager: !!item.eager, - }; - if (options.enhanced) { - const enhancedItem: ProvidesConfig = item; - return { - ...raw, - singleton: enhancedItem.singleton, - requiredVersion: enhancedItem.requiredVersion, - strictVersion: enhancedItem.strictVersion, - }; - } - return raw; - }, + options.shareScope, + options.enhanced, ); this._enhanced = options.enhanced; } diff --git a/packages/rspack/src/sharing/SharePlugin.ts b/packages/rspack/src/sharing/SharePlugin.ts index 23bf56465e16..116b44570011 100644 --- a/packages/rspack/src/sharing/SharePlugin.ts +++ b/packages/rspack/src/sharing/SharePlugin.ts @@ -14,6 +14,12 @@ export type SharedItem = string; export type SharedObject = { [k: string]: SharedConfig | SharedItem; }; +export type TreeShakingConfig = { + usedExports?: string[]; + mode?: 'server-calc' | 'runtime-infer'; + filename?: string; +}; + export type SharedConfig = { eager?: boolean; import?: false | SharedItem; @@ -24,62 +30,86 @@ export type SharedConfig = { singleton?: boolean; strictVersion?: boolean; version?: false | string; + treeShaking?: TreeShakingConfig; }; -export class SharePlugin { - _shareScope; - _consumes; - _provides; - _enhanced; +export type NormalizedSharedOptions = [string, SharedConfig][]; - constructor(options: SharePluginOptions) { - const sharedOptions = parseOptions( - options.shared, - (item, key) => { - if (typeof item !== 'string') - throw new Error('Unexpected array in shared'); - const config: SharedConfig = - item === key || !isRequiredVersion(item) - ? { - import: item, - } - : { - import: key, - requiredVersion: item, - }; - return config; - }, - (item) => item, - ); - const consumes = sharedOptions.map(([key, options]) => ({ - [key]: { - import: options.import, +export function normalizeSharedOptions( + shared: Shared, +): NormalizedSharedOptions { + return parseOptions( + shared, + (item, key) => { + if (typeof item !== 'string') + throw new Error('Unexpected array in shared'); + const config: SharedConfig = + item === key || !isRequiredVersion(item) + ? { + import: item, + } + : { + import: key, + requiredVersion: item, + }; + return config; + }, + (item) => item, + ); +} + +export function createProvideShareOptions( + normalizedSharedOptions: NormalizedSharedOptions, +) { + return normalizedSharedOptions + .filter(([, options]) => options.import !== false) + .map(([key, options]) => ({ + [options.import || key]: { shareKey: options.shareKey || key, shareScope: options.shareScope, + version: options.version, + eager: options.eager, + singleton: options.singleton, requiredVersion: options.requiredVersion, strictVersion: options.strictVersion, - singleton: options.singleton, - packageName: options.packageName, - eager: options.eager, + treeShakingMode: options.treeShaking?.mode, }, })); - const provides = sharedOptions - .filter(([, options]) => options.import !== false) - .map(([key, options]) => ({ - [options.import || key]: { - shareKey: options.shareKey || key, - shareScope: options.shareScope, - version: options.version, - eager: options.eager, - singleton: options.singleton, - requiredVersion: options.requiredVersion, - strictVersion: options.strictVersion, - }, - })); +} + +export function createConsumeShareOptions( + normalizedSharedOptions: NormalizedSharedOptions, +) { + return normalizedSharedOptions.map(([key, options]) => ({ + [key]: { + import: options.import, + shareKey: options.shareKey || key, + shareScope: options.shareScope, + requiredVersion: options.requiredVersion, + strictVersion: options.strictVersion, + singleton: options.singleton, + packageName: options.packageName, + eager: options.eager, + treeShakingMode: options.treeShaking?.mode, + }, + })); +} +export class SharePlugin { + _shareScope; + _consumes; + _provides; + _enhanced; + _sharedOptions; + + constructor(options: SharePluginOptions) { + const sharedOptions = normalizeSharedOptions(options.shared); + const consumes = createConsumeShareOptions(sharedOptions); + const provides = createProvideShareOptions(sharedOptions); this._shareScope = options.shareScope; this._consumes = consumes; this._provides = provides; this._enhanced = options.enhanced ?? false; + this._sharedOptions = sharedOptions; } apply(compiler: Compiler) { diff --git a/packages/rspack/src/sharing/SharedContainerPlugin.ts b/packages/rspack/src/sharing/SharedContainerPlugin.ts new file mode 100644 index 000000000000..b0e89adb430b --- /dev/null +++ b/packages/rspack/src/sharing/SharedContainerPlugin.ts @@ -0,0 +1,116 @@ +import { + type BuiltinPlugin, + BuiltinPluginName, + type RawSharedContainerPluginOptions, +} from '@rspack/binding'; +import { + createBuiltinPlugin, + RspackBuiltinPlugin, +} from '../builtin-plugin/base'; +import type { Compilation } from '../Compilation'; +import type { Compiler } from '../Compiler'; +import type { LibraryOptions } from '../config'; +import { encodeName } from './utils'; + +export type SharedContainerPluginOptions = { + mfName: string; + shareName: string; + version: string; + request: string; + library?: LibraryOptions; + independentShareFileName?: string; +}; + +function assert(condition: any, msg: string): asserts condition { + if (!condition) { + throw new Error(msg); + } +} + +const HOT_UPDATE_SUFFIX = '.hot-update'; + +export class SharedContainerPlugin extends RspackBuiltinPlugin { + name = BuiltinPluginName.SharedContainerPlugin; + filename = ''; + _options: RawSharedContainerPluginOptions; + _shareName: string; + _globalName: string; + + constructor(options: SharedContainerPluginOptions) { + super(); + const { shareName, library, request, independentShareFileName, mfName } = + options; + const version = options.version || '0.0.0'; + this._globalName = encodeName(`${mfName}_${shareName}_${version}`); + const fileName = independentShareFileName || `${version}/share-entry.js`; + this._shareName = shareName; + this._options = { + name: shareName, + request: request, + library: (library + ? { ...library, name: this._globalName } + : undefined) || { + type: 'global', + name: this._globalName, + }, + version, + fileName, + }; + } + getData() { + return [this._options.fileName, this._globalName, this._options.version]; + } + + raw(compiler: Compiler): BuiltinPlugin { + const { library } = this._options; + if (!compiler.options.output.enabledLibraryTypes!.includes(library.type)) { + compiler.options.output.enabledLibraryTypes!.push(library.type); + } + return createBuiltinPlugin(this.name, this._options); + } + + apply(compiler: Compiler) { + super.apply(compiler); + const shareName = this._shareName; + compiler.hooks.thisCompilation.tap( + this.name, + (compilation: Compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'getShareContainerFile', + }, + () => { + const remoteEntryPoint = compilation.entrypoints.get(shareName); + assert( + remoteEntryPoint, + `Can not get shared ${shareName} entryPoint!`, + ); + const remoteEntryNameChunk = compilation.namedChunks.get(shareName); + assert( + remoteEntryNameChunk, + `Can not get shared ${shareName} chunk!`, + ); + + const files = Array.from( + remoteEntryNameChunk.files as Iterable, + ).filter( + (f: string) => + !f.includes(HOT_UPDATE_SUFFIX) && !f.endsWith('.css'), + ); + assert( + files.length > 0, + `no files found for shared ${shareName} chunk`, + ); + assert( + files.length === 1, + `shared ${shareName} chunk should not have multiple files!, current files: ${files.join( + ',', + )}`, + ); + this.filename = files[0]; + }, + ); + }, + ); + } +} diff --git a/packages/rspack/src/sharing/SharedUsedExportsOptimizerPlugin.ts b/packages/rspack/src/sharing/SharedUsedExportsOptimizerPlugin.ts new file mode 100644 index 000000000000..37252d999eb4 --- /dev/null +++ b/packages/rspack/src/sharing/SharedUsedExportsOptimizerPlugin.ts @@ -0,0 +1,65 @@ +import type { + BuiltinPlugin, + RawSharedUsedExportsOptimizerPluginOptions, +} from '@rspack/binding'; +import { BuiltinPluginName } from '@rspack/binding'; + +import { + createBuiltinPlugin, + RspackBuiltinPlugin, +} from '../builtin-plugin/base'; +import { + getFileName, + type ModuleFederationManifestPluginOptions, +} from '../container/ModuleFederationManifestPlugin'; +import type { NormalizedSharedOptions } from './SharePlugin'; + +type OptimizeSharedConfig = { + shareKey: string; + treeShaking: boolean; + usedExports?: string[]; +}; + +export class SharedUsedExportsOptimizerPlugin extends RspackBuiltinPlugin { + name = BuiltinPluginName.SharedUsedExportsOptimizerPlugin; + private sharedOptions: NormalizedSharedOptions; + private injectTreeShakingUsedExports: boolean; + private manifestOptions: ModuleFederationManifestPluginOptions; + + constructor( + sharedOptions: NormalizedSharedOptions, + injectTreeShakingUsedExports?: boolean, + manifestOptions?: ModuleFederationManifestPluginOptions, + ) { + super(); + this.sharedOptions = sharedOptions; + this.injectTreeShakingUsedExports = injectTreeShakingUsedExports ?? true; + this.manifestOptions = manifestOptions ?? {}; + } + + private buildOptions(): RawSharedUsedExportsOptimizerPluginOptions { + const shared: OptimizeSharedConfig[] = this.sharedOptions.map( + ([shareKey, config]) => ({ + shareKey, + treeShaking: !!config.treeShaking, + usedExports: config.treeShaking?.usedExports, + }), + ); + const { manifestFileName, statsFileName } = getFileName( + this.manifestOptions, + ); + return { + shared, + injectTreeShakingUsedExports: this.injectTreeShakingUsedExports, + manifestFileName, + statsFileName, + }; + } + + raw(): BuiltinPlugin | undefined { + if (!this.sharedOptions.length) { + return; + } + return createBuiltinPlugin(this.name, this.buildOptions()); + } +} diff --git a/packages/rspack/src/sharing/TreeShakingSharedPlugin.ts b/packages/rspack/src/sharing/TreeShakingSharedPlugin.ts new file mode 100644 index 000000000000..5d812bfc6c94 --- /dev/null +++ b/packages/rspack/src/sharing/TreeShakingSharedPlugin.ts @@ -0,0 +1,74 @@ +import { createRequire } from 'node:module'; +import type { Compiler } from '../Compiler'; +import type { ModuleFederationPluginOptions } from '../container/ModuleFederationPlugin'; +import { IndependentSharedPlugin } from './IndependentSharedPlugin'; +import { SharedUsedExportsOptimizerPlugin } from './SharedUsedExportsOptimizerPlugin'; +import { normalizeSharedOptions } from './SharePlugin'; + +const require = createRequire(import.meta.url); + +export interface TreeshakingSharedPluginOptions { + mfConfig: ModuleFederationPluginOptions; + secondary?: boolean; +} + +export class TreeShakingSharedPlugin { + mfConfig: ModuleFederationPluginOptions; + outputDir: string; + secondary?: boolean; + private _independentSharePlugin?: IndependentSharedPlugin; + + name = 'TreeShakingSharedPlugin'; + constructor(options: TreeshakingSharedPluginOptions) { + const { mfConfig, secondary } = options; + this.mfConfig = mfConfig; + this.outputDir = mfConfig.treeShakingSharedDir || 'independent-packages'; + this.secondary = Boolean(secondary); + } + + apply(compiler: Compiler) { + const { mfConfig, outputDir, secondary } = this; + const { name, shared, library, treeShakingSharedPlugins } = mfConfig; + if (!shared) { + return; + } + const sharedOptions = normalizeSharedOptions(shared); + if (!sharedOptions.length) { + return; + } + + if ( + sharedOptions.some( + ([_, config]) => config.treeShaking && config.import !== false, + ) + ) { + if (!secondary) { + new SharedUsedExportsOptimizerPlugin( + sharedOptions, + mfConfig.injectTreeShakingUsedExports, + mfConfig.manifest, + ).apply(compiler); + } + this._independentSharePlugin = new IndependentSharedPlugin({ + name: name, + shared: shared, + outputDir, + plugins: + treeShakingSharedPlugins?.map((p) => { + const _constructor = require(p); + return new _constructor(); + }) || [], + treeShaking: secondary, + library, + manifest: mfConfig.manifest, + treeShakingSharedExcludePlugins: + mfConfig.treeShakingSharedExcludePlugins, + }); + this._independentSharePlugin.apply(compiler); + } + } + + get buildAssets() { + return this._independentSharePlugin?.buildAssets || {}; + } +} diff --git a/packages/rspack/src/sharing/utils.ts b/packages/rspack/src/sharing/utils.ts index 79d104851205..36c169cea888 100644 --- a/packages/rspack/src/sharing/utils.ts +++ b/packages/rspack/src/sharing/utils.ts @@ -3,3 +3,16 @@ const VERSION_PATTERN_REGEXP = /^([\d^=v<>~]|[*xX]$)/; export function isRequiredVersion(str: string) { return VERSION_PATTERN_REGEXP.test(str); } + +export const encodeName = function ( + name: string, + prefix = '', + withExt = false, +): string { + const ext = withExt ? '.js' : ''; + return `${prefix}${name + .replace(/@/g, 'scope_') + .replace(/-/g, '_') + .replace(/\//g, '__') + .replace(/\./g, '')}${ext}`; +}; diff --git a/packages/rspack/src/taps/types.ts b/packages/rspack/src/taps/types.ts index c0a598f845f1..54fadda356bd 100644 --- a/packages/rspack/src/taps/types.ts +++ b/packages/rspack/src/taps/types.ts @@ -19,6 +19,7 @@ type RegisterTapKeys< T, L extends string, > = T extends keyof binding.RegisterJsTaps ? (T extends L ? T : never) : never; + type PartialRegisters = { [K in RegisterTapKeys< keyof binding.RegisterJsTaps, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 780625342c75..403b8b0322f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -363,8 +363,8 @@ importers: packages/rspack: dependencies: '@module-federation/runtime-tools': - specifier: '>=0.22.0' - version: 0.22.0 + specifier: 0.24.1 + version: 0.24.1 '@rspack/binding': specifier: workspace:* version: link:../../crates/node_binding @@ -383,7 +383,7 @@ importers: version: 1.0.7 '@rsbuild/plugin-node-polyfill': specifier: ^1.4.3 - version: 1.4.3(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)) + version: 1.4.3(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)) '@rslib/core': specifier: 0.19.4 version: 0.19.4(@microsoft/api-extractor@7.55.2(@types/node@20.19.31))(typescript@5.9.3) @@ -661,8 +661,8 @@ importers: specifier: ^7.28.5 version: 7.28.5(@babel/core@7.29.0) '@module-federation/runtime-tools': - specifier: ^0.22.0 - version: 0.22.0 + specifier: 0.24.1 + version: 0.24.1 '@playwright/test': specifier: 1.57.0 version: 1.57.0 @@ -736,8 +736,8 @@ importers: specifier: ^7.28.5 version: 7.28.5(@babel/core@7.29.0) '@module-federation/runtime-tools': - specifier: ^0.22.0 - version: 0.22.0 + specifier: 0.24.1 + version: 0.24.1 '@rspack/binding-testing': specifier: workspace:* version: link:../../crates/rspack_binding_builder_testing @@ -984,22 +984,22 @@ importers: devDependencies: '@rsbuild/plugin-sass': specifier: ^1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)) + version: 1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)) '@rspress/core': specifier: ^2.0.2 - version: 2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0) + version: 2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0) '@rspress/plugin-algolia': specifier: ^2.0.2 - version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3) + version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3) '@rspress/plugin-client-redirects': specifier: ^2.0.2 - version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0)) + version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0)) '@rspress/plugin-rss': specifier: ^2.0.2 - version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0)) + version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0)) '@rspress/plugin-sitemap': specifier: ^2.0.2 - version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0)) + version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0)) '@shikijs/transformers': specifier: ^3.21.0 version: 3.21.0 @@ -1026,13 +1026,13 @@ importers: version: 1.0.4 rsbuild-plugin-google-analytics: specifier: 1.0.5 - version: 1.0.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)) + version: 1.0.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)) rsbuild-plugin-open-graph: specifier: 1.1.2 - version: 1.1.2(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)) + version: 1.1.2(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)) rspress-plugin-font-open-sans: specifier: 1.0.3 - version: 1.0.3(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0)) + version: 1.0.3(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -2405,21 +2405,39 @@ packages: '@module-federation/error-codes@0.22.0': resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} + '@module-federation/error-codes@0.24.1': + resolution: {integrity: sha512-aHmtFvFJqlM6tWJtSqO6KlbM9QZfM92wK3EFYW1khak9Xrgc78UHJScBQ37w/9X9GCNOfBIpvsT29Gvp7JtWUA==} + '@module-federation/runtime-core@0.22.0': resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} + '@module-federation/runtime-core@0.24.1': + resolution: {integrity: sha512-O0LYJ6ADJQ4fLrQNHLJc9IQGhWM3Dl613yCinR7EefmvBwUk7lmTUPWaSEOMNIbMq8oXV4ky4wFDSIEtwihAdg==} + '@module-federation/runtime-tools@0.22.0': resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} + '@module-federation/runtime-tools@0.24.1': + resolution: {integrity: sha512-+P6Yvyc+uaAvF7YceGxtiOg0wJDv6qfHv+AXsmzz54pC3N0PKu5azhBeKO28ILRmmsalnau8dkNPIv/JC04AmA==} + '@module-federation/runtime@0.22.0': resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} + '@module-federation/runtime@0.24.1': + resolution: {integrity: sha512-dnCnmFzC+w49tOKvh/EgqxMR7ADefA/QMtHgicc2KDF9DDcWs4v/GszEn/2PeBalQOj1+Gr5mV7JbB73MVgiVw==} + '@module-federation/sdk@0.22.0': resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} + '@module-federation/sdk@0.24.1': + resolution: {integrity: sha512-BHWIMoBCyLaJud4JlBRQ5RTOz9Q/ws3aH9On1erpU38AijVkwOkXYwNG6xrJXKVIZ8WnCAGhaeKmkHB5R6vAEw==} + '@module-federation/webpack-bundler-runtime@0.22.0': resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} + '@module-federation/webpack-bundler-runtime@0.24.1': + resolution: {integrity: sha512-HydbmybyGhsriUltT02HBW73cNSX70s8XNku5g4GV+YVP1bu+jm0ADDGP86t84rhKH2Zm1gW8S57/ocPpmRsEQ==} + '@napi-rs/cli@3.0.4': resolution: {integrity: sha512-ilbCI69DVDQcIUSUB504LM1+Nhvo0jKycWAzzPJ22YwUoWrru/w0+V5sfjPINgkshQ4Ykv+oZOJXk9Kg1ZBUvg==} engines: {node: '>= 16'} @@ -9690,29 +9708,54 @@ snapshots: '@module-federation/error-codes@0.22.0': {} + '@module-federation/error-codes@0.24.1': {} + '@module-federation/runtime-core@0.22.0': dependencies: '@module-federation/error-codes': 0.22.0 '@module-federation/sdk': 0.22.0 + '@module-federation/runtime-core@0.24.1': + dependencies: + '@module-federation/error-codes': 0.24.1 + '@module-federation/sdk': 0.24.1 + '@module-federation/runtime-tools@0.22.0': dependencies: '@module-federation/runtime': 0.22.0 '@module-federation/webpack-bundler-runtime': 0.22.0 + '@module-federation/runtime-tools@0.24.1': + dependencies: + '@module-federation/runtime': 0.24.1 + '@module-federation/webpack-bundler-runtime': 0.24.1 + '@module-federation/runtime@0.22.0': dependencies: '@module-federation/error-codes': 0.22.0 '@module-federation/runtime-core': 0.22.0 '@module-federation/sdk': 0.22.0 + '@module-federation/runtime@0.24.1': + dependencies: + '@module-federation/error-codes': 0.24.1 + '@module-federation/runtime-core': 0.24.1 + '@module-federation/sdk': 0.24.1 + '@module-federation/sdk@0.22.0': {} + '@module-federation/sdk@0.24.1': {} + '@module-federation/webpack-bundler-runtime@0.22.0': dependencies: '@module-federation/runtime': 0.22.0 '@module-federation/sdk': 0.22.0 + '@module-federation/webpack-bundler-runtime@0.24.1': + dependencies: + '@module-federation/runtime': 0.24.1 + '@module-federation/sdk': 0.24.1 + '@napi-rs/cli@3.0.4(@emnapi/runtime@1.5.0)(@types/node@20.19.31)(emnapi@1.8.1(node-addon-api@7.1.1))': dependencies: '@inquirer/prompts': 7.8.6(@types/node@20.19.31) @@ -10179,9 +10222,9 @@ snapshots: core-js: 3.47.0 jiti: 2.6.1 - '@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)': + '@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)': dependencies: - '@rspack/core': 2.0.0-alpha.1(@module-federation/runtime-tools@0.22.0)(@swc/helpers@0.5.18) + '@rspack/core': 2.0.0-alpha.1(@module-federation/runtime-tools@0.24.1)(@swc/helpers@0.5.18) '@swc/helpers': 0.5.18 jiti: 2.6.1 optionalDependencies: @@ -10189,7 +10232,7 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/plugin-node-polyfill@1.4.3(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0))': + '@rsbuild/plugin-node-polyfill@1.4.3(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0))': dependencies: assert: 2.1.0 browserify-zlib: 0.2.0 @@ -10215,19 +10258,19 @@ snapshots: util: 0.12.5 vm-browserify: 1.1.2 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) - '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0))': + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) '@rspack/plugin-react-refresh': 1.6.0(react-refresh@0.18.0) react-refresh: 0.18.0 transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0))': + '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.6 @@ -10369,12 +10412,12 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.18 - '@rspack/core@2.0.0-alpha.1(@module-federation/runtime-tools@0.22.0)(@swc/helpers@0.5.18)': + '@rspack/core@2.0.0-alpha.1(@module-federation/runtime-tools@0.24.1)(@swc/helpers@0.5.18)': dependencies: '@rspack/binding': 2.0.0-alpha.1 '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@module-federation/runtime-tools': 0.22.0 + '@module-federation/runtime-tools': 0.24.1 '@swc/helpers': 0.5.18 '@rspack/dev-server@1.2.1(@rspack/core@packages+rspack)(webpack@5.102.1)': @@ -10428,13 +10471,13 @@ snapshots: html-entities: 2.6.0 react-refresh: 0.18.0 - '@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0)': + '@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0)': dependencies: '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.10)(react@19.2.4) - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) - '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)) - '@rspress/shared': 2.0.2(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) + '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)) + '@rspress/shared': 2.0.2(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) '@shikijs/rehype': 3.21.0 '@types/unist': 3.0.3 '@unhead/react': 2.1.2(react@19.2.4) @@ -10479,33 +10522,33 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/plugin-algolia@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)': + '@rspress/plugin-algolia@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)': dependencies: '@docsearch/css': 4.5.3 '@docsearch/react': 4.5.3(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3) - '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0) + '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0) transitivePeerDependencies: - '@types/react' - react - react-dom - search-insights - '@rspress/plugin-client-redirects@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0))': + '@rspress/plugin-client-redirects@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0))': dependencies: - '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0) + '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0) - '@rspress/plugin-rss@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0))': + '@rspress/plugin-rss@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0))': dependencies: - '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0) + '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0) feed: 4.2.2 - '@rspress/plugin-sitemap@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0))': + '@rspress/plugin-sitemap@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0))': dependencies: - '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0) + '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0) - '@rspress/shared@2.0.2(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)': + '@rspress/shared@2.0.2(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)': dependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) '@shikijs/rehype': 3.21.0 gray-matter: 4.0.3 lodash-es: 4.17.23 @@ -15262,13 +15305,13 @@ snapshots: '@microsoft/api-extractor': 7.55.2(@types/node@20.19.31) typescript: 5.9.3 - rsbuild-plugin-google-analytics@1.0.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)): + rsbuild-plugin-google-analytics@1.0.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)): optionalDependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) - rsbuild-plugin-open-graph@1.1.2(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0)): + rsbuild-plugin-open-graph@1.1.2(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0)): optionalDependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.22.0)(core-js@3.47.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@0.24.1)(core-js@3.47.0) rspack-vue-loader@17.4.5(vue@3.5.27(typescript@5.9.3))(webpack@5.102.1): dependencies: @@ -15278,9 +15321,9 @@ snapshots: optionalDependencies: vue: 3.5.27(typescript@5.9.3) - rspress-plugin-font-open-sans@1.0.3(@rspress/core@2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0)): + rspress-plugin-font-open-sans@1.0.3(@rspress/core@2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0)): dependencies: - '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.22.0)(@types/react@19.2.10)(core-js@3.47.0) + '@rspress/core': 2.0.2(@module-federation/runtime-tools@0.24.1)(@types/react@19.2.10)(core-js@3.47.0) run-applescript@7.1.0: {} diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 12f027f1084a..63aae082ed63 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -16,7 +16,7 @@ "@rspack/browser": "workspace:*", "@rspack/dev-server": "~1.2.1", "@rspack/plugin-react-refresh": "^1.6.0", - "@module-federation/runtime-tools": "^0.22.0", + "@module-federation/runtime-tools": "0.24.1", "@swc/helpers": "0.5.18", "@types/fs-extra": "11.0.4", "babel-loader": "^10.0.0", diff --git a/tests/rspack-test/configCases/container-1-5/manifest-disable-assets-analyze/package.json b/tests/rspack-test/configCases/container-1-5/manifest-disable-assets-analyze/package.json new file mode 100644 index 000000000000..275b33fac442 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/manifest-disable-assets-analyze/package.json @@ -0,0 +1,7 @@ +{ + "name": "manifest-disable-assets-analyze", + "version": "1.0.0", + "dependencies": { + "react": "^1.0.0" + } +} \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/manifest-file-name/package.json b/tests/rspack-test/configCases/container-1-5/manifest-file-name/package.json new file mode 100644 index 000000000000..23dc364280c4 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/manifest-file-name/package.json @@ -0,0 +1,7 @@ +{ + "name": "manifest-file-name", + "version": "1.0.0", + "dependencies": { + "react": "^1.0.0" + } +} \ No newline at end of file diff --git a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-params/plugin-with-params.js b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-params/plugin-with-params.js index 4aa407133e22..99684bc4fdf2 100644 --- a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-params/plugin-with-params.js +++ b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-params/plugin-with-params.js @@ -11,7 +11,10 @@ module.exports = function(params) { shareScopeMap[scope][pkgName][version] = { lib: ()=>()=> 'This is react 0.2.1' }; - return shareScopeMap[scope][pkgName][version]; + return { + shared: shareScopeMap[scope][pkgName][version], + useTreesShaking:false + }; }; return args; } diff --git a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/index.js b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/index.js index f5c09dd12d54..b09ff411cf5f 100644 --- a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/index.js +++ b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/index.js @@ -3,8 +3,9 @@ it("should generate correct worker runtime code with tree shaking and MF runtime expect(getMessage()).toBe('App rendered with [This is react 0.2.1]'); const plugins = __webpack_require__.federation.initOptions.plugins; - expect(plugins.length).toBe(2); - expect(plugins.map(p => p.name)).toEqual(['my-runtime-plugin', 'my-runtime-plugin-esm']); + // mf webpack bundler runtime has 1 built-in plugin + expect(plugins.length).toBe(3); + expect(plugins.map(p => p.name)).toEqual(['my-runtime-plugin', 'my-runtime-plugin-esm','tree-shake-plugin']); expect(await getWorkerMessage()).toBe('Echo: Hello, Rspack!'); }); diff --git a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin-esm.js b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin-esm.js index 61c15c088332..6f60bf6f5eec 100644 --- a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin-esm.js +++ b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin-esm.js @@ -2,12 +2,15 @@ export default function MyRuntimePlugin() { return { name: 'my-runtime-plugin-esm', resolveShare: function(args) { - const { shareScopeMap, scope, pkgName, version, GlobalFederation } = args; + const { shareScopeMap, scope, pkgName, version } = args; args.resolver = function () { shareScopeMap[scope][pkgName][version] = { lib: ()=>()=> 'This is react 0.2.1' }; - return shareScopeMap[scope][pkgName][version]; + return { + shared:shareScopeMap[scope][pkgName][version], + useTreesShaking:false + }; }; return args; } diff --git a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin.js b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin.js index c09dfa834686..5f57931b1445 100644 --- a/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin.js +++ b/tests/rspack-test/configCases/container-1-5/runtime-plugin-with-used-exports/runtime-plugin.js @@ -2,12 +2,15 @@ module.exports = function MyRuntimePlugin() { return { name: 'my-runtime-plugin', resolveShare: function(args) { - const { shareScopeMap, scope, pkgName, version, GlobalFederation } = args; + const { shareScopeMap, scope, pkgName, version, } = args; args.resolver = function () { shareScopeMap[scope][pkgName][version] = { lib: ()=>()=> 'This is react 0.2.1' }; - return shareScopeMap[scope][pkgName][version]; + return { + shared: shareScopeMap[scope][pkgName][version], + useTreesShaking:false + }; }; return args; } diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/App.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/App.js new file mode 100644 index 000000000000..00ff133010d7 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/App.js @@ -0,0 +1,30 @@ +import UiLib from 'ui-lib'; +import { Button } from 'ui-lib-es'; +import UiLibScopeSc from '@scope-sc/ui-lib'; + +export default () => { + return `default Uilib has ${Object.keys(UiLib).join( + ', ', + )} exports not tree shaking, and ui-lib-es Button value is ${Button} should tree shaking`; +}; + +export const scopeScUILib = () => { + return `scope-sc Uilib has ${Object.keys(UiLibScopeSc).join( + ', ', + )}`; +}; + +export const dynamicUISpecificExport = async () => { + const { List } = await import('ui-lib-dynamic-specific-export'); + return `dynamic Uilib has ${List} exports tree shaking`; +}; + +export const dynamicUIDefaultExport = async () => { + const uiLib = await import('ui-lib-dynamic-default-export'); + return `dynamic Uilib has ${uiLib.List} exports tree shaking`; +}; + +export const dynamicUISideEffectExport = async () => { + const uiLibSideEffect = await import('ui-lib-side-effect'); + return `dynamic Uilib has ${uiLibSideEffect.List} exports not tree shaking`; +}; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/index.js new file mode 100644 index 000000000000..5ed69631e85b --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/index.js @@ -0,0 +1,137 @@ +const fs = __non_webpack_require__('fs'); +const path = __non_webpack_require__('path'); + +__webpack_require__.p = 'PUBLIC_PATH'; +it('should load tree shaking shared via set "runtime-infer" mode', async () => { + const app = await import('./App.js'); + expect(app.default()).toEqual( + 'default Uilib has Button, List, Badge exports not tree shaking, and ui-lib-es Button value is Button should tree shaking', + ); + + const bundlePath = path.join(__dirname, 'node_modules_ui-lib_index_js.js'); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('Button'); + expect(bundleContent).toContain('Badge'); + expect(bundleContent).toContain('List'); + + const uiLibShared = + __FEDERATION__.__SHARE__['tree_shaking_share'].default['ui-lib'][ + '1.0.0' + ]; + expect(uiLibShared.loaded).toEqual(undefined); + expect(uiLibShared.treeShaking.loaded).toEqual(true); + expect(Object.keys(uiLibShared.treeShaking.lib()).sort()).toEqual([ + 'Button', + 'default', + ]); + + const uiLibFallback = (await uiLibShared.get())(); + expect(Object.keys(uiLibFallback).sort()).toEqual([ + 'Badge', + 'Button', + 'List', + 'default', + ]); + + const uiLibESBundlePath = path.join( + __dirname, + 'node_modules_ui-lib-es_index_js.js', + ); + const uiLibESBundleContent = fs.readFileSync(uiLibESBundlePath, 'utf-8'); + expect(uiLibESBundleContent).toContain('Button'); + expect(uiLibESBundleContent).not.toContain('Badge'); + expect(uiLibESBundleContent).not.toContain('List'); + + const uiLibESShared = + __FEDERATION__.__SHARE__['tree_shaking_share'].default['ui-lib-es'][ + '1.0.0' + ]; + expect(uiLibESShared.loaded).toEqual(undefined); + expect(uiLibESShared.treeShaking.loaded).toEqual(true); + + expect(Object.keys(uiLibESShared.treeShaking.lib()).sort()).toEqual(['Button']); + + const esFallback = (await uiLibESShared.get())(); + expect(Object.keys(esFallback).sort()).toEqual(['Badge', 'Button', 'List']); +}); + +it('should tree shaking ui-lib-dynamic-specific-export correctly', async () => { + const { dynamicUISpecificExport } = await import('./App.js'); + expect(await dynamicUISpecificExport()).toEqual( + 'dynamic Uilib has List exports tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-dynamic-specific-export_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).not.toContain('Button'); + expect(bundleContent).not.toContain('Badge'); +}); + +// different from webpack, webpack can not tree shaking dynamic import +it('should tree shaking ui-lib-dynamic-default-export', async () => { + const { dynamicUIDefaultExport } = await import('./App.js'); + expect(await dynamicUIDefaultExport()).toEqual( + 'dynamic Uilib has List exports tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-dynamic-default-export_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).not.toContain('Button'); + expect(bundleContent).not.toContain('Badge'); +}); + +it('should not tree shaking ui-lib-side-effect if not set sideEffect:false ', async () => { + const { dynamicUISideEffectExport } = await import('./App.js'); + expect(await dynamicUISideEffectExport()).toEqual( + 'dynamic Uilib has List exports not tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-side-effect_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).toContain('Button'); + expect(bundleContent).toContain('Badge'); +}); + +it('should inject usedExports into entry chunk by default', async () => { + expect( + __webpack_require__.federation.usedExports['ui-lib'].sort(), + ).toEqual(['Button', 'default']); +}); + +it('should inject usedExports into manifest and stats if enable manifest', async () => { + const { Button } = await import('ui-lib'); + expect(Button).toEqual('Button'); + + const statsPath = path.join(__dirname, 'mf-stats.json'); + const statsContent = JSON.parse(fs.readFileSync(statsPath, 'utf-8')); + expect( + JSON.stringify( + statsContent.shared.find((s) => s.name === 'ui-lib').usedExports.sort(), + ), + ).toEqual(JSON.stringify(['Button', 'default'])); +}); + +it('should tree shaking scope-sc ui-lib correctly', async () => { + const { scopeScUILib } = await import('./App.js'); + expect(scopeScUILib()).toEqual('scope-sc Uilib has Button, List, Badge'); + + const bundlePath = path.join( + __dirname, + 'node_modules_scope-sc_ui-lib_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('Button'); + expect(bundleContent).toContain('default'); +}); diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/@scope-sc/ui-lib/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/@scope-sc/ui-lib/index.js new file mode 100644 index 000000000000..dffb614be886 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/@scope-sc/ui-lib/index.js @@ -0,0 +1,9 @@ +export const List = 'List' +export const Button = 'Button'; +export const Badge = 'Badge' + +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/@scope-sc/ui-lib/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/@scope-sc/ui-lib/package.json new file mode 100644 index 000000000000..485bb4d62618 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/@scope-sc/ui-lib/package.json @@ -0,0 +1,6 @@ +{ + "name": "@scope-sc/ui-lib", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-default-export/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-default-export/index.js new file mode 100644 index 000000000000..9dd1824aaaad --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-default-export/index.js @@ -0,0 +1,9 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-default-export/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-default-export/package.json new file mode 100644 index 000000000000..34effdfca837 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-default-export/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-dynamic-default-export", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-specific-export/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-specific-export/index.js new file mode 100644 index 000000000000..6d4fe4c20ea5 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-specific-export/index.js @@ -0,0 +1,3 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-specific-export/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-specific-export/package.json new file mode 100644 index 000000000000..e1e4830eb438 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-dynamic-specific-export/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-dynamic-specific-export", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-es/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-es/index.js new file mode 100644 index 000000000000..6d4fe4c20ea5 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-es/index.js @@ -0,0 +1,3 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-es/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-es/package.json new file mode 100644 index 000000000000..4e05f3fb8558 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-es/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-es", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-side-effect/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-side-effect/index.js new file mode 100644 index 000000000000..0568c23344ad --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-side-effect/index.js @@ -0,0 +1,12 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +globalThis.Button = Button; +globalThis.List = List; +globalThis.Badge = Badge; +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-side-effect/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-side-effect/package.json new file mode 100644 index 000000000000..08f52aa758de --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib-side-effect/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-side-effect", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": true +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib/index.js new file mode 100644 index 000000000000..dffb614be886 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib/index.js @@ -0,0 +1,9 @@ +export const List = 'List' +export const Button = 'Button'; +export const Badge = 'Badge' + +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib/package.json new file mode 100644 index 000000000000..90f9db2691b3 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/node_modules/ui-lib/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/rspack.config.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/rspack.config.js new file mode 100644 index 000000000000..2b5050150a72 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/rspack.config.js @@ -0,0 +1,74 @@ +// eslint-disable-next-line node/no-unpublished-require +const { ModuleFederationPlugin } = require("@rspack/core").container; + +/** @type {import("@rspack/core").Configuration} */ + +module.exports = { + entry: './index.js', + output: { + ignoreBrowserWarnings: true + }, + optimization: { + minimize: true, + chunkIds: 'named', + moduleIds: 'named', + }, + output: { + publicPath: '/', + chunkFilename: '[id].js', + }, + target: 'async-node', + plugins: [ + new ModuleFederationPlugin({ + name: 'tree_shaking_share', + manifest: true, + filename: 'remoteEntry.js', + library: { + type: 'commonjs-module', + name: 'tree_shaking_share', + }, + exposes: { + './App': './App.js', + }, + runtimePlugins: [require.resolve('./runtime-plugin.js')], + shared: { + '@scope-sc/ui-lib': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-es': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-dynamic-specific-export': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-dynamic-default-export': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-side-effect': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + }, + }), + ], +}; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/runtime-plugin.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/runtime-plugin.js new file mode 100644 index 000000000000..af15582195be --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/runtime-plugin.js @@ -0,0 +1,18 @@ +// const path = require('path'); + +// default mode will use fallback asset if no server data. And the fallback will load asset via fetch + eval. +// Cause the asset not deploy, so we need to proxy the asset to local. +module.exports = function () { + return { + name: 'proxy-shared-asset', + loadEntry: ({ remoteInfo }) => { + const { entry, entryGlobalName } = remoteInfo; + if (entry.includes('PUBLIC_PATH')) { + const relativePath = entry.replace('PUBLIC_PATH', './'); + globalThis[entryGlobalName] = + __non_webpack_require__(relativePath)[entryGlobalName]; + return globalThis[entryGlobalName]; + } + }, + }; +}; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/test.config.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/test.config.js new file mode 100644 index 000000000000..37df1bd8548c --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-infer-mode/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function (i, options) { + return './bundle0.js'; + }, +}; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/App.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/App.js new file mode 100644 index 000000000000..0fcdcc8a4bef --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/App.js @@ -0,0 +1,23 @@ +import UiLib from 'ui-lib'; +import { Button } from 'ui-lib-es'; + +export default () => { + return `default Uilib has ${Object.values(UiLib).join( + ', ', + )} exports not tree shaking, and ui-lib-es Button value is ${Button} should tree shaking`; +}; + +export const dynamicUISpecificExport = async () => { + const { List } = await import('ui-lib-dynamic-specific-export'); + return `dynamic Uilib has ${List} exports tree shaking`; +}; + +export const dynamicUIDefaultExport = async () => { + const uiLib = await import('ui-lib-dynamic-default-export'); + return `dynamic Uilib has ${uiLib.List} exports tree shaking`; +}; + +export const dynamicUISideEffectExport = async () => { + const uiLibSideEffect = await import('ui-lib-side-effect'); + return `dynamic Uilib has ${uiLibSideEffect.List} exports not tree shaking`; +}; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/index.js new file mode 100644 index 000000000000..e268319c5710 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/index.js @@ -0,0 +1,125 @@ +const fs = __non_webpack_require__('fs'); +const path = __non_webpack_require__('path'); + +__webpack_require__.p = 'PUBLIC_PATH'; +it('should not load tree shaking shared via set "server-calc" mode and no server data dispatch', async () => { + const app = await import('./App.js'); + expect(app.default()).toEqual( + 'default Uilib has Button, List, Badge exports not tree shaking, and ui-lib-es Button value is Button should tree shaking', + ); + + const bundlePath = path.join(__dirname, 'node_modules_ui-lib_index_js.js'); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('Button'); + expect(bundleContent).toContain('Badge'); + expect(bundleContent).toContain('List'); + + const uiLibShared = + __FEDERATION__.__SHARE__['tree_shaking_share_server'].default['ui-lib'][ + '1.0.0' + ]; + expect(uiLibShared.loaded).toEqual(true); + expect(uiLibShared.treeShaking.loaded).toEqual(undefined); + expect(Object.keys(uiLibShared.lib()).sort()).toEqual([ + 'Badge', + 'Button', + 'List', + 'default', + ]); + + const uiLibTreeShaking = (await uiLibShared.treeShaking.get())(); + expect(Object.keys(uiLibTreeShaking).sort()).toEqual(['Button', 'default']); + + const uiLibESBundlePath = path.join( + __dirname, + 'node_modules_ui-lib-es_index_js.js', + ); + const uiLibESBundleContent = fs.readFileSync(uiLibESBundlePath, 'utf-8'); + expect(uiLibESBundleContent).toContain('Button'); + expect(uiLibESBundleContent).not.toContain('Badge'); + expect(uiLibESBundleContent).not.toContain('List'); + + const uiLibESShared = + __FEDERATION__.__SHARE__['tree_shaking_share_server'].default[ + 'ui-lib-es' + ]['1.0.0']; + expect(uiLibESShared.loaded).toEqual(true); + expect(uiLibESShared.treeShaking.loaded).toEqual(undefined); + + expect(Object.keys(uiLibESShared.lib()).sort()).toEqual([ + 'Badge', + 'Button', + 'List', + ]); + + const esTreeShaking = (await uiLibESShared.treeShaking.get())(); + expect(Object.keys(esTreeShaking).sort()).toEqual(['Button']); +}); + +it('should tree shaking ui-lib-dynamic-specific-export correctly', async () => { + const { dynamicUISpecificExport } = await import('./App.js'); + expect(await dynamicUISpecificExport()).toEqual( + 'dynamic Uilib has List exports tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-dynamic-specific-export_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).not.toContain('Button'); + expect(bundleContent).not.toContain('Badge'); +}); + +it('should tree shaking ui-lib-dynamic-default-export', async () => { + const { dynamicUIDefaultExport } = await import('./App.js'); + expect(await dynamicUIDefaultExport()).toEqual( + 'dynamic Uilib has List exports tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-dynamic-default-export_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).not.toContain('Button'); + expect(bundleContent).not.toContain('Badge'); +}); + +it('should not tree shaking ui-lib-side-effect if not set sideEffect:false ', async () => { + const { dynamicUISideEffectExport } = await import('./App.js'); + expect(await dynamicUISideEffectExport()).toEqual( + 'dynamic Uilib has List exports not tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-side-effect_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).toContain('Button'); + expect(bundleContent).toContain('Badge'); +}); + +it('should inject usedExports into entry chunk by default', async () => { + expect(__webpack_require__.federation.usedExports['ui-lib'].sort()).toEqual([ + 'Button', + 'default', + ]); +}); + +it('should inject usedExports into manifest and stats if enable manifest', async () => { + const { Button } = await import('ui-lib'); + expect(Button).toEqual('Button'); + + const statsPath = path.join(__dirname, 'mf-stats.json'); + const statsContent = JSON.parse(fs.readFileSync(statsPath, 'utf-8')); + expect( + JSON.stringify( + statsContent.shared.find((s) => s.name === 'ui-lib').usedExports.sort(), + ), + ).toEqual(JSON.stringify(['Button', 'default'])); +}); diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/.federation/shared-entry.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/.federation/shared-entry.js new file mode 100644 index 000000000000..5d7cf88327ca --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/.federation/shared-entry.js @@ -0,0 +1,5 @@ +import shared_0 from 'ui-lib'; +import shared_1 from 'ui-lib-es'; +import shared_2 from 'ui-lib-dynamic-specific-export'; +import shared_3 from 'ui-lib-dynamic-default-export'; +import shared_4 from 'ui-lib-side-effect'; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-default-export/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-default-export/index.js new file mode 100644 index 000000000000..9dd1824aaaad --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-default-export/index.js @@ -0,0 +1,9 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-default-export/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-default-export/package.json new file mode 100644 index 000000000000..34effdfca837 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-default-export/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-dynamic-default-export", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-specific-export/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-specific-export/index.js new file mode 100644 index 000000000000..6d4fe4c20ea5 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-specific-export/index.js @@ -0,0 +1,3 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-specific-export/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-specific-export/package.json new file mode 100644 index 000000000000..e1e4830eb438 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-dynamic-specific-export/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-dynamic-specific-export", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-es/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-es/index.js new file mode 100644 index 000000000000..6d4fe4c20ea5 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-es/index.js @@ -0,0 +1,3 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-es/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-es/package.json new file mode 100644 index 000000000000..4e05f3fb8558 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-es/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-es", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-side-effect/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-side-effect/index.js new file mode 100644 index 000000000000..0568c23344ad --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-side-effect/index.js @@ -0,0 +1,12 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +globalThis.Button = Button; +globalThis.List = List; +globalThis.Badge = Badge; +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-side-effect/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-side-effect/package.json new file mode 100644 index 000000000000..08f52aa758de --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib-side-effect/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-side-effect", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": true +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib/index.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib/index.js new file mode 100644 index 000000000000..9dd1824aaaad --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib/index.js @@ -0,0 +1,9 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib/package.json b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib/package.json new file mode 100644 index 000000000000..90f9db2691b3 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/node_modules/ui-lib/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/rspack.config.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/rspack.config.js new file mode 100644 index 000000000000..08bea5e46ae8 --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/rspack.config.js @@ -0,0 +1,68 @@ +// eslint-disable-next-line node/no-unpublished-require +const { ModuleFederationPlugin } = require("@rspack/core").container; + +/** @type {import("@rspack/core").Configuration} */ + +module.exports = { + entry: './index.js', + output: { + ignoreBrowserWarnings: true + }, + optimization: { + minimize: true, + chunkIds: 'named', + moduleIds: 'named', + }, + output: { + publicPath: '/', + chunkFilename: '[id].js', + }, + target: 'async-node', + plugins: [ + new ModuleFederationPlugin({ + name: 'tree_shaking_share_server', + manifest: true, + filename: 'remoteEntry.js', + library: { + type: 'commonjs-module', + name: 'tree_shaking_share', + }, + exposes: { + './App': './App.js', + }, + runtimePlugins: [require.resolve('./runtime-plugin.js')], + shared: { + 'ui-lib': { + requiredVersion: '*', + treeShaking: { + mode: 'server-calc', + }, + }, + 'ui-lib-es': { + requiredVersion: '*', + treeShaking: { + mode: 'server-calc', + }, + }, + 'ui-lib-dynamic-specific-export': { + requiredVersion: '*', + treeShaking: { + mode: 'server-calc', + }, + }, + 'ui-lib-dynamic-default-export': { + requiredVersion: '*', + treeShaking: { + mode: 'server-calc', + }, + }, + 'ui-lib-side-effect': { + requiredVersion: '*', + treeShaking: { + mode: 'server-calc', + }, + }, + }, + }), + ], +}; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/runtime-plugin.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/runtime-plugin.js new file mode 100644 index 000000000000..af15582195be --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/runtime-plugin.js @@ -0,0 +1,18 @@ +// const path = require('path'); + +// default mode will use fallback asset if no server data. And the fallback will load asset via fetch + eval. +// Cause the asset not deploy, so we need to proxy the asset to local. +module.exports = function () { + return { + name: 'proxy-shared-asset', + loadEntry: ({ remoteInfo }) => { + const { entry, entryGlobalName } = remoteInfo; + if (entry.includes('PUBLIC_PATH')) { + const relativePath = entry.replace('PUBLIC_PATH', './'); + globalThis[entryGlobalName] = + __non_webpack_require__(relativePath)[entryGlobalName]; + return globalThis[entryGlobalName]; + } + }, + }; +}; diff --git a/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/test.config.js b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/test.config.js new file mode 100644 index 000000000000..37df1bd8548c --- /dev/null +++ b/tests/rspack-test/configCases/container-1-5/tree-shaking-shared-server-mode/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function (i, options) { + return './bundle0.js'; + }, +}; diff --git a/tests/rspack-test/configCases/sharing/reshake-share/CustomPlugin.js b/tests/rspack-test/configCases/sharing/reshake-share/CustomPlugin.js new file mode 100644 index 000000000000..33acd3bf6441 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/CustomPlugin.js @@ -0,0 +1,16 @@ +class CustomPlugin { + apply(compiler) { + compiler.hooks.thisCompilation.tap('applyPlugins', (compilation) => { + compilation.hooks.processAssets.tapPromise( + { + name: 'applyPlugins', + }, + async () => { + compilation.emitAsset('apply-plugin.json', new compilation.compiler.rspack.sources.RawSource(JSON.stringify({ + secondary: true + }))) + }) + }) + } +} +module.exports = CustomPlugin; \ No newline at end of file diff --git a/tests/rspack-test/configCases/sharing/reshake-share/index.js b/tests/rspack-test/configCases/sharing/reshake-share/index.js new file mode 100644 index 000000000000..243bcb1b2050 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/index.js @@ -0,0 +1,68 @@ +import shared_0 from 'ui-lib'; +import shared_1 from 'ui-lib-dep'; + +const fs = __non_webpack_require__("fs"); +const path = __non_webpack_require__("path"); +__webpack_require__.p = 'PUBLIC_PATH'; + +const treeShakingSharedDir = path.join( + __dirname, + "independent-packages" +); + +const uiLibShareContainerPath = path.join( + treeShakingSharedDir, + "ui_lib/1.0.0", + "share-entry.js" +); + +const uiLibDepShareContainerPath = path.join( + treeShakingSharedDir, + "ui_lib_dep/1.0.0", + "share-entry.js" +); + +const customPluginAssetPath = path.join( + uiLibDepShareContainerPath, + '../..', + "apply-plugin.json" +); + + +it("should build independent share file", () => { + expect(fs.existsSync(uiLibShareContainerPath)).toBe(true); + expect(fs.existsSync(uiLibDepShareContainerPath)).toBe(true); + expect(fs.existsSync(customPluginAssetPath)).toBe(true); +}); + +it("secondary tree shaking shared container should only have specify usedExports", async () => { + const uiLibDepShareContainerModule = __non_webpack_require__(uiLibDepShareContainerPath).secondary_tree_shaking_share_t_ui_lib_dep_100; + await uiLibDepShareContainerModule.init({},{ + installInitialConsumes: async ()=>{ + return 'call init' + } + }); + const shareModulesGetter = await uiLibDepShareContainerModule.get(); + const shareModules = shareModulesGetter(); + expect(shareModules.Message).toBe('Message'); + expect(shareModules.Text).not.toBeDefined(); +}); + + +it("correct handle share dep while secondary tree shaking", async () => { + const uiLibShareContainerModule = __non_webpack_require__(uiLibShareContainerPath).secondary_tree_shaking_share_t_ui_lib_100; + await uiLibShareContainerModule.init({},{ + installInitialConsumes: async ({webpackRequire})=>{ + webpackRequire.m['webpack/sharing/consume/default/ui-lib-dep'] = (m)=>{ + m.exports = { + Message: 'Message', + } + } + return 'call init' + } + }); + const shareModulesGetter = await uiLibShareContainerModule.get(); + const shareModules = shareModulesGetter(); + expect(shareModules.Badge).toBe('Badge'); + expect(shareModules.MessagePro).toBe('MessagePro'); +}); diff --git a/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib-dep/index.js b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib-dep/index.js new file mode 100644 index 000000000000..996b135f366f --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib-dep/index.js @@ -0,0 +1,3 @@ +export const Message = 'Message'; +export const Spin = 'Spin' +export const Text = 'Text' diff --git a/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib-dep/package.json b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib-dep/package.json new file mode 100644 index 000000000000..436a74b6795a --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib-dep/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-dep", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib/index.js b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib/index.js new file mode 100644 index 000000000000..29f02fec6588 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib/index.js @@ -0,0 +1,8 @@ +import { Message, Spin } from 'ui-lib-dep'; + +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +export const MessagePro = `${Message}Pro`; +export const SpinPro = `${Spin}Pro`; diff --git a/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib/package.json b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib/package.json new file mode 100644 index 000000000000..90f9db2691b3 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/sharing/reshake-share/rspack.config.js b/tests/rspack-test/configCases/sharing/reshake-share/rspack.config.js new file mode 100644 index 000000000000..d6756beb9c61 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/rspack.config.js @@ -0,0 +1,45 @@ +const { TreeShakingSharedPlugin } = require("@rspack/core").sharing; +const path = require("path"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + // entry:'./index.js', + optimization: { + minimize: true, + chunkIds: "named", + moduleIds: "named" + }, + output: { + chunkFilename: "[id].js" + }, + plugins: [ + new TreeShakingSharedPlugin({ + secondary: true, + mfConfig: { + name: 'secondary_tree_shaking_share', + library: { + type: 'commonjs2', + }, + shared: { + 'ui-lib': { + version:'1.0.0', + treeShaking: { + mode:'runtime-infer', + usedExports:['Badge','MessagePro'] + }, + requiredVersion: '^1.0.0', + }, + 'ui-lib-dep': { + version:'1.0.0', + treeShaking: { + mode:'runtime-infer', + usedExports:['Message'] + }, + requiredVersion: '^1.0.0', + } + }, + treeShakingSharedPlugins:[path.resolve(__dirname, './CustomPlugin.js')] + } + }) + ] +}; diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/App.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/App.js new file mode 100644 index 000000000000..229f97c61803 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/App.js @@ -0,0 +1,23 @@ +import UiLib from 'ui-lib'; +import { Button } from 'ui-lib-es'; + +export default () => { + return `default Uilib has ${Object.keys(UiLib).join( + ', ', + )} exports not tree shaking, and ui-lib-es Button value is ${Button} should tree shaking`; +}; + +export const dynamicUISpecificExport = async () => { + const { List } = await import('ui-lib-dynamic-specific-export'); + return `dynamic Uilib has ${List} exports not tree shaking`; +}; + +export const dynamicUIDefaultExport = async () => { + const uiLib = await import('ui-lib-dynamic-default-export'); + return `dynamic Uilib has ${uiLib.List} exports not tree shaking`; +}; + +export const dynamicUISideEffectExport = async () => { + const uiLibSideEffect = await import('ui-lib-side-effect'); + return `dynamic Uilib has ${uiLibSideEffect.List} exports not tree shaking`; +}; diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/index.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/index.js new file mode 100644 index 000000000000..147f5db16058 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/index.js @@ -0,0 +1,122 @@ +const fs = __non_webpack_require__('fs'); +const path = __non_webpack_require__('path'); + +__webpack_require__.p = 'PUBLIC_PATH'; +it('should load tree shaking shared via set "runtime-infer" mode', async () => { + const app = await import('./App.js'); + expect(app.default()).toEqual( + 'default Uilib has Button, List, Badge exports not tree shaking, and ui-lib-es Button value is Button should tree shaking', + ); + + const bundlePath = path.join(__dirname, 'node_modules_ui-lib_index_js.js'); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('Button'); + expect(bundleContent).toContain('Badge'); + expect(bundleContent).toContain('List'); + + const uiLibShared = + __FEDERATION__.__SHARE__['tree_shaking_share'].default['ui-lib'][ + '1.0.0' + ]; + expect(uiLibShared.loaded).toEqual(undefined); + expect(uiLibShared.treeShaking.loaded).toEqual(true); + expect(Object.keys(uiLibShared.treeShaking.lib()).sort()).toEqual([ + 'Button', + 'default', + ]); + + const uiLibFallback = (await uiLibShared.get())(); + expect(Object.keys(uiLibFallback).sort()).toEqual([ + 'Badge', + 'Button', + 'List', + 'default', + ]); + + const uiLibESBundlePath = path.join( + __dirname, + 'node_modules_ui-lib-es_index_js.js', + ); + const uiLibESBundleContent = fs.readFileSync(uiLibESBundlePath, 'utf-8'); + expect(uiLibESBundleContent).toContain('Button'); + expect(uiLibESBundleContent).not.toContain('Badge'); + expect(uiLibESBundleContent).not.toContain('List'); + + const uiLibESShared = + __FEDERATION__.__SHARE__['tree_shaking_share'].default['ui-lib-es'][ + '1.0.0' + ]; + expect(uiLibESShared.loaded).toEqual(undefined); + expect(uiLibESShared.treeShaking.loaded).toEqual(true); + + expect(Object.keys(uiLibESShared.treeShaking.lib()).sort()).toEqual(['Button']); + + const esFallback = (await uiLibESShared.get())(); + expect(Object.keys(esFallback).sort()).toEqual(['Badge', 'Button', 'List']); +}); + +it('should tree shaking ui-lib-dynamic-specific-export correctly', async () => { + const { dynamicUISpecificExport } = await import('./App.js'); + expect(await dynamicUISpecificExport()).toEqual( + 'dynamic Uilib has List exports not tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-dynamic-specific-export_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).not.toContain('Button'); + expect(bundleContent).not.toContain('Badge'); +}); + +it('should not tree shaking ui-lib-dynamic-default-export', async () => { + const { dynamicUIDefaultExport } = await import('./App.js'); + expect(await dynamicUIDefaultExport()).toEqual( + 'dynamic Uilib has List exports not tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-dynamic-default-export_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); +}); + +it('should not tree shaking ui-lib-side-effect if not set sideEffect:false ', async () => { + const { dynamicUISideEffectExport } = await import('./App.js'); + expect(await dynamicUISideEffectExport()).toEqual( + 'dynamic Uilib has List exports not tree shaking', + ); + + const bundlePath = path.join( + __dirname, + 'node_modules_ui-lib-side-effect_index_js.js', + ); + const bundleContent = fs.readFileSync(bundlePath, 'utf-8'); + expect(bundleContent).toContain('List'); + expect(bundleContent).toContain('Button'); + expect(bundleContent).toContain('Badge'); +}); + +it('should inject usedExports into entry chunk by default', async () => { + expect(__webpack_require__.federation.usedExports['ui-lib'].sort()).toEqual([ + 'Button', + 'default', + ]); +}); + +it('should inject usedExports into manifest and stats if enable manifest', async () => { + const { Button } = await import('ui-lib'); + expect(Button).toEqual('Button'); + + const statsPath = path.join(__dirname, 'mf-stats.json'); + const statsContent = JSON.parse(fs.readFileSync(statsPath, 'utf-8')); + expect( + JSON.stringify( + statsContent.shared.find((s) => s.name === 'ui-lib').usedExports.sort(), + ), + ).toEqual(JSON.stringify(['Button', 'default'])); +}); diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-default-export/index.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-default-export/index.js new file mode 100644 index 000000000000..9dd1824aaaad --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-default-export/index.js @@ -0,0 +1,9 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-default-export/package.json b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-default-export/package.json new file mode 100644 index 000000000000..34effdfca837 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-default-export/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-dynamic-default-export", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-specific-export/index.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-specific-export/index.js new file mode 100644 index 000000000000..6d4fe4c20ea5 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-specific-export/index.js @@ -0,0 +1,3 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-specific-export/package.json b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-specific-export/package.json new file mode 100644 index 000000000000..e1e4830eb438 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-dynamic-specific-export/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-dynamic-specific-export", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-es/index.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-es/index.js new file mode 100644 index 000000000000..6d4fe4c20ea5 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-es/index.js @@ -0,0 +1,3 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-es/package.json b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-es/package.json new file mode 100644 index 000000000000..4e05f3fb8558 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-es/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-es", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-side-effect/index.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-side-effect/index.js new file mode 100644 index 000000000000..0568c23344ad --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-side-effect/index.js @@ -0,0 +1,12 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +globalThis.Button = Button; +globalThis.List = List; +globalThis.Badge = Badge; +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-side-effect/package.json b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-side-effect/package.json new file mode 100644 index 000000000000..08f52aa758de --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib-side-effect/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib-side-effect", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": true +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib/index.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib/index.js new file mode 100644 index 000000000000..9dd1824aaaad --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib/index.js @@ -0,0 +1,9 @@ +export const Button = 'Button'; +export const List = 'List' +export const Badge = 'Badge' + +export default { + Button, + List, + Badge +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib/package.json b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib/package.json new file mode 100644 index 000000000000..90f9db2691b3 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/node_modules/ui-lib/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/rspack.config.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/rspack.config.js new file mode 100644 index 000000000000..9b215235d487 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/rspack.config.js @@ -0,0 +1,60 @@ +const { container } = require("@rspack/core"); + +const { ModuleFederationPlugin } = container; + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + // entry: './index.js', + target:'async-node', + optimization:{ + minimize: true, + chunkIds:'named', + moduleIds: 'named' + }, + output: { + chunkFilename: "[id].js" + }, + plugins: [ + new ModuleFederationPlugin({ + name:'tree_shaking_share', + manifest: true, + runtimePlugins: [require.resolve('./runtime-plugin.js')], + library: { + type: 'commonjs-module', + name: 'tree_shaking_share', + }, + shared: { + 'ui-lib': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-es': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-dynamic-specific-export': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-dynamic-default-export': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + 'ui-lib-side-effect': { + requiredVersion: '*', + treeShaking: { + mode: 'runtime-infer', + }, + }, + }, + }) + ] +}; diff --git a/tests/rspack-test/configCases/sharing/tree-shaking-shared/runtime-plugin.js b/tests/rspack-test/configCases/sharing/tree-shaking-shared/runtime-plugin.js new file mode 100644 index 000000000000..af15582195be --- /dev/null +++ b/tests/rspack-test/configCases/sharing/tree-shaking-shared/runtime-plugin.js @@ -0,0 +1,18 @@ +// const path = require('path'); + +// default mode will use fallback asset if no server data. And the fallback will load asset via fetch + eval. +// Cause the asset not deploy, so we need to proxy the asset to local. +module.exports = function () { + return { + name: 'proxy-shared-asset', + loadEntry: ({ remoteInfo }) => { + const { entry, entryGlobalName } = remoteInfo; + if (entry.includes('PUBLIC_PATH')) { + const relativePath = entry.replace('PUBLIC_PATH', './'); + globalThis[entryGlobalName] = + __non_webpack_require__(relativePath)[entryGlobalName]; + return globalThis[entryGlobalName]; + } + }, + }; +}; diff --git a/tests/rspack-test/configCases/sri/mf/rspack.config.js b/tests/rspack-test/configCases/sri/mf/rspack.config.js index aaea5d7779d8..659ea28b29ed 100644 --- a/tests/rspack-test/configCases/sri/mf/rspack.config.js +++ b/tests/rspack-test/configCases/sri/mf/rspack.config.js @@ -5,6 +5,9 @@ module.exports = { optimization: { moduleIds: "named" }, + performance: { + hints: false + }, plugins: [ new SubresourceIntegrityPlugin(), new container.ModuleFederationPlugin({ diff --git a/tests/rspack-test/hotCases/sharing/share-plugin/__snapshots__/web/1.snap.txt b/tests/rspack-test/hotCases/sharing/share-plugin/__snapshots__/web/1.snap.txt index 9ad70c921ba8..4cbaa6b63730 100644 --- a/tests/rspack-test/hotCases/sharing/share-plugin/__snapshots__/web/1.snap.txt +++ b/tests/rspack-test/hotCases/sharing/share-plugin/__snapshots__/web/1.snap.txt @@ -7,7 +7,7 @@ - Bundle: bundle.js - Bundle: common_js_2.chunk.CURRENT_HASH.js - Manifest: main.LAST_HASH.hot-update.json, size: 28 -- Update: main.LAST_HASH.hot-update.js, size: 42288 +- Update: main.LAST_HASH.hot-update.js, size: 42334 ## Manifest @@ -101,7 +101,7 @@ __webpack_require__.h = () => ("CURRENT_HASH") // webpack/runtime/consumes_loading (() => { -__webpack_require__.consumesLoadingData = { chunkMapping: {"main":["webpack/sharing/consume/default/common/./common?1"],"webpack_sharing_consume_default_common2_common_2":["webpack/sharing/consume/default/common2/./common?2"]}, moduleIdToConsumeDataMapping: {"webpack/sharing/consume/default/common2/./common?2": { shareScope: "default", shareKey: "common2", import: "./common?2", requiredVersion: "1.1.1", strictVersion: true, singleton: false, eager: false, fallback: () => (__webpack_require__.e("common_js_2").then(() => (() => (__webpack_require__("./common.js?2"))))) }, "webpack/sharing/consume/default/common/./common?1": { shareScope: "default", shareKey: "common", import: "./common?1", requiredVersion: "1.1.1", strictVersion: true, singleton: false, eager: true, fallback: () => (() => (__webpack_require__("./common.js?1"))) }}, initialConsumes: ["webpack/sharing/consume/default/common/./common?1"] }; +__webpack_require__.consumesLoadingData = { chunkMapping: {"main":["webpack/sharing/consume/default/common/./common?1"],"webpack_sharing_consume_default_common2_common_2":["webpack/sharing/consume/default/common2/./common?2"]}, moduleIdToConsumeDataMapping: {"webpack/sharing/consume/default/common2/./common?2": { shareScope: "default", shareKey: "common2", import: "./common?2", requiredVersion: "1.1.1", strictVersion: true, singleton: false, eager: false, fallback: () => (__webpack_require__.e("common_js_2").then(() => (() => (__webpack_require__("./common.js?2"))))), treeShakingMode: null }, "webpack/sharing/consume/default/common/./common?1": { shareScope: "default", shareKey: "common", import: "./common?1", requiredVersion: "1.1.1", strictVersion: true, singleton: false, eager: true, fallback: () => (() => (__webpack_require__("./common.js?1"))), treeShakingMode: null }}, initialConsumes: ["webpack/sharing/consume/default/common/./common?1"] }; var splitAndConvert = function(str) { return str.split(".").map(function(item) { return +item == item ? +item : item; diff --git a/tests/rspack-test/package.json b/tests/rspack-test/package.json index 735538c4ce48..4d39e870045e 100644 --- a/tests/rspack-test/package.json +++ b/tests/rspack-test/package.json @@ -15,7 +15,7 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/preset-react": "^7.28.5", - "@module-federation/runtime-tools": "^0.22.0", + "@module-federation/runtime-tools": "0.24.1", "@rspack/binding-testing": "workspace:*", "@rspack/cli": "workspace:*", "@rspack/core": "workspace:*", diff --git a/tests/rspack-test/rstest.config.ts b/tests/rspack-test/rstest.config.ts index 980d9d962b80..e24273d26e80 100644 --- a/tests/rspack-test/rstest.config.ts +++ b/tests/rspack-test/rstest.config.ts @@ -6,120 +6,120 @@ const root = path.resolve(__dirname, "../../"); process.env.NO_COLOR = '1'; const setupFilesAfterEnv = [ - "@rspack/test-tools/setup-env", - "@rspack/test-tools/setup-expect", - "./expects/stats-string-comparator.js", + "@rspack/test-tools/setup-env", + "@rspack/test-tools/setup-expect", + "./expects/stats-string-comparator.js", ]; const wasmConfig = process.env.WASM && defineProject({ - setupFiles: [...setupFilesAfterEnv, "@rspack/test-tools/setup-wasm"], - exclude: [ - // Skip because they rely on snapshots - "Diagnostics.test.js", - "Error.test.js", - "StatsAPI.test.js", - "StatsOutput.test.js", - // Skip because the loader can not be loaded in CI - "Hot*.test.js", + setupFiles: [...setupFilesAfterEnv, "@rspack/test-tools/setup-wasm"], + exclude: [ + // Skip because they rely on snapshots + "Diagnostics.test.js", + "Error.test.js", + "StatsAPI.test.js", + "StatsOutput.test.js", + // Skip because the loader can not be loaded in CI + "Hot*.test.js", - // Skip temporarily and should investigate in the future - "Cache.test.js", - "Compiler.test.js", - "MultiCompiler.test.js", - "Serial.test.js", - "Defaults.test.js", - "Example.test.js", - "Incremental-*.test.js", - "NativeWatcher*.test.js", - ], - maxConcurrency: 1, + // Skip temporarily and should investigate in the future + "Cache.test.js", + "Compiler.test.js", + "MultiCompiler.test.js", + "Serial.test.js", + "Defaults.test.js", + "Example.test.js", + "Incremental-*.test.js", + "NativeWatcher*.test.js", + ], + maxConcurrency: 1, }); const testFilter = process.argv.includes("--test") || process.argv.includes("-t") - ? process.argv[ - (process.argv.includes("-t") - ? process.argv.indexOf("-t") - : process.argv.indexOf("--test")) + 1 - ] - : undefined; + ? process.argv[ + (process.argv.includes("-t") + ? process.argv.indexOf("-t") + : process.argv.indexOf("--test")) + 1 + ] + : undefined; const sharedConfig = defineProject({ - setupFiles: setupFilesAfterEnv, - testTimeout: process.env.CI ? 60000 : 30000, - include: [ - "*.test.js", - ], - slowTestThreshold: 5000, - // Retry on CI to reduce flakes - retry: process.env.CI ? 3 : 0, - resolve: { - alias: { - // Fixed jest-serialize-path not working when non-ascii code contains. - slash: path.join(__dirname, "../../scripts/test/slash.cjs"), - // disable sourcemap remapping for ts file - "source-map-support/register": "identity-obj-proxy" - } - }, - source: { - exclude: [root], - }, - disableConsoleIntercept: true, - globals: true, - output: { - externals: [/.*/], - }, - passWithNoTests: true, - snapshotFormat: { - escapeString: true, - printBasicPrototype: true - }, - chaiConfig: process.env.CI ? { - // show all info on CI - truncateThreshold: 5000, - } : undefined, - env: { - RUST_BACKTRACE: 'full', - updateSnapshot: - process.argv.includes("-u") || process.argv.includes("--updateSnapshot") ? 'true' : 'false', - RSPACK_DEV: 'false', - RSPACK_EXPERIMENTAL: 'true', - RSPACK_CONFIG_VALIDATE: "strict", - testFilter, - printLogger: process.env.DEBUG === "test" ? 'true' : 'false', - __TEST_PATH__: __dirname, - __TEST_FIXTURES_PATH__: path.resolve(__dirname, "fixtures"), - __TEST_DIST_PATH__: path.resolve(__dirname, "js"), - __ROOT_PATH__: root, - DEFAULT_MAX_CONCURRENT: process.argv.includes("--maxConcurrency") - ? process.argv[ - process.argv.indexOf("--maxConcurrency") + 1 - ] - : undefined, - __RSPACK_PATH__: path.resolve(root, "packages/rspack"), - __RSPACK_TEST_TOOLS_PATH__: path.resolve(root, "packages/rspack-test-tools"), - __DEBUG__: process.env.DEBUG === "test" ? 'true' : 'false', - }, - ...(wasmConfig || {}), + setupFiles: setupFilesAfterEnv, + testTimeout: process.env.CI ? 60000 : 30000, + include: [ + "*.test.js", + ], + slowTestThreshold: 5000, + // Retry on CI to reduce flakes + retry: process.env.CI ? 3 : 0, + resolve: { + alias: { + // Fixed jest-serialize-path not working when non-ascii code contains. + slash: path.join(__dirname, "../../scripts/test/slash.cjs"), + // disable sourcemap remapping for ts file + "source-map-support/register": "identity-obj-proxy" + } + }, + source: { + exclude: [root], + }, + disableConsoleIntercept: true, + globals: true, + output: { + externals: [/.*/], + }, + passWithNoTests: true, + snapshotFormat: { + escapeString: true, + printBasicPrototype: true + }, + chaiConfig: process.env.CI ? { + // show all info on CI + truncateThreshold: 5000, + } : undefined, + env: { + RUST_BACKTRACE: 'full', + updateSnapshot: + process.argv.includes("-u") || process.argv.includes("--updateSnapshot") ? 'true' : 'false', + RSPACK_DEV: 'false', + RSPACK_EXPERIMENTAL: 'true', + RSPACK_CONFIG_VALIDATE: "strict", + testFilter, + printLogger: process.env.DEBUG === "test" ? 'true' : 'false', + __TEST_PATH__: __dirname, + __TEST_FIXTURES_PATH__: path.resolve(__dirname, "fixtures"), + __TEST_DIST_PATH__: path.resolve(__dirname, "js"), + __ROOT_PATH__: root, + DEFAULT_MAX_CONCURRENT: process.argv.includes("--maxConcurrency") + ? process.argv[ + process.argv.indexOf("--maxConcurrency") + 1 + ] + : undefined, + __RSPACK_PATH__: path.resolve(root, "packages/rspack"), + __RSPACK_TEST_TOOLS_PATH__: path.resolve(root, "packages/rspack-test-tools"), + __DEBUG__: process.env.DEBUG === "test" ? 'true' : 'false', + }, + ...(wasmConfig || {}), }) as ProjectConfig; export default defineConfig({ - projects: [{ - extends: sharedConfig, - name: 'base', - }, { - extends: sharedConfig, - name: 'hottest', - include: process.env.WASM ? [] : ["/*.hottest.js"], - env: { - RSPACK_HOT_TEST: 'true', - }, - }], - reporters: testFilter ? ['verbose'] : ['default'], - hideSkippedTests: true, - hideSkippedTestFiles: true, - pool: { - maxWorkers: process.env.WASM ? 1 : "80%", - execArgv: ['--no-warnings', '--expose-gc', '--max-old-space-size=8192', '--experimental-vm-modules'], - }, + projects: [{ + extends: sharedConfig, + name: 'base', + }, { + extends: sharedConfig, + name: 'hottest', + include: process.env.WASM ? [] : ["/*.hottest.js"], + env: { + RSPACK_HOT_TEST: 'true', + }, + }], + reporters: testFilter ? ['verbose'] : ['default'], + hideSkippedTests: true, + hideSkippedTestFiles: true, + pool: { + maxWorkers: process.env.WASM ? 1 : "80%", + execArgv: ['--no-warnings', '--expose-gc', '--max-old-space-size=8192', '--experimental-vm-modules'], + }, }); diff --git a/website/docs/en/plugins/webpack/module-federation-plugin.mdx b/website/docs/en/plugins/webpack/module-federation-plugin.mdx index 6f0efa590541..35b0a908316e 100644 --- a/website/docs/en/plugins/webpack/module-federation-plugin.mdx +++ b/website/docs/en/plugins/webpack/module-federation-plugin.mdx @@ -162,10 +162,15 @@ The SharedConfig can include the following sub-options: - singleton: Ensure that shared modules are only loaded once between different versions, following the singleton pattern. This is necessary for libraries designed to run as singletons, such as React, as it can prevent various issues caused by instantiating multiple library instances. - strictVersion: Used to strengthen `requiredVersion`. If set to `true`, the shared module must match the version specified in requiredVersion exactly, otherwise an error will be reported and the module will not be loaded. If set to `false`, it can tolerate imprecise matching. - version: Explicitly set the version of the shared module. By default, the version in `package.json` will be used. +- treeShaking: Configure the treeshaking behavior of shared dependencies. By enabling treeshaking for shared dependencies, the size of shared dependencies can be reduced. Only effective when using Rspack as the build tool. + - usedExports: Manually **add** export members used by shared dependencies. + - mode: Configure the loading strategy for treeshaking. + - `runtime-infer`: Infer based on the consumer's `usedExports`. If the provider's `usedExports` match the consumer's `usedExports`, the provider's `shared` will be used; otherwise, the consumer's own `shared` will be used. If neither condition is met, the full version is used. + - `server-calc`: Decide whether to load shared dependencies based on the snapshot sent by the server. ### manifest - + - Type: ```ts @@ -190,6 +195,73 @@ When enabled, the plugin emits both `mf-manifest.json` and `mf-stats.json` (you - `fileName`: Manifest file name. When set, the stats file automatically appends a `-stats` suffix (for example, `fileName: 'mf.json'` produces `mf.json` and `mf-stats.json`). All files are emitted into the directory defined by `filePath` (if provided). - `disableAssetsAnalyze`: Disables asset analysis. When `true`, the manifest omits the `shared` and `exposes` fields, and the `remotes` entries will not include asset information. +### injectTreeShakingUsedExports + + + +Whether to inject the exports used by shared into the bundler runtime. + +- Type: `boolean` +- Required: No +- Default: `undefined` + +This option is used to control whether to inject the actual used export information of shared modules into the bundler runtime for more precise dependency management and optimization. + +If you are using mode as 'server-calc', it is recommended to set this option to `false`. + +### treeShakingDir + +Directory for outputting tree shaking shared fallback resources. + +- Type: `string` +- Required: No +- Default: `undefined` + +When shared dependency tree shaking is enabled, Module Federation splits unused shared module exports. This option specifies the output directory for these fallback resources. + +### treeShakingSharedExcludePlugins + +Configure plugin names to exclude during the shared dependency tree shaking/fallback build process. + +- Type: `string[]` +- Required: No +- Default: `['HtmlWebpackPlugin','HtmlRspackPlugin']` + +Allows users to specify a set of plugin names that will be ignored or not involved in processing during the shared dependency tree shaking/fallback build process. + +### treeShakingSharedPlugins + +Allows users to explicitly specify which plugins should participate in the second treeshaking process for shared modules. + +- Type: `string[]` +- Required: No +- Default: `undefined` + +If `shared.treeShaking.mode` is set to 'server-calc', then in the deployment service, the shared dependencies that need tree shaking will be rebuilt. At this time, only shared dependencies are built, and the original project's build configuration will not be loaded. +If your project has special build configurations, such as setting externals, you can integrate these build configurations into an NPM Package, and then fill in the name and version of this plugin in `treeShakingSharedPlugins`, so that it can participate in the treeshaking process of shared modules in the second treeshaking process. + +For example, a plugin `my-build-plugin` is provided, which sets `externals`: + +```ts title='my-build-plugin' +class MyBuildPlugin { + apply(compiler) { + compiler.options.externals = { + react: 'React', + }; + } +} +export default MyBuildPlugin; +``` + +Publish this plugin version as `0.0.2`, then you only need to fill in the plugin name and version in `treeShakingSharedPlugins`: + +```ts title='module-federation.config.ts' +export default { + // ... + treeShakingSharedPlugins: ['my-build-plugin@0.0.2'], +}; +``` + ## FAQ - Found non-downgraded syntax in the build output? diff --git a/website/docs/en/plugins/webpack/tree-shaking-shared-plugin.mdx b/website/docs/en/plugins/webpack/tree-shaking-shared-plugin.mdx new file mode 100644 index 000000000000..630cbaf41c03 --- /dev/null +++ b/website/docs/en/plugins/webpack/tree-shaking-shared-plugin.mdx @@ -0,0 +1,45 @@ +import { Stability, ApiMeta } from '@components/ApiMeta'; + +# TreeShakingSharedPlugin + + + +**Overview** + +- Performs on-demand build and export optimization for `shared` dependencies based on Module Federation configuration. + +**Options** + +- `mfConfig`: `ModuleFederationPluginOptions`, configuration passed to the Module Federation plugin. +- `plugins`: extra plugins reused during independent builds. +- `secondary`: whether to perform a second tree-shake during independent builds (typically used when the deployment platform has determined complete dependency info and triggers a fresh build to improve tree-shake accuracy for shared dependencies). + +**Usage** + +```js +// rspack.config.js +const { TreeShakingSharedPlugin } = require('@rspack/core'); + +module.exports = { + plugins: [ + new TreeShakingSharedPlugin({ + secondary: true, + mfConfig: { + name: 'app', + shared: { + 'lodash-es': { treeShaking: { mode: 'server-calc' } }, + }, + library: { type: 'var', name: 'App' }, + manifest: true, + }, + plugins: [], + }), + ], +}; +``` + +**Behavior** + +- Normalizes `shared` into `[shareName, SharedConfig][]`. +- Registers `SharedUsedExportsOptimizerPlugin` when `secondary` is `false` to inject used-exports from the stats manifest. +- Triggers independent compilation for shared entries with `tree shaking: true`, and writes the produced assets back to the `stats/manifest` (fallback fields). diff --git a/website/docs/zh/plugins/webpack/module-federation-plugin.mdx b/website/docs/zh/plugins/webpack/module-federation-plugin.mdx index e142e78cb811..e2b35d6d68e6 100644 --- a/website/docs/zh/plugins/webpack/module-federation-plugin.mdx +++ b/website/docs/zh/plugins/webpack/module-federation-plugin.mdx @@ -146,6 +146,10 @@ export default { singleton?: boolean; strictVersion?: boolean; version?: false | string; + treeShaking?: { + mode: 'runtime-infer' | 'server-calc'; + usedExports?: string[]; + }; } ``` @@ -162,10 +166,15 @@ export default { - singleton:确保共享模块在不同版本间只会被加载一次,遵守单例模式。这对于一些设计为单例运行的库(如 React)是很有必要的,因为这样可以避免由于实例化了多个库实例而导致的各种问题。 - strictVersion:用来强化 `requiredVersion`。如果设置为 `true`,那么必须精确地匹配 `requiredVersion` 中规定的版本,否则共享模块会报错并且不会加载该模块。如果设置为 `false`,那么可以容忍不精确的匹配。 - version:显式地设置共享模块的版本。默认会使用 `package.json` 中的版本。 +- treeShaking:配置 shared 依赖的 treeshaking 行为。通过开启 shared 依赖的 treeshaking,可以减少共享依赖的体积。 + - usedExports:手动**添加**共享依赖被使用的导出成员。 + - mode:配置 treeshaking 的加载策略。 + - `runtime-infer`: 根据消费方的 `usedExports` 进行推断,如果提供方的 `usedExports` 符合当前消费方的 `usedExports`,那么就会使用提供方的 `shared`,否则会使用消费方自身的 `shared`,如果都不满足,就使用全量的。 + - `server-calc`: 根据服务端下发的 snapshot 来决定是否加载共享依赖。 ### manifest - + - 类型: ```ts @@ -188,6 +197,73 @@ export default { - fileName:manifest 文件名称,如果设置了 `fileName`,对应的 stats 文件名会自动附加 `-stats` 后缀(例如 `fileName: 'mf.json'` 时会同时生成 `mf.json` 与 `mf-stats.json`)。所有文件都会写入 `filePath`(若配置)指定的子目录。 - disableAssetsAnalyze:禁用产物分析,如果设置为 true ,那么 manifest 中将不会有 shared 、exposes 字段,且 remotes 中也不会有 assets 。 +### injectTreeShakingUsedExports + + + +是否将 shared 使用的 exports 注入到 bundler runtime 中。 + +- 类型:`boolean` +- 是否必填:否 +- 默认值:`undefined` + +此选项用于控制是否将共享模块中实际使用的导出信息注入到打包器的运行时中,以便进行更精确的依赖管理和优化。 + +如果你使用的是 mode 为 'server-calc',那么推荐将此选项设置为 `false`。 + +### treeShakingDir + +用于输出 tree shaking 共享 fallback 资源的目录。 + +- 类型:`string` +- 是否必填:否 +- 默认值:`undefined` + +当启用共享依赖 tree shaking 功能时,Module Federation 会将未使用的共享模块导出拆分出来。该选项指定了这些 fallback 资源的输出目录。 + +### treeShakingSharedExcludePlugins + +配置在构建共享依赖 tree shaking/fallback 过程中需要排除的插件名称。 + +- 类型:`string[]` +- 是否必填:否 +- 默认值:`['HtmlWebpackPlugin','HtmlRspackPlugin']` + +允许用户指定一组插件名称,这些插件在构建共享依赖 tree shaking/fallback 过程 时将被忽略或不参与处理。 + +### treeShakingSharedPlugins + +允许用户显式指定哪些插件应该参与 shared 模块的第二次 treeshaking 过程。 + +- 类型:`string[]` +- 是否必填:否 +- 默认值:`undefined` + +如果设置了 `shared.treeShaking.mode` 为 'server-calc',那么在部署服务中,会重新构建需要 tree shaking 的共享依赖,此时仅构建共享依赖,不会加载原项目的构建配置。 +如果你的项目有特殊的构建配置,例如设置了 externals ,那么你可以把这些构建配置集成到一个 NPM Package ,然后在 `treeShakingSharedPlugins` 中填写此插件的名称和版本,这样就可以在第二次 treeshaking 过程中参与 shared 模块的 treeshaking 过程。 + +例如提供了一个插件 `my-build-plugin`,它会设置 `externals`: + +```ts title='my-build-plugin' +class MyBuildPlugin { + apply(compiler) { + compiler.options.externals = { + react: 'React', + }; + } +} +export default MyBuildPlugin; +``` + +将此插件发布的版本为 `0.0.2` ,那么此时只需要在 `treeShakingSharedPlugins` 中填写插件的名称和版本即可: + +```ts title='module-federation.config.ts' +export default { + // ... + treeShakingSharedPlugins: ['my-build-plugin@0.0.2'], +}; +``` + ## 常见问题 - 构建产物中存在未降级语法? diff --git a/website/docs/zh/plugins/webpack/tree-shaking-shared-plugin.mdx b/website/docs/zh/plugins/webpack/tree-shaking-shared-plugin.mdx new file mode 100644 index 000000000000..221c2047671a --- /dev/null +++ b/website/docs/zh/plugins/webpack/tree-shaking-shared-plugin.mdx @@ -0,0 +1,45 @@ +import { Stability, ApiMeta } from '@components/ApiMeta'; + +# TreeShakingSharedPlugin + + + +**概览** + +基于模块联邦配置对 shared 依赖进行按需构建与导出优化。 + +**选项** + +- `mfConfig`:`ModuleFederationPluginOptions`,传入模块联邦插件所需要的配置项。 +- `plugins`:额外插件列表,可在独立编译中复用。 +- `secondary`:是否在独立编译阶段执行二次摇树优化(二次摇树通常在部署平台确定了完整的依赖信息后重新触发的一次全新构建,提高共享依赖 tree shaking 命中的准确率)。 + +**使用示例** + +```js +// rspack.config.js +const { TreeShakingSharedPlugin } = require('@rspack/core'); + +module.exports = { + plugins: [ + new TreeShakingSharedPlugin({ + secondary: true, + mfConfig: { + name: 'app', + shared: { + 'lodash-es': { treeShaking: true }, + }, + library: { type: 'var', name: 'App' }, + manifest: true, + }, + plugins: [], + }), + ], +}; +``` + +**行为说明** + +- 读取 `shared` 配置后标准化为 `[shareName, SharedConfig][]`。 +- 当 `secondary` 为 `false` 时,注册 `SharedUsedExportsOptimizerPlugin`,基于 stats 清单注入已用导出集合。 +- 对 `tree shaking` 的共享包触发独立编译,产出回写到 stats/manifest 中的 fallback 字段。