From c53f5f60b8df3142f0962d158c60b1c01229313c Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Tue, 14 Apr 2026 14:55:14 -0700 Subject: [PATCH 1/2] Turbopack: Use FrozenMap for module export information --- Cargo.lock | 1 + .../crates/turbopack-ecmascript/Cargo.toml | 1 + .../src/references/esm/export.rs | 31 ++++++++------- .../src/references/mod.rs | 26 ++++++------- .../turbopack-ecmascript/src/rename/module.rs | 38 ++++++++----------- .../side_effect_optimization/facade/module.rs | 24 +++++------- .../side_effect_optimization/locals/module.rs | 13 +++---- 7 files changed, 64 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c67eed0ac8f..e89f47f90370 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10481,6 +10481,7 @@ dependencies = [ "tracing", "turbo-bincode", "turbo-esregex", + "turbo-frozenmap", "turbo-rcstr", "turbo-tasks", "turbo-tasks-backend", diff --git a/turbopack/crates/turbopack-ecmascript/Cargo.toml b/turbopack/crates/turbopack-ecmascript/Cargo.toml index 48eea0823f42..95dddb6fbb29 100644 --- a/turbopack/crates/turbopack-ecmascript/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript/Cargo.toml @@ -49,6 +49,7 @@ tokio = { workspace = true } tracing = { workspace = true } turbo-bincode = { workspace = true } turbo-esregex = { workspace = true } +turbo-frozenmap = { workspace = true } turbo-rcstr = { workspace = true } turbo-tasks = { workspace = true } turbo-tasks-fs = { workspace = true } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs index b4840be0cf7f..8fe34f6fbe96 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs @@ -11,6 +11,7 @@ use swc_core::{ }, quote, quote_expr, }; +use turbo_frozenmap::FrozenMap; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ FxIndexMap, NonLocalValue, ResolvedVc, TryFlatJoinIterExt, Vc, trace::TraceRawVcs, turbofmt, @@ -529,7 +530,7 @@ async fn emit_star_exports_issue(source_ident: Vc, message: RcStr) - #[derive(Hash, Debug)] pub struct EsmExports { /// Explicit exports - pub exports: BTreeMap, + pub exports: FrozenMap, /// Unexpanded `export * from ...` statements (expanded in `expand_star_exports`) pub star_exports: Vec>>, } @@ -541,7 +542,7 @@ pub struct EsmExports { #[turbo_tasks::value(shared)] #[derive(Hash, Debug)] pub struct ExpandedExports { - pub exports: BTreeMap, + pub exports: FrozenMap, /// Modules we couldn't analyze all exports of. pub dynamic_exports: Vec>>, } @@ -559,16 +560,16 @@ impl EsmExports { module_reference: Vc>, ) -> Result> { let module_reference = module_reference.to_resolved().await?; - let mut exports = BTreeMap::new(); + let mut exports = Vec::new(); let default = rcstr!("default"); - exports.insert( + exports.push(( default.clone(), EsmExport::ImportedBinding(module_reference, default, false), - ); + )); Ok(EcmascriptExports::EsmExports( EsmExports { - exports, + exports: FrozenMap::from(exports), star_exports: vec![module_reference], } .resolved_cell(), @@ -581,7 +582,11 @@ impl EsmExports { &self, export_usage_info: Vc, ) -> Result> { - let mut exports: BTreeMap = self.exports.clone(); + let mut exports: BTreeMap<_, _> = self + .exports + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); let mut dynamic_exports = vec![]; let export_usage_info = export_usage_info.await?; @@ -608,12 +613,10 @@ impl EsmExports { continue; } - if !exports.contains_key(export) { - exports.insert( - export.clone(), - EsmExport::ImportedBinding(esm_ref, export.clone(), false), - ); - } + // the spec indicates first-one-wins: https://tc39.es/ecma262/#_ref_9060 + exports + .entry(export.clone()) + .or_insert_with(|| EsmExport::ImportedBinding(esm_ref, export.clone(), false)); } if !export_info.dynamic_exporting_modules.is_empty() { @@ -622,7 +625,7 @@ impl EsmExports { } Ok(ExpandedExports { - exports, + exports: FrozenMap::from_unique_sorted_box_unchecked(exports.into_iter().collect()), dynamic_exports, } .cell()) diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 5f382371e6fc..aa494082f7c0 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -22,7 +22,6 @@ pub mod util; pub mod worker; use std::{ - collections::BTreeMap, future::Future, mem::take, ops::Deref, @@ -62,6 +61,7 @@ use swc_core::{ }; use tokio::sync::OnceCell; use tracing::Instrument; +use turbo_frozenmap::FrozenMap; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ FxIndexMap, FxIndexSet, NonLocalValue, PrettyPrintError, ReadRef, ResolvedVc, TaskInput, @@ -907,21 +907,21 @@ async fn analyze_ecmascript_module_internal( analysis.add_esm_reexport_reference(i); } Reexport::Namespace { exported: n } => { - esm_exports.insert( + esm_exports.push(( n.as_str().into(), EsmExport::ImportedNamespace(ResolvedVc::upcast(reference)), - ); + )); analysis.add_esm_reexport_reference(i); } Reexport::Named { imported, exported } => { - esm_exports.insert( + esm_exports.push(( exported.as_str().into(), EsmExport::ImportedBinding( ResolvedVc::upcast(reference), imported.to_string().into(), false, ), - ); + )); analysis.add_esm_reexport_reference(i); } } @@ -939,7 +939,7 @@ async fn analyze_ecmascript_module_internal( } let esm_exports = EsmExports { - exports: esm_exports, + exports: FrozenMap::from(esm_exports), star_exports: esm_star_exports, } .cell(); @@ -4003,7 +4003,7 @@ struct ModuleReferencesVisitor<'a> { old_analyzer: StaticAnalyser, import_references: &'a [ResolvedVc], analysis: &'a mut AnalyzeEcmascriptModuleResultBuilder, - esm_exports: BTreeMap, + esm_exports: Vec<(RcStr, EsmExport)>, webpack_runtime: Option<(RcStr, Span)>, webpack_entry: bool, webpack_chunks: Vec, @@ -4026,7 +4026,7 @@ impl<'a> ModuleReferencesVisitor<'a> { old_analyzer: StaticAnalyser::default(), import_references, analysis, - esm_exports: BTreeMap::new(), + esm_exports: Vec::new(), webpack_runtime: None, webpack_entry: false, webpack_chunks: Vec::new(), @@ -4186,7 +4186,7 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> { ) } }; - self.esm_exports.insert(key, export); + self.esm_exports.push((key, export)); } } } @@ -4212,7 +4212,7 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> { let liveness = self.get_export_ident_liveness((id.clone(), ctx)); let name: RcStr = id.as_str().into(); self.esm_exports - .insert(name.clone(), EsmExport::LocalBinding(name, liveness)); + .push((name.clone(), EsmExport::LocalBinding(name, liveness))); }; match decl { Decl::Class(ClassDecl { ident, .. }) | Decl::Fn(FnDecl { ident, .. }) => { @@ -4252,14 +4252,14 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> { export: &'ast ExportDefaultExpr, ast_path: &mut AstNodePath>, ) { - self.esm_exports.insert( + self.esm_exports.push(( rcstr!("default"), EsmExport::LocalBinding( magic_identifier::mangle("default export").into(), // The expression passed to `export default` cannot be mutated Liveness::Constant, ), - ); + )); if self.analyze_mode.is_code_gen() { self.analysis.add_code_gen(EsmModuleItem::new( as_parent_path(ast_path).into(), @@ -4287,7 +4287,7 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> { Liveness::Constant, ), }; - self.esm_exports.insert(rcstr!("default"), export); + self.esm_exports.push((rcstr!("default"), export)); } DefaultDecl::TsInterfaceDecl(..) => { // ignore diff --git a/turbopack/crates/turbopack-ecmascript/src/rename/module.rs b/turbopack/crates/turbopack-ecmascript/src/rename/module.rs index f61d69a0e212..47918005dddc 100644 --- a/turbopack/crates/turbopack-ecmascript/src/rename/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/rename/module.rs @@ -1,6 +1,5 @@ -use std::collections::BTreeMap; - use anyhow::{Result, bail}; +use turbo_frozenmap::FrozenMap; use turbo_tasks::{ResolvedVc, Vc}; use turbo_tasks_fs::{File, FileContent}; use turbopack_core::{ @@ -208,33 +207,28 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleRenameModule { #[turbo_tasks::function] async fn get_exports(&self) -> Result> { let reference = self.module_reference().await?; - let mut exports = BTreeMap::new(); - match &self.part { + let export = match &self.part { ModulePart::RenamedExport { original_export, export, - } => { - exports.insert( - export.clone(), - EsmExport::ImportedBinding( - ResolvedVc::upcast(reference), - original_export.clone(), - false, - ), - ); - } - ModulePart::RenamedNamespace { export } => { - exports.insert( - export.clone(), - EsmExport::ImportedNamespace(ResolvedVc::upcast(reference)), - ); - } + } => ( + export.clone(), + EsmExport::ImportedBinding( + ResolvedVc::upcast(reference), + original_export.clone(), + false, + ), + ), + ModulePart::RenamedNamespace { export } => ( + export.clone(), + EsmExport::ImportedNamespace(ResolvedVc::upcast(reference)), + ), _ => bail!("Unexpected ModulePart for EcmascriptModuleRenameModule"), - } + }; let exports = EsmExports { - exports, + exports: FrozenMap::from_unique_sorted_box(Box::new([export])), star_exports: Vec::new(), } .resolved_cell(); diff --git a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs index 3241cae3c3e3..34c235158b95 100644 --- a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs @@ -1,6 +1,5 @@ -use std::collections::BTreeMap; - use anyhow::{Result, bail}; +use turbo_frozenmap::FrozenMap; use turbo_tasks::{ResolvedVc, Vc}; use turbopack_core::{ chunk::{ @@ -189,18 +188,16 @@ impl EcmascriptAnalyzable for EcmascriptModuleFacadeModule { impl EcmascriptChunkPlaceable for EcmascriptModuleFacadeModule { #[turbo_tasks::function] async fn get_exports(&self) -> Result> { - let mut exports = BTreeMap::new(); - let mut star_exports = Vec::new(); - let EcmascriptExports::EsmExports(esm_exports) = &*self.module.get_exports().await? else { bail!("EcmascriptModuleFacadeModule must only be used on modules with EsmExports"); }; let esm_exports = esm_exports.await?; + let mut exports = Vec::with_capacity(esm_exports.exports.len()); for (name, export) in &esm_exports.exports { let name = name.clone(); match export { EsmExport::LocalBinding(_, liveness) => { - exports.insert( + exports.push(( name.clone(), EsmExport::ImportedBinding( ResolvedVc::upcast( @@ -215,27 +212,26 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleFacadeModule { name, *liveness == Liveness::Mutable, ), - ); + )); } EsmExport::ImportedNamespace(reference) => { - exports.insert(name, EsmExport::ImportedNamespace(*reference)); + exports.push((name, EsmExport::ImportedNamespace(*reference))); } EsmExport::ImportedBinding(reference, imported_name, mutable) => { - exports.insert( + exports.push(( name, EsmExport::ImportedBinding(*reference, imported_name.clone(), *mutable), - ); + )); } EsmExport::Error => { - exports.insert(name, EsmExport::Error); + exports.push((name, EsmExport::Error)); } } } - star_exports.extend(esm_exports.star_exports.iter().copied()); let exports = EsmExports { - exports, - star_exports, + exports: FrozenMap::from_unique_sorted_box(exports.into_boxed_slice()), + star_exports: esm_exports.star_exports.clone(), } .resolved_cell(); Ok(EcmascriptExports::EsmExports(exports).cell()) diff --git a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs index ca4f6420bd6a..b16a8a187198 100644 --- a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs @@ -1,6 +1,5 @@ -use std::collections::BTreeMap; - use anyhow::{Result, bail}; +use turbo_frozenmap::FrozenMap; use turbo_tasks::{ResolvedVc, Vc}; use turbopack_core::{ chunk::{ @@ -142,7 +141,7 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleLocalsModule { bail!("EcmascriptModuleLocalsModule must only be used on modules with EsmExports"); }; let esm_exports = exports.await?; - let mut exports = BTreeMap::new(); + let mut exports = Vec::new(); for (name, export) in &esm_exports.exports { match export { @@ -150,19 +149,19 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleLocalsModule { // not included in locals module } EsmExport::LocalBinding(local_name, liveness) => { - exports.insert( + exports.push(( name.clone(), EsmExport::LocalBinding(local_name.clone(), *liveness), - ); + )); } EsmExport::Error => { - exports.insert(name.clone(), EsmExport::Error); + exports.push((name.clone(), EsmExport::Error)); } } } let exports = EsmExports { - exports, + exports: FrozenMap::from_unique_sorted_box(exports.into_boxed_slice()), star_exports: vec![], } .resolved_cell(); From bb97d4e7eea6105d302df1da0441094e7bc1c248 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Tue, 14 Apr 2026 15:11:10 -0700 Subject: [PATCH 2/2] Use FrozenMap::from for BTreeMap conversion --- .../crates/turbopack-ecmascript/src/references/esm/export.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs index 8fe34f6fbe96..20b8ff9fd921 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs @@ -625,7 +625,7 @@ impl EsmExports { } Ok(ExpandedExports { - exports: FrozenMap::from_unique_sorted_box_unchecked(exports.into_iter().collect()), + exports: FrozenMap::from(exports), dynamic_exports, } .cell())