-
Notifications
You must be signed in to change notification settings - Fork 737
[Bug]: strictExecutionOrder + sideEffects: false drops init calls for named re-exports, leaving variables undefined #8777
Description
Reproduction link or steps
What is expected?
Foo and Bar should be defined objects. The init_Foo wrapper (for Foo/index.js) should call init_Foo$1() (for Foo.js) so that the Foo variable gets assigned:
var init_Foo = __esmMin((() => {
init_Foo$1(); // ← initialize Foo.js (assigns the Foo variable)
init_fooClasses();
init_fooClasses();
})); What is actually happening?
Foo and Bar are undefined. The Foo.js module wrapper becomes anonymous and is never called. The Foo/index.js wrapper only initializes fooClasses, skipping Foo.js:
// Foo.js — anonymous wrapper, never called! Foo stays undefined.
var Foo;
__esmMin((() => {
init_fooClasses();
Foo = { name: "Foo", root: fooClasses.root };
})); // Foo/index.js — missing init_Foo$1() call
var init_Foo = __esmMin((() => {
init_fooClasses(); // ← only classes — no Foo.js init!
init_fooClasses();
}));System Info
rolldown v1.0.0-rc.9
Node.js v22.21.1
Linux x86_64Any additional comments?
A lot of iterating found that we needed the following to be true to hit this:
- strictExecutionOrder: true
- "sideEffects": false in a parent package.json
- Nested barrel re-export pattern (barrel → sub-barrel → component)
This is true seemingly in our usage of the mui material-ui package which is how we first encountered it.
A very very rough and novice look suggests that this is because in
rolldown/crates/rolldown/src/stages/link_stage/reference_needed_symbols.rs
Lines 187 to 194 in e17f131
| WrapKind::Esm => { | |
| // Turn `import ... from 'bar_esm'` into `init_bar_esm()` | |
| stmt_info.side_effect = | |
| (is_reexport_all || importee.side_effects.has_side_effects()).into(); | |
| // Reference to `init_foo` | |
| stmt_info | |
| .referenced_symbols | |
| .push(importee_linking_info.wrapper_ref.unwrap().into()); |
For a named re-export like export { default } from './Foo.js':
- is_reexport_all is false (it's not export *)
- importee.side_effects is UserDefined(false) (from package.json)
So stmt_info.side_effect is set to false
rolldown/crates/rolldown/src/stages/link_stage/tree_shaking/include_statements.rs
Lines 647 to 665 in e17f131
| if ctx.tree_shaking && !forced_no_treeshake { | |
| module.stmt_infos.iter_enumerated_without_namespace_stmt().for_each( | |
| |(stmt_info_id, stmt_info)| { | |
| // No need to handle the namespace statement specially, because it doesn't have side effects and will only be included if it is used. | |
| let bail_eval = module.meta.has_eval() | |
| && !stmt_info.declared_symbols.is_empty() | |
| && stmt_info_id.index() != 0; | |
| let has_side_effects = if module.meta.contains(EcmaViewMeta::SafelyTreeshakeCommonjs) | |
| && ctx.options.treeshake.commonjs() | |
| { | |
| stmt_info.side_effect.contains(SideEffectDetail::Unknown) | |
| } else { | |
| stmt_info.side_effect.has_side_effect() | |
| }; | |
| if has_side_effects || bail_eval { | |
| include_statement(ctx, module, stmt_info_id); | |
| } | |
| }, | |
| ); |
The export { default } from './Foo.js' re-export in Foo/index.js is excluded from stmt_info_included. When module_finalizers/mod.rs:remove_unused_top_level_stmt() later processes Foo/index.js, it never sees this re-export statement, never calls transform_or_remove_import_export_stmt for it, and never
generates the init_Foo$1() call that would initialise the Foo variable.
Then, UserDefined(false) from package.json short-circuits
rolldown/crates/rolldown/src/stages/link_stage/tree_shaking/determine_side_effects.rs
Lines 54 to 56 in e17f131
| DeterminedSideEffects::Analyzed(true) | |
| | DeterminedSideEffects::UserDefined(_) | |
| | DeterminedSideEffects::NoTreeshake => module_side_effects, |
while normally
determine_side_effects would detect the wrapped ESM importee via the export * from / WrapKind::Esm check and return Analyzed(true).
The semantics here seem kinda deliberate so I wanted to discuss but I can raise a change that marks re-export statements as always having side_effect: true when strictExecutionOrder is enabled, importee has WrapKind:Esm? But I'm open to any solutions.
Apologies again if this overview is totally in the wrong direction, first forays into rolldown and the bundler internals so feel free to course correct
Metadata
Metadata
Assignees
Labels
Type
Fields
Give feedbackPriority
Effort