diff --git a/turbopack/crates/turbopack-core/src/reference/mod.rs b/turbopack/crates/turbopack-core/src/reference/mod.rs index 7f47d085c0268..70fd893ea4905 100644 --- a/turbopack/crates/turbopack-core/src/reference/mod.rs +++ b/turbopack/crates/turbopack-core/src/reference/mod.rs @@ -8,6 +8,7 @@ use turbo_tasks::{ }; use crate::{ + chunk::{ChunkableModuleReference, ChunkingType, ChunkingTypeOption}, issue::IssueDescriptionExt, module::{Module, Modules}, output::{OutputAsset, OutputAssets}, @@ -84,6 +85,44 @@ impl SingleModuleReference { } } +#[turbo_tasks::value] +pub struct SingleChunkableModuleReference { + asset: ResolvedVc>, + description: Vc, +} + +#[turbo_tasks::value_impl] +impl SingleChunkableModuleReference { + #[turbo_tasks::function] + pub fn new(asset: ResolvedVc>, description: Vc) -> Vc { + Self::cell(SingleChunkableModuleReference { asset, description }) + } +} + +#[turbo_tasks::value_impl] +impl ChunkableModuleReference for SingleChunkableModuleReference { + #[turbo_tasks::function] + fn chunking_type(self: Vc) -> Vc { + Vc::cell(Some(ChunkingType::ParallelInheritAsync)) + } +} + +#[turbo_tasks::value_impl] +impl ModuleReference for SingleChunkableModuleReference { + #[turbo_tasks::function] + fn resolve_reference(&self) -> Vc { + ModuleResolveResult::module(self.asset).cell() + } +} + +#[turbo_tasks::value_impl] +impl ValueToString for SingleChunkableModuleReference { + #[turbo_tasks::function] + fn to_string(&self) -> Vc { + self.description + } +} + /// A reference that always resolves to a single module. #[turbo_tasks::value] pub struct SingleOutputAssetReference { diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs index 46559061626cb..7499e4e28efe1 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -167,11 +167,11 @@ impl ModuleReference for EsmAssetReference { .await? .expect("EsmAssetReference origin should be a EcmascriptModuleAsset"); - return Ok(ModuleResolveResult::module( - EcmascriptModulePartAsset::select_part(*module, *part) + return Ok(ModuleResolveResult::module(ResolvedVc::upcast( + EcmascriptModulePartAsset::new(*module, *part) .to_resolved() .await?, - ) + )) .cell()); } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs index e853a06886d4b..eed3a0acc5fbe 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs @@ -128,10 +128,12 @@ pub async fn follow_reexports( module: Vc>, export_name: RcStr, side_effect_free_packages: Vc, + ignore_side_effect_of_entry: bool, ) -> Result> { - if !*module - .is_marked_as_side_effect_free(side_effect_free_packages) - .await? + if !ignore_side_effect_of_entry + && !*module + .is_marked_as_side_effect_free(side_effect_free_packages) + .await? { return Ok(FollowExportsResult::cell(FollowExportsResult { module, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs index 1e44b3656b61f..f9b41f846adfc 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs @@ -11,7 +11,7 @@ pub use self::{ base::EsmAssetReference, binding::EsmBinding, dynamic::EsmAsyncAssetReference, - export::{EsmExport, EsmExports}, + export::{EsmExport, EsmExports, FoundExportType}, meta::{ImportMetaBinding, ImportMetaRef}, module_item::EsmModuleItem, url::{UrlAssetReference, UrlRewriteBehavior}, diff --git a/turbopack/crates/turbopack-ecmascript/src/tree_shake/asset.rs b/turbopack/crates/turbopack-ecmascript/src/tree_shake/asset.rs index 6c37c96622bec..63d178c6bd50e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/tree_shake/asset.rs +++ b/turbopack/crates/turbopack-ecmascript/src/tree_shake/asset.rs @@ -1,12 +1,15 @@ use anyhow::{Context, Result}; +use turbo_rcstr::RcStr; use turbo_tasks::{ResolvedVc, Vc}; +use turbo_tasks_fs::glob::Glob; use turbopack_core::{ asset::{Asset, AssetContent}, chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext, EvaluatableAsset}, + context::AssetContext, ident::AssetIdent, module::Module, reference::{ModuleReference, ModuleReferences, SingleModuleReference}, - resolve::ModulePart, + resolve::{origin::ResolveOrigin, ModulePart}, }; use super::{ @@ -16,7 +19,11 @@ use super::{ use crate::{ chunk::{EcmascriptChunkPlaceable, EcmascriptExports}, parse::ParseResult, - references::analyse_ecmascript_module, + references::{ + analyse_ecmascript_module, esm::FoundExportType, follow_reexports, FollowExportsResult, + }, + side_effect_optimization::facade::module::EcmascriptModuleFacadeModule, + tree_shake::{side_effect_module::SideEffectsModule, Key}, AnalyzeEcmascriptModuleResult, EcmascriptAnalyzable, EcmascriptModuleAsset, EcmascriptModuleAssetType, EcmascriptModuleContent, EcmascriptParsable, }; @@ -93,15 +100,70 @@ impl EcmascriptModulePartAsset { #[turbo_tasks::function] pub async fn select_part( module: Vc, - part: Vc, + part: ResolvedVc, ) -> Result>> { - let split_result = split_module(module).await?; + let SplitResult::Ok { entrypoints, .. } = &*split_module(module).await? else { + return Ok(Vc::upcast(module)); + }; - Ok(if matches!(&*split_result, SplitResult::Failed { .. }) { - Vc::upcast(module) - } else { - Vc::upcast(EcmascriptModulePartAsset::new(module, part)) - }) + // We follow reexports here + if let ModulePart::Export(export) = &*part.await? { + let export_name = export.await?.clone_value(); + + // If a local binding or reexport with the same name exists, we stop here. + // Side effects of the barrel file are preserved. + if entrypoints.contains_key(&Key::Export(export_name.clone())) { + return Ok(Vc::upcast(EcmascriptModulePartAsset::new(module, *part))); + } + + let side_effect_free_packages = module.asset_context().side_effect_free_packages(); + + // Exclude local bindings by using exports module part. + let source_module = Vc::upcast(EcmascriptModulePartAsset::new(module, *part)); + + let FollowExportsWithSideEffectsResult { + side_effects, + result, + } = &*follow_reexports_with_side_effects( + source_module, + export_name.clone(), + side_effect_free_packages, + ) + .await?; + + let FollowExportsResult { + module: final_module, + export_name: new_export, + .. + } = &*result.await?; + + let final_module = if let Some(new_export) = new_export { + if *new_export == export_name { + *final_module + } else { + Vc::upcast(EcmascriptModuleFacadeModule::new( + *final_module, + ModulePart::renamed_export(new_export.clone(), export_name.clone()), + )) + } + } else { + Vc::upcast(EcmascriptModuleFacadeModule::new( + *final_module, + ModulePart::renamed_namespace(export_name.clone()), + )) + }; + + if side_effects.is_empty() { + return Ok(Vc::upcast(final_module)); + } + + let side_effects_module = + SideEffectsModule::new(module, *part, final_module, side_effects.to_vec()); + + return Ok(Vc::upcast(side_effects_module)); + } + + Ok(Vc::upcast(EcmascriptModulePartAsset::new(module, *part))) } #[turbo_tasks::function] @@ -117,6 +179,61 @@ impl EcmascriptModulePartAsset { } } +#[turbo_tasks::value] +struct FollowExportsWithSideEffectsResult { + side_effects: Vec>>, + result: Vc, +} + +#[turbo_tasks::function] +async fn follow_reexports_with_side_effects( + module: Vc>, + export_name: RcStr, + side_effect_free_packages: Vc, +) -> Result> { + let mut side_effects = vec![]; + + let mut current_module = module; + let mut current_export_name = export_name; + let result = loop { + let is_side_effect_free = *current_module + .is_marked_as_side_effect_free(side_effect_free_packages) + .await?; + + if !is_side_effect_free { + side_effects.push(only_effects(current_module)); + } + + // We ignore the side effect of the entry module here, because we need to proceed. + let result = follow_reexports( + current_module, + current_export_name.clone(), + side_effect_free_packages, + true, + ); + + let FollowExportsResult { + module, + export_name, + ty, + } = &*result.await?; + + match ty { + FoundExportType::SideEffects => { + current_module = *module; + current_export_name = export_name.clone().unwrap_or(current_export_name); + } + _ => break result, + } + }; + + Ok(FollowExportsWithSideEffectsResult { + side_effects, + result, + } + .cell()) +} + #[turbo_tasks::value_impl] impl Module for EcmascriptModulePartAsset { #[turbo_tasks::function] @@ -199,6 +316,21 @@ impl EcmascriptChunkPlaceable for EcmascriptModulePartAsset { async fn get_exports(self: Vc) -> Result> { Ok(*self.analyze().await?.exports) } + + #[turbo_tasks::function] + async fn is_marked_as_side_effect_free( + self: Vc, + side_effect_free_packages: Vc, + ) -> Result> { + let this = self.await?; + + match *this.part.await? { + ModulePart::Exports | ModulePart::Export(..) => Ok(Vc::cell(true)), + _ => Ok(this + .full_module + .is_marked_as_side_effect_free(side_effect_free_packages)), + } + } } #[turbo_tasks::value_impl] @@ -236,3 +368,15 @@ fn analyze( #[turbo_tasks::value_impl] impl EvaluatableAsset for EcmascriptModulePartAsset {} + +#[turbo_tasks::function] +async fn only_effects( + module: Vc>, +) -> Result>> { + if let Some(module) = Vc::try_resolve_downcast_type::(module).await? { + let module = EcmascriptModulePartAsset::new(module, ModulePart::evaluation()); + return Ok(Vc::upcast(module)); + } + + Ok(module) +} diff --git a/turbopack/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs b/turbopack/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs index 28d1428927656..61d37da9093a2 100644 --- a/turbopack/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs +++ b/turbopack/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use turbo_tasks::{ResolvedVc, Vc}; +use turbo_tasks::{ResolvedVc, ValueDefault, ValueToString, Vc}; +use turbo_tasks_fs::rope::RopeBuilder; use turbopack_core::{ chunk::{AsyncModuleInfo, ChunkItem, ChunkType, ChunkingContext}, ident::AssetIdent, @@ -9,7 +10,13 @@ use turbopack_core::{ use super::{asset::EcmascriptModulePartAsset, part_of_module, split_module}; use crate::{ - chunk::{EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkType}, + chunk::{ + EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkItemOptions, + EcmascriptChunkPlaceable, EcmascriptChunkType, + }, + references::async_module::AsyncModuleOptions, + tree_shake::side_effect_module::SideEffectsModule, + utils::StringifyJs, EcmascriptModuleContent, }; @@ -106,3 +113,108 @@ impl ChunkItem for EcmascriptModulePartChunkItem { self.module.is_async_module() } } + +#[turbo_tasks::value(shared)] +pub(super) struct SideEffectsModuleChunkItem { + pub module: Vc, + pub chunking_context: Vc>, +} + +#[turbo_tasks::value_impl] +impl ChunkItem for SideEffectsModuleChunkItem { + #[turbo_tasks::function] + fn references(&self) -> Vc { + self.module.references() + } + + #[turbo_tasks::function] + fn asset_ident(&self) -> Vc { + self.module.ident() + } + + #[turbo_tasks::function] + fn ty(&self) -> Vc> { + Vc::upcast(EcmascriptChunkType::value_default()) + } + + #[turbo_tasks::function] + fn module(&self) -> Vc> { + Vc::upcast(self.module) + } + + #[turbo_tasks::function] + fn chunking_context(&self) -> Vc> { + self.chunking_context + } +} + +#[turbo_tasks::value_impl] +impl EcmascriptChunkItem for SideEffectsModuleChunkItem { + #[turbo_tasks::function] + async fn content(&self) -> Result> { + let mut code = RopeBuilder::default(); + let mut has_top_level_await = false; + + let module = self.module.await?; + + for &side_effect in self.module.await?.side_effects.iter() { + let need_await = 'need_await: { + let async_module = *side_effect.get_async_module().await?; + if let Some(async_module) = async_module { + if async_module.await?.has_top_level_await { + break 'need_await true; + } + } + false + }; + + if !has_top_level_await && need_await { + has_top_level_await = true; + } + + code.push_bytes( + format!( + "{}__turbopack_import__({});\n", + if need_await { "await " } else { "" }, + StringifyJs(&*side_effect.ident().to_string().await?) + ) + .as_bytes(), + ); + } + + code.push_bytes( + format!( + "__turbopack_export_namespace__(__turbopack_import__({}));\n", + StringifyJs(&*module.resolved_as.ident().to_string().await?) + ) + .as_bytes(), + ); + + let code = code.build(); + + Ok(EcmascriptChunkItemContent { + inner_code: code, + source_map: None, + rewrite_source_path: None, + options: EcmascriptChunkItemOptions { + strict: true, + exports: true, + async_module: if has_top_level_await { + Some(AsyncModuleOptions { + has_top_level_await: true, + }) + } else { + None + }, + ..Default::default() + }, + placeholder_for_future_extensions: (), + } + .cell()) + } + + #[turbo_tasks::function] + fn chunking_context(&self) -> Vc> { + self.chunking_context + } +} diff --git a/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs b/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs index b64043161a6c0..4f2caed87ff1d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/tree_shake/mod.rs @@ -27,6 +27,7 @@ pub mod chunk_item; mod graph; pub mod merge; mod optimizations; +pub mod side_effect_module; #[cfg(test)] mod tests; mod util; diff --git a/turbopack/crates/turbopack-ecmascript/src/tree_shake/side_effect_module.rs b/turbopack/crates/turbopack-ecmascript/src/tree_shake/side_effect_module.rs new file mode 100644 index 0000000000000..a25f9de244b9e --- /dev/null +++ b/turbopack/crates/turbopack-ecmascript/src/tree_shake/side_effect_module.rs @@ -0,0 +1,134 @@ +use anyhow::Result; +use turbo_rcstr::RcStr; +use turbo_tasks::{ResolvedVc, Value, Vc}; +use turbo_tasks_fs::glob::Glob; +use turbopack_core::{ + asset::{Asset, AssetContent}, + chunk::{ChunkableModule, ChunkingContext, EvaluatableAsset}, + ident::AssetIdent, + module::Module, + reference::{ModuleReferences, SingleChunkableModuleReference}, + resolve::ModulePart, +}; + +use crate::{ + chunk::{EcmascriptChunkPlaceable, EcmascriptExports}, + tree_shake::chunk_item::SideEffectsModuleChunkItem, + EcmascriptModuleAsset, +}; + +#[turbo_tasks::value] +pub(super) struct SideEffectsModule { + /// Original module + pub module: Vc, + /// The part of the original module that is the binding + pub part: ResolvedVc, + /// The module that is the binding + pub resolved_as: Vc>, + /// Side effects from the original module to the binding. + pub side_effects: Vec>>, +} + +#[turbo_tasks::value_impl] +impl SideEffectsModule { + #[turbo_tasks::function] + pub fn new( + module: Vc, + part: ResolvedVc, + resolved_as: Vc>, + side_effects: Vec>>, + ) -> Vc { + SideEffectsModule { + module, + part, + resolved_as, + side_effects, + } + .cell() + } +} + +#[turbo_tasks::value_impl] +impl Module for SideEffectsModule { + #[turbo_tasks::function] + async fn ident(&self) -> Result> { + let mut ident = self.module.ident().await?.clone_value(); + ident.parts.push(self.part); + + ident.add_asset( + ResolvedVc::cell(RcStr::from("resolved")), + self.resolved_as.ident().to_resolved().await?, + ); + + ident.add_modifier(Vc::cell(RcStr::from("side effects"))); + + for (i, side_effect) in self.side_effects.iter().enumerate() { + ident.add_asset( + ResolvedVc::cell(RcStr::from(format!("side effect {}", i))), + side_effect.ident().to_resolved().await?, + ); + } + + Ok(AssetIdent::new(Value::new(ident))) + } + + #[turbo_tasks::function] + async fn references(&self) -> Result> { + let mut references = vec![]; + + for &side_effect in self.side_effects.iter() { + references.push(Vc::upcast(SingleChunkableModuleReference::new( + Vc::upcast(side_effect), + Vc::cell(RcStr::from("side effect")), + ))); + } + + references.push(Vc::upcast(SingleChunkableModuleReference::new( + Vc::upcast(self.resolved_as), + Vc::cell(RcStr::from("resolved as")), + ))); + + Ok(Vc::cell(references)) + } +} + +#[turbo_tasks::value_impl] +impl Asset for SideEffectsModule { + #[turbo_tasks::function] + fn content(&self) -> Vc { + unreachable!("SideEffectsModule has no content") + } +} + +#[turbo_tasks::value_impl] +impl EcmascriptChunkPlaceable for SideEffectsModule { + #[turbo_tasks::function] + async fn get_exports(&self) -> Vc { + self.resolved_as.get_exports() + } + + #[turbo_tasks::function] + async fn is_marked_as_side_effect_free(self: Vc, _: Vc) -> Vc { + Vc::cell(true) + } +} + +#[turbo_tasks::value_impl] +impl ChunkableModule for SideEffectsModule { + #[turbo_tasks::function] + async fn as_chunk_item( + self: Vc, + chunking_context: Vc>, + ) -> Result>> { + Ok(Vc::upcast( + SideEffectsModuleChunkItem { + module: self, + chunking_context, + } + .cell(), + )) + } +} + +#[turbo_tasks::value_impl] +impl EvaluatableAsset for SideEffectsModule {} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/.basic/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/basic/input/index.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/.basic/input/index.js rename to turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/basic/input/index.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/.basic/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/basic/options.json similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/.basic/options.json rename to turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/basic/options.json diff --git a/turbopack/crates/turbopack/src/lib.rs b/turbopack/crates/turbopack/src/lib.rs index 60e2b3f85c6cc..218ee19c510b6 100644 --- a/turbopack/crates/turbopack/src/lib.rs +++ b/turbopack/crates/turbopack/src/lib.rs @@ -287,7 +287,13 @@ async fn apply_reexport_tree_shaking( module: final_module, export_name: new_export, .. - } = &*follow_reexports(module, export.clone_value(), side_effect_free_packages).await?; + } = &*follow_reexports( + module, + export.clone_value(), + side_effect_free_packages, + false, + ) + .await?; let module = if let Some(new_export) = new_export { if *new_export == *export { Vc::upcast(*final_module)