Follow-up to #9463 / #9997. #9997 closed the strictExecutionOrder case (and the rest of the strict family is isolated, because under strictExecutionOrder every side-effectful module is wrapped, so the only eager cross-entry path was the entry-chunk fold fixed there). The same logical leak still exists without strictExecutionOrder.
Reproduction (verified)
// rolldown.config.ts — two entries, NO strictExecutionOrder
export default {
input: { a: './a.js', b: './b.js' },
experimental: { chunkOptimization: true },
output: {
codeSplitting: {
groups: [{ name: 'common', entriesAware: true, entriesAwareMergeThreshold: 10_000 }],
},
},
}
// a.js
import { foo } from './shared.js';
(globalThis.se ??= []).push('a');
foo();
// b.js
(globalThis.se ??= []).push('b');
import('./shared.js').then(({ foo }) => foo());
// shared.js
export function foo() {
(globalThis.se ??= []).push('shared-foo');
}
Loading only the built b.js yields se === ["a", "shared-foo", "b", "shared-foo"] — entry a's top-level side effect ran. Expected: ["b", "shared-foo"].
(The same config with strictExecutionOrder: true is correct — that is what #9997 covers.)
Cause
A manual codeSplitting group co-locates modules of different entry-reachability into one chunk (its bits becomes a union — via entriesAwareMergeThreshold subgroup merging, or directly for a plain group). Under strictExecutionOrder the co-located modules are wrapped in init_* functions, so each entry only runs what it calls. Without it, the co-located modules are emitted eagerly, so loading one entry runs another entry's top-level code.
Two facets:
- Entry modules (the repro above): a genuine entry-isolation bug — executing entry
b must never run entry a.
- Non-entry modules: the pre-existing manual-group over-inclusion tradeoff — e.g.
crates/rolldown/tests/rolldown/function/advanced_chunks/entries_aware_merge_shared_deps already has entry-a load a chunk containing lib-b.
Possible directions
- Make manual code splitting respect reachability boundaries (don't union
bits across modules reachable from disjoint entry sets when it would co-locate eagerly-executed code).
- Or keep cross-entry-shared modules wrapped even in non-strict mode.
- Or, at minimum, do not co-locate user-defined entry modules into a chunk shared with another entry.
Follow-up to #9463 / #9997. #9997 closed the
strictExecutionOrdercase (and the rest of the strict family is isolated, because understrictExecutionOrderevery side-effectful module is wrapped, so the only eager cross-entry path was the entry-chunk fold fixed there). The same logical leak still exists withoutstrictExecutionOrder.Reproduction (verified)
Loading only the built
b.jsyieldsse === ["a", "shared-foo", "b", "shared-foo"]— entrya's top-level side effect ran. Expected:["b", "shared-foo"].(The same config with
strictExecutionOrder: trueis correct — that is what #9997 covers.)Cause
A manual
codeSplittinggroup co-locates modules of different entry-reachability into one chunk (itsbitsbecomes a union — viaentriesAwareMergeThresholdsubgroup merging, or directly for a plain group). UnderstrictExecutionOrderthe co-located modules are wrapped ininit_*functions, so each entry only runs what it calls. Without it, the co-located modules are emitted eagerly, so loading one entry runs another entry's top-level code.Two facets:
bmust never run entrya.crates/rolldown/tests/rolldown/function/advanced_chunks/entries_aware_merge_shared_depsalready hasentry-aload a chunk containinglib-b.Possible directions
bitsacross modules reachable from disjoint entry sets when it would co-locate eagerly-executed code).