diff --git a/Cargo.lock b/Cargo.lock index 0d7bd97b1d73..b76b7e339ede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4892,6 +4892,7 @@ dependencies = [ "rspack_hook", "rspack_paths", "rspack_plugin_devtool", + "rspack_plugin_javascript", "rspack_plugin_json", "rspack_util", "rustc-hash", diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index b89ba51e6cce..770bb84a8db6 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -1187,6 +1187,7 @@ export interface JsRsdoctorModuleGraph { dependencies: Array chunkModules: Array connectionsOnlyImports: Array + exportUsageEdges: Array<[number, Array | null, number, Array | null]> } export interface JsRsdoctorModuleGraphModule { @@ -2939,6 +2940,7 @@ export interface RawRsdoctorPluginOptions { moduleGraphFeatures: boolean | Array<'graph' | 'ids' | 'sources'> chunkGraphFeatures: boolean | Array<'graph' | 'assets'> sourceMapFeatures?: { module?: boolean; cheap?: boolean } | undefined + exportUsageGraph?: boolean } export interface RawRslibPluginOptions { diff --git a/crates/rspack_binding_api/src/rsdoctor.rs b/crates/rspack_binding_api/src/rsdoctor.rs index 2faf7ff3a95f..63678926cd36 100644 --- a/crates/rspack_binding_api/src/rsdoctor.rs +++ b/crates/rspack_binding_api/src/rsdoctor.rs @@ -87,6 +87,8 @@ impl From for JsRsdoctorDependency { } } +pub type JsRsdoctorExportUsageEdge = (i32, Option>, i32, Option>); + #[napi(object)] pub struct JsRsdoctorConnection { pub ukey: i32, @@ -394,6 +396,8 @@ pub struct JsRsdoctorModuleGraph { pub dependencies: Vec, pub chunk_modules: Vec, pub connections_only_imports: Vec, + #[napi(ts_type = "Array<[number, Array | null, number, Array | null]>")] + pub export_usage_edges: Vec, } impl From for JsRsdoctorModuleGraph { @@ -407,6 +411,18 @@ impl From for JsRsdoctorModuleGraph { .into_iter() .map(|s| s.into()) .collect(), + export_usage_edges: value + .export_usage_edges + .into_iter() + .map(|edge| { + ( + edge.origin_module, + edge.origin_export, + edge.target_module, + edge.target_export, + ) + }) + .collect(), } } } @@ -559,6 +575,7 @@ pub struct RawRsdoctorPluginOptions { pub chunk_graph_features: Either>, #[napi(ts_type = "{ module?: boolean; cheap?: boolean } | undefined")] pub source_map_features: Option, + pub export_usage_graph: Option, } #[napi(object)] @@ -608,6 +625,7 @@ impl From for RsdoctorPluginOptions { .collect::>(), }, source_map_features, + export_usage_graph: value.export_usage_graph.unwrap_or_default(), } } } 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 cbb6bf08b04f..1284fe5e02df 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 @@ -121,23 +121,25 @@ impl ESMImportSpecifierDependency { self.ids.first().unwrap_or(&self.name) } + pub fn get_destructuring_referenced_exports(&self, ids: &[Atom]) -> Vec> { + let mut refs = Vec::new(); + if let Some(referenced_properties) = &self.referenced_properties_in_destructuring { + referenced_properties.traverse_on_leaf(&mut |stack| { + let mut ids = ids.to_vec(); + ids.extend(stack.iter().map(|p| p.id.clone())); + refs.push(ids); + }); + } + refs + } + pub fn get_referenced_exports_in_destructuring( &self, ids: Option<&[Atom]>, ) -> Vec { - if let Some(referenced_properties) = &self.referenced_properties_in_destructuring { - let mut refs = Vec::new(); - referenced_properties.traverse_on_leaf(&mut |stack| { - let ids_in_destructuring = stack.iter().map(|p| p.id.clone()); - if let Some(ids) = ids { - let mut ids = ids.to_vec(); - ids.extend(ids_in_destructuring); - refs.push(ids); - } else { - refs.push(ids_in_destructuring.collect::>()); - } - }); - refs + let destructuring_refs = self.get_destructuring_referenced_exports(ids.unwrap_or_default()); + if !destructuring_refs.is_empty() { + destructuring_refs .into_iter() // Do not inline if there are any places where used as destructuring .map(|name| { @@ -173,6 +175,10 @@ impl ESMImportSpecifierDependency { self.used_by_exports = used_by_exports; } + pub fn used_by_exports(&self) -> Option<&UsedByExports> { + self.used_by_exports.as_ref() + } + pub fn add_branch_guards(&mut self, guards: impl IntoIterator) { self.branch_guards.get_or_insert_default().extend(guards); } diff --git a/crates/rspack_plugin_rsdoctor/Cargo.toml b/crates/rspack_plugin_rsdoctor/Cargo.toml index 74c755b13c28..019490523f80 100644 --- a/crates/rspack_plugin_rsdoctor/Cargo.toml +++ b/crates/rspack_plugin_rsdoctor/Cargo.toml @@ -8,22 +8,23 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -atomic_refcell = { workspace = true } -futures = { workspace = true } -json = { workspace = true } -rayon = { workspace = true } -rspack_collections = { workspace = true } -rspack_core = { workspace = true } -rspack_error = { workspace = true } -rspack_hook = { workspace = true } -rspack_paths = { workspace = true } -rspack_plugin_devtool = { workspace = true } -rspack_plugin_json = { workspace = true } -rspack_util = { workspace = true } -rustc-hash = { workspace = true } -thread_local = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } +atomic_refcell = { workspace = true } +futures = { workspace = true } +json = { workspace = true } +rayon = { workspace = true } +rspack_collections = { workspace = true } +rspack_core = { workspace = true } +rspack_error = { workspace = true } +rspack_hook = { workspace = true } +rspack_paths = { workspace = true } +rspack_plugin_devtool = { workspace = true } +rspack_plugin_javascript = { workspace = true } +rspack_plugin_json = { workspace = true } +rspack_util = { workspace = true } +rustc-hash = { workspace = true } +thread_local = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } [package.metadata.cargo-shear] ignored = ["tracing"] diff --git a/crates/rspack_plugin_rsdoctor/src/data.rs b/crates/rspack_plugin_rsdoctor/src/data.rs index ccbe7e8a93fc..bd6a39b07cd5 100644 --- a/crates/rspack_plugin_rsdoctor/src/data.rs +++ b/crates/rspack_plugin_rsdoctor/src/data.rs @@ -1,5 +1,5 @@ use rspack_collections::Identifier; -use rspack_core::{BuildMetaExportsType, DependencyType}; +use rspack_core::{BuildMetaExportsType, DependencyId, DependencyType}; use rustc_hash::FxHashSet as HashSet; pub type ConnectionUkey = i32; @@ -72,6 +72,23 @@ pub struct RsdoctorDependency { pub dependency: ModuleUkey, } +#[derive(Debug, Default, Clone)] +pub struct RsdoctorExportUsageDependency { + pub dependency_id: DependencyId, + pub origin_module_identifier: Identifier, + pub target_module_identifier: Identifier, + pub origin_export: Option>, + pub target_export: Option>, +} + +#[derive(Debug, Default)] +pub struct RsdoctorExportUsageEdge { + pub origin_module: ModuleUkey, + pub origin_export: Option>, + pub target_module: ModuleUkey, + pub target_export: Option>, +} + #[derive(Debug, Default)] pub struct RsdoctorConnection { pub ukey: ConnectionUkey, @@ -206,6 +223,7 @@ pub struct RsdoctorModuleGraph { pub dependencies: Vec, pub chunk_modules: Vec, pub connections_only_imports: Vec, + pub export_usage_edges: Vec, } #[derive(Debug, Default)] diff --git a/crates/rspack_plugin_rsdoctor/src/module_graph.rs b/crates/rspack_plugin_rsdoctor/src/module_graph.rs index a555fe518d97..9334b243919e 100644 --- a/crates/rspack_plugin_rsdoctor/src/module_graph.rs +++ b/crates/rspack_plugin_rsdoctor/src/module_graph.rs @@ -3,22 +3,32 @@ use std::sync::{Arc, atomic::AtomicI32}; use rayon::iter::{IntoParallelRefIterator, ParallelBridge, ParallelIterator}; use rspack_collections::{Identifiable, IdentifierMap, IdentifierSet}; use rspack_core::{ - BoxModule, ChunkGraph, Compilation, Context, DependencyId, DependencyType, ExportsInfoArtifact, - Module, ModuleGraph, ModuleGraphCacheArtifact, ModuleIdsArtifact, ModuleType, - OptimizationBailoutItem, SideEffectsStateArtifact, UsageState, + BoxModule, ChunkGraph, Compilation, Context, Dependency, DependencyId, DependencyType, + ExportProvided, ExportsInfoArtifact, Module, ModuleGraph, ModuleGraphCacheArtifact, + ModuleIdsArtifact, ModuleType, OptimizationBailoutItem, SideEffectsStateArtifact, UsageState, + UsedByExports, UsedByExportsCondition, rspack_sources::{MapOptions, ObjectPool}, }; use rspack_paths::Utf8PathBuf; +use rspack_plugin_javascript::dependency::{ + ESMExportImportedSpecifierDependency, ESMImportSpecifierDependency, +}; use rspack_plugin_json::create_object_for_exports_info; +use rspack_util::atom::Atom; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use thread_local::ThreadLocal; use crate::{ ChunkUkey, ModuleKind, ModuleUkey, RsdoctorConnectionsOnlyImport, - RsdoctorConnectionsOnlyImportConnection, RsdoctorDependency, RsdoctorJsonModuleSizes, - RsdoctorModule, RsdoctorModuleId, RsdoctorModuleOriginalSource, RsdoctorSideEffectLocation, + RsdoctorConnectionsOnlyImportConnection, RsdoctorDependency, RsdoctorExportUsageDependency, + RsdoctorExportUsageEdge, RsdoctorJsonModuleSizes, RsdoctorModule, RsdoctorModuleId, + RsdoctorModuleOriginalSource, RsdoctorSideEffectLocation, }; +type ExportUsageExport = Option>; +type ExportUsageExports = Vec; +type DependencyExportUsage = Vec<(ExportUsageExport, ExportUsageExport)>; + pub fn collect_json_module_sizes( modules: &IdentifierMap<&BoxModule>, exports_info_artifact: &ExportsInfoArtifact, @@ -292,6 +302,250 @@ pub fn collect_module_dependencies( .collect::>>() } +#[inline(never)] +fn get_origin_exports(used_by_exports: Option<&UsedByExports>) -> Vec>> { + match used_by_exports.map(|used_by_exports| &used_by_exports.condition) { + None | Some(UsedByExportsCondition::Bool(true)) => vec![None], + Some(UsedByExportsCondition::Bool(false)) => { + if used_by_exports + .is_some_and(|used_by_exports| !used_by_exports.deferred_pure_checks.is_empty()) + { + vec![None] + } else { + vec![] + } + } + Some(UsedByExportsCondition::Set(exports)) => exports + .iter() + .map(|export| Some(vec![export.to_string()])) + .collect::>(), + } +} + +fn get_esm_import_specifier_target_exports( + dependency: &ESMImportSpecifierDependency, + module_graph: &ModuleGraph, +) -> Vec>> { + let ids = dependency.get_ids(module_graph); + let destructuring_exports = dependency.get_destructuring_referenced_exports(ids); + if !destructuring_exports.is_empty() { + return destructuring_exports + .into_iter() + .map(|ids| Some(ids.into_iter().map(|id| id.to_string()).collect())) + .collect(); + } + if ids.is_empty() { + vec![None] + } else { + vec![Some(ids.iter().map(|id| id.to_string()).collect())] + } +} + +#[inline(never)] +fn cross_export_usage( + origin_exports: ExportUsageExports, + target_exports: ExportUsageExports, +) -> DependencyExportUsage { + let mut export_usage = Vec::with_capacity(origin_exports.len() * target_exports.len()); + for origin_export in origin_exports { + for target_export in &target_exports { + export_usage.push((origin_export.clone(), target_export.clone())); + } + } + export_usage +} + +#[inline(never)] +fn dependency_export_usage( + dependency: &dyn Dependency, + module_graph: &ModuleGraph, + exports_info_artifact: &ExportsInfoArtifact, +) -> Option { + if let Some(dependency) = dependency.downcast_ref::() { + return Some(cross_export_usage( + get_origin_exports(dependency.used_by_exports()), + get_esm_import_specifier_target_exports(dependency, module_graph), + )); + } + if let Some(dependency) = dependency.downcast_ref::() { + return Some(get_esm_export_imported_specifier_exports( + dependency, + module_graph, + exports_info_artifact, + )); + } + None +} + +fn get_esm_export_imported_specifier_target_exports( + dependency: &ESMExportImportedSpecifierDependency, + module_graph: &ModuleGraph, +) -> Vec>> { + let ids = dependency.get_ids(module_graph); + if ids.is_empty() { + vec![None] + } else { + vec![Some(ids.iter().map(|id| id.to_string()).collect())] + } +} + +#[inline(never)] +fn get_esm_export_imported_specifier_exports( + dependency: &ESMExportImportedSpecifierDependency, + module_graph: &ModuleGraph, + exports_info_artifact: &ExportsInfoArtifact, +) -> DependencyExportUsage { + if let Some(name) = &dependency.name { + return cross_export_usage( + vec![Some(vec![name.to_string()])], + get_esm_export_imported_specifier_target_exports(dependency, module_graph), + ); + } + + let Some(origin_module_identifier) = module_graph.get_parent_module(&dependency.id) else { + return vec![(None, None)]; + }; + let origin_exports_info = exports_info_artifact.get_exports_info_data(origin_module_identifier); + if origin_exports_info.other_exports_info().get_used(None) != UsageState::Unused { + return vec![(None, None)]; + } + let active_exports = dependency.active_exports(module_graph); + let target_exports_info = module_graph + .module_identifier_by_dependency_id(&dependency.id) + .map(|identifier| exports_info_artifact.get_exports_info_data(identifier)); + origin_exports_info + .exports() + .values() + .filter_map(|export_info| { + let name = export_info.name()?; + if name == "default" + || active_exports.contains(name) + || export_info.get_used(None) == UsageState::Unused + || target_exports_info.is_some_and(|target_exports_info| { + matches!( + target_exports_info + .is_export_provided(exports_info_artifact, std::slice::from_ref(name)), + Some(ExportProvided::NotProvided) + ) + }) + { + return None; + } + Some(Some(vec![name.to_string()])) + }) + .map(|export| (export.clone(), export)) + .collect() +} + +pub fn collect_export_usage_dependencies( + modules: &IdentifierMap<&BoxModule>, + module_graph: &ModuleGraph, + exports_info_artifact: &ExportsInfoArtifact, +) -> Vec { + modules + .keys() + .flat_map(|module_id| { + module_graph + .get_outgoing_connections(module_id) + .flat_map(|conn| { + let dependency = module_graph.dependency_by_id(&conn.dependency_id); + let Some(export_usages) = + dependency_export_usage(dependency.as_ref(), module_graph, exports_info_artifact) + else { + return vec![]; + }; + if export_usages.is_empty() { + return vec![]; + } + + let dependency_id = conn.dependency_id; + let origin_module_identifier = conn.original_module_identifier.unwrap_or(*module_id); + let target_module_identifier = *conn.module_identifier(); + + export_usages + .into_iter() + .map( + |(origin_export, target_export)| RsdoctorExportUsageDependency { + dependency_id, + origin_module_identifier, + target_module_identifier, + origin_export, + target_export, + }, + ) + .collect::>() + }) + .collect::>() + }) + .collect::>() +} + +#[inline(never)] +fn is_origin_export_used( + candidate: &RsdoctorExportUsageDependency, + exports_info_artifact: &ExportsInfoArtifact, +) -> bool { + let Some(origin_export) = &candidate.origin_export else { + return true; + }; + let origin_export = origin_export + .iter() + .map(|name| Atom::from(name.as_str())) + .collect::>(); + let origin_exports_info = + exports_info_artifact.get_exports_info_data(&candidate.origin_module_identifier); + origin_exports_info.get_used(exports_info_artifact, &origin_export, None) != UsageState::Unused +} + +pub fn collect_active_export_usage_dependencies( + candidates: &[RsdoctorExportUsageDependency], + module_graph: &ModuleGraph, + module_graph_cache: &ModuleGraphCacheArtifact, + side_effects_state_artifact: &SideEffectsStateArtifact, + exports_info_artifact: &ExportsInfoArtifact, +) -> Vec { + candidates + .iter() + .filter(|candidate| { + if !is_origin_export_used(candidate, exports_info_artifact) { + return false; + } + module_graph + .connection_by_dependency_id(&candidate.dependency_id) + .is_some_and(|connection| { + connection.is_active( + module_graph, + None, + module_graph_cache, + side_effects_state_artifact, + exports_info_artifact, + ) + }) + }) + .cloned() + .collect::>() +} + +pub fn collect_export_usage_edges( + dependencies: Vec, + module_ukeys: &IdentifierMap, +) -> Vec { + dependencies + .into_iter() + .filter_map(|dependency| { + let origin_module = *module_ukeys.get(&dependency.origin_module_identifier)?; + let target_module = *module_ukeys.get(&dependency.target_module_identifier)?; + + Some(RsdoctorExportUsageEdge { + origin_module, + origin_export: dependency.origin_export, + target_module, + target_export: dependency.target_export, + }) + }) + .collect::>() +} + pub fn collect_module_ids( modules: &IdentifierMap<&BoxModule>, module_ukeys: &IdentifierMap, diff --git a/crates/rspack_plugin_rsdoctor/src/plugin.rs b/crates/rspack_plugin_rsdoctor/src/plugin.rs index df20b3891651..8220093c4fb5 100644 --- a/crates/rspack_plugin_rsdoctor/src/plugin.rs +++ b/crates/rspack_plugin_rsdoctor/src/plugin.rs @@ -7,9 +7,11 @@ use atomic_refcell::AtomicRefCell; use futures::future::BoxFuture; use rspack_collections::IdentifierMap; use rspack_core::{ - ChunkGroupUkey, Compilation, CompilationAfterCodeGeneration, CompilationAfterProcessAssets, - CompilationId, CompilationModuleIds, CompilationOptimizeChunkModules, CompilationOptimizeChunks, - CompilationParams, CompilerCompilation, ModuleIdsArtifact, OptimizationBailoutItem, Plugin, + BuildModuleGraphArtifact, ChunkGroupUkey, Compilation, CompilationAfterCodeGeneration, + CompilationAfterProcessAssets, CompilationId, CompilationModuleIds, + CompilationOptimizeChunkModules, CompilationOptimizeChunks, CompilationOptimizeDependencies, + CompilationParams, CompilerCompilation, ExportsInfoArtifact, ModuleIdsArtifact, + OptimizationBailoutItem, Plugin, SideEffectsOptimizeArtifact, }; use rspack_error::{Diagnostic, Result}; use rspack_hook::{plugin, plugin_hook}; @@ -25,17 +27,19 @@ use rspack_util::{ use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use crate::{ - EntrypointUkey, ModuleUkey, RsdoctorAssetPatch, RsdoctorChunkGraph, RsdoctorModuleGraph, - RsdoctorModuleIdsPatch, RsdoctorModuleSourcesPatch, RsdoctorPluginHooks, - RsdoctorStatsModuleIssuer, + EntrypointUkey, ModuleUkey, RsdoctorAssetPatch, RsdoctorChunkGraph, + RsdoctorExportUsageDependency, RsdoctorModuleGraph, RsdoctorModuleIdsPatch, + RsdoctorModuleSourcesPatch, RsdoctorPluginHooks, RsdoctorStatsModuleIssuer, chunk_graph::{ collect_assets, collect_chunk_assets, collect_chunk_dependencies, collect_chunk_modules, collect_chunks, collect_entrypoint_assets, collect_entrypoints, }, module_graph::{ - collect_concatenated_modules, collect_connections_only_imports, collect_json_module_sizes, - collect_module_dependencies, collect_module_ids, collect_module_original_sources, - collect_module_side_effects_locations, collect_modules, + collect_active_export_usage_dependencies, collect_concatenated_modules, + collect_connections_only_imports, collect_export_usage_dependencies, + collect_export_usage_edges, collect_json_module_sizes, collect_module_dependencies, + collect_module_ids, collect_module_original_sources, collect_module_side_effects_locations, + collect_modules, }, }; @@ -70,6 +74,11 @@ static ENTRYPOINT_UKEY_MAP: LazyLock< static JSON_MODULE_SIZE_MAP: LazyLock> = LazyLock::new(FxDashMap::default); +#[cfg_attr(allocative, allocative::root)] +static ACTIVE_EXPORT_USAGE_DEPENDENCY_MAP: LazyLock< + FxDashMap>, +> = LazyLock::new(FxDashMap::default); + #[derive(Debug, Hash, PartialEq, Eq)] pub enum RsdoctorPluginModuleGraphFeature { ModuleGraph, @@ -134,6 +143,7 @@ pub struct RsdoctorPluginOptions { pub module_graph_features: FxHashSet, pub chunk_graph_features: FxHashSet, pub source_map_features: RsdoctorPluginSourceMapFeature, + pub export_usage_graph: bool, } #[plugin] @@ -175,6 +185,20 @@ impl RsdoctorPlugin { panic!("chunk graph feature \"{feature}\" need \"graph\" to be enabled"); } + pub fn has_export_usage_graph_feature(&self) -> bool { + if !self.options.export_usage_graph { + return false; + } + if self + .options + .module_graph_features + .contains(&RsdoctorPluginModuleGraphFeature::ModuleGraph) + { + return true; + } + panic!("export usage graph feature need module graph \"graph\" to be enabled"); + } + pub fn get_compilation_hooks(id: CompilationId) -> ArcRsdoctorPluginHooks { if !COMPILATION_HOOKS_MAP.contains_key(&id) { COMPILATION_HOOKS_MAP.insert(id, Default::default()); @@ -198,9 +222,42 @@ async fn compilation( ) -> Result<()> { MODULE_UKEY_MAP.insert(compilation.id(), IdentifierMap::default()); ENTRYPOINT_UKEY_MAP.insert(compilation.id(), HashMap::default()); + ACTIVE_EXPORT_USAGE_DEPENDENCY_MAP.remove(&compilation.id()); Ok(()) } +#[plugin_hook(CompilationOptimizeDependencies for RsdoctorPlugin, stage = 9999)] +async fn optimize_dependencies( + &self, + compilation: &Compilation, + _side_effects_optimize_artifact: &mut SideEffectsOptimizeArtifact, + build_module_graph_artifact: &mut BuildModuleGraphArtifact, + exports_info_artifact: &mut ExportsInfoArtifact, + _diagnostics: &mut Vec, +) -> Result> { + if !self.has_export_usage_graph_feature() { + return Ok(None); + } + + let module_graph = build_module_graph_artifact.get_module_graph(); + let modules = module_graph + .modules() + .map(|(id, module)| (*id, module)) + .collect::>(); + let dependencies = + collect_export_usage_dependencies(&modules, module_graph, exports_info_artifact); + let active_dependencies = collect_active_export_usage_dependencies( + &dependencies, + module_graph, + &compilation.module_graph_cache_artifact, + &build_module_graph_artifact.side_effects_state_artifact, + exports_info_artifact, + ); + ACTIVE_EXPORT_USAGE_DEPENDENCY_MAP.insert(compilation.id(), active_dependencies); + + Ok(None) +} + #[plugin_hook(CompilationOptimizeChunks for RsdoctorPlugin, stage = 9999)] async fn optimize_chunks(&self, compilation: &mut Compilation) -> Result> { if !self.has_chunk_graph_feature(RsdoctorPluginChunkGraphFeature::ChunkGraph) { @@ -294,10 +351,10 @@ async fn optimize_chunk_modules(&self, compilation: &mut Compilation) -> Result< let hooks = RsdoctorPlugin::get_compilation_hooks(compilation.id()); + let module_graph = compilation.get_module_graph(); let mut rsd_modules = HashMap::default(); let mut rsd_dependencies = HashMap::default(); - let module_graph = compilation.get_module_graph(); let chunk_graph = &compilation.build_chunk_graph_artifact.chunk_graph; let chunk_by_ukey = &compilation.build_chunk_graph_artifact.chunk_by_ukey; let modules = module_graph @@ -423,6 +480,15 @@ async fn optimize_chunk_modules(&self, compilation: &mut Compilation) -> Result< let chunk_modules = collect_chunk_modules(chunk_by_ukey, &module_ukey_map, chunk_graph, module_graph); + let export_usage_edges = if self.has_export_usage_graph_feature() { + ACTIVE_EXPORT_USAGE_DEPENDENCY_MAP + .remove(&compilation.id()) + .map(|(_, dependencies)| collect_export_usage_edges(dependencies, &module_ukey_map)) + .unwrap_or_default() + } else { + Vec::new() + }; + tokio::spawn(async move { match hooks .borrow() @@ -432,6 +498,7 @@ async fn optimize_chunk_modules(&self, compilation: &mut Compilation) -> Result< dependencies: rsd_dependencies.into_values().collect::>(), chunk_modules, connections_only_imports, + export_usage_edges, }) .await { @@ -588,6 +655,10 @@ impl Plugin for RsdoctorPlugin { fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> { ctx.compiler_hooks.compilation.tap(compilation::new(self)); + ctx + .compilation_hooks + .optimize_dependencies + .tap(optimize_dependencies::new(self)); // Collect JSON module sizes before concatenation (after tree-shaking) ctx .compilation_hooks diff --git a/packages/rspack/src/builtin-plugin/RsdoctorPlugin.ts b/packages/rspack/src/builtin-plugin/RsdoctorPlugin.ts index f4b6e23f3a96..eca5c5fc6cbb 100644 --- a/packages/rspack/src/builtin-plugin/RsdoctorPlugin.ts +++ b/packages/rspack/src/builtin-plugin/RsdoctorPlugin.ts @@ -68,6 +68,7 @@ export type RsdoctorPluginOptions = { module?: boolean; cheap?: boolean; }; + exportUsageGraph?: boolean; }; const RsdoctorPluginImpl = create( @@ -83,6 +84,7 @@ const RsdoctorPluginImpl = create( moduleGraphFeatures: c.moduleGraphFeatures ?? true, chunkGraphFeatures: c.chunkGraphFeatures ?? true, sourceMapFeatures: c.sourceMapFeatures, + exportUsageGraph: c.exportUsageGraph, }; }, ); diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/b.js b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/b.js new file mode 100644 index 000000000000..48644353e3d6 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/b.js @@ -0,0 +1,9 @@ +import { baz, unusedBaz } from "./c"; + +export function bar() { + return baz(); +} + +export function unusedBar() { + return unusedBaz(); +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/c.js b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/c.js new file mode 100644 index 000000000000..587b14f0f02f --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/c.js @@ -0,0 +1,7 @@ +export function baz() { + return 42; +} + +export function unusedBaz() { + return 0; +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/entryA.js b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/entryA.js new file mode 100644 index 000000000000..ec60960cf189 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/entryA.js @@ -0,0 +1,7 @@ +import { bar } from "./b"; + +export class EntryA { + constructor() { + this.value = bar(); + } +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/index.js b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/index.js new file mode 100644 index 000000000000..b7210bc80a67 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/index.js @@ -0,0 +1,7 @@ +import { EntryA } from "./entryA"; + +const instance = new EntryA(); + +it("should instantiate EntryA with baz value", () => { + expect(instance.value).toBe(42); +}); diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/rspack.config.js b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/rspack.config.js new file mode 100644 index 000000000000..d4d22e65fc87 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageClassGraph/rspack.config.js @@ -0,0 +1,128 @@ +const { + experiments: { RsdoctorPlugin }, +} = require('@rspack/core'); +const path = require('path'); + +function normalizeRequest(request) { + return request.replaceAll('\\', '/'); +} + +function hasEdge(edges, expected) { + return edges.some((edge) => + Object.keys(expected).every((key) => { + if (Array.isArray(expected[key])) { + return ( + Array.isArray(edge[key]) && + edge[key].length === expected[key].length && + edge[key].every((item, index) => item === expected[key][index]) + ); + } + return edge[key] === expected[key]; + }), + ); +} + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + mode: 'production', + entry: './index.js', + optimization: { + concatenateModules: false, + usedExports: true, + }, + plugins: [ + new RsdoctorPlugin({ + moduleGraphFeatures: ['graph'], + chunkGraphFeatures: false, + exportUsageGraph: true, + }), + { + apply(compiler) { + let moduleGraphCalled = false; + compiler.hooks.compilation.tap( + 'TestPlugin::ExportUsageClassGraph', + (compilation) => { + const hooks = RsdoctorPlugin.getCompilationHooks(compilation); + hooks.moduleGraph.tap( + 'TestPlugin::ExportUsageClassGraph', + (moduleGraph) => { + moduleGraphCalled = true; + const modulePathByUkey = new Map( + moduleGraph.modules.map((module) => [ + module.ukey, + normalizeRequest(module.path), + ]), + ); + const edges = moduleGraph.exportUsageEdges.map( + ([ + originModule, + originExport, + targetModule, + targetExport, + ]) => ({ + originModulePath: modulePathByUkey.get(originModule), + originExport, + targetModulePath: modulePathByUkey.get(targetModule), + targetExport, + }), + ); + + expect( + hasEdge(edges, { + originModulePath: normalizeRequest( + path.join(__dirname, 'index.js'), + ), + originExport: null, + targetModulePath: normalizeRequest( + path.join(__dirname, 'entryA.js'), + ), + targetExport: ['EntryA'], + }), + ).toBe(true); + expect( + hasEdge(edges, { + originModulePath: normalizeRequest( + path.join(__dirname, 'entryA.js'), + ), + originExport: ['EntryA'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'b.js'), + ), + targetExport: ['bar'], + }), + ).toBe(true); + expect( + hasEdge(edges, { + originModulePath: normalizeRequest( + path.join(__dirname, 'b.js'), + ), + originExport: ['bar'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'c.js'), + ), + targetExport: ['baz'], + }), + ).toBe(true); + expect( + hasEdge(edges, { + originModulePath: normalizeRequest( + path.join(__dirname, 'b.js'), + ), + originExport: ['unusedBar'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'c.js'), + ), + targetExport: ['unusedBaz'], + }), + ).toBe(false); + }, + ); + }, + ); + compiler.hooks.done.tap('TestPlugin::ExportUsageClassGraph', () => { + expect(moduleGraphCalled).toBe(true); + }); + }, + }, + ], +}; diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/dep.js b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/dep.js new file mode 100644 index 000000000000..611ebbf58a3f --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/dep.js @@ -0,0 +1,5 @@ +export function foo() {} + +export function sideEffect(fn) { + fn(); +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/index.js b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/index.js new file mode 100644 index 000000000000..0deb7dd17ac2 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/index.js @@ -0,0 +1,5 @@ +import { c } from "./re-export"; + +it("should keep deferred impure pure-expression usage in rsdoctor graph", () => { + expect(c).toBeUndefined(); +}); diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/re-export.js b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/re-export.js new file mode 100644 index 000000000000..2810cac84ed4 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/re-export.js @@ -0,0 +1,5 @@ +import { foo, sideEffect } from "./dep"; + +export const a = foo; +export const b = sideEffect(a); +export const c = sideEffect(function () {}); diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/rspack.config.js b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/rspack.config.js new file mode 100644 index 000000000000..d1c4afd6396d --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageDeferredPure/rspack.config.js @@ -0,0 +1,108 @@ +const { + experiments: { RsdoctorPlugin }, +} = require('@rspack/core'); +const path = require('path'); + +function normalizeRequest(request) { + return request.replaceAll('\\', '/'); +} + +function hasEdge(edges, expected) { + return edges.some((edge) => + Object.keys(expected).every((key) => { + if (Array.isArray(expected[key])) { + return ( + Array.isArray(edge[key]) && + edge[key].length === expected[key].length && + edge[key].every((item, index) => item === expected[key][index]) + ); + } + return edge[key] === expected[key]; + }), + ); +} + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + mode: 'production', + optimization: { + sideEffects: true, + innerGraph: true, + usedExports: true, + concatenateModules: false, + }, + experiments: { + pureFunctions: true, + }, + plugins: [ + new RsdoctorPlugin({ + moduleGraphFeatures: ['graph'], + chunkGraphFeatures: false, + exportUsageGraph: true, + }), + { + apply(compiler) { + let moduleGraphCalled = false; + compiler.hooks.compilation.tap( + 'TestPlugin::ExportUsageDeferredPure', + (compilation) => { + const hooks = RsdoctorPlugin.getCompilationHooks(compilation); + hooks.moduleGraph.tap( + 'TestPlugin::ExportUsageDeferredPure', + (moduleGraph) => { + moduleGraphCalled = true; + const modulePathByUkey = new Map( + moduleGraph.modules.map((module) => [ + module.ukey, + normalizeRequest(module.path), + ]), + ); + const edges = moduleGraph.exportUsageEdges.map( + ([ + originModule, + originExport, + targetModule, + targetExport, + ]) => ({ + originModulePath: modulePathByUkey.get(originModule), + originExport, + targetModulePath: modulePathByUkey.get(targetModule), + targetExport, + }), + ); + + expect( + hasEdge(edges, { + originModulePath: normalizeRequest( + path.join(__dirname, 'index.js'), + ), + originExport: null, + targetModulePath: normalizeRequest( + path.join(__dirname, 're-export.js'), + ), + targetExport: ['c'], + }), + ).toBe(true); + expect( + hasEdge(edges, { + originModulePath: normalizeRequest( + path.join(__dirname, 're-export.js'), + ), + originExport: ['c'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'dep.js'), + ), + targetExport: ['sideEffect'], + }), + ).toBe(true); + }, + ); + }, + ); + compiler.hooks.done.tap('TestPlugin::ExportUsageDeferredPure', () => { + expect(moduleGraphCalled).toBe(true); + }); + }, + }, + ], +}; diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/barrel.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/barrel.js new file mode 100644 index 000000000000..5f423c8e86e6 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/barrel.js @@ -0,0 +1 @@ +export { bar as reexportedBar, unused as unusedReexport } from "./shared"; diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/index.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/index.js new file mode 100644 index 000000000000..eb88b6985cb4 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/index.js @@ -0,0 +1,5 @@ +import { foo } from "./lib"; + +it("should collect rsdoctor export usage graph", () => { + expect(foo()).toBe(42); +}); diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/lib.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/lib.js new file mode 100644 index 000000000000..c8924f07b59b --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/lib.js @@ -0,0 +1,17 @@ +import { reexportedBar, unusedReexport } from "./barrel"; +import { bar as starBar, local } from "./star"; +import { multiBar, multiFoo } from "./multi-star"; +import * as shared from "./shared"; + +export function foo() { + const { namespaceFoo } = shared; + return reexportedBar() + starBar() + namespaceFoo() + local + multiFoo() + multiBar() - 57; +} + +export function unusedFoo() { + return reexportedBar(); +} + +export function unusedOther() { + return unusedReexport(); +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/multi-star.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/multi-star.js new file mode 100644 index 000000000000..fa4fb781e745 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/multi-star.js @@ -0,0 +1,2 @@ +export * from "./star-a"; +export * from "./star-b"; diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/rspack.config.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/rspack.config.js new file mode 100644 index 000000000000..adc4664bf716 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/rspack.config.js @@ -0,0 +1,181 @@ +const { + experiments: { RsdoctorPlugin }, +} = require('@rspack/core'); +const path = require('path'); + +function normalizeRequest(request) { + return request.replaceAll('\\', '/'); +} + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + mode: 'production', + optimization: { + concatenateModules: false, + usedExports: true, + }, + plugins: [ + new RsdoctorPlugin({ + moduleGraphFeatures: ['graph'], + chunkGraphFeatures: false, + exportUsageGraph: true, + }), + { + apply(compiler) { + let moduleGraphCalled = false; + compiler.hooks.compilation.tap( + 'TestPlugin::ExportUsageGraph', + (compilation) => { + const hooks = RsdoctorPlugin.getCompilationHooks(compilation); + hooks.moduleGraph.tap( + 'TestPlugin::ExportUsageGraph', + (moduleGraph) => { + moduleGraphCalled = true; + const modulePathByUkey = new Map( + moduleGraph.modules.map((module) => [ + module.ukey, + normalizeRequest(module.path), + ]), + ); + const edges = moduleGraph.exportUsageEdges + .map( + ([ + originModule, + originExport, + targetModule, + targetExport, + ]) => ({ + originModulePath: modulePathByUkey.get(originModule), + originExport, + targetModulePath: modulePathByUkey.get(targetModule), + targetExport, + }), + ) + .sort((a, b) => + `${a.originModulePath}:${a.originExport}:${a.targetModulePath}:${a.targetExport}` > + `${b.originModulePath}:${b.originExport}:${b.targetModulePath}:${b.targetExport}` + ? 1 + : -1, + ); + + expect(edges).toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'index.js'), + ), + originExport: null, + targetModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + targetExport: ['foo'], + }); + expect(edges).toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['foo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'shared.js'), + ), + targetExport: ['bar'], + }); + expect(edges).toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['foo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'shared.js'), + ), + targetExport: ['namespaceFoo'], + }); + expect(edges).toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['foo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'star-a.js'), + ), + targetExport: ['multiFoo'], + }); + expect(edges).toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['foo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'star-b.js'), + ), + targetExport: ['multiBar'], + }); + expect(edges).not.toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['foo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'star-b.js'), + ), + targetExport: ['multiFoo'], + }); + expect(edges).not.toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['foo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'star-a.js'), + ), + targetExport: ['multiBar'], + }); + expect(edges).not.toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['foo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'shared.js'), + ), + targetExport: null, + }); + expect(edges).not.toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'lib.js'), + ), + originExport: ['unusedFoo'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'shared.js'), + ), + targetExport: ['bar'], + }); + expect(edges).not.toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'barrel.js'), + ), + originExport: ['unusedReexport'], + targetModulePath: normalizeRequest( + path.join(__dirname, 'shared.js'), + ), + targetExport: ['unused'], + }); + expect(edges).not.toContainEqual({ + originModulePath: normalizeRequest( + path.join(__dirname, 'star.js'), + ), + originExport: null, + targetModulePath: normalizeRequest( + path.join(__dirname, 'shared.js'), + ), + targetExport: null, + }); + }, + ); + }, + ); + compiler.hooks.done.tap('TestPlugin::ExportUsageGraph', () => { + expect(moduleGraphCalled).toBe(true); + }); + }, + }, + ], +}; diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/shared.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/shared.js new file mode 100644 index 000000000000..d8afa6ec3ce2 --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/shared.js @@ -0,0 +1,11 @@ +export function bar() { + return 42; +} + +export function namespaceFoo() { + return 7; +} + +export function unused() { + return 0; +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star-a.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star-a.js new file mode 100644 index 000000000000..91334f6657ab --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star-a.js @@ -0,0 +1,3 @@ +export function multiFoo() { + return 3; +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star-b.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star-b.js new file mode 100644 index 000000000000..aff0be6d8cea --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star-b.js @@ -0,0 +1,3 @@ +export function multiBar() { + return 4; +} diff --git a/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star.js b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star.js new file mode 100644 index 000000000000..71be402fbc9c --- /dev/null +++ b/tests/rspack-test/configCases/rsdoctor/exportUsageGraph/star.js @@ -0,0 +1,3 @@ +export * from "./shared"; + +export const local = 1;