Skip to content

Commit e919a00

Browse files
refactor: eliminate error-prone may_partial_namespace"
1 parent 68f6274 commit e919a00

File tree

2 files changed

+66
-46
lines changed

2 files changed

+66
-46
lines changed

crates/rolldown/src/stages/generate_stage/chunk_optimizer.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,6 @@ impl GenerateStage<'_> {
897897
options: self.options,
898898
normal_symbol_exports_chain_map: &self.link_output.normal_symbol_exports_chain_map,
899899
bailout_cjs_tree_shaking_modules: FxHashSet::default(),
900-
may_partial_namespace: false,
901900
module_inclusion_changed: false,
902901
module_namespace_included_reason: &mut module_namespace_reason_vec,
903902
inline_const_smart: self.options.optimization.is_inline_const_smart_mode(),

crates/rolldown/src/stages/link_stage/tree_shaking/include_statements.rs

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ bitflags::bitflags! {
5050
}
5151
}
5252

53-
#[expect(clippy::struct_excessive_bools)]
5453
pub 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

672700
pub 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

Comments
 (0)