@@ -50,7 +50,6 @@ bitflags::bitflags! {
5050 }
5151}
5252
53- #[ expect( clippy:: struct_excessive_bools) ]
5453pub struct IncludeContext < ' a > {
5554 pub modules : & ' a IndexModules ,
5655 pub symbols : & ' a SymbolRefDb ,
@@ -67,7 +66,6 @@ pub struct IncludeContext<'a> {
6766 /// It is necessary since we can't mutate `module.meta` during the tree shaking process.
6867 /// see [rolldown_common::ecmascript::ecma_view::EcmaViewMeta]
6968 pub bailout_cjs_tree_shaking_modules : FxHashSet < ModuleIdx > ,
70- pub may_partial_namespace : bool ,
7169 /// Tracks whether any new module was included during the current convergence iteration.
7270 /// Used to detect fixpoint without O(N) scanning of `is_module_included_vec`.
7371 pub module_inclusion_changed : bool ,
@@ -104,7 +102,6 @@ impl<'a> IncludeContext<'a> {
104102 options,
105103 normal_symbol_exports_chain_map,
106104 bailout_cjs_tree_shaking_modules : FxHashSet :: default ( ) ,
107- may_partial_namespace : false ,
108105 module_inclusion_changed : false ,
109106 module_namespace_included_reason,
110107 json_module_none_self_reference_included_symbol : FxHashMap :: default ( ) ,
@@ -124,10 +121,46 @@ fn include_cjs_bailout_exports(
124121 . filter_map ( |( _name, local) | local. came_from_cjs . then_some ( local) )
125122 . for_each ( |local| {
126123 include_symbol ( context, local. symbol_ref , SymbolIncludeReason :: Normal ) ;
124+ check_cjs_bailout ( context, local. symbol_ref ) ;
127125 } ) ;
128126 }
129127}
130128
129+ /// Check if including this symbol should trigger CJS tree-shaking bailout.
130+ /// This is called at `include_symbol` call sites where the symbol is NOT accessed
131+ /// via a resolved member expression on a CJS namespace (i.e., where the full namespace
132+ /// might be used opaquely). When we know only a specific property is accessed
133+ /// (member expression with `target_commonjs_exported_symbol`), we skip this check
134+ /// to allow CJS tree-shaking.
135+ fn check_cjs_bailout ( ctx : & mut IncludeContext , symbol_ref : SymbolRef ) {
136+ let canonical_ref = ctx. symbols . canonical_ref_for ( symbol_ref) ;
137+
138+ // If the symbol is a CJS namespace import ref, bail out the target CJS module.
139+ if let Some ( idx) =
140+ ctx. metas [ canonical_ref. owner ] . import_record_ns_to_cjs_module . get ( & canonical_ref)
141+ {
142+ ctx. bailout_cjs_tree_shaking_modules . insert ( * idx) ;
143+ }
144+ // If the symbol IS a CJS module's namespace object, bail out that module.
145+ if ctx. modules [ canonical_ref. owner ] . namespace_object_ref ( ) == Some ( canonical_ref) {
146+ ctx. bailout_cjs_tree_shaking_modules . insert ( canonical_ref. owner ) ;
147+ }
148+
149+ // If the symbol has a namespace_alias importing "default" from a CJS module,
150+ // bail out that module (default import is the whole module.exports).
151+ let canonical_ref_symbol = ctx. symbols . get ( canonical_ref) ;
152+ if let Some ( namespace_alias) = & canonical_ref_symbol. namespace_alias {
153+ if let Some ( idx) = ctx. metas [ namespace_alias. namespace_ref . owner ]
154+ . import_record_ns_to_cjs_module
155+ . get ( & namespace_alias. namespace_ref )
156+ {
157+ if namespace_alias. property_name . as_str ( ) == "default" {
158+ ctx. bailout_cjs_tree_shaking_modules . insert ( * idx) ;
159+ }
160+ }
161+ }
162+ }
163+
131164/// Collects all depended runtime helpers from included modules only.
132165/// Eliminated modules may have runtime helpers set (for propagation to importers),
133166/// but we should only include the runtime if an included module actually needs it.
@@ -205,6 +238,7 @@ impl LinkStage<'_> {
205238 } ,
206239 ) ;
207240 include_symbol ( context, * symbol_ref, SymbolIncludeReason :: EntryExport ) ;
241+ check_cjs_bailout ( context, * symbol_ref) ;
208242 }
209243 } ,
210244 ) ;
@@ -387,6 +421,7 @@ impl LinkStage<'_> {
387421 } ,
388422 ) ;
389423 include_symbol ( context, * symbol_ref, SymbolIncludeReason :: EntryExport ) ;
424+ check_cjs_bailout ( context, * symbol_ref) ;
390425 }
391426 } ) ;
392427 include_module ( context, module) ;
@@ -577,13 +612,6 @@ pub fn include_module(ctx: &mut IncludeContext, module: &NormalModule) {
577612 return ;
578613 }
579614
580- // Save and reset may_partial_namespace. When including a module's
581- // side-effectful statements, we should not inherit the partial namespace
582- // context from a specific member expression resolution — the module's
583- // own statements need independent bailout evaluation.
584- let prev_may_partial_namespace = ctx. may_partial_namespace ;
585- ctx. may_partial_namespace = false ;
586-
587615 ctx. is_module_included_vec . set_bit ( module. idx ) ;
588616 ctx. module_inclusion_changed = true ;
589617
@@ -650,11 +678,13 @@ pub fn include_module(ctx: &mut IncludeContext, module: &NormalModule) {
650678 if module. meta . has_eval ( ) && matches ! ( module. module_type, ModuleType :: Js | ModuleType :: Jsx ) {
651679 module. named_imports . keys ( ) . for_each ( |symbol| {
652680 include_symbol ( ctx, * symbol, SymbolIncludeReason :: Normal ) ;
681+ check_cjs_bailout ( ctx, * symbol) ;
653682 } ) ;
654683 }
655684
656685 ctx. metas [ module. idx ] . included_commonjs_export_symbol . iter ( ) . for_each ( |symbol_ref| {
657686 include_symbol ( ctx, * symbol_ref, SymbolIncludeReason :: Normal ) ;
687+ check_cjs_bailout ( ctx, * symbol_ref) ;
658688 } ) ;
659689
660690 // With enabling HMR, rolldown will register included esm module's namespace object to the runtime.
@@ -665,8 +695,6 @@ pub fn include_module(ctx: &mut IncludeContext, module: &NormalModule) {
665695 include_statement ( ctx, module, StmtInfos :: NAMESPACE_STMT_IDX ) ;
666696 ctx. module_namespace_included_reason [ module. idx ] . insert ( ModuleNamespaceIncludedReason :: Unknown ) ;
667697 }
668-
669- ctx. may_partial_namespace = prev_may_partial_namespace;
670698}
671699
672700pub fn include_symbol (
@@ -691,42 +719,31 @@ pub fn include_symbol(
691719 // Also include the symbol that points to the canonical ref.
692720 ctx. used_symbol_refs . insert ( symbol_ref) ;
693721
694- if !ctx. may_partial_namespace {
695- if let Some ( idx) =
696- ctx. metas [ canonical_ref. owner ] . import_record_ns_to_cjs_module . get ( & canonical_ref)
697- {
698- ctx. bailout_cjs_tree_shaking_modules . insert ( * idx) ;
699- }
700- if ctx. modules [ canonical_ref. owner ] . namespace_object_ref ( ) == Some ( canonical_ref) {
701- ctx. bailout_cjs_tree_shaking_modules . insert ( canonical_ref. owner ) ;
702- }
703- }
722+ // CJS bailout checks are handled by `check_cjs_bailout` at each call site,
723+ // not here. This keeps `include_symbol` focused on inclusion only.
704724
705725 let canonical_ref_symbol = ctx. symbols . get ( canonical_ref) ;
706726 if let Some ( namespace_alias) = & canonical_ref_symbol. namespace_alias {
707727 canonical_ref = namespace_alias. namespace_ref ;
708728 if let Some ( idx) =
709729 ctx. metas [ canonical_ref. owner ] . import_record_ns_to_cjs_module . get ( & canonical_ref)
710730 {
711- if !ctx. may_partial_namespace && namespace_alias. property_name . as_str ( ) == "default" {
712- ctx. bailout_cjs_tree_shaking_modules . insert ( * idx) ;
713- } else {
714- // handle case:
715- // ```js
716- // import {a} from './cjs.js'
717- // console.log(a)
718- // ```
719- ctx. modules [ * idx] . as_normal ( ) . inspect ( |_| {
720- let Some ( export_symbol) =
721- ctx. metas [ * idx] . resolved_exports . get ( & namespace_alias. property_name )
722- else {
723- return ;
724- } ;
725- if namespace_alias. property_name . as_str ( ) != "default" {
726- include_symbol ( ctx, export_symbol. symbol_ref , SymbolIncludeReason :: Normal ) ;
727- }
728- } ) ;
729- }
731+ // Include specific named export from CJS module.
732+ // Default import bailout is handled by check_cjs_bailout at call sites.
733+ // ```js
734+ // import {a} from './cjs.js'
735+ // console.log(a)
736+ // ```
737+ ctx. modules [ * idx] . as_normal ( ) . inspect ( |_| {
738+ let Some ( export_symbol) =
739+ ctx. metas [ * idx] . resolved_exports . get ( & namespace_alias. property_name )
740+ else {
741+ return ;
742+ } ;
743+ if namespace_alias. property_name . as_str ( ) != "default" {
744+ include_symbol ( ctx, export_symbol. symbol_ref , SymbolIncludeReason :: Normal ) ;
745+ }
746+ } ) ;
730747 }
731748 }
732749
@@ -874,9 +891,6 @@ pub fn include_statement(
874891 // Caveat: If we can get the `MemberExprRefResolution` from the `resolved_member_expr_refs`,
875892 // it means this member expr definitely contains module namespace ref.
876893 if let Some ( resolved_ref) = member_expr_resolution. resolved {
877- let pre = ctx. may_partial_namespace ;
878- ctx. may_partial_namespace =
879- member_expr_resolution. target_commonjs_exported_symbol . is_some ( ) ;
880894 member_expr_resolution. depended_refs . iter ( ) . for_each ( |sym_ref| {
881895 if let Module :: Normal ( module) = & ctx. modules [ sym_ref. owner ] {
882896 module. stmt_infos . declared_stmts_by_symbol ( sym_ref) . iter ( ) . copied ( ) . for_each (
@@ -887,7 +901,13 @@ pub fn include_statement(
887901 }
888902 } ) ;
889903 include_symbol ( ctx, resolved_ref, include_kind) ;
890- ctx. may_partial_namespace = pre;
904+ // When the member expression resolves to a specific CJS export property
905+ // (e.g., `ns.x`), we skip the bailout check — we know the access is partial
906+ // and CJS tree-shaking can work. Otherwise, the full namespace may be used
907+ // opaquely, so we check for bailout.
908+ if member_expr_resolution. target_commonjs_exported_symbol . is_none ( ) {
909+ check_cjs_bailout ( ctx, resolved_ref) ;
910+ }
891911 } else {
892912 // If it points to nothing, the expression will be rewritten as `void 0` and there's nothing we need to include
893913 }
@@ -907,6 +927,7 @@ pub fn include_statement(
907927 }
908928 } ) ;
909929 include_symbol ( ctx, * original_ref, include_kind) ;
930+ check_cjs_bailout ( ctx, * original_ref) ;
910931 }
911932 } ) ;
912933}
0 commit comments