diff --git a/Cargo.lock b/Cargo.lock index 3277b35240c9..2685476f2a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4452,6 +4452,7 @@ dependencies = [ "cow-utils", "either", "fast-glob", + "futures", "indexmap", "indoc", "itertools 0.14.0", diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index 6c2c106b8d82..7440ecbb5c35 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -540,10 +540,13 @@ export declare enum BuiltinPluginName { SplitChunksPlugin = 'SplitChunksPlugin', RemoveDuplicateModulesPlugin = 'RemoveDuplicateModulesPlugin', ShareRuntimePlugin = 'ShareRuntimePlugin', + SharedUsedExportsOptimizerPlugin = 'SharedUsedExportsOptimizerPlugin', ContainerPlugin = 'ContainerPlugin', ContainerReferencePlugin = 'ContainerReferencePlugin', ProvideSharedPlugin = 'ProvideSharedPlugin', ConsumeSharedPlugin = 'ConsumeSharedPlugin', + CollectSharedEntryPlugin = 'CollectSharedEntryPlugin', + ShareContainerPlugin = 'ShareContainerPlugin', ModuleFederationRuntimePlugin = 'ModuleFederationRuntimePlugin', ModuleFederationManifestPlugin = 'ModuleFederationManifestPlugin', NamedModuleIdsPlugin = 'NamedModuleIdsPlugin', @@ -1837,6 +1840,11 @@ export interface RawCircularDependencyRspackPluginOptions { onEnd?: () => void } +export interface RawCollectShareEntryPluginOptions { + consumes: Array + filename?: string +} + export interface RawConsumeOptions { key: string import?: string @@ -2584,6 +2592,12 @@ export interface RawOptimizationOptions { avoidEntryIife: boolean } +export interface RawOptimizeSharedConfig { + shareKey: string + treeshake: boolean + usedExports?: Array +} + export interface RawOptions { name?: string mode?: undefined | 'production' | 'development' | 'none' @@ -2820,6 +2834,21 @@ export interface RawRuntimeChunkOptions { name: string | ((entrypoint: { name: string }) => string) } +export interface RawShareContainerPluginOptions { + name: string + request: string + version: string + fileName?: string + library: JsLibraryOptions +} + +export interface RawSharedUsedExportsOptimizerPluginOptions { + shared: Array + injectUsedExports?: boolean + manifestFileName?: string + statsFileName?: string +} + export interface RawSizeLimitsPluginOptions { assetFilter?: (assetFilename: string) => boolean hints?: "error" | "warning" diff --git a/crates/node_binding/rspack.wasi-browser.js b/crates/node_binding/rspack.wasi-browser.js index e3e5c0a99d48..ee65959b37bc 100644 --- a/crates/node_binding/rspack.wasi-browser.js +++ b/crates/node_binding/rspack.wasi-browser.js @@ -63,63 +63,4 @@ const { }, }) export default __napiModule.exports -export const Assets = __napiModule.exports.Assets -export const AsyncDependenciesBlock = __napiModule.exports.AsyncDependenciesBlock -export const Chunk = __napiModule.exports.Chunk -export const ChunkGraph = __napiModule.exports.ChunkGraph -export const ChunkGroup = __napiModule.exports.ChunkGroup -export const Chunks = __napiModule.exports.Chunks -export const CodeGenerationResult = __napiModule.exports.CodeGenerationResult -export const CodeGenerationResults = __napiModule.exports.CodeGenerationResults -export const ConcatenatedModule = __napiModule.exports.ConcatenatedModule -export const ContextModule = __napiModule.exports.ContextModule -export const Dependency = __napiModule.exports.Dependency -export const Diagnostics = __napiModule.exports.Diagnostics -export const EntryDataDto = __napiModule.exports.EntryDataDto -export const EntryDataDTO = __napiModule.exports.EntryDataDTO -export const EntryDependency = __napiModule.exports.EntryDependency -export const EntryOptionsDto = __napiModule.exports.EntryOptionsDto -export const EntryOptionsDTO = __napiModule.exports.EntryOptionsDTO -export const ExternalModule = __napiModule.exports.ExternalModule -export const JsCompilation = __napiModule.exports.JsCompilation -export const JsCompiler = __napiModule.exports.JsCompiler -export const JsContextModuleFactoryAfterResolveData = __napiModule.exports.JsContextModuleFactoryAfterResolveData -export const JsContextModuleFactoryBeforeResolveData = __napiModule.exports.JsContextModuleFactoryBeforeResolveData -export const JsDependencies = __napiModule.exports.JsDependencies -export const JsEntries = __napiModule.exports.JsEntries -export const JsExportsInfo = __napiModule.exports.JsExportsInfo -export const JsModuleGraph = __napiModule.exports.JsModuleGraph -export const JsResolver = __napiModule.exports.JsResolver -export const JsResolverFactory = __napiModule.exports.JsResolverFactory -export const JsStats = __napiModule.exports.JsStats -export const KnownBuildInfo = __napiModule.exports.KnownBuildInfo -export const Module = __napiModule.exports.Module -export const ModuleGraphConnection = __napiModule.exports.ModuleGraphConnection -export const NativeWatcher = __napiModule.exports.NativeWatcher -export const NativeWatchResult = __napiModule.exports.NativeWatchResult -export const NormalModule = __napiModule.exports.NormalModule -export const RawExternalItemFnCtx = __napiModule.exports.RawExternalItemFnCtx -export const ReadonlyResourceData = __napiModule.exports.ReadonlyResourceData -export const ResolverFactory = __napiModule.exports.ResolverFactory -export const Sources = __napiModule.exports.Sources -export const VirtualFileStore = __napiModule.exports.VirtualFileStore -export const JsVirtualFileStore = __napiModule.exports.JsVirtualFileStore -export const async = __napiModule.exports.async -export const BuiltinPluginName = __napiModule.exports.BuiltinPluginName -export const cleanupGlobalTrace = __napiModule.exports.cleanupGlobalTrace -export const EnforceExtension = __napiModule.exports.EnforceExtension -export const EXPECTED_RSPACK_CORE_VERSION = __napiModule.exports.EXPECTED_RSPACK_CORE_VERSION -export const formatDiagnostic = __napiModule.exports.formatDiagnostic -export const JsLoaderState = __napiModule.exports.JsLoaderState -export const JsRspackSeverity = __napiModule.exports.JsRspackSeverity -export const loadBrowserslist = __napiModule.exports.loadBrowserslist -export const minify = __napiModule.exports.minify -export const minifySync = __napiModule.exports.minifySync -export const RawJavascriptParserCommonjsExports = __napiModule.exports.RawJavascriptParserCommonjsExports -export const RawRuleSetConditionType = __napiModule.exports.RawRuleSetConditionType -export const registerGlobalTrace = __napiModule.exports.registerGlobalTrace -export const RegisterJsTapKind = __napiModule.exports.RegisterJsTapKind -export const sync = __napiModule.exports.sync -export const syncTraceEvent = __napiModule.exports.syncTraceEvent -export const transform = __napiModule.exports.transform -export const transformSync = __napiModule.exports.transformSync + diff --git a/crates/node_binding/rspack.wasi.cjs b/crates/node_binding/rspack.wasi.cjs index a251ce4d0d7d..1ad96db4aac4 100644 --- a/crates/node_binding/rspack.wasi.cjs +++ b/crates/node_binding/rspack.wasi.cjs @@ -108,63 +108,4 @@ const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule }, }) module.exports = __napiModule.exports -module.exports.Assets = __napiModule.exports.Assets -module.exports.AsyncDependenciesBlock = __napiModule.exports.AsyncDependenciesBlock -module.exports.Chunk = __napiModule.exports.Chunk -module.exports.ChunkGraph = __napiModule.exports.ChunkGraph -module.exports.ChunkGroup = __napiModule.exports.ChunkGroup -module.exports.Chunks = __napiModule.exports.Chunks -module.exports.CodeGenerationResult = __napiModule.exports.CodeGenerationResult -module.exports.CodeGenerationResults = __napiModule.exports.CodeGenerationResults -module.exports.ConcatenatedModule = __napiModule.exports.ConcatenatedModule -module.exports.ContextModule = __napiModule.exports.ContextModule -module.exports.Dependency = __napiModule.exports.Dependency -module.exports.Diagnostics = __napiModule.exports.Diagnostics -module.exports.EntryDataDto = __napiModule.exports.EntryDataDto -module.exports.EntryDataDTO = __napiModule.exports.EntryDataDTO -module.exports.EntryDependency = __napiModule.exports.EntryDependency -module.exports.EntryOptionsDto = __napiModule.exports.EntryOptionsDto -module.exports.EntryOptionsDTO = __napiModule.exports.EntryOptionsDTO -module.exports.ExternalModule = __napiModule.exports.ExternalModule -module.exports.JsCompilation = __napiModule.exports.JsCompilation -module.exports.JsCompiler = __napiModule.exports.JsCompiler -module.exports.JsContextModuleFactoryAfterResolveData = __napiModule.exports.JsContextModuleFactoryAfterResolveData -module.exports.JsContextModuleFactoryBeforeResolveData = __napiModule.exports.JsContextModuleFactoryBeforeResolveData -module.exports.JsDependencies = __napiModule.exports.JsDependencies -module.exports.JsEntries = __napiModule.exports.JsEntries -module.exports.JsExportsInfo = __napiModule.exports.JsExportsInfo -module.exports.JsModuleGraph = __napiModule.exports.JsModuleGraph -module.exports.JsResolver = __napiModule.exports.JsResolver -module.exports.JsResolverFactory = __napiModule.exports.JsResolverFactory -module.exports.JsStats = __napiModule.exports.JsStats -module.exports.KnownBuildInfo = __napiModule.exports.KnownBuildInfo -module.exports.Module = __napiModule.exports.Module -module.exports.ModuleGraphConnection = __napiModule.exports.ModuleGraphConnection -module.exports.NativeWatcher = __napiModule.exports.NativeWatcher -module.exports.NativeWatchResult = __napiModule.exports.NativeWatchResult -module.exports.NormalModule = __napiModule.exports.NormalModule -module.exports.RawExternalItemFnCtx = __napiModule.exports.RawExternalItemFnCtx -module.exports.ReadonlyResourceData = __napiModule.exports.ReadonlyResourceData -module.exports.ResolverFactory = __napiModule.exports.ResolverFactory -module.exports.Sources = __napiModule.exports.Sources -module.exports.VirtualFileStore = __napiModule.exports.VirtualFileStore -module.exports.JsVirtualFileStore = __napiModule.exports.JsVirtualFileStore -module.exports.async = __napiModule.exports.async -module.exports.BuiltinPluginName = __napiModule.exports.BuiltinPluginName -module.exports.cleanupGlobalTrace = __napiModule.exports.cleanupGlobalTrace -module.exports.EnforceExtension = __napiModule.exports.EnforceExtension -module.exports.EXPECTED_RSPACK_CORE_VERSION = __napiModule.exports.EXPECTED_RSPACK_CORE_VERSION -module.exports.formatDiagnostic = __napiModule.exports.formatDiagnostic -module.exports.JsLoaderState = __napiModule.exports.JsLoaderState -module.exports.JsRspackSeverity = __napiModule.exports.JsRspackSeverity -module.exports.loadBrowserslist = __napiModule.exports.loadBrowserslist -module.exports.minify = __napiModule.exports.minify -module.exports.minifySync = __napiModule.exports.minifySync -module.exports.RawJavascriptParserCommonjsExports = __napiModule.exports.RawJavascriptParserCommonjsExports -module.exports.RawRuleSetConditionType = __napiModule.exports.RawRuleSetConditionType -module.exports.registerGlobalTrace = __napiModule.exports.registerGlobalTrace -module.exports.RegisterJsTapKind = __napiModule.exports.RegisterJsTapKind -module.exports.sync = __napiModule.exports.sync -module.exports.syncTraceEvent = __napiModule.exports.syncTraceEvent -module.exports.transform = __napiModule.exports.transform -module.exports.transformSync = __napiModule.exports.transformSync + diff --git a/crates/rspack_binding_api/src/plugins/interceptor.rs b/crates/rspack_binding_api/src/plugins/interceptor.rs index bd3854839883..5a8c8b10b166 100644 --- a/crates/rspack_binding_api/src/plugins/interceptor.rs +++ b/crates/rspack_binding_api/src/plugins/interceptor.rs @@ -78,6 +78,7 @@ use crate::{ JsContextModuleFactoryAfterResolveDataWrapper, JsContextModuleFactoryAfterResolveResult, JsContextModuleFactoryBeforeResolveDataWrapper, JsContextModuleFactoryBeforeResolveResult, }, + dependency::DependencyWrapper, html::{ JsAfterEmitData, JsAfterTemplateExecutionData, JsAlterAssetTagGroupsData, JsAlterAssetTagsData, JsBeforeAssetTagGenerationData, JsBeforeEmitData, @@ -94,7 +95,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 8733f3a79aa0..56a7674d6e37 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}; @@ -75,8 +79,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, + ShareContainerPlugin, ShareRuntimePlugin, SharedUsedExportsOptimizerPlugin, }; use rspack_plugin_module_info_header::ModuleInfoHeaderPlugin; use rspack_plugin_module_replacement::{ContextReplacementPlugin, NormalModuleReplacementPlugin}; @@ -117,7 +122,7 @@ use self::{ raw_limit_chunk_count::RawLimitChunkCountPluginOptions, raw_mf::{ RawConsumeSharedPluginOptions, RawContainerPluginOptions, RawContainerReferencePluginOptions, - RawProvideOptions, + RawShareContainerPluginOptions, }, raw_normal_replacement::RawNormalModuleReplacementPluginOptions, raw_runtime_chunk::RawRuntimeChunkOptions, @@ -167,10 +172,13 @@ pub enum BuiltinPluginName { SplitChunksPlugin, RemoveDuplicateModulesPlugin, ShareRuntimePlugin, + SharedUsedExportsOptimizerPlugin, ContainerPlugin, ContainerReferencePlugin, ProvideSharedPlugin, ConsumeSharedPlugin, + CollectSharedEntryPlugin, + ShareContainerPlugin, ModuleFederationRuntimePlugin, ModuleFederationManifestPlugin, NamedModuleIdsPlugin, @@ -463,6 +471,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( @@ -492,6 +506,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::ShareContainerPlugin => { + let options = downcast_into::(self.options) + .map_err(|report| napi::Error::from_reason(report.to_string()))? + .into(); + plugins.push(ShareContainerPlugin::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 03adc9c05724..52e48ec1788f 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,10 +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, ModuleFederationRuntimePluginOptions, ProvideOptions, - ProvideVersion, RemoteAliasTarget, RemoteOptions, StatsBuildInfo, + CollectSharedEntryPluginOptions, ConsumeOptions, ConsumeSharedPluginOptions, ConsumeVersion, + ContainerPluginOptions, ContainerReferencePluginOptions, ExposeOptions, ManifestExposeOption, + ManifestSharedOption, ModuleFederationManifestPluginOptions, + ModuleFederationRuntimePluginOptions, OptimizeSharedConfig, ProvideOptions, ProvideVersion, + RemoteAliasTarget, RemoteOptions, ShareContainerEntryOptions, ShareContainerPluginOptions, + SharedUsedExportsOptimizerPluginOptions, StatsBuildInfo, }; use crate::options::{ @@ -133,6 +135,51 @@ impl From for (String, ProvideOptions) { } } +#[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 RawShareContainerPluginOptions { + pub name: String, + pub request: String, + pub version: String, + pub file_name: Option, + pub library: JsLibraryOptions, +} + +impl From for ShareContainerPluginOptions { + fn from(value: RawShareContainerPluginOptions) -> Self { + ShareContainerPluginOptions { + 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 { @@ -154,6 +201,52 @@ impl From for ConsumeSharedPluginOptions { } } +#[derive(Debug)] +#[napi(object)] +pub struct RawOptimizeSharedConfig { + pub share_key: String, + pub treeshake: bool, + pub used_exports: Option>, +} + +impl From for OptimizeSharedConfig { + fn from(value: RawOptimizeSharedConfig) -> Self { + Self { + share_key: value.share_key, + treeshake: value.treeshake, + used_exports: value.used_exports.unwrap_or_default(), + } + } +} + +#[derive(Debug)] +#[napi(object)] +pub struct RawSharedUsedExportsOptimizerPluginOptions { + pub shared: Vec, + pub inject_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_used_exports: value.inject_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 { diff --git a/crates/rspack_core/src/compilation/mod.rs b/crates/rspack_core/src/compilation/mod.rs index b39db6bc086a..776f761c09bb 100644 --- a/crates/rspack_core/src/compilation/mod.rs +++ b/crates/rspack_core/src/compilation/mod.rs @@ -46,14 +46,14 @@ use crate::{ ChunkIdsArtifact, ChunkKind, ChunkRenderArtifact, ChunkRenderCacheArtifact, ChunkRenderResult, ChunkUkey, CodeGenerationJob, CodeGenerationResult, CodeGenerationResults, CompilationLogger, CompilationLogging, CompilerOptions, ConcatenationScope, DependenciesDiagnosticsArtifact, - DependencyCodeGeneration, DependencyTemplate, DependencyTemplateType, DependencyType, Entry, - EntryData, EntryOptions, EntryRuntime, Entrypoint, ExecuteModuleId, Filename, ImportPhase, - ImportVarMap, ImportedByDeferModulesArtifact, Logger, MemoryGCStorage, ModuleFactory, - ModuleGraph, ModuleGraphCacheArtifact, ModuleGraphMut, ModuleGraphPartial, ModuleGraphRef, - ModuleIdentifier, ModuleIdsArtifact, ModuleStaticCacheArtifact, PathData, ResolverFactory, - RuntimeGlobals, RuntimeKeyMap, RuntimeMode, RuntimeModule, RuntimeSpec, RuntimeSpecMap, - RuntimeTemplate, SharedPluginDriver, SideEffectsOptimizeArtifact, SourceType, Stats, - ValueCacheVersions, + DependencyCodeGeneration, DependencyId, DependencyTemplate, DependencyTemplateType, + DependencyType, Entry, EntryData, EntryOptions, EntryRuntime, Entrypoint, ExecuteModuleId, + ExtendedReferencedExport, Filename, ImportPhase, ImportVarMap, ImportedByDeferModulesArtifact, + Logger, MemoryGCStorage, ModuleFactory, ModuleGraph, ModuleGraphCacheArtifact, ModuleGraphMut, + ModuleGraphPartial, ModuleGraphRef, ModuleIdentifier, ModuleIdsArtifact, + ModuleStaticCacheArtifact, PathData, ResolverFactory, RuntimeGlobals, RuntimeKeyMap, RuntimeMode, + RuntimeModule, RuntimeSpec, RuntimeSpecMap, RuntimeTemplate, SharedPluginDriver, + SideEffectsOptimizeArtifact, SourceType, Stats, ValueCacheVersions, build_chunk_graph::artifact::BuildChunkGraphArtifact, compilation::build_module_graph::{ BuildModuleGraphArtifact, ModuleExecutor, UpdateParam, build_module_graph, @@ -76,6 +76,12 @@ define_hook!(CompilationExecuteModule: Series(module: &ModuleIdentifier, runtime_modules: &IdentifierSet, code_generation_results: &BindingCell, execute_module_id: &ExecuteModuleId)); define_hook!(CompilationFinishModules: Series(compilation: &mut Compilation)); define_hook!(CompilationSeal: Series(compilation: &mut Compilation)); +define_hook!(CompilationDependencyReferencedExports: Series( + compilation: &Compilation, + dependency: &DependencyId, + referenced_exports: &Option> , + runtime: Option<&RuntimeSpec> +)); define_hook!(CompilationConcatenationScope: SeriesBail(compilation: &Compilation, curr_module: ModuleIdentifier) -> ConcatenationScope); define_hook!(CompilationOptimizeDependencies: SeriesBail(compilation: &mut Compilation) -> bool); define_hook!(CompilationOptimizeModules: SeriesBail(compilation: &mut Compilation) -> bool); @@ -113,6 +119,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 26d53d4941bf..6aee44ed4220 100644 --- a/crates/rspack_core/src/dependency/dependency_type.rs +++ b/crates/rspack_core/src/dependency/dependency_type.rs @@ -95,6 +95,10 @@ pub enum DependencyType { ContainerExposed, /// container entry, ContainerEntry, + /// share container entry + ShareContainerEntry, + /// share container fallback + ShareContainerFallback, /// remote to external, RemoteToExternal, /// fallback @@ -179,6 +183,8 @@ impl DependencyType { DependencyType::ImportMetaContext => "import.meta context", 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 0b4f649d05eb..d06ae800db13 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 7b66d7311427..e222db26422a 100644 --- a/crates/rspack_core/src/lib.rs +++ b/crates/rspack_core/src/lib.rs @@ -123,6 +123,7 @@ pub enum SourceType { Remote, ShareInit, ConsumeShared, + ShareContainerShared, Custom(#[cacheable(with=AsPreset)] Ustr), #[default] Unknown, @@ -142,6 +143,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), @@ -161,6 +163,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()), @@ -176,6 +179,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), } @@ -270,6 +275,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/Cargo.toml b/crates/rspack_plugin_javascript/Cargo.toml index 0f1197865f19..bd0d65911a7c 100644 --- a/crates/rspack_plugin_javascript/Cargo.toml +++ b/crates/rspack_plugin_javascript/Cargo.toml @@ -13,6 +13,7 @@ bitflags = { workspace = true } cow-utils = { workspace = true } either = { workspace = true } fast-glob = { workspace = true } +futures = { workspace = true } indexmap = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } @@ -61,5 +62,5 @@ winnow = { workspace = true } ignored = ["tracing"] [lints.rust.unexpected_cfgs] -level = "warn" check-cfg = ['cfg(allocative)'] +level = "warn" diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs index da1012f92600..88d553f052ae 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs @@ -107,6 +107,45 @@ impl ESMImportSpecifierDependency { .unwrap_or_else(|| self.ids.as_slice()) } + pub fn get_esm_import_specifier_referenced_exports( + &self, + module_graph: &ModuleGraph, + exports_type: Option, + ) -> Vec { + let mut ids = self.get_ids(module_graph); + if ids.is_empty() { + return self.get_referenced_exports_in_destructuring(None); + } + + let mut namespace_object_as_context = self.namespace_object_as_context; + if let Some(id) = ids.first() + && id == "default" + { + match exports_type { + Some(ExportsType::DefaultOnly) | Some(ExportsType::DefaultWithNamed) => { + if ids.len() == 1 { + return self.get_referenced_exports_in_destructuring(None); + } + ids = &ids[1..]; + namespace_object_as_context = true; + } + Some(ExportsType::Dynamic) => { + return create_exports_object_referenced(); + } + _ => {} + } + } + + if self.call && !self.direct_import && (namespace_object_as_context || ids.len() > 1) { + if ids.len() == 1 { + return create_exports_object_referenced(); + } + // remove last one + ids = &ids[..ids.len() - 1]; + } + self.get_referenced_exports_in_destructuring(Some(ids)) + } + pub fn get_referenced_exports_in_destructuring( &self, ids: Option<&[Atom]>, @@ -236,44 +275,28 @@ impl Dependency for ESMImportSpecifierDependency { module_graph_cache: &ModuleGraphCacheArtifact, _runtime: Option<&RuntimeSpec>, ) -> Vec { - let mut ids = self.get_ids(module_graph); - // namespace import + let ids = self.get_ids(module_graph); if ids.is_empty() { return self.get_referenced_exports_in_destructuring(None); } - let mut namespace_object_as_context = self.namespace_object_as_context; - if let Some(id) = ids.first() + let exports_type = if let Some(id) = ids.first() && id == "default" { let parent_module = module_graph .get_parent_module(&self.id) .expect("should have parent module"); - let exports_type = - get_exports_type(module_graph, module_graph_cache, &self.id, parent_module); - match exports_type { - ExportsType::DefaultOnly | ExportsType::DefaultWithNamed => { - if ids.len() == 1 { - return self.get_referenced_exports_in_destructuring(None); - } - ids = &ids[1..]; - namespace_object_as_context = true; - } - ExportsType::Dynamic => { - return create_exports_object_referenced(); - } - _ => {} - } - } + Some(get_exports_type( + module_graph, + module_graph_cache, + &self.id, + parent_module, + )) + } else { + None + }; - if self.call && !self.direct_import && (namespace_object_as_context || ids.len() > 1) { - if ids.len() == 1 { - return create_exports_object_referenced(); - } - // remove last one - ids = &ids[..ids.len() - 1]; - } - self.get_referenced_exports_in_destructuring(Some(ids)) + self.get_esm_import_specifier_referenced_exports(module_graph, exports_type) } fn could_affect_referencing_module(&self) -> rspack_core::AffectType { 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 284d555d9a47..894b2ca930f0 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 @@ -45,7 +45,7 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { } } - fn apply(&mut self) { + async fn apply(&mut self) { let mut module_graph = self.compilation.get_module_graph_mut(); module_graph.active_all_exports_info(); module_graph.reset_all_exports_info_used(); @@ -95,20 +95,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(); @@ -235,7 +234,7 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { } } - fn process_module( + async fn process_module( &self, block_id: ModuleOrAsyncDependenciesBlock, runtime: Option<&RuntimeSpec>, @@ -259,19 +258,39 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { for (dep_id, module_id) in dependencies.into_iter() { 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.compilation.get_module_graph(), &self.compilation.module_graph_cache_artifact, runtime, - ) else { - continue; - }; + ); - if let Some(new_referenced_exports) = - merge_referenced_exports(old_referenced_exports, referenced_exports) - { - map.insert(module_id, new_referenced_exports); + // 直接使用 await 调用异步钩子 + self + .compilation + .plugin_driver + .compilation_hooks + .dependency_referenced_exports + .call( + &*self.compilation, + &dep_id, + &referenced_exports_result, + runtime, + ) + .await; + + match referenced_exports_result { + Some(mut referenced_exports) => { + if let Some(new_referenced_exports) = + merge_referenced_exports(old_referenced_exports, referenced_exports) + { + map.insert(module_id, new_referenced_exports); + } + } + None => { + continue; + } } } @@ -490,7 +509,7 @@ async fn optimize_dependencies(&self, compilation: &mut Compilation) -> Result, @@ -16,7 +16,7 @@ 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, @@ -24,7 +24,7 @@ pub struct StatsBuildInfo { pub build_name: Option, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct StatsExpose { pub path: String, pub id: String, @@ -35,7 +35,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, @@ -48,9 +48,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, @@ -62,7 +64,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, @@ -76,7 +78,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, @@ -86,7 +88,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 17cb95e13ecd..3214cc0606fd 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, @@ -84,7 +84,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 @@ -169,6 +169,7 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { singleton: shared.singleton, assets: StatsAssetsGroup::default(), usedIn: Vec::new(), + usedExports: Vec::new(), }) .collect::>(); let remote_list = self diff --git a/crates/rspack_plugin_mf/src/manifest/utils.rs b/crates/rspack_plugin_mf/src/manifest/utils.rs index ec2e5d2c219e..ec7ab97c95ba 100644 --- a/crates/rspack_plugin_mf/src/manifest/utils.rs +++ b/crates/rspack_plugin_mf/src/manifest/utils.rs @@ -46,6 +46,7 @@ pub fn ensure_shared_entry<'a>( singleton: None, 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..cad6092f4bba --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/collect_shared_entry_plugin.rs @@ -0,0 +1,233 @@ +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 { + if let Some(caps) = re.captures(request) { + if 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() { + if let Ok(content) = std::fs::read_to_string(&package_json_path) { + if let Ok(json) = serde_json::from_str::(&content) { + if 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().into_iter() { + 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..]; + let at = rest.find('@').unwrap_or(rest.len()); + key = rest[..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) { + if let Some(name) = target.name_for_condition() { + let resource: String = name.into(); + let version = self + .infer_version(&resource) + .await + .unwrap_or_else(|| "".to_string()); + dbg!(&version, &resource); + 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(|s| s.as_str()) + .unwrap_or(""); + 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 + .as_ref() + .map(|f| f.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_plugin.rs b/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs index f95c99e4370a..ae62aa99d916 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs @@ -54,21 +54,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 +104,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 +137,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 { @@ -404,12 +404,15 @@ async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> 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/mod.rs b/crates/rspack_plugin_mf/src/sharing/mod.rs index a2f9e246e08b..38747ea9fada 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; @@ -7,5 +8,13 @@ pub mod provide_shared_dependency; pub mod provide_shared_module; pub mod provide_shared_module_factory; pub mod provide_shared_plugin; +pub mod share_container_dependency; +pub mod share_container_entry_dependency; +pub mod share_container_entry_module; +pub mod share_container_entry_module_factory; +pub mod share_container_plugin; +pub mod share_container_runtime_module; pub mod share_runtime_module; pub mod share_runtime_plugin; +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_module.rs b/crates/rspack_plugin_mf/src/sharing/provide_shared_module.rs index 888afd1f2301..7a0110f31b42 100644 --- a/crates/rspack_plugin_mf/src/sharing/provide_shared_module.rs +++ b/crates/rspack_plugin_mf/src/sharing/provide_shared_module.rs @@ -84,6 +84,10 @@ impl ProvideSharedModule { source_map_kind: SourceMapKind::empty(), } } + + pub fn share_key(&self) -> &str { + &self.name + } } impl Identifiable for ProvideSharedModule { diff --git a/crates/rspack_plugin_mf/src/sharing/share_container_dependency.rs b/crates/rspack_plugin_mf/src/sharing/share_container_dependency.rs new file mode 100644 index 000000000000..bd52ccdee416 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/share_container_dependency.rs @@ -0,0 +1,71 @@ +use rspack_cacheable::{cacheable, cacheable_dyn}; +use rspack_core::{ + AsContextDependency, AsDependencyCodeGeneration, Dependency, DependencyCategory, DependencyId, + DependencyType, FactorizeInfo, ModuleDependency, +}; + +#[cacheable] +#[derive(Debug, Clone)] +pub struct ShareContainerDependency { + id: DependencyId, + request: String, + resource_identifier: String, + factorize_info: FactorizeInfo, +} + +impl ShareContainerDependency { + pub fn new(request: String) -> Self { + let resource_identifier = format!("share-container-fallback:{}", request); + Self { + id: DependencyId::new(), + request, + resource_identifier, + factorize_info: Default::default(), + } + } +} + +#[cacheable_dyn] +impl Dependency for ShareContainerDependency { + fn id(&self) -> &DependencyId { + &self.id + } + + fn category(&self) -> &DependencyCategory { + &DependencyCategory::Esm + } + + fn dependency_type(&self) -> &DependencyType { + &DependencyType::ShareContainerFallback + } + + fn resource_identifier(&self) -> Option<&str> { + Some(&self.resource_identifier) + } + + fn could_affect_referencing_module(&self) -> rspack_core::AffectType { + rspack_core::AffectType::True + } +} + +#[cacheable_dyn] +impl ModuleDependency for ShareContainerDependency { + fn request(&self) -> &str { + &self.request + } + + fn user_request(&self) -> &str { + &self.request + } + + fn factorize_info(&self) -> &FactorizeInfo { + &self.factorize_info + } + + fn factorize_info_mut(&mut self) -> &mut FactorizeInfo { + &mut self.factorize_info + } +} + +impl AsContextDependency for ShareContainerDependency {} +impl AsDependencyCodeGeneration for ShareContainerDependency {} diff --git a/crates/rspack_plugin_mf/src/sharing/share_container_entry_dependency.rs b/crates/rspack_plugin_mf/src/sharing/share_container_entry_dependency.rs new file mode 100644 index 000000000000..d14f778a8805 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/share_container_entry_dependency.rs @@ -0,0 +1,78 @@ +use rspack_cacheable::{cacheable, cacheable_dyn}; +use rspack_core::{ + AsContextDependency, AsDependencyCodeGeneration, Dependency, DependencyCategory, DependencyId, + DependencyType, FactorizeInfo, ModuleDependency, +}; +use serde::Serialize; + +#[cacheable] +#[derive(Debug, Clone)] +pub struct ShareContainerEntryDependency { + id: DependencyId, + pub name: String, + pub request: String, + pub version: String, + resource_identifier: String, + factorize_info: FactorizeInfo, +} + +#[cacheable] +#[derive(Debug, Clone, Serialize)] +pub struct ShareContainerEntryOptions { + pub request: String, +} + +impl ShareContainerEntryDependency { + pub fn new(name: String, request: String, version: String) -> Self { + let resource_identifier = format!("share-container-entry-{}", &name); + Self { + id: DependencyId::new(), + name, + request, + version, + resource_identifier, + factorize_info: Default::default(), + } + } +} + +#[cacheable_dyn] +impl Dependency for ShareContainerEntryDependency { + fn id(&self) -> &DependencyId { + &self.id + } + + fn category(&self) -> &DependencyCategory { + &DependencyCategory::Esm + } + + fn dependency_type(&self) -> &DependencyType { + &DependencyType::ShareContainerEntry + } + + fn resource_identifier(&self) -> Option<&str> { + Some(&self.resource_identifier) + } + + fn could_affect_referencing_module(&self) -> rspack_core::AffectType { + rspack_core::AffectType::Transitive + } +} + +#[cacheable_dyn] +impl ModuleDependency for ShareContainerEntryDependency { + fn request(&self) -> &str { + &self.resource_identifier + } + + fn factorize_info(&self) -> &FactorizeInfo { + &self.factorize_info + } + + fn factorize_info_mut(&mut self) -> &mut FactorizeInfo { + &mut self.factorize_info + } +} + +impl AsContextDependency for ShareContainerEntryDependency {} +impl AsDependencyCodeGeneration for ShareContainerEntryDependency {} diff --git a/crates/rspack_plugin_mf/src/sharing/share_container_entry_module.rs b/crates/rspack_plugin_mf/src/sharing/share_container_entry_module.rs new file mode 100644 index 000000000000..82a5a852e666 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/share_container_entry_module.rs @@ -0,0 +1,268 @@ +use std::borrow::Cow; + +use async_trait::async_trait; +use rspack_cacheable::{cacheable, cacheable_dyn}; +use rspack_collections::{Identifiable, Identifier}; +use rspack_core::{ + AsyncDependenciesBlock, AsyncDependenciesBlockIdentifier, BoxDependency, BuildContext, BuildInfo, + BuildMeta, BuildMetaExportsType, BuildResult, CodeGenerationResult, Compilation, + ConcatenationScope, Context, DependenciesBlock, DependencyId, FactoryMeta, LibIdentOptions, + Module, ModuleDependency, ModuleGraph, ModuleIdentifier, ModuleType, RuntimeGlobals, RuntimeSpec, + SourceType, StaticExportsDependency, StaticExportsSpec, impl_module_meta_info, + module_update_hash, + rspack_sources::{BoxSource, RawStringSource, SourceExt}, +}; +use rspack_error::{Result, impl_empty_diagnosable_trait}; +use rspack_hash::{RspackHash, RspackHashDigest}; +use rspack_util::source_map::{ModuleSourceMapConfig, SourceMapKind}; +use rustc_hash::FxHashSet; + +use super::share_container_dependency::ShareContainerDependency; + +#[cacheable] +#[derive(Debug)] +pub struct ShareContainerEntryModule { + blocks: Vec, + dependencies: Vec, + identifier: ModuleIdentifier, + lib_ident: String, + name: String, + request: String, + version: String, + factory_meta: Option, + build_info: BuildInfo, + build_meta: BuildMeta, + source_map_kind: SourceMapKind, +} + +impl ShareContainerEntryModule { + pub fn new(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, + name, + request, + version, + 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() + }, + source_map_kind: SourceMapKind::empty(), + } + } +} + +impl Identifiable for ShareContainerEntryModule { + fn identifier(&self) -> Identifier { + self.identifier + } +} + +impl DependenciesBlock for ShareContainerEntryModule { + fn add_block_id(&mut self, block: AsyncDependenciesBlockIdentifier) { + self.blocks.push(block) + } + + fn get_blocks(&self) -> &[AsyncDependenciesBlockIdentifier] { + &self.blocks + } + + fn add_dependency_id(&mut self, dependency: DependencyId) { + self.dependencies.push(dependency) + } + + fn remove_dependency_id(&mut self, dependency: DependencyId) { + self.dependencies.retain(|d| d != &dependency) + } + + fn get_dependencies(&self) -> &[DependencyId] { + &self.dependencies + } +} + +#[cacheable_dyn] +#[async_trait] +impl Module for ShareContainerEntryModule { + impl_module_meta_info!(); + + fn size(&self, _source_type: Option<&SourceType>, _compilation: Option<&Compilation>) -> f64 { + 42.0 + } + + fn module_type(&self) -> &ModuleType { + &ModuleType::ShareContainerShared + } + + fn source_types(&self, _module_graph: &ModuleGraph) -> &[SourceType] { + &[SourceType::JavaScript, SourceType::Expose] + } + + fn source(&self) -> Option<&BoxSource> { + None + } + + fn readable_identifier(&self, _context: &Context) -> Cow<'_, str> { + "share container entry".into() + } + + fn lib_ident(&self, _options: LibIdentOptions) -> Option> { + Some(self.lib_ident.as_str().into()) + } + + async fn build( + &mut self, + _build_context: BuildContext, + _: Option<&Compilation>, + ) -> Result { + let mut dependencies: Vec = Vec::new(); + + dependencies.push(Box::new(StaticExportsDependency::new( + StaticExportsSpec::Array(vec!["get".into(), "init".into()]), + false, + ))); + dependencies.push(Box::new(ShareContainerDependency::new(self.name.clone()))); + + Ok(BuildResult { + dependencies, + blocks: Vec::>::new(), + ..Default::default() + }) + } + + async fn code_generation( + &self, + compilation: &Compilation, + _runtime: Option<&RuntimeSpec>, + _: Option, + ) -> Result { + let mut code_generation_result = CodeGenerationResult::default(); + code_generation_result + .runtime_requirements + .insert(RuntimeGlobals::DEFINE_PROPERTY_GETTERS); + code_generation_result + .runtime_requirements + .insert(RuntimeGlobals::EXPORTS); + code_generation_result + .runtime_requirements + .insert(RuntimeGlobals::REQUIRE); + + 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) + .expect("share container dependency should exist"); + if let Some(dependency) = dependency.downcast_ref::() { + let module_expr = compilation.runtime_template.module_raw( + compilation, + &mut code_generation_result.runtime_requirements, + dependency_id, + dependency.user_request(), + false, + ); + factory = compilation + .runtime_template + .returning_function(&module_expr, ""); + } + } + + let federation_global = format!( + "{}.federation", + compilation + .runtime_template + .render_runtime_globals(&RuntimeGlobals::REQUIRE) + ); + + // Generate installInitialConsumes function using returning_function + let install_initial_consumes_call = format!( + 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 = compilation + .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(); + "#, + federation_global = federation_global, + install_initial_consumes_fn = install_initial_consumes_fn + ); + let init_share_container_fn = compilation + .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 = compilation + .runtime_template + .render_runtime_globals(&RuntimeGlobals::DEFINE_PROPERTY_GETTERS), + 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()); + Ok(code_generation_result) + } + + async fn get_runtime_hash( + &self, + compilation: &Compilation, + runtime: Option<&RuntimeSpec>, + ) -> Result { + let mut hasher = RspackHash::from(&compilation.options.output); + module_update_hash(self, &mut hasher, compilation, runtime); + Ok(hasher.digest(&compilation.options.output.hash_digest)) + } +} + +impl_empty_diagnosable_trait!(ShareContainerEntryModule); + +impl ModuleSourceMapConfig for ShareContainerEntryModule { + fn get_source_map_kind(&self) -> &SourceMapKind { + &self.source_map_kind + } + + fn set_source_map_kind(&mut self, source_map: SourceMapKind) { + self.source_map_kind = source_map; + } +} diff --git a/crates/rspack_plugin_mf/src/sharing/share_container_entry_module_factory.rs b/crates/rspack_plugin_mf/src/sharing/share_container_entry_module_factory.rs new file mode 100644 index 000000000000..d9939660f2aa --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/share_container_entry_module_factory.rs @@ -0,0 +1,26 @@ +use async_trait::async_trait; +use rspack_core::{ModuleExt, ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult}; +use rspack_error::Result; + +use super::{ + share_container_entry_dependency::ShareContainerEntryDependency, + share_container_entry_module::ShareContainerEntryModule, +}; + +#[derive(Debug)] +pub struct ShareContainerEntryModuleFactory; + +#[async_trait] +impl ModuleFactory for ShareContainerEntryModuleFactory { + async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result { + let dep = data.dependencies[0] + .downcast_ref::() + .expect( + "dependency of ShareContainerEntryModuleFactory should be ShareContainerEntryDependency", + ); + Ok(ModuleFactoryResult::new_with_module( + ShareContainerEntryModule::new(dep.name.clone(), dep.request.clone(), dep.version.clone()) + .boxed(), + )) + } +} diff --git a/crates/rspack_plugin_mf/src/sharing/share_container_plugin.rs b/crates/rspack_plugin_mf/src/sharing/share_container_plugin.rs new file mode 100644 index 000000000000..7868172a9bf1 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/share_container_plugin.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use rspack_core::{ + ChunkUkey, Compilation, CompilationAdditionalTreeRuntimeRequirements, CompilationParams, + CompilerCompilation, CompilerMake, DependencyType, Filename, LibraryOptions, Plugin, + RuntimeGlobals, RuntimeModuleExt, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; + +use super::{ + share_container_entry_dependency::ShareContainerEntryDependency, + share_container_entry_module_factory::ShareContainerEntryModuleFactory, +}; +use crate::sharing::share_container_runtime_module::ShareContainerRuntimeModule; + +#[derive(Debug)] +pub struct ShareContainerPluginOptions { + pub name: String, + pub request: String, + pub version: String, + pub file_name: Option, + pub library: LibraryOptions, +} + +#[plugin] +#[derive(Debug)] +pub struct ShareContainerPlugin { + options: ShareContainerPluginOptions, +} + +impl ShareContainerPlugin { + pub fn new(options: ShareContainerPluginOptions) -> Self { + Self::new_inner(options) + } +} + +#[plugin_hook(CompilerCompilation for ShareContainerPlugin)] +async fn compilation( + &self, + compilation: &mut Compilation, + params: &mut CompilationParams, +) -> Result<()> { + compilation.set_dependency_factory( + DependencyType::ShareContainerEntry, + Arc::new(ShareContainerEntryModuleFactory), + ); + compilation.set_dependency_factory( + DependencyType::ShareContainerFallback, + params.normal_module_factory.clone(), + ); + Ok(()) +} + +#[plugin_hook(CompilerMake for ShareContainerPlugin)] +async fn make(&self, compilation: &mut Compilation) -> Result<()> { + let dep = ShareContainerEntryDependency::new( + 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 ShareContainerPlugin)] +async fn additional_tree_runtime_requirements( + &self, + compilation: &mut Compilation, + chunk_ukey: &ChunkUkey, + _runtime_requirements: &mut RuntimeGlobals, +) -> Result<()> { + let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey); + if let Some(name) = chunk.name() + && name == self.options.name + { + compilation.add_runtime_module(chunk_ukey, ShareContainerRuntimeModule::new().boxed())?; + } + Ok(()) +} + +impl Plugin for ShareContainerPlugin { + fn name(&self) -> &'static str { + "rspack.ShareContainerPlugin" + } + + 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/share_container_runtime_module.rs b/crates/rspack_plugin_mf/src/sharing/share_container_runtime_module.rs new file mode 100644 index 000000000000..09c88b12441d --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/share_container_runtime_module.rs @@ -0,0 +1,40 @@ +use rspack_collections::Identifier; +use rspack_core::{ChunkUkey, Compilation, RuntimeModule, RuntimeModuleStage, impl_runtime_module}; + +#[impl_runtime_module] +#[derive(Debug)] +pub struct ShareContainerRuntimeModule { + id: Identifier, + chunk: Option, +} + +impl ShareContainerRuntimeModule { + pub fn new() -> Self { + Self::with_default( + Identifier::from("webpack/runtime/share_container_federation"), + None, + ) + } +} + +#[async_trait::async_trait] +impl RuntimeModule for ShareContainerRuntimeModule { + fn name(&self) -> Identifier { + self.id + } + + async fn generate(&self, _compilation: &Compilation) -> rspack_error::Result { + Ok( + "__webpack_require__.federation = { instance: undefined,bundlerRuntime: undefined };" + .to_string(), + ) + } + + fn attach(&mut self, chunk: ChunkUkey) { + self.chunk = Some(chunk); + } + + 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..f9d7f4a4e55d --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_plugin.rs @@ -0,0 +1,561 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::{Arc, RwLock}, +}; + +use rspack_collections::Identifiable; +use rspack_core::{ + AsyncDependenciesBlockIdentifier, ChunkUkey, Compilation, + CompilationAdditionalTreeRuntimeRequirements, CompilationDependencyReferencedExports, + CompilationOptimizeDependencies, CompilationProcessAssets, DependenciesBlock, DependencyId, + DependencyType, ExportsType, ExtendedReferencedExport, Module, ModuleGraph, ModuleIdentifier, + NormalModule, Plugin, RuntimeGlobals, RuntimeModuleExt, RuntimeSpec, + rspack_sources::{RawStringSource, SourceExt, SourceValue}, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; +use rspack_plugin_javascript::dependency::ESMImportSpecifierDependency; +use rspack_util::atom::Atom; +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{ + consume_shared_module::ConsumeSharedModule, provide_shared_module::ProvideSharedModule, + share_container_entry_module::ShareContainerEntryModule, + shared_used_exports_optimizer_runtime_module::SharedUsedExportsOptimizerRuntimeModule, +}; +use crate::manifest::StatsRoot; +#[derive(Debug, Clone)] +pub struct OptimizeSharedConfig { + pub share_key: String, + pub treeshake: bool, + pub used_exports: Vec, +} + +#[derive(Debug, Clone)] +pub struct SharedUsedExportsOptimizerPluginOptions { + pub shared: Vec, + pub inject_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_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_used_exports = options.inject_used_exports.clone(); + for config in options.shared.into_iter().filter(|c| c.treeshake) { + 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::< + String, + FxHashMap>, + >::default())); + + Self::new_inner( + shared_map, + shared_referenced_exports, + inject_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 { + if let Some(runtime_map) = shared_referenced_exports.get_mut(share_key) { + for export_set in runtime_map.values_mut() { + 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: &mut Compilation) -> Result> { + let module_ids: Vec<_> = { + let module_graph = compilation.get_module_graph(); + module_graph + .modules() + .into_iter() + .map(|(id, _)| id) + .collect() + }; + self.apply_custom_exports(); + + for module_id in module_ids { + let (share_key, modules_to_process) = match { + let module_graph = compilation.get_module_graph(); + 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 = 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::()?; + // Use the identifier to extract the share key + // The identifier is in format "share container entry {name}@{version}" + let identifier = share_container_entry_module.identifier().to_string(); + let parts: Vec<&str> = identifier.split(' ').collect(); + if parts.len() < 3 { + return None; + } + let name_part = parts[3]; + let name_end = name_part.find('@').unwrap_or(name_part.len()); + let sk = name_part[..name_end].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)) + }) + } { + 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 treeshake 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 = { + let module_graph = compilation.get_module_graph(); + module_graph.modules().into_iter().find_map(|(id, module)| { + module + .as_any() + .downcast_ref::() + .filter(|normal| normal.raw_request() == share_key) + .map(|_| id) + }) + }; + + // Check if the real shared module is side effect free + if let Some(real_shared_identifier) = real_shared_identifier { + let is_side_effect_free = { + let module_graph = compilation.get_module_graph(); + 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 { + continue; + } + + let mut module_graph_mut = compilation.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(&mut module_graph_mut); + + for (_runtime, exports) in runtime_reference_exports.iter() { + for export_name in exports { + let export_atom = Atom::from(export_name.as_str()); + if let Some(export_info) = exports_info_data.named_exports_mut(&export_atom) { + // let runtime_spec = RuntimeSpec::from_iter([runtime.clone().into()]); + // let status = + // export_info.set_used(rspack_core::UsageState::Used, Some(&runtime_spec)); + // set used by runtime when set_owned_used_in_unknown_way remove + 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(&mut module_graph_mut); + let can_update_module_used_stage = { + let exports_view = exports_info_data.exports(); + runtime_reference_exports.iter().all(|(runtime_name, _)| { + let runtime_spec = RuntimeSpec::from_iter([runtime_name.clone().into()]); + exports_view.iter().all(|(name, export_info)| { + let used = export_info.get_used(Some(&runtime_spec)); + // if unknown module is all we mark, means we have known all module exports info , and can shake + if used != rspack_core::UsageState::Unknown { + runtime_reference_exports + .values() + .any(|exports| exports.contains(&name.to_string())) + } else { + true + } + }) + }) + }; + if can_update_module_used_stage { + // mark used exports for dependencies and blocks dependencies + // create a helper closure to update dependency's export info + // set used by runtime when set_owned_used_in_unknown_way remove + 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)); + } + exports_info_data + .other_exports_info_mut() + .set_used_conditionally( + Box::new(|used| *used == rspack_core::UsageState::Unknown), + rspack_core::UsageState::Unused, + None, + ); + } + } + } + } + + 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 { + if let Some(file) = compilation.assets().get(file_name) { + if let Some(source) = file.get_source() { + if let SourceValue::String(content) = source.source() { + if 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(runtime_map) = shared_referenced_exports.get(&shared.name) { + let mut used_exports_set = HashSet::new(); + + for (_runtime, exports) in runtime_map { + if !exports.is_empty() { + for export in exports { + used_exports_set.insert(export.clone()); + } + } + } + + shared.usedExports = used_exports_set.into_iter().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: &mut Compilation, + chunk_ukey: &ChunkUkey, + runtime_requirements: &mut RuntimeGlobals, +) -> Result<()> { + if self.shared_map.is_empty() { + return Ok(()); + } + + runtime_requirements.insert(RuntimeGlobals::RUNTIME_ID); + compilation.add_runtime_module( + chunk_ukey, + SharedUsedExportsOptimizerRuntimeModule::new( + self + .shared_referenced_exports + .read() + .expect("lock poisoned") + .iter() + .map(|(share_key, runtime_map)| { + ( + share_key.clone(), + runtime_map + .iter() + .map(|(runtime, exports)| { + (runtime.clone(), exports.iter().cloned().collect::>()) + }) + .collect::>(), + ) + }) + .collect::>() + .into(), + ) + .boxed(), + )?; + + Ok(()) +} + +#[plugin_hook(CompilationDependencyReferencedExports for SharedUsedExportsOptimizerPlugin)] +async fn dependency_referenced_exports( + &self, + compilation: &Compilation, + dependency_id: &DependencyId, + referenced_exports: &Option>, + runtime: Option<&RuntimeSpec>, +) -> Result<()> { + let module_graph = compilation.get_module_graph(); + + if referenced_exports.is_none() { + return Ok(()); + } + let Some(exports) = referenced_exports else { + return Ok(()); + }; + + let Some(dependency) = module_graph.dependency_by_id(dependency_id) else { + return Ok(()); + }; + + let Some(module_dependency) = dependency.as_module_dependency() else { + return Ok(()); + }; + + let share_key = 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 final_exports.is_empty() { + if dependency.dependency_type() == &DependencyType::EsmImportSpecifier { + if let Some(esm_dep) = dependency + .as_any() + .downcast_ref::() + { + let ids = esm_dep.get_ids(&module_graph); + if ids.is_empty() { + return Ok(()); + } + final_exports = esm_dep.get_esm_import_specifier_referenced_exports( + &module_graph, + Some(ExportsType::DefaultWithNamed), + ); + } + } + } + if final_exports.is_empty() { + return Ok(()); + } + + // Process each referenced export + if let Some(runtime) = runtime { + if self.shared_map.contains_key(share_key) { + let mut shared_referenced_exports = self + .shared_referenced_exports + .write() + .expect("lock poisoned"); + let runtime_map: &mut HashMap< + String, + std::collections::HashSet, + rustc_hash::FxBuildHasher, + > = shared_referenced_exports + .entry(share_key.to_string()) + .or_insert_with(|| FxHashMap::default()); + + let export_set = runtime_map + .entry(runtime.as_str().to_string()) + .or_insert_with(|| FxHashSet::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; + } + let flattened = referenced + .name + .iter() + .map(|atom| atom.to_string()) + .collect::>() + .join("."); + export_set.insert(flattened); + } + } + } + } + } + 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_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..11540696a2b7 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/shared_used_exports_optimizer_runtime_module.rs @@ -0,0 +1,66 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use async_trait::async_trait; +use rspack_collections::Identifier; +use rspack_core::{ + ChunkUkey, Compilation, RuntimeGlobals, RuntimeModule, RuntimeModuleStage, impl_runtime_module, +}; +use rspack_error::{Result, error}; + +#[impl_runtime_module] +#[derive(Debug)] +pub struct SharedUsedExportsOptimizerRuntimeModule { + id: Identifier, + chunk: Option, + shared_used_exports: Arc>>>, +} + +impl SharedUsedExportsOptimizerRuntimeModule { + pub fn new(shared_used_exports: Arc>>>) -> Self { + Self::with_default( + Identifier::from("module_federation/shared_used_exports"), + None, + shared_used_exports, + ) + } +} + +#[async_trait] +impl RuntimeModule for SharedUsedExportsOptimizerRuntimeModule { + fn name(&self) -> Identifier { + self.id + } + + fn attach(&mut self, chunk: ChunkUkey) { + self.chunk = Some(chunk); + } + + 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) + ); + let used_exports_json = serde_json::to_string(&*self.shared_used_exports).map_err(|err| { + error!( + "OptimizeDependencyReferencedExportsRuntimeModule: failed to serialize used exports: {err}" + ) + })?; + Ok(format!( + r#" +if(!{federation_global}){{return;}} +{federation_global}.usedExports = {used_exports_json}; +"#, + federation_global = federation_global, + used_exports_json = used_exports_json + )) + } +} diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index f1f8543281d0..af2d39761125 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -3412,6 +3412,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) @@ -4819,16 +4831,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 { @@ -4842,7 +4845,13 @@ export interface ModuleFederationPluginOptions extends Omit; + independentShareDir?: string; + // (undocumented) + independentShareFilePath?: string; + // (undocumented) + injectUsedExports?: boolean; + // (undocumented) + manifest?: ModuleFederationManifestPluginOptions; // (undocumented) runtimePlugins?: RuntimePlugins; // (undocumented) @@ -5235,6 +5244,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; @@ -6627,6 +6639,7 @@ declare namespace rspackExports { SharedItem, SharedObject, SharePluginOptions, + TreeshakeSharedPluginOptions, sharing, LightningcssFeatureOptions, LightningcssLoaderOptions, @@ -7347,6 +7360,9 @@ export type SharedConfig = { singleton?: boolean; strictVersion?: boolean; version?: false | string; + treeshake?: boolean; + usedExports?: string[]; + independentShareFileName?: string; }; // @public (undocumented) @@ -7375,6 +7391,9 @@ type SharedOptimizationSplitChunksCacheGroup = { automaticNameDelimiter?: string; }; +// @public (undocumented) +type ShareFallback = Record; + // @public (undocumented) class SharePlugin { constructor(options: SharePluginOptions); @@ -7408,6 +7427,8 @@ class SharePlugin { }; }[]; // (undocumented) + _sharedOptions: NormalizedSharedOptions; + // (undocumented) _shareScope: string | undefined; } @@ -7421,6 +7442,7 @@ export type SharePluginOptions = { // @public (undocumented) export const sharing: { ProvideSharedPlugin: typeof ProvideSharedPlugin; + TreeShakeSharedPlugin: typeof TreeShakeSharedPlugin; ConsumeSharedPlugin: typeof ConsumeSharedPlugin; SharePlugin: typeof SharePlugin; }; @@ -8404,6 +8426,35 @@ interface TransformConfig { // @public (undocumented) function transformSync(source: string, options?: Options): TransformOutput; +// @public (undocumented) +class TreeShakeSharedPlugin { + constructor(options: TreeshakeSharedPluginOptions); + // (undocumented) + apply(compiler: Compiler): void; + // (undocumented) + get buildAssets(): ShareFallback; + // (undocumented) + mfConfig: ModuleFederationPluginOptions; + // (undocumented) + name: string; + // (undocumented) + outputDir: string; + // (undocumented) + plugins?: Plugins; + // (undocumented) + reshake?: boolean; +} + +// @public (undocumented) +export interface TreeshakeSharedPluginOptions { + // (undocumented) + mfConfig: ModuleFederationPluginOptions; + // (undocumented) + plugins?: Plugins; + // (undocumented) + reshake?: boolean; +} + // @public (undocumented) type TruePlusMinus = true | "+" | "-"; diff --git a/packages/rspack/src/container/ContainerPlugin.ts b/packages/rspack/src/container/ContainerPlugin.ts index aecdf9b78d9b..b8540fe07b88 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 c26bc37909bb..601bf7f5eca2 100644 --- a/packages/rspack/src/container/ModuleFederationManifestPlugin.ts +++ b/packages/rspack/src/container/ModuleFederationManifestPlugin.ts @@ -10,6 +10,13 @@ import { RspackBuiltinPlugin } from "../builtin-plugin/base"; import type { Compiler } from "../Compiler"; +import 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"; @@ -88,7 +95,7 @@ export type ManifestSharedOption = { singleton?: boolean; }; -export type ModuleFederationManifestPluginOptions = { +type InternalManifestPluginOptions = { name?: string; globalName?: string; filePath?: string; @@ -99,11 +106,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,16 +158,140 @@ 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 opts: InternalManifestPluginOptions; + constructor(opts: ModuleFederationPluginOptions) { super(); - this.opts = opts; + this.opts = normalizeManifestOptions(opts); } raw(compiler: Compiler): BuiltinPlugin { diff --git a/packages/rspack/src/container/ModuleFederationPlugin.ts b/packages/rspack/src/container/ModuleFederationPlugin.ts index 505e56bd5b58..f73ef27372ab 100644 --- a/packages/rspack/src/container/ModuleFederationPlugin.ts +++ b/packages/rspack/src/container/ModuleFederationPlugin.ts @@ -1,13 +1,12 @@ import type { Compiler } from "../Compiler"; import type { ExternalsType } from "../config"; +import type { ShareFallback } from "../sharing/IndependentSharedPlugin"; import type { SharedConfig } from "../sharing/SharePlugin"; +import { TreeShakeSharedPlugin } from "../sharing/TreeShakeSharedPlugin"; import { isRequiredVersion } from "../sharing/utils"; import { - type ManifestExposeOption, - type ManifestSharedOption, ModuleFederationManifestPlugin, - type ModuleFederationManifestPluginOptions, - type RemoteAliasMap + type ModuleFederationManifestPluginOptions } from "./ModuleFederationManifestPlugin"; import type { ModuleFederationPluginV1Options } from "./ModuleFederationPluginV1"; import { ModuleFederationRuntimePlugin } from "./ModuleFederationRuntimePlugin"; @@ -20,16 +19,16 @@ export interface ModuleFederationPluginOptions runtimePlugins?: RuntimePlugins; implementation?: string; shareStrategy?: "version-first" | "loaded-first"; - manifest?: - | boolean - | Omit< - ModuleFederationManifestPluginOptions, - "remoteAliasMap" | "globalName" | "name" | "exposes" | "shared" - >; + injectUsedExports?: boolean; + independentShareDir?: string; + independentShareFilePath?: string; + manifest?: ModuleFederationManifestPluginOptions; } export type RuntimePlugins = string[] | [string, Record][]; export class ModuleFederationPlugin { + private _treeShakeSharedPlugin?: TreeShakeSharedPlugin; + constructor(private _options: ModuleFederationPluginOptions) {} apply(compiler: Compiler) { @@ -41,13 +40,38 @@ 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 treeshakeEntries = sharedOptions.filter( + ([, config]) => config.treeshake + ); + if (treeshakeEntries.length > 0) { + this._treeShakeSharedPlugin = new TreeShakeSharedPlugin({ + mfConfig: this._options, + reshake: false + }); + this._treeShakeSharedPlugin.apply(compiler); + } - // Pass only the entry runtime to the Rust-side plugin - new ModuleFederationRuntimePlugin({ - entryRuntime - }).apply(compiler); + let runtimePluginApplied = false; + compiler.hooks.beforeRun.tapPromise( + { + name: "ModuleFederationPlugin", + stage: 100 + }, + async () => { + if (runtimePluginApplied) return; + runtimePluginApplied = true; + const entryRuntime = getDefaultEntryRuntime( + paths, + this._options, + compiler, + this._treeShakeSharedPlugin?.buildAssets || {} + ); + new ModuleFederationRuntimePlugin({ + entryRuntime + }).apply(compiler); + } + ); new webpack.container.ModuleFederationPluginV1({ ...this._options, @@ -55,46 +79,7 @@ export class ModuleFederationPlugin { }).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); } } } @@ -115,90 +100,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, key) => ({ - 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 {}; } @@ -281,6 +185,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): RuntimePaths { if (IS_BROWSER) { return { @@ -310,12 +232,14 @@ function getPaths(options: ModuleFederationPluginOptions): RuntimePaths { function getDefaultEntryRuntime( paths: RuntimePaths, options: ModuleFederationPluginOptions, - compiler: Compiler + compiler: Compiler, + treeshakeShareFallbacks: 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]; @@ -345,6 +269,10 @@ function getDefaultEntryRuntime( `const __module_federation_share_strategy__ = ${JSON.stringify( options.shareStrategy ?? "version-first" )}`, + `const __module_federation_share_fallbacks__ = ${JSON.stringify( + treeshakeShareFallbacks + )}`, + `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 40fef1ca250e..52cee9868820 100644 --- a/packages/rspack/src/exports.ts +++ b/packages/rspack/src/exports.ts @@ -270,6 +270,7 @@ export const container = { import { ConsumeSharedPlugin } from "./sharing/ConsumeSharedPlugin"; import { ProvideSharedPlugin } from "./sharing/ProvideSharedPlugin"; import { SharePlugin } from "./sharing/SharePlugin"; +import { TreeShakeSharedPlugin } from "./sharing/TreeShakeSharedPlugin"; export type { ConsumeSharedPluginOptions, @@ -292,8 +293,10 @@ export type { SharedObject, SharePluginOptions } from "./sharing/SharePlugin"; +export type { TreeshakeSharedPluginOptions } from "./sharing/TreeShakeSharedPlugin"; export const sharing = { ProvideSharedPlugin, + TreeShakeSharedPlugin, ConsumeSharedPlugin, SharePlugin }; diff --git a/packages/rspack/src/runtime/moduleFederationDefaultRuntime.js b/packages/rspack/src/runtime/moduleFederationDefaultRuntime.js index a19796cf01ab..a8d0a27f0717 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__; module.exports = function () { if ( (__webpack_require__.initializeSharingData || @@ -51,6 +53,16 @@ module.exports = function () { __module_federation_bundler_runtime__[key]; } + early( + __webpack_require__.federation, + "libraryType", + () => __module_federation_library_type__ + ); + early( + __webpack_require__.federation, + "sharedFallback", + () => __module_federation_share_fallbacks__ + ); early( __webpack_require__.federation, "consumesLoadingModuleToHandlerMapping", @@ -187,6 +199,13 @@ module.exports = function () { "webpackRequire", () => __webpack_require__ ); + if (Object.keys(shareFallbacks).length) { + early( + __webpack_require__.federation.bundlerRuntimeOptions, + "sharedEntries", + () => shareFallbacks + ); + } merge( __webpack_require__.federation.bundlerRuntimeOptions.remotes, "idToRemoteMap", @@ -273,9 +292,9 @@ module.exports = 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..19f97ea92c69 --- /dev/null +++ b/packages/rspack/src/sharing/CollectSharedEntryPlugin.ts @@ -0,0 +1,89 @@ +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.tapPromise( + { + name: "CollectSharedEntry", + stage: + compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE + }, + async () => { + 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 fe5e24105797..eea6ffdcb671 100644 --- a/packages/rspack/src/sharing/ConsumeSharedPlugin.ts +++ b/packages/rspack/src/sharing/ConsumeSharedPlugin.ts @@ -33,6 +33,57 @@ export type ConsumesConfig = { strictVersion?: boolean; }; +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 + } + : // 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 + }; + 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 + }) + ); +} + export class ConsumeSharedPlugin extends RspackBuiltinPlugin { name = BuiltinPluginName.ConsumeSharedPlugin; _options; @@ -40,51 +91,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..e0452c822d38 --- /dev/null +++ b/packages/rspack/src/sharing/IndependentSharedPlugin.ts @@ -0,0 +1,449 @@ +import { basename, 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 { + ShareContainerPlugin, + type ShareContainerPluginOptions +} from "./ShareContainerPlugin"; +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]) => { + if (!plugin) { + return true; + } + const pluginName = plugin.name || plugin.constructor?.name; + if (!pluginName) { + return true; + } + return ![ + "TreeShakeSharedPlugin", + "IndependentSharedPlugin", + "ModuleFederationPlugin", + "SharedUsedExportsOptimizerPlugin", + "HtmlWebpackPlugin" + ].includes(pluginName); +}; + +export interface IndependentSharePluginOptions { + name: string; + shared: Shared; + library?: LibraryOptions; + outputDir?: string; + outputFilePath?: string; + plugins?: Plugins; + treeshake?: boolean; + manifest?: ModuleFederationManifestPluginOptions; + injectUsedExports?: boolean; +} + +// { react: [ [ react/19.0.0/index.js , 19.0.0, react_global_name ] ] } +export type ShareFallback = Record; + +class VirtualEntryPlugin { + sharedOptions: [string, SharedConfig][]; + constructor(sharedOptions: [string, SharedConfig][]) { + this.sharedOptions = sharedOptions; + } + createEntry() { + const { sharedOptions } = this; + const entryContent = sharedOptions.reduce((acc, cur, index) => { + return `${acc}import shared_${index} from '${cur[0]}';\n`; + }, ""); + 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.tapPromise( + { + name: "RemoveVirtualEntryAsset", + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE + }, + async () => { + 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!"); + } + } + ); + } + ); + } +} + +export class IndependentSharedPlugin { + mfName: string; + shared: Shared; + library?: LibraryOptions; + sharedOptions: [string, SharedConfig][]; + outputDir: string; + outputFilePath?: string; + plugins: Plugins; + treeshake?: boolean; + manifest?: ModuleFederationManifestPluginOptions; + buildAssets: ShareFallback = {}; + injectUsedExports?: boolean; + + name = "IndependentSharedPlugin"; + constructor(options: IndependentSharePluginOptions) { + const { + outputDir, + outputFilePath, + plugins, + treeshake, + shared, + name, + manifest, + injectUsedExports, + library + } = options; + this.shared = shared; + this.mfName = name; + this.outputDir = outputFilePath ? "" : outputDir || "independent-packages"; + this.outputFilePath = outputFilePath; + this.plugins = plugins || []; + this.treeshake = treeshake; + this.manifest = manifest; + this.injectUsedExports = injectUsedExports ?? true; + this.library = library; + 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; + compiler.hooks.beforeRun.tapPromise( + "IndependentSharedPlugin", + async compiler => { + await this.createIndependentCompilers(compiler); + } + ); + + // clean hooks + compiler.hooks.shutdown.tapAsync("IndependentSharedPlugin", callback => { + callback(); + }); + + // inject buildAssets to stats + if (manifest) { + compiler.hooks.compilation.tap("IndependentSharedPlugin", compilation => { + compilation.hooks.processAssets.tapPromise( + { + name: "injectBuildAssets", + stage: (compilation.constructor as any) + .PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER + }, + async () => { + 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 } = this; + console.log("🚀 Start creating a standalone compiler..."); + + const parentOutputDir = parentCompiler.options.output.path + ? basename(parentCompiler.options.output.path) + : ""; + // collect share requests for each shareName and then build share container + const shareRequestsMap: ShareRequestsMap = + await this.createIndependentCompiler(parentCompiler, parentOutputDir); + + await Promise.all( + sharedOptions.map(async ([shareName, shareConfig]) => { + if (!shareConfig.treeshake || 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, + parentOutputDir, + { + shareRequestsMap, + currentShare: { + shareName, + version, + request, + independentShareFileName: + sharedConfig?.independentShareFileName + } + } + ); + if (typeof shareFileName === "string") { + buildAssets[shareName] ||= []; + buildAssets[shareName].push([ + shareFileName, + sharedVersion, + globalName + ]); + } + }) + ); + }) + ); + + console.log("✅ All independent packages have been compiled successfully"); + } + + private async createIndependentCompiler( + parentCompiler: Compiler, + parentOutputDir: string, + extraOptions?: { + currentShare: Omit; + shareRequestsMap: ShareRequestsMap; + } + ) { + const { + mfName, + plugins, + outputDir, + outputFilePath, + sharedOptions, + treeshake, + library + } = this; + const outputDirWithShareName = + outputFilePath || + join(outputDir, encodeName(extraOptions?.currentShare?.shareName || "")); + + const parentConfig = parentCompiler.options; + + const finalPlugins = []; + const rspack = parentCompiler.rspack; + let extraPlugin: CollectSharedEntryPlugin | ShareContainerPlugin; + if (!extraOptions) { + extraPlugin = new CollectSharedEntryPlugin({ + sharedOptions, + shareScope: "default" + }); + } else { + extraPlugin = new ShareContainerPlugin({ + mfName, + library, + ...extraOptions.currentShare + }); + } + (parentConfig.plugins || []).forEach(plugin => { + if ( + plugin !== undefined && + typeof plugin !== "string" && + filterPlugin(plugin) + ) { + 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 (treeshake) { + finalPlugins.push( + new SharedUsedExportsOptimizerPlugin( + sharedOptions, + this.injectUsedExports + ) + ); + } + finalPlugins.push( + new VirtualEntryPlugin(sharedOptions) + // new rspack.experiments.VirtualModulesPlugin({ + // [VIRTUAL_ENTRY]: this.createEntry() + // }) + ); + const fullOutputDir = resolve( + parentCompiler.outputPath, + outputDirWithShareName + ); + const compilerConfig: RspackOptions = { + ...parentConfig, + 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 : "收集依赖"; + console.error( + `❌ ${target} 编译失败:`, + err || + stats + .toJson() + .errors.map((e: Error) => e.message) + .join("\n") + ); + reject(err || new Error(`${target} 编译失败`)); + return; + } + + currentShare && + console.log(`✅ 独立包 ${currentShare.shareName} 编译成功`); + + if (stats) { + currentShare && console.log(`📊 ${currentShare.shareName} 编译统计:`); + console.log( + stats.toString({ + colors: true, + chunks: false, + modules: false + }) + ); + } + + resolve(extraPlugin.getData()); + }); + }); + } +} diff --git a/packages/rspack/src/sharing/ProvideSharedPlugin.ts b/packages/rspack/src/sharing/ProvideSharedPlugin.ts index e3c048de42ed..a35152b564f6 100644 --- a/packages/rspack/src/sharing/ProvideSharedPlugin.ts +++ b/packages/rspack/src/sharing/ProvideSharedPlugin.ts @@ -39,6 +39,42 @@ type ProvidesEnhancedExtraConfig = { requiredVersion?: false | string; }; +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 + }; + } + return raw; + } + ); +} export class ProvideSharedPlugin< Enhanced extends boolean = false > extends RspackBuiltinPlugin { @@ -48,36 +84,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/ShareContainerPlugin.ts b/packages/rspack/src/sharing/ShareContainerPlugin.ts new file mode 100644 index 000000000000..4839a0fe8919 --- /dev/null +++ b/packages/rspack/src/sharing/ShareContainerPlugin.ts @@ -0,0 +1,116 @@ +import { + type BuiltinPlugin, + BuiltinPluginName, + type RawShareContainerPluginOptions +} 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 ShareContainerPluginOptions = { + 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 ShareContainerPlugin extends RspackBuiltinPlugin { + name = BuiltinPluginName.ShareContainerPlugin; + filename = ""; + _options: RawShareContainerPluginOptions; + _shareName: string; + _globalName: string; + + constructor(options: ShareContainerPluginOptions) { + 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.tapPromise( + { + name: "getShareContainerFile" + }, + async () => { + 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/SharePlugin.ts b/packages/rspack/src/sharing/SharePlugin.ts index 571ebf8bf194..1737d45e553a 100644 --- a/packages/rspack/src/sharing/SharePlugin.ts +++ b/packages/rspack/src/sharing/SharePlugin.ts @@ -24,62 +24,86 @@ export type SharedConfig = { singleton?: boolean; strictVersion?: boolean; version?: false | string; + treeshake?: boolean; + usedExports?: string[]; + independentShareFileName?: string; }; +export type NormalizedSharedOptions = [string, SharedConfig][]; + +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 + } + })); +} + +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 + } + })); +} export class SharePlugin { _shareScope; _consumes; _provides; _enhanced; + _sharedOptions; 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, - shareKey: options.shareKey || key, - shareScope: options.shareScope, - requiredVersion: options.requiredVersion, - strictVersion: options.strictVersion, - singleton: options.singleton, - packageName: options.packageName, - eager: options.eager - } - })); - 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 - } - })); + 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/SharedUsedExportsOptimizerPlugin.ts b/packages/rspack/src/sharing/SharedUsedExportsOptimizerPlugin.ts new file mode 100644 index 000000000000..11d6f1a19793 --- /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; + treeshake: boolean; + usedExports?: string[]; +}; + +export class SharedUsedExportsOptimizerPlugin extends RspackBuiltinPlugin { + name = BuiltinPluginName.SharedUsedExportsOptimizerPlugin; + private sharedOptions: NormalizedSharedOptions; + private injectUsedExports: boolean; + private manifestOptions: ModuleFederationManifestPluginOptions; + + constructor( + sharedOptions: NormalizedSharedOptions, + injectUsedExports?: boolean, + manifestOptions?: ModuleFederationManifestPluginOptions + ) { + super(); + this.sharedOptions = sharedOptions; + this.injectUsedExports = injectUsedExports ?? true; + this.manifestOptions = manifestOptions ?? {}; + } + + private buildOptions(): RawSharedUsedExportsOptimizerPluginOptions { + const shared: OptimizeSharedConfig[] = this.sharedOptions.map( + ([shareKey, config]) => ({ + shareKey, + treeshake: !!config.treeshake, + usedExports: config.usedExports + }) + ); + const { manifestFileName, statsFileName } = getFileName( + this.manifestOptions + ); + return { + shared, + injectUsedExports: this.injectUsedExports, + manifestFileName, + statsFileName + }; + } + + raw(): BuiltinPlugin | undefined { + if (!this.sharedOptions.length) { + return; + } + return createBuiltinPlugin(this.name, this.buildOptions()); + } +} diff --git a/packages/rspack/src/sharing/TreeShakeSharedPlugin.ts b/packages/rspack/src/sharing/TreeShakeSharedPlugin.ts new file mode 100644 index 000000000000..2b737a1ed517 --- /dev/null +++ b/packages/rspack/src/sharing/TreeShakeSharedPlugin.ts @@ -0,0 +1,70 @@ +import type { Compiler } from "../Compiler"; +import type { Plugins } from "../config"; +import type { ModuleFederationPluginOptions } from "../container/ModuleFederationPlugin"; +import { IndependentSharedPlugin } from "./IndependentSharedPlugin"; +import { SharedUsedExportsOptimizerPlugin } from "./SharedUsedExportsOptimizerPlugin"; +import { normalizeSharedOptions } from "./SharePlugin"; + +export interface TreeshakeSharedPluginOptions { + mfConfig: ModuleFederationPluginOptions; + plugins?: Plugins; + reshake?: boolean; +} + +export class TreeShakeSharedPlugin { + mfConfig: ModuleFederationPluginOptions; + outputDir: string; + plugins?: Plugins; + reshake?: boolean; + private _independentSharePlugin?: IndependentSharedPlugin; + + name = "TreeShakeSharedPlugin"; + constructor(options: TreeshakeSharedPluginOptions) { + const { mfConfig, plugins, reshake } = options; + this.mfConfig = mfConfig; + this.outputDir = mfConfig.independentShareDir || "independent-packages"; + this.plugins = plugins; + this.reshake = Boolean(reshake); + } + + apply(compiler: Compiler) { + const { mfConfig, outputDir, plugins, reshake } = this; + const { name, shared, library } = mfConfig; + if (!shared) { + return; + } + const sharedOptions = normalizeSharedOptions(shared); + if (!sharedOptions.length) { + return; + } + + if (!reshake) { + new SharedUsedExportsOptimizerPlugin( + sharedOptions, + mfConfig.injectUsedExports, + mfConfig.manifest + ).apply(compiler); + } + + if ( + sharedOptions.some( + ([_, config]) => config.treeshake && config.import !== false + ) + ) { + this._independentSharePlugin = new IndependentSharedPlugin({ + name: name, + shared: shared, + outputDir, + plugins, + treeshake: reshake, + library, + manifest: mfConfig.manifest + }); + 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 3a89dbd573f3..83d5d62e145d 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 2e914497d233..aa56f3cb73fb 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/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/index.js b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/index.js new file mode 100644 index 000000000000..1831496118e5 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/index.js @@ -0,0 +1,14 @@ +const fs = __non_webpack_require__("fs"); +const path = __non_webpack_require__("path"); + +it("should emit collect share entry asset with expected requests", async () => { + await import('./module'); + const assetPath = path.join(__dirname, "independent-packages/collect-shared-entries-copy.json"); + const content = JSON.parse(fs.readFileSync(assetPath, "utf-8")); + + const collectInfo = content.xreact; + expect(collectInfo).toBeDefined(); + expect(collectInfo.shareScope).toBe("default"); + expect(collectInfo.requests[0][0]).toContain("sharing/collect-share-entry-plugin/node_modules/xreact/index.js"); + expect(collectInfo.requests[0][1]).toEqual("1.0.0"); +}); diff --git a/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/module.js b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/module.js new file mode 100644 index 000000000000..70a7cb73256d --- /dev/null +++ b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/module.js @@ -0,0 +1,3 @@ +import "xreact"; + +export default "collect-share-entry-plugin"; diff --git a/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/node_modules/xreact/index.js b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/node_modules/xreact/index.js new file mode 100644 index 000000000000..ecafa5007699 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/node_modules/xreact/index.js @@ -0,0 +1,4 @@ +module.exports = { + default: "react-mock", + version: "1.0.0" +}; diff --git a/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/node_modules/xreact/package.json b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/node_modules/xreact/package.json new file mode 100644 index 000000000000..2b5f633e943a --- /dev/null +++ b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/node_modules/xreact/package.json @@ -0,0 +1,5 @@ +{ + "name": "xreact", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/package.json b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/package.json new file mode 100644 index 000000000000..2f0551fad629 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "xreact": "1.2.3" + } +} diff --git a/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/rspack.config.js b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/rspack.config.js new file mode 100644 index 000000000000..c59903ca11af --- /dev/null +++ b/tests/rspack-test/configCases/sharing/collect-share-entry-plugin/rspack.config.js @@ -0,0 +1,39 @@ + +const { container,sources } = require("@rspack/core"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + plugins: [ + new container.ModuleFederationPlugin({ + shared: { + xreact: { + import: "xreact", + shareKey: "xreact", + shareScope: "default", + version: "1.0.0", + treeshake: true + } + }, + }), + { + apply(compiler) { + compiler.hooks.thisCompilation.tap("CollectSharedEntryPlugin", (compilation) => { + compilation.hooks.processAssets.tapPromise( + { + name: "emitCollectSharedEntry", + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE + }, + async () => { + const filename = 'collect-shared-entries.json'; + const asset = compilation.getAsset(filename); + if (!asset) { + return; + } + compilation.emitAsset('collect-shared-entries-copy.json', new sources.RawSource(asset.source.source().toString())); + } + ); + }); + } + } + ] +}; 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..d1143f0d5890 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/index.js @@ -0,0 +1,61 @@ +const fs = __non_webpack_require__("fs"); +const path = __non_webpack_require__("path"); + +const independentShareDir = path.join( + __dirname, + "independent-packages" +); + +const customPluginAssetPath = path.join( + independentShareDir, + "apply-plugin.json" +); + +const uiLibShareContainerPath = path.join( + independentShareDir, + "ui_lib/1.0.0", + "share-entry.js" +); + +const uiLibDepShareContainerPath = path.join( + independentShareDir, + "ui_lib_dep/1.0.0", + "share-entry.js" +); + + +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("reshake share container should only have specify usedExports", async () => { + const uiLibDepShareContainerModule = __non_webpack_require__(uiLibDepShareContainerPath).reshake_share_ui_lib_dep_100; + await uiLibDepShareContainerModule.init({},{ + installInitialConsumes: async ()=>{ + return 'call init' + } + }); + const shareModules = await uiLibDepShareContainerModule.get(); + expect(shareModules.Message).toBe('Message'); + expect(shareModules.Text).not.toBeDefined(); +}); + + +it("correct handle share dep while reshake", async () => { + const uiLibShareContainerModule = __non_webpack_require__(uiLibShareContainerPath).reshake_share_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 shareModules = await uiLibShareContainerModule.get(); + 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..c6939e2c6206 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib-dep/index.js @@ -0,0 +1,9 @@ +export const Message = 'Message'; +export const Spin = 'Spin' +export const Text = 'Text' + +export default { + Message, + Spin, + 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..c158056185a4 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/node_modules/ui-lib/index.js @@ -0,0 +1,16 @@ +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`; + +export default { + Button, + List, + Badge, + MessagePro, + SpinPro +} 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..47d774a07270 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/reshake-share/rspack.config.js @@ -0,0 +1,55 @@ +const { TreeShakeSharedPlugin } = require("@rspack/core").sharing; + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + // entry:'./index.js', + optimization: { + // minimize:false, + chunkIds: "named", + moduleIds: "named" + }, + output: { + chunkFilename: "[id].js" + }, + plugins: [ + new TreeShakeSharedPlugin({ + reshake: true, + plugins: [ + { + 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({ + reshake: true + }))) + }) + }) + } + } + ], + mfConfig: { + name: 'reshake_share', + library: { + type: 'commonjs2', + }, + shared: { + 'ui-lib': { + treeshake: true, + requiredVersion: '*', + usedExports:['Badge','MessagePro'] + }, + 'ui-lib-dep': { + treeshake: true, + requiredVersion: '*', + usedExports:['Message'] + } + }, + + } + }) + ] +}; diff --git a/tests/rspack-test/configCases/sharing/treeshake-share/index.js b/tests/rspack-test/configCases/sharing/treeshake-share/index.js new file mode 100644 index 000000000000..c65b698326b3 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-share/index.js @@ -0,0 +1,74 @@ +const fs = __non_webpack_require__("fs"); +const path = __non_webpack_require__("path"); + +it("should treeshake ui-lib correctly", async () => { + const { Button } = await import("ui-lib"); + expect(Button).toEqual('Button'); + + 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).not.toContain("List"); +}); + +it("should treeshake ui-lib2 correctly", async () => { + const uiLib2 = await import("ui-lib2"); + expect(uiLib2.List).toEqual('List'); + + const bundlePath = path.join( + __dirname, + "node_modules_ui-lib2_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 treeshake ui-lib-side-effect if not set sideEffect:false ", async () => { + const uiLibSideEffect = await import("ui-lib-side-effect"); + expect(uiLibSideEffect.List).toEqual('List'); + + 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']['main'].sort()).toEqual([ 'Badge', 'Button' ]) +}); + +it("should inject usedExports into manifest and stats if enable manifest", async () => { + const { Button } = await import("ui-lib"); + expect(Button).toEqual('Button'); + + const manifestPath = path.join( + __dirname, + "mf-manifest.json" + ); + const manifestContent = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); + expect(JSON.stringify(manifestContent.shared.find(s=>s.name === 'ui-lib').usedExports.sort())).toEqual(JSON.stringify([ + "Badge", + "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([ + "Badge", + "Button" + ])); +}); + +// it("should ") diff --git a/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib-side-effect/index.js b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib-side-effect/index.js new file mode 100644 index 000000000000..0568c23344ad --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-share/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/treeshake-share/node_modules/ui-lib-side-effect/package.json b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib-side-effect/package.json new file mode 100644 index 000000000000..08f52aa758de --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-share/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/treeshake-share/node_modules/ui-lib/index.js b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib/index.js new file mode 100644 index 000000000000..9dd1824aaaad --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-share/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/treeshake-share/node_modules/ui-lib/package.json b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib/package.json new file mode 100644 index 000000000000..90f9db2691b3 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-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/treeshake-share/node_modules/ui-lib2/index.js b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib2/index.js new file mode 100644 index 000000000000..9dd1824aaaad --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib2/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/treeshake-share/node_modules/ui-lib2/package.json b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib2/package.json new file mode 100644 index 000000000000..1299fb07efbe --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-share/node_modules/ui-lib2/package.json @@ -0,0 +1,6 @@ +{ + "name": "ui-lib2", + "main": "./index.js", + "version": "1.0.0", + "sideEffects": false +} diff --git a/tests/rspack-test/configCases/sharing/treeshake-share/rspack.config.js b/tests/rspack-test/configCases/sharing/treeshake-share/rspack.config.js new file mode 100644 index 000000000000..560a589ff4a5 --- /dev/null +++ b/tests/rspack-test/configCases/sharing/treeshake-share/rspack.config.js @@ -0,0 +1,40 @@ +const { container } = require("@rspack/core"); + +const { ModuleFederationPlugin } = container; + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + entry: './index.js', + optimization:{ + minimize: false, + chunkIds:'named', + moduleIds: 'named' + }, + output: { + chunkFilename: "[id].js" + }, + entry: { + main: "./index.js" + }, + plugins: [ + new ModuleFederationPlugin({ + name:'treeshake_share', + manifest: true, + shared: { + 'ui-lib': { + requiredVersion:'*', + treeshake:true, + usedExports: ['Badge'] + }, + 'ui-lib2': { + requiredVersion:'*', + treeshake:true, + }, + 'ui-lib-side-effect': { + requiredVersion:'*', + treeshake:true, + } + } + }) + ] +}; diff --git a/website/docs/en/plugins/webpack/tree-shake-shared-plugin.mdx b/website/docs/en/plugins/webpack/tree-shake-shared-plugin.mdx new file mode 100644 index 000000000000..1b16d5496b58 --- /dev/null +++ b/website/docs/en/plugins/webpack/tree-shake-shared-plugin.mdx @@ -0,0 +1,45 @@ +import { Stability, ApiMeta } from '@components/ApiMeta'; + +# TreeShakeSharedPlugin + + + +**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. +- `reshake`: 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 { TreeShakeSharedPlugin } = require('@rspack/core'); + +module.exports = { + plugins: [ + new TreeShakeSharedPlugin({ + reshake: true, + mfConfig: { + name: 'app', + shared: { + 'lodash-es': { treeshake: true }, + }, + library: { type: 'var', name: 'App' }, + manifest: true, + }, + plugins: [], + }), + ], +}; +``` + +**Behavior** + +- Normalizes `shared` into `[shareName, SharedConfig][]`. +- Registers `SharedUsedExportsOptimizerPlugin` when `reshake` is `false` to inject used-exports from the stats manifest. +- Triggers independent compilation for shared entries with `treeshake: true`, and writes the produced assets back to the `stats/manifest` (fallback fields). diff --git a/website/docs/zh/plugins/webpack/tree-shake-shared-plugin.mdx b/website/docs/zh/plugins/webpack/tree-shake-shared-plugin.mdx new file mode 100644 index 000000000000..eda8c1f393bc --- /dev/null +++ b/website/docs/zh/plugins/webpack/tree-shake-shared-plugin.mdx @@ -0,0 +1,45 @@ +import { Stability, ApiMeta } from '@components/ApiMeta'; + +# TreeShakeSharedPlugin + + + +**概览** + +基于模块联邦配置对 shared 依赖进行按需构建与导出优化。 + +**选项** + +- `mfConfig`:`ModuleFederationPluginOptions`,传入模块联邦插件所需要的配置项。 +- `plugins`:额外插件列表,可在独立编译中复用。 +- `reshake`:是否在独立编译阶段执行二次摇树优化(二次摇树通常在部署平台确定了完整的依赖信息后重新触发的一次全新构建,提高共享依赖 treeshake 命中的准确率)。 + +**使用示例** + +```js +// rspack.config.js +const { TreeShakeSharedPlugin } = require('@rspack/core'); + +module.exports = { + plugins: [ + new TreeShakeSharedPlugin({ + reshake: true, + mfConfig: { + name: 'app', + shared: { + 'lodash-es': { treeshake: true }, + }, + library: { type: 'var', name: 'App' }, + manifest: true, + }, + plugins: [], + }), + ], +}; +``` + +**行为说明** + +- 读取 `shared` 配置后标准化为 `[shareName, SharedConfig][]`。 +- 当 `reshake` 为 `false` 时,注册 `SharedUsedExportsOptimizerPlugin`,基于 stats 清单注入已用导出集合。 +- 对 `treeshake` 的共享包触发独立编译,产出回写到 stats/manifest 中的 fallback 字段。