Skip to content

Manual codeSplitting groups leak cross-entry execution without strictExecutionOrder (#9463 follow-up) #9998

Description

@hyf0

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.

Metadata

Metadata

Assignees

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions